import type { Api } from "@earendil-works/pi-ai"; export const PROVIDER_ID = "kimchi-dev"; export const DEFAULT_ENDPOINT = "https://llm.kimchi.dev"; export type KimchiModel = { id: string; name: string; reasoning: boolean; input: ("text" | "image")[]; contextWindow: number; maxTokens: number; provider?: string; }; /** Fallback when metadata API is unreachable (matches typical kimchi harness). */ export const FALLBACK_MODELS: KimchiModel[] = [ { id: "glm-5.2-fp8", name: "glm-5.2-fp8", reasoning: true, input: ["text"], contextWindow: 1048576, maxTokens: 1048576, }, { id: "kimi-k2.7", name: "kimi-k2.7", reasoning: true, input: ["text", "image"], contextWindow: 262144, maxTokens: 262144, }, { id: "nemotron-3-ultra-fp4", name: "nemotron-3-ultra-fp4", reasoning: true, input: ["text"], contextWindow: 1048576, maxTokens: 1048576, }, { id: "deepseek-v4-flash", name: "deepseek-v4-flash", reasoning: true, input: ["text"], contextWindow: 1048576, maxTokens: 1048576, }, { id: "minimax-m3", name: "minimax-m3", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 1048576, }, ]; interface ModelMetadata { slug: string; display_name: string; provider: string; reasoning: boolean; input_modalities: ("text" | "image")[]; status?: string; limits: { context_window: number; max_output_tokens: number }; } function normalizeEndpoint(endpoint?: string): string { const t = endpoint?.trim(); return (t && t.length > 0 ? t : DEFAULT_ENDPOINT).replace(/\/+$/, ""); } export function chatCompletionsBaseUrl(endpoint?: string): string { return `${normalizeEndpoint(endpoint)}/openai/v1`; } export async function fetchKimchiModels( apiKey: string, endpoint?: string, signal?: AbortSignal, ): Promise { const base = normalizeEndpoint(endpoint); const url = `${base}/v1/models/metadata?include_in_cli=true`; const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` }, signal, }); if (!res.ok) { throw new Error(`Kimchi models metadata failed (${res.status})`); } const body = (await res.json()) as { models?: ModelMetadata[] }; const list = body.models ?? []; const active = list.filter( (m) => m.status !== "sunset" && m.limits.max_output_tokens > 0, ); if (active.length === 0) return FALLBACK_MODELS; return active.map((m) => ({ id: m.slug, name: m.display_name?.trim() || m.slug, reasoning: m.reasoning, input: m.input_modalities, contextWindow: m.limits.context_window, maxTokens: m.limits.max_output_tokens, provider: m.provider, })); } export function toPiModels(models: KimchiModel[], api: Api = "openai-completions") { const zero = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; return models.map((m) => ({ id: m.id, name: m.name, api, reasoning: m.reasoning, input: m.input, contextWindow: m.contextWindow, maxTokens: m.maxTokens, cost: zero, })); }