/** * Model discovery + Pi-tuned metadata mapping. */ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent"; import { OPENFERENCE_BASE_URL, OPENFERENCE_PROVIDER, OPENFERENCE_USER_AGENT, } from "./constants.ts"; export { OPENFERENCE_PROVIDER, OPENFERENCE_BASE_URL }; /** Raw shape of one entry in the /v1/models response. */ export interface OpenferenceModelInfo { id: string; context_length?: number; max_output_tokens?: number | null; pricing?: { prompt?: string; completion?: string; } | null; reasoning?: { supported_efforts?: string[] } | null; architecture?: { modality?: string; input_modalities?: string[]; output_modalities?: string[]; } | null; } const PI_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const; /** * Safe input budget for Pi compaction: total context minus max output, with * a ~2% buffer (matches openference.com/docs/pi table). */ export function safeContextWindow( contextLength: number | undefined, maxOutput: number | undefined | null, ): number { const ctx = contextLength && contextLength > 0 ? contextLength : 1_000_000; const out = maxOutput && maxOutput > 0 ? maxOutput : 16_384; const budget = Math.floor((ctx - out) * 0.98); return Math.max(budget, 8_192); } /** Convert OpenRouter-style per-token USD string to $/1M for Pi. */ export function perTokenToPerMillion(perToken: string | undefined): number { if (!perToken) return 0; const n = Number(perToken); if (!Number.isFinite(n) || n < 0) return 0; return Number((n * 1_000_000).toFixed(6)); } /** * Map Openference supported_efforts onto Pi thinking levels. * Unsupported levels are null (hidden in the UI). */ export function buildThinkingLevelMap( efforts: string[] | undefined, ): ProviderModelConfig["thinkingLevelMap"] | undefined { if (!efforts || efforts.length === 0) return undefined; const set = new Set(efforts.map((e) => e.toLowerCase())); const map: NonNullable = { minimal: null, low: null, medium: null, high: null, xhigh: null, max: null, }; for (const level of PI_LEVELS) { if (set.has(level)) { map[level] = level; } } // If only a subset exists, alias closest available for common levels. const available = PI_LEVELS.filter((l) => set.has(l)); if (available.length === 0) return undefined; if (map.high === null && available.includes("medium")) map.high = "medium"; if (map.medium === null && available.includes("high")) map.medium = "high"; if (map.low === null && available.includes("medium")) map.low = "medium"; if (map.low === null && available.includes("high")) map.low = "high"; if (map.max === null && available.includes("high")) map.max = "high"; if (map.xhigh === null && available.includes("high")) map.xhigh = "high"; if (map.xhigh === null && available.includes("max")) map.xhigh = "max"; return map; } function isImageGenerationOnly(info: OpenferenceModelInfo): boolean { const outs = info.architecture?.output_modalities ?? []; const modality = info.architecture?.modality ?? ""; return modality === "text->image" || (outs.includes("image") && !outs.includes("text")); } function inputModalities(info: OpenferenceModelInfo): ("text" | "image")[] { const inputs = info.architecture?.input_modalities; if (inputs?.includes("image")) return ["text", "image"]; return ["text"]; } /** Build a Pi ProviderModelConfig from a live /v1/models entry. */ export function toModelConfig(info: OpenferenceModelInfo): ProviderModelConfig { const id = info.id; const maxTokens = info.max_output_tokens && info.max_output_tokens > 0 ? info.max_output_tokens : 16_384; const efforts = info.reasoning?.supported_efforts ?? []; const thinkingLevelMap = buildThinkingLevelMap(efforts); const reasoning = efforts.length > 0; return { id, name: id, reasoning, input: inputModalities(info), cost: { input: perTokenToPerMillion(info.pricing?.prompt), output: perTokenToPerMillion(info.pricing?.completion), cacheRead: 0, cacheWrite: 0, }, contextWindow: safeContextWindow(info.context_length, info.max_output_tokens), maxTokens, ...(thinkingLevelMap ? { thinkingLevelMap } : {}), compat: { maxTokensField: "max_tokens" as const, } as ProviderModelConfig["compat"], }; } /** Fallback when /v1/models is unreachable before login. */ export const FALLBACK_MODELS: OpenferenceModelInfo[] = [ { id: "GLM-5.2", context_length: 1_000_000, max_output_tokens: 128_000, reasoning: { supported_efforts: ["high", "medium", "low"] }, }, { id: "DeepSeek-V4-Pro", context_length: 1_000_000, max_output_tokens: 131_072, reasoning: { supported_efforts: ["max", "high", "medium", "low"] }, }, { id: "Qwen3.7 Plus", context_length: 1_000_000, max_output_tokens: 65_536, reasoning: { supported_efforts: ["max", "high", "medium", "low"] }, }, ]; /** Fetch live models from GET /v1/models. Throws on failure. */ export async function fetchModels( apiKey: string | undefined, signal?: AbortSignal, ): Promise { const headers: Record = { "User-Agent": OPENFERENCE_USER_AGENT }; if (apiKey) headers.Authorization = `Bearer ${apiKey}`; const res = await fetch(`${OPENFERENCE_BASE_URL}/models`, { headers, signal }); if (!res.ok) { throw new Error(`GET /v1/models -> HTTP ${res.status}`); } const json = (await res.json()) as { data?: OpenferenceModelInfo[] }; return (json.data ?? []) .filter((m) => typeof m.id === "string" && m.id.length > 0) .filter((m) => !isImageGenerationOnly(m)); }