repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/gratis/index.ts
Rawimport type { Api, Model } from "@earendil-works/pi-ai";
import type {
ExtensionAPI,
ExtensionContext,
ModelRoute,
ModelRouteRequest,
} from "@earendil-works/pi-coding-agent";
import { closeDebug, dbg, span } from "./src/debug.ts";
const PROVIDER = "grts";
const ID = "auto";
const MAX_TRIED = 8;
const PRIORITY = ["openrouter", "google", "mistral", "nvidia"] as const;
const MISTRAL_FREE_IDS = new Set([
"labs-devstral-small-2512",
"labs-leanstral-1-5-1",
]);
type GratisState = { tried: string[] };
type Request = ModelRouteRequest<GratisState>;
type ChatModel = Model<Api>;
function zeroPrice(model: ChatModel): boolean {
const cost = model.cost;
return (
cost?.input === 0 &&
cost.output === 0 &&
cost.cacheRead === 0 &&
cost.cacheWrite === 0
);
}
function eligible(model: ChatModel): boolean {
if (!zeroPrice(model)) return false;
if (model.provider === "openrouter")
return model.id === "openrouter/free" || model.id.endsWith(":free");
if (model.provider === "google") return model.id.startsWith("gemma-");
if (model.provider === "mistral") return MISTRAL_FREE_IDS.has(model.id);
return model.provider === "nvidia";
}
function candidates(ctx: ExtensionContext): ChatModel[] {
const available = ctx.modelRegistry.getAvailable();
const groups = PRIORITY.map((provider) => {
const models = available.filter(
(model) =>
model.provider === provider &&
model.api !== "pi-virtual" &&
eligible(model),
);
const pool = models.findIndex((model) => model.id === "openrouter/free");
if (pool > 0) models.unshift(...models.splice(pool, 1));
return models;
});
const selected: ChatModel[] = [];
// Alternate providers before trying their next model: quotas are often provider-wide.
for (let index = 0; index < MAX_TRIED; index++) {
for (const group of groups) {
const model = group[index];
if (model) selected.push(model);
if (selected.length === MAX_TRIED) return selected;
}
}
return selected;
}
function key(model: ChatModel): string {
return `${model.provider}/${model.id}`;
}
function routeTo(
request: Request,
model: ChatModel,
state: GratisState | undefined,
): ModelRoute<GratisState> {
return {
model,
thinkingLevel: request.thinkingLevel,
...(state === undefined ? {} : { state }),
};
}
function select(
request: Request,
ctx: ExtensionContext,
): ModelRoute<GratisState> {
const end = span?.("route", { reason: request.reason });
const available = candidates(ctx);
if (available.length === 0) {
end?.("error", { outcome: "none" });
throw new Error("Gratis: no eligible free native models are available.");
}
const previousKey = request.previous && key(request.previous.model);
const previous = available.find((model) => key(model) === previousKey);
if (request.reason === "continuation" && previous) {
end?.("finish", { outcome: "sticky" });
return routeTo(request, previous, request.state);
}
const tried =
request.reason === "retry"
? [
...new Set([
...(request.state?.tried ?? []),
...(request.failed ? [key(request.failed.model)] : []),
]),
].slice(-MAX_TRIED)
: [];
const model = available.find((candidate) => !tried.includes(key(candidate)));
if (!model) {
end?.("error", { outcome: "exhausted" });
throw new Error(
"Gratis: all eligible free native models were exhausted by Pi retries.",
);
}
const state =
request.reason === "direct" ? undefined : { tried: [...tried, key(model)] };
end?.("finish", { outcome: "selected", candidates: available.length });
return routeTo(request, model, state);
}
export default function gratis(pi: ExtensionAPI): void {
pi.registerVirtualModel<GratisState>({
provider: PROVIDER,
id: ID,
name: "Gratis auto",
thinkingLevels: ["off", "low", "medium", "high"],
route: select,
});
pi.on("session_start", () => dbg?.("session.start"));
pi.on("session_shutdown", () => {
dbg?.("session.shutdown");
closeDebug();
});
}