repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/gratis/src/catalog.ts
Rawimport type { Api, Model } from "@earendil-works/pi-ai";
export const PROVIDER = "grts";
export const AUTO_ID = "auto";
/**
* Cascade priority for `grts/auto`, answer quality first; also the display order.
* NVIDIA sits behind Mistral: its strongest free models queue past the deadline
* (measured twice). Keyless Kilo trails: always there, but its anonymous router
* lands on weak models.
*/
export const BACKENDS = [
"google",
"zai",
"groq",
"mistral",
"nvidia",
"openrouter",
"hetzner",
"kilo",
] as const;
export type Backend = (typeof BACKENDS)[number];
export type GratisModel = Model<Api>;
type Input = ("text" | "image")[];
interface BackendInfo {
label: string;
/** Backend-specific key: step 1 of key resolution. */
gratisEnv: string;
/** Native env of the backend: step 2. */
nativeEnv: string;
/** Pi provider id for credential-store lookup: step 3. */
nativeProvider?: string;
/** Keyless backends register without any key. */
keyless: boolean;
/** Advisory cooldowns after observed 429 or quota exhaustion. */
rateCooldownMs: number;
quotaCooldownMs: number;
}
const MINUTE = 60_000;
const HOUR = 60 * MINUTE;
export const BACKEND_INFO: Record<Backend, BackendInfo> = {
kilo: {
label: "Kilo",
gratisEnv: "GRATIS_KILO_TOKEN",
nativeEnv: "KILO_API_KEY",
keyless: true,
rateCooldownMs: 5 * MINUTE,
quotaCooldownMs: HOUR,
},
google: {
label: "Google",
gratisEnv: "GRATIS_GOOGLE_TOKEN",
nativeEnv: "GEMINI_API_KEY",
nativeProvider: "google",
keyless: false,
rateCooldownMs: MINUTE,
quotaCooldownMs: HOUR,
},
nvidia: {
label: "NVIDIA",
gratisEnv: "GRATIS_NVIDIA_TOKEN",
nativeEnv: "NVIDIA_API_KEY",
nativeProvider: "nvidia",
keyless: false,
rateCooldownMs: MINUTE,
quotaCooldownMs: HOUR,
},
openrouter: {
label: "OpenRouter",
gratisEnv: "GRATIS_OPENROUTER_TOKEN",
nativeEnv: "OPENROUTER_API_KEY",
nativeProvider: "openrouter",
keyless: false,
rateCooldownMs: MINUTE,
quotaCooldownMs: HOUR,
},
groq: {
label: "Groq",
gratisEnv: "GRATIS_GROQ_TOKEN",
nativeEnv: "GROQ_API_KEY",
nativeProvider: "groq",
keyless: false,
rateCooldownMs: MINUTE,
quotaCooldownMs: HOUR,
},
mistral: {
label: "Mistral",
gratisEnv: "GRATIS_MISTRAL_TOKEN",
nativeEnv: "MISTRAL_API_KEY",
nativeProvider: "mistral",
keyless: false,
rateCooldownMs: MINUTE,
quotaCooldownMs: HOUR,
},
zai: {
label: "Z.ai",
gratisEnv: "GRATIS_ZAI_TOKEN",
nativeEnv: "ZAI_API_KEY",
nativeProvider: "zai",
keyless: false,
rateCooldownMs: MINUTE,
quotaCooldownMs: HOUR,
},
hetzner: {
label: "Hetzner",
gratisEnv: "GRATIS_HETZNER_TOKEN",
nativeEnv: "HETZNER_INFERENCE_API_KEY",
// Pi has no built-in Hetzner provider; a models.json `hetzner` entry supplies one.
nativeProvider: "hetzner",
keyless: false,
rateCooldownMs: MINUTE,
quotaCooldownMs: HOUR,
},
};
export const KILO_BASE_URL = "https://api.kilo.ai/api/gateway";
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
const OPENROUTER_ANTHROPIC_BASE_URL = "https://openrouter.ai/api";
const GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
const GROQ_BASE_URL = "https://api.groq.com/openai/v1";
const MISTRAL_BASE_URL = "https://api.mistral.ai";
const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
// General endpoint only: Coding Plan endpoints (`/api/coding/paas/v4`, `/api/anthropic`) spend plan quota.
const ZAI_BASE_URL = "https://api.z.ai/api/paas/v4";
const HETZNER_BASE_URL = "https://inference.hetzner.com/api/v1";
/** Backends whose catalog is refreshed from a live models endpoint. */
export type LiveBackend = "kilo" | "openrouter" | "nvidia" | "hetzner" | "zai";
export const LIVE_BACKENDS: readonly LiveBackend[] = [
"kilo",
"openrouter",
"nvidia",
"hetzner",
"zai",
];
/** Live catalog endpoints; only Hetzner's requires the backend key. */
export const DISCOVERY_URL: Record<Exclude<LiveBackend, "zai">, string> = {
kilo: `${KILO_BASE_URL}/models`,
openrouter: `${OPENROUTER_BASE_URL}/models`,
nvidia: `${NVIDIA_BASE_URL}/models`,
hetzner: `${HETZNER_BASE_URL}/models`,
};
export const DISCOVERY_NEEDS_KEY: Record<
Exclude<LiveBackend, "zai">,
boolean
> = {
kilo: false,
openrouter: false,
nvidia: false,
hetzner: true,
};
/**
* Gratis-maintained free set. Discovery keeps a live model only when its id
* matches one of these and its published pricing is zero.
*/
const VIRTUAL_MODELS: Partial<Record<Backend, readonly string[]>> = {
kilo: ["kilo-auto/free", "openrouter/free"],
openrouter: ["openrouter/free"],
};
/** Applied only when a catalog publishes no value. */
const FALLBACK_CONTEXT_WINDOW = 32_768;
const FALLBACK_MAX_TOKENS = 8_192;
/** `grts/auto` candidates: top-ranked models per backend with a usable context. */
export const AUTO_CANDIDATES_PER_BACKEND = 4;
export const AUTO_MIN_CONTEXT_WINDOW = 131_072;
const FREE_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
const CAVEAT = "free tier, prompts may be logged";
const OPENROUTER_COMPAT = {
supportsDeveloperRole: false,
thinkingFormat: "openrouter",
} as const;
/**
* Pi sends `reasoning: { effort: "none" }` for thinking off unless off is
* unsupported; routers and free pools may land on models that reject that
* ("Reasoning is mandatory"), so thinking off sends no reasoning setting.
*/
const NO_FORCED_OFF = { off: null } as const;
/** vLLM-style OpenAI servers (NVIDIA NIM, Hetzner): plain max_tokens, no store, system role only. */
const VLLM_COMPAT = {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
supportsStrictMode: false,
} as const;
/** Pi's own Z.ai GLM compatibility settings. */
const ZAI_COMPAT = {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
thinkingFormat: "zai",
supportsStrictMode: true,
zaiToolStream: true,
} as const;
const GOOGLE_THINKING = {
off: null,
minimal: "minimal",
low: "low",
medium: "medium",
high: "high",
xhigh: null,
max: null,
} as const;
/** Newer Gemini Flash models reject MINIMAL, the level Pi uses for thinking off. */
const GOOGLE_THINKING_NO_MINIMAL = {
...GOOGLE_THINKING,
minimal: null,
} as const;
const GROQ_GPT_OSS_THINKING = {
off: null,
minimal: null,
low: "low",
medium: "medium",
high: "high",
xhigh: null,
max: null,
} as const;
/** One curated or confirmed catalog entry before gratis naming and zero cost. */
export type ZaiEntry = Entry;
interface Entry {
id: string;
name: string;
api: Api;
baseUrl: string;
contextWindow: number;
maxTokens: number;
input: Input;
reasoning: boolean;
compat?: GratisModel["compat"];
thinkingLevelMap?: GratisModel["thinkingLevelMap"];
}
export function gratisModel(backend: Backend, entry: Entry): GratisModel {
const model: GratisModel = {
id: `${backend}/${entry.id}`,
name: `${BACKEND_INFO[backend].label} ${entry.name} (${CAVEAT})`,
api: entry.api,
provider: PROVIDER,
baseUrl: entry.baseUrl,
reasoning: entry.reasoning,
input: entry.input,
cost: { ...FREE_COST },
contextWindow: entry.contextWindow,
maxTokens: entry.maxTokens,
};
if (entry.compat) model.compat = entry.compat;
if (entry.thinkingLevelMap) model.thinkingLevelMap = entry.thinkingLevelMap;
return model;
}
export function backendOf(model: Pick<GratisModel, "id">): Backend | undefined {
const prefix = model.id.slice(0, model.id.indexOf("/"));
return (BACKENDS as readonly string[]).includes(prefix)
? (prefix as Backend)
: undefined;
}
export function upstreamId(model: Pick<GratisModel, "id">): string {
return model.id.slice(model.id.indexOf("/") + 1);
}
const google = (
id: string,
name: string,
thinkingLevelMap?: Entry["thinkingLevelMap"],
): Entry => ({
id,
name,
api: "google-generative-ai",
baseUrl: GOOGLE_BASE_URL,
contextWindow: 1_048_576,
maxTokens: 65_536,
input: ["text", "image"],
reasoning: true,
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
});
const groq = (
id: string,
name: string,
maxTokens: number,
thinkingLevelMap?: Entry["thinkingLevelMap"],
): Entry => ({
id,
name,
api: "openai-completions",
baseUrl: GROQ_BASE_URL,
contextWindow: 131_072,
maxTokens,
input: ["text"],
reasoning: thinkingLevelMap !== undefined,
compat: { supportsStrictMode: true },
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
});
const mistral = (
id: string,
name: string,
contextWindow: number,
maxTokens: number,
input: Input,
reasoning: boolean,
): Entry => ({
id,
name,
api: "mistral-conversations",
baseUrl: MISTRAL_BASE_URL,
contextWindow,
maxTokens,
input,
reasoning,
});
const openai = (
baseUrl: string,
compat: Entry["compat"],
id: string,
name: string,
contextWindow: number,
maxTokens: number,
input: Input,
): Entry => ({
id,
name,
api: "openai-completions",
baseUrl,
contextWindow,
maxTokens,
input,
reasoning: true,
compat,
});
const nvidia = openai.bind(undefined, NVIDIA_BASE_URL, VLLM_COMPAT);
const zai = openai.bind(undefined, ZAI_BASE_URL, ZAI_COMPAT);
const hetzner = openai.bind(undefined, HETZNER_BASE_URL, VLLM_COMPAT);
/** Curated static catalogs, ranked. Stale entries are fixed here, never by user config. */
const STATIC: Record<Exclude<Backend, "kilo">, readonly Entry[]> = {
// Only models Pi's NVIDIA catalog prices at 0; priced ones (Nemotron 3 Super/Ultra) stay out.
nvidia: [
nvidia("moonshotai/kimi-k3", "Kimi K3", 1_048_576, 131_072, [
"text",
"image",
]),
nvidia("z-ai/glm-5.3", "GLM 5.3", 1_000_000, 131_072, ["text"]),
nvidia("moonshotai/kimi-k2.6", "Kimi K2.6", 262_144, 262_144, [
"text",
"image",
]),
nvidia("z-ai/glm-5.3-flash", "GLM 5.3 Flash", 1_000_000, 131_072, [
"text",
"image",
]),
nvidia(
"nvidia/nemotron-3.5-lightning-30b-a3b",
"Nemotron 3.5 Lightning",
262_144,
262_144,
["text"],
),
nvidia("openai/gpt-oss-20b", "GPT OSS 20B", 131_072, 32_768, ["text"]),
],
// Exactly the models Z.ai prices Free; "flash" alone is not proof (glm-5.3-flash is paid).
zai: [
zai("glm-4.7-flash", "GLM 4.7 Flash", 204_800, 131_072, ["text"]),
zai("glm-4.5-flash", "GLM 4.5 Flash", 131_072, 98_304, ["text"]),
zai("glm-4.6v-flash", "GLM 4.6V Flash", 128_000, 32_768, ["text", "image"]),
],
// Fallback only: the keyed live models list replaces it when reachable.
hetzner: [
hetzner("Qwen/Qwen3.6-35B-A3B-FP8", "Qwen 3.6 35B", 262_144, 65_536, [
"text",
"image",
]),
hetzner("Qwen3.8-27B", "Qwen 3.8 27B", 262_144, 65_536, ["text", "image"]),
],
// Concrete ids only: `-latest` aliases move to models whose thinking levels differ.
google: [
google("gemini-3.8-flash", "Gemini 3.8 Flash", GOOGLE_THINKING_NO_MINIMAL),
google("gemini-3.7-flash", "Gemini 3.7 Flash", GOOGLE_THINKING_NO_MINIMAL),
google("gemini-3.5-flash", "Gemini 3.5 Flash", GOOGLE_THINKING),
google("gemini-3.5-flash-lite", "Gemini 3.5 Flash-Lite", GOOGLE_THINKING),
],
// Fallback only: live discovery replaces it when reachable.
openrouter: [
{
id: "openrouter/free",
name: "Free Models Router",
api: "openai-completions",
baseUrl: OPENROUTER_BASE_URL,
contextWindow: 200_000,
maxTokens: FALLBACK_MAX_TOKENS,
input: ["text", "image"],
reasoning: true,
compat: OPENROUTER_COMPAT,
thinkingLevelMap: NO_FORCED_OFF,
},
],
groq: [
groq("openai/gpt-oss-120b", "GPT OSS 120B", 65_536, GROQ_GPT_OSS_THINKING),
groq("llama-3.3-70b-versatile", "Llama 3.3 70B", 32_768),
groq("openai/gpt-oss-20b", "GPT OSS 20B", 65_536, GROQ_GPT_OSS_THINKING),
groq("llama-3.1-8b-instant", "Llama 3.1 8B", 131_072),
],
mistral: [
mistral("devstral-latest", "Devstral", 262_144, 262_144, ["text"], false),
mistral(
"mistral-medium-latest",
"Mistral Medium",
262_144,
262_144,
["text", "image"],
true,
),
mistral(
"mistral-small-latest",
"Mistral Small",
256_000,
256_000,
["text", "image"],
true,
),
mistral(
"mistral-large-latest",
"Mistral Large",
262_144,
262_144,
["text", "image"],
false,
),
mistral("codestral-latest", "Codestral", 256_000, 4_096, ["text"], false),
],
};
export function staticModels(backend: Backend): GratisModel[] {
if (backend === "kilo") return [];
return STATIC[backend].map((entry) => gratisModel(backend, entry));
}
const liveIds = (payload: unknown): string[] => {
const data = (payload as { data?: unknown } | undefined)?.data;
if (!Array.isArray(data)) throw new Error("catalog payload has no data");
return data.flatMap((raw) =>
typeof raw?.id === "string" ? [raw.id as string] : [],
);
};
/** NVIDIA publishes bare ids: keep curated free models that are live. */
export function parseNvidiaCatalog(payload: unknown): GratisModel[] {
const live = new Set(liveIds(payload));
return staticModels("nvidia").filter((model) => live.has(upstreamId(model)));
}
/**
* Hetzner's list is definitive and every model is free while experimental.
* Known ids keep curated metadata; new ids use the published context length.
*/
export function parseHetznerCatalog(payload: unknown): GratisModel[] {
const data = (payload as { data?: unknown } | undefined)?.data;
if (!Array.isArray(data)) throw new Error("catalog payload has no data");
const curated = new Map(
staticModels("hetzner").map((model) => [upstreamId(model), model]),
);
return data.flatMap((raw: { id?: unknown; max_model_len?: unknown }) => {
if (typeof raw?.id !== "string") return [];
const known = curated.get(raw.id);
const contextWindow =
positive(raw.max_model_len) ??
known?.contextWindow ??
FALLBACK_CONTEXT_WINDOW;
if (known) return [{ ...known, contextWindow }];
return [
gratisModel(
"hetzner",
hetzner(
raw.id,
raw.id,
contextWindow,
Math.min(FALLBACK_MAX_TOKENS, contextWindow),
["text"],
),
),
];
});
}
export function parseCatalog(
backend: Exclude<LiveBackend, "zai">,
payload: unknown,
): GratisModel[] {
if (backend === "nvidia") return parseNvidiaCatalog(payload);
if (backend === "hetzner") return parseHetznerCatalog(payload);
return parseLiveCatalog(backend, payload);
}
/** Model ids the NVIDIA public list currently serves, for intersecting Pi's catalog. */
export function liveModelIds(payload: unknown): Set<string> {
return new Set(liveIds(payload));
}
/** Z.ai's first-party pricing page, served as markdown. */
export const ZAI_PRICING_URL = "https://docs.z.ai/guides/overview/pricing.md";
/** models.dev confirms cost and limits for Z.ai ids gratis does not know. */
export const MODELS_DEV_URL = "https://models.dev/api.json";
export interface ZaiPricing {
/** Lowercased model ids whose input and output are both listed as Free. */
free: string[];
/** Free ids listed under Vision Models. */
vision: Set<string>;
}
/**
* Parse the pricing page's model tables. Returns undefined when no model table
* is found (format changed), so callers keep their last confirmed list.
*/
export function parseZaiPricing(markdown: string): ZaiPricing | undefined {
let section = "";
let tables = 0;
const free: string[] = [];
const vision = new Set<string>();
for (const line of markdown.split("\n")) {
if (line.startsWith("#")) section = line;
const cells = line.split("|").map((cell) => cell.trim());
// | Model | Input | Cached Input | Cached Input Storage | Output |
if (cells.length !== 7) continue;
const [, name = "", input, , , output] = cells;
if (name === "Model" && input === "Input" && output === "Output") {
tables++;
continue;
}
const id = name.toLowerCase();
if (input !== "Free" || output !== "Free" || !/^glm-[a-z0-9.-]+$/.test(id))
continue;
free.push(id);
if (/vision/i.test(section)) vision.add(id);
}
return tables > 0 ? { free, vision } : undefined;
}
/** A models.dev `zai` entry confirming cost 0 on the general endpoint. */
export function modelsDevZaiEntry(
payload: unknown,
id: string,
): Entry | undefined {
const provider = (payload as { zai?: { api?: unknown; models?: unknown } })
?.zai;
if (provider?.api !== ZAI_BASE_URL) return undefined;
const raw = (provider.models as Record<string, LiveModelsDev> | undefined)?.[
id
];
if (raw?.cost?.input !== 0 || raw.cost?.output !== 0) return undefined;
const inputs = strings(raw.modalities?.input);
return zai(
id,
typeof raw.name === "string" ? raw.name : id,
positive(raw.limit?.context) ?? FALLBACK_CONTEXT_WINDOW,
positive(raw.limit?.output) ?? FALLBACK_MAX_TOKENS,
inputs.includes("image") ? ["text", "image"] : ["text"],
);
}
interface LiveModelsDev {
name?: unknown;
cost?: { input?: unknown; output?: unknown };
limit?: { context?: unknown; output?: unknown };
modalities?: { input?: unknown };
}
/**
* Z.ai models the pricing page lists as Free: known ids keep curated
* metadata; unknown ids need a models.dev confirmation (`confirmed`).
*/
export function zaiFreeModels(
pricing: ZaiPricing,
confirmed: ReadonlyMap<string, Entry>,
): GratisModel[] {
const known = new Map(STATIC.zai.map((entry) => [entry.id, entry]));
return pricing.free.flatMap((id) => {
const entry = known.get(id) ?? confirmed.get(id);
return entry ? [gratisModel("zai", entry)] : [];
});
}
/** Z.ai free ids that need models.dev before they can be listed. */
export function unknownZaiIds(pricing: ZaiPricing): string[] {
const known = new Set(STATIC.zai.map((entry) => entry.id));
return pricing.free.filter((id) => !known.has(id));
}
/** Backends whose models come from Pi's runtime-refreshed built-in catalog. */
export type PiCatalogBackend = "google" | "groq" | "mistral" | "nvidia";
export const PI_CATALOG_BACKENDS: readonly PiCatalogBackend[] = [
"google",
"groq",
"mistral",
"nvidia",
];
const GEMINI_FLASH = /^gemini-(\d+(?:\.\d+)?)-flash(-lite)?$/;
const NOT_CHAT = /safeguard|guard|whisper|tts|embed|ocr|voxtral|moderation/i;
const version = (id: string) =>
(/(\d+(?:\.\d+)?)/.exec(id)?.[1] ?? "0")
.split(".")
.map(Number)
.reduce((sum, part, index) => sum + part / 1000 ** index, 0);
/** Pi's free rule per backend; `live` narrows NVIDIA to ids its public list serves. */
function piFree(
backend: PiCatalogBackend,
model: Model<Api>,
live: ReadonlySet<string> | undefined,
): boolean {
if (NOT_CHAT.test(model.id)) return false;
if (backend === "google") return GEMINI_FLASH.test(model.id);
if (backend === "mistral") return model.id.endsWith("-latest");
if (backend === "nvidia")
return (
model.cost.input === 0 &&
model.cost.output === 0 &&
(live === undefined || live.has(model.id))
);
return true;
}
/**
* Gratis models from Pi's built-in catalog for one backend, ranked: gratis's
* known preferences first, then newer versions, then larger contexts.
* Empty when Pi's catalog has no match, so callers fall back to the static list.
*/
export function fromPiCatalog(
backend: PiCatalogBackend,
catalog: readonly Model<Api>[],
live?: ReadonlySet<string>,
): GratisModel[] {
// Gemini Flash versions are ordered, so newest wins; elsewhere ids carry no
// comparable version and gratis's known preferences lead.
const preference =
backend === "google" ? [] : STATIC[backend].map((entry) => entry.id);
const rank = (id: string) => {
const index = preference.indexOf(id);
return index === -1 ? preference.length : index;
};
const flashLite = (id: string) => Number(id.endsWith("-lite"));
return catalog
.filter((model) => piFree(backend, model, live))
.sort(
(a, b) =>
rank(a.id) - rank(b.id) ||
version(b.id) - version(a.id) ||
flashLite(a.id) - flashLite(b.id) ||
b.contextWindow - a.contextWindow,
)
.map((model) =>
gratisModel(backend, {
id: model.id,
name: model.name,
api: model.api,
baseUrl: model.baseUrl,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
input: model.input,
reasoning: model.reasoning,
...(model.compat ? { compat: model.compat } : {}),
...(model.thinkingLevelMap
? { thinkingLevelMap: model.thinkingLevelMap }
: {}),
}),
);
}
interface LiveModel {
id?: unknown;
name?: unknown;
context_length?: unknown;
pricing?: { prompt?: unknown; completion?: unknown };
isFree?: unknown;
architecture?: { input_modalities?: unknown; output_modalities?: unknown };
top_provider?: {
context_length?: unknown;
max_completion_tokens?: unknown;
};
supported_parameters?: unknown;
}
const positive = (value: unknown): number | undefined =>
typeof value === "number" && Number.isFinite(value) && value > 0
? Math.floor(value)
: undefined;
const zeroPrice = (value: unknown): boolean =>
(typeof value === "string" || typeof value === "number") &&
Number(value) === 0;
const strings = (value: unknown): string[] =>
Array.isArray(value)
? value.filter((item): item is string => typeof item === "string")
: [];
/**
* Parse an OpenRouter-shaped `/models` payload into ranked free models:
* virtual routers first, then upstream order. Anything unexpected is skipped,
* so arbitrary churn and empty pools yield fewer models, never errors.
*/
export function parseLiveCatalog(
backend: "kilo" | "openrouter",
payload: unknown,
): GratisModel[] {
const data = (payload as { data?: unknown } | undefined)?.data;
if (!Array.isArray(data)) throw new Error("catalog payload has no data");
const virtual = VIRTUAL_MODELS[backend] ?? [];
const models: GratisModel[] = [];
for (const raw of data as LiveModel[]) {
if (typeof raw?.id !== "string") continue;
const id = raw.id;
const isVirtualModel = virtual.includes(id);
if (!isVirtualModel && !id.endsWith(":free")) continue;
if (raw.isFree === false) continue;
if (!zeroPrice(raw.pricing?.prompt) || !zeroPrice(raw.pricing?.completion))
continue;
const params = strings(raw.supported_parameters);
if (!params.includes("tools")) continue;
const outputs = strings(raw.architecture?.output_modalities);
if (outputs.length > 0 && !outputs.includes("text")) continue;
const inputs = strings(raw.architecture?.input_modalities);
const input: Input = inputs.includes("image")
? ["text", "image"]
: ["text"];
const anthropic = backend === "openrouter" && id.startsWith("anthropic/");
models.push(
gratisModel(backend, {
id,
name: typeof raw.name === "string" ? raw.name : id,
api: anthropic ? "anthropic-messages" : "openai-completions",
baseUrl:
backend === "kilo"
? KILO_BASE_URL
: anthropic
? OPENROUTER_ANTHROPIC_BASE_URL
: OPENROUTER_BASE_URL,
contextWindow:
positive(raw.top_provider?.context_length) ??
positive(raw.context_length) ??
FALLBACK_CONTEXT_WINDOW,
maxTokens:
positive(raw.top_provider?.max_completion_tokens) ??
FALLBACK_MAX_TOKENS,
input,
reasoning: params.includes("reasoning"),
...(anthropic
? {}
: {
compat: OPENROUTER_COMPAT,
...(params.includes("reasoning")
? { thinkingLevelMap: NO_FORCED_OFF }
: {}),
}),
}),
);
}
const rank = (model: GratisModel) => {
const index = virtual.indexOf(upstreamId(model));
return index === -1 ? virtual.length : index;
};
// Array sort is stable, so equal ranks keep upstream order.
return models.sort((a, b) => rank(a) - rank(b));
}