import { type Context, complete, getEnvApiKey, getModel, type Model, type OpenAICompletionsOptions, type Tool, } from "@mariozechner/pi-ai"; import { Type } from "@sinclair/typebox"; const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models"; const DEFAULT_TIMEOUT_MS = 12_000; const DEFAULT_CONCURRENCY = 3; const BASE_IMAGE_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X3mIAAAAASUVORK5CYII="; const TOOL_PING: Tool = { name: "ping", description: "Return OK.", parameters: Type.Object({}), }; type OpenRouterModelMeta = { id: string; name: string; contextLength: number | null; maxCompletionTokens: number | null; supportedParametersCount: number; modality: string | null; inferredParamB: number | null; createdAtMs: number | null; }; export type ProbeResult = { ok: boolean; latencyMs: number | null; error?: string; skipped?: boolean; }; export type ModelScanResult = { id: string; name: string; provider: string; modelRef: string; contextLength: number | null; maxCompletionTokens: number | null; supportedParametersCount: number; modality: string | null; inferredParamB: number | null; createdAtMs: number | null; tool: ProbeResult; image: ProbeResult; }; export type OpenRouterScanOptions = { apiKey?: string; fetchImpl?: typeof fetch; timeoutMs?: number; concurrency?: number; minParamB?: number; maxAgeDays?: number; providerFilter?: string; }; type OpenAIModel = Model<"openai-completions">; function normalizeCreatedAtMs(value: unknown): number | null { if (typeof value !== "number" || !Number.isFinite(value)) return null; if (value <= 0) return null; if (value > 1e12) return Math.round(value); return Math.round(value * 1000); } function inferParamBFromIdOrName(text: string): number | null { const raw = text.toLowerCase(); const matches = raw.matchAll( /(?:^|[^a-z0-9])[a-z]?(\d+(?:\.\d+)?)b(?:[^a-z0-9]|$)/g, ); let best: number | null = null; for (const match of matches) { const numRaw = match[1]; if (!numRaw) continue; const value = Number(numRaw); if (!Number.isFinite(value) || value <= 0) continue; if (best === null || value > best) best = value; } return best; } function parseModality(modality: string | null): Array<"text" | "image"> { if (!modality) return ["text"]; const normalized = modality.toLowerCase(); const parts = normalized.split(/[^a-z]+/).filter(Boolean); const hasImage = parts.includes("image"); return hasImage ? ["text", "image"] : ["text"]; } async function withTimeout( timeoutMs: number, fn: (signal: AbortSignal) => Promise, ): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return await fn(controller.signal); } finally { clearTimeout(timer); } } async function fetchOpenRouterModels( fetchImpl: typeof fetch, ): Promise { const res = await fetchImpl(OPENROUTER_MODELS_URL, { headers: { Accept: "application/json" }, }); if (!res.ok) { throw new Error(`OpenRouter /models failed: HTTP ${res.status}`); } const payload = (await res.json()) as { data?: unknown }; const entries = Array.isArray(payload.data) ? payload.data : []; return entries .map((entry) => { if (!entry || typeof entry !== "object") return null; const obj = entry as Record; const id = typeof obj.id === "string" ? obj.id.trim() : ""; if (!id) return null; const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id; const contextLength = typeof obj.context_length === "number" && Number.isFinite(obj.context_length) ? obj.context_length : null; const maxCompletionTokens = typeof obj.max_completion_tokens === "number" && Number.isFinite(obj.max_completion_tokens) ? obj.max_completion_tokens : typeof obj.max_output_tokens === "number" && Number.isFinite(obj.max_output_tokens) ? obj.max_output_tokens : null; const supportedParametersCount = Array.isArray(obj.supported_parameters) ? obj.supported_parameters.length : 0; const modality = typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null; const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`); const createdAtMs = normalizeCreatedAtMs(obj.created_at); return { id, name, contextLength, maxCompletionTokens, supportedParametersCount, modality, inferredParamB, createdAtMs, } satisfies OpenRouterModelMeta; }) .filter((entry): entry is OpenRouterModelMeta => Boolean(entry)); } async function probeTool( model: OpenAIModel, apiKey: string, timeoutMs: number, ): Promise { const context: Context = { messages: [ { role: "user", content: "Call the ping tool with {} and nothing else.", timestamp: Date.now(), }, ], tools: [TOOL_PING], }; const startedAt = Date.now(); try { const message = await withTimeout(timeoutMs, (signal) => complete(model, context, { apiKey, maxTokens: 32, temperature: 0, toolChoice: "required", signal, } satisfies OpenAICompletionsOptions), ); const hasToolCall = message.content.some( (block) => block.type === "toolCall", ); if (!hasToolCall) { return { ok: false, latencyMs: Date.now() - startedAt, error: "No tool call returned", }; } return { ok: true, latencyMs: Date.now() - startedAt }; } catch (err) { return { ok: false, latencyMs: Date.now() - startedAt, error: err instanceof Error ? err.message : String(err), }; } } async function probeImage( model: OpenAIModel, apiKey: string, timeoutMs: number, ): Promise { const context: Context = { messages: [ { role: "user", content: [ { type: "text", text: "Reply with OK." }, { type: "image", data: BASE_IMAGE_PNG, mimeType: "image/png" }, ], timestamp: Date.now(), }, ], }; const startedAt = Date.now(); try { await withTimeout(timeoutMs, (signal) => complete(model, context, { apiKey, maxTokens: 16, temperature: 0, signal, } satisfies OpenAICompletionsOptions), ); return { ok: true, latencyMs: Date.now() - startedAt }; } catch (err) { return { ok: false, latencyMs: Date.now() - startedAt, error: err instanceof Error ? err.message : String(err), }; } } function ensureImageInput(model: OpenAIModel): OpenAIModel { if (model.input.includes("image")) return model; return { ...model, input: Array.from(new Set([...model.input, "image"])), }; } async function mapWithConcurrency( items: T[], concurrency: number, fn: (item: T, index: number) => Promise, ): Promise { const limit = Math.max(1, Math.floor(concurrency)); const results = Array.from({ length: items.length }) as R[]; let nextIndex = 0; const worker = async () => { while (true) { const current = nextIndex; nextIndex += 1; if (current >= items.length) return; results[current] = await fn(items[current] as T, current); } }; await Promise.all( Array.from({ length: Math.min(limit, items.length) }, () => worker()), ); return results; } export async function scanOpenRouterModels( options: OpenRouterScanOptions = {}, ): Promise { const fetchImpl = options.fetchImpl ?? fetch; const apiKey = options.apiKey?.trim() || getEnvApiKey("openrouter") || ""; if (!apiKey) { throw new Error( "Missing OpenRouter API key. Set OPENROUTER_API_KEY to run models scan.", ); } const timeoutMs = Math.max( 1, Math.floor(options.timeoutMs ?? DEFAULT_TIMEOUT_MS), ); const concurrency = Math.max( 1, Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY), ); const minParamB = Math.max(0, Math.floor(options.minParamB ?? 0)); const maxAgeDays = Math.max(0, Math.floor(options.maxAgeDays ?? 0)); const providerFilter = options.providerFilter?.trim().toLowerCase() ?? ""; const catalog = await fetchOpenRouterModels(fetchImpl); const now = Date.now(); const filtered = catalog.filter((entry) => { if (!entry.id.endsWith(":free")) return false; if (providerFilter) { const prefix = entry.id.split("/")[0]?.toLowerCase() ?? ""; if (prefix !== providerFilter) return false; } if (minParamB > 0) { const params = entry.inferredParamB ?? 0; if (params < minParamB) return false; } if (maxAgeDays > 0 && entry.createdAtMs) { const ageMs = now - entry.createdAtMs; const ageDays = ageMs / (24 * 60 * 60 * 1000); if (ageDays > maxAgeDays) return false; } return true; }); const baseModel = getModel("openrouter", "openrouter/auto") as OpenAIModel; return mapWithConcurrency(filtered, concurrency, async (entry) => { const model: OpenAIModel = { ...baseModel, id: entry.id, name: entry.name || entry.id, contextWindow: entry.contextLength ?? baseModel.contextWindow, maxTokens: entry.maxCompletionTokens ?? baseModel.maxTokens, input: parseModality(entry.modality), reasoning: baseModel.reasoning, }; const toolResult = await probeTool(model, apiKey, timeoutMs); const imageResult = model.input.includes("image") ? await probeImage(ensureImageInput(model), apiKey, timeoutMs) : { ok: false, latencyMs: null, skipped: true }; return { id: entry.id, name: entry.name, provider: "openrouter", modelRef: `openrouter/${entry.id}`, contextLength: entry.contextLength, maxCompletionTokens: entry.maxCompletionTokens, supportedParametersCount: entry.supportedParametersCount, modality: entry.modality, inferredParamB: entry.inferredParamB, createdAtMs: entry.createdAtMs, tool: toolResult, image: imageResult, } satisfies ModelScanResult; }); } export { OPENROUTER_MODELS_URL }; export type { OpenRouterModelMeta };