import type { Api, Model } from "@mariozechner/pi-ai"; import type { ModelRegistry } from "@mariozechner/pi-coding-agent"; import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js"; import { normalizeModelCompat } from "./model-compat.js"; import { normalizeProviderId } from "./model-selection.js"; const OPENAI_CODEX_GPT_53_MODEL_ID = "gpt-5.3-codex"; const OPENAI_CODEX_TEMPLATE_MODEL_IDS = ["gpt-5.2-codex"] as const; const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6"; const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6"; const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] as const; const ANTHROPIC_SONNET_46_MODEL_ID = "claude-sonnet-4-6"; const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6"; const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"] as const; const ZAI_GLM5_MODEL_ID = "glm-5"; const ZAI_GLM5_TEMPLATE_MODEL_IDS = ["glm-4.7"] as const; // gemini-3.1-pro-preview / gemini-3.1-flash-preview are not yet in pi-ai's built-in // google-gemini-cli catalog. Clone the gemini-3-pro/flash-preview template so users // don't get "Unknown model" errors when Google releases a new minor version. const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro"; const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash"; const GEMINI_3_1_PRO_TEMPLATE_IDS = ["gemini-3-pro-preview"] as const; const GEMINI_3_1_FLASH_TEMPLATE_IDS = ["gemini-3-flash-preview"] as const; function cloneFirstTemplateModel(params: { normalizedProvider: string; trimmedModelId: string; templateIds: string[]; modelRegistry: ModelRegistry; patch?: Partial>; }): Model | undefined { const { normalizedProvider, trimmedModelId, templateIds, modelRegistry } = params; for (const templateId of [...new Set(templateIds)].filter(Boolean)) { const template = modelRegistry.find(normalizedProvider, templateId) as Model | null; if (!template) { continue; } return normalizeModelCompat({ ...template, id: trimmedModelId, name: trimmedModelId, ...params.patch, } as Model); } return undefined; } const CODEX_GPT53_ELIGIBLE_PROVIDERS = new Set(["openai-codex", "github-copilot"]); function resolveOpenAICodexGpt53FallbackModel( provider: string, modelId: string, modelRegistry: ModelRegistry, ): Model | undefined { const normalizedProvider = normalizeProviderId(provider); const trimmedModelId = modelId.trim(); if (!CODEX_GPT53_ELIGIBLE_PROVIDERS.has(normalizedProvider)) { return undefined; } if (trimmedModelId.toLowerCase() !== OPENAI_CODEX_GPT_53_MODEL_ID) { return undefined; } for (const templateId of OPENAI_CODEX_TEMPLATE_MODEL_IDS) { const template = modelRegistry.find(normalizedProvider, templateId) as Model | null; if (!template) { continue; } return normalizeModelCompat({ ...template, id: trimmedModelId, name: trimmedModelId, } as Model); } return normalizeModelCompat({ id: trimmedModelId, name: trimmedModelId, api: "openai-codex-responses", provider: normalizedProvider, baseUrl: "https://chatgpt.com/backend-api", reasoning: true, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: DEFAULT_CONTEXT_TOKENS, maxTokens: DEFAULT_CONTEXT_TOKENS, } as Model); } function resolveAnthropic46ForwardCompatModel(params: { provider: string; modelId: string; modelRegistry: ModelRegistry; dashModelId: string; dotModelId: string; dashTemplateId: string; dotTemplateId: string; fallbackTemplateIds: readonly string[]; }): Model | undefined { const { provider, modelId, modelRegistry, dashModelId, dotModelId } = params; const normalizedProvider = normalizeProviderId(provider); if (normalizedProvider !== "anthropic") { return undefined; } const trimmedModelId = modelId.trim(); const lower = trimmedModelId.toLowerCase(); const is46Model = lower === dashModelId || lower === dotModelId || lower.startsWith(`${dashModelId}-`) || lower.startsWith(`${dotModelId}-`); if (!is46Model) { return undefined; } const templateIds: string[] = []; if (lower.startsWith(dashModelId)) { templateIds.push(lower.replace(dashModelId, params.dashTemplateId)); } if (lower.startsWith(dotModelId)) { templateIds.push(lower.replace(dotModelId, params.dotTemplateId)); } templateIds.push(...params.fallbackTemplateIds); return cloneFirstTemplateModel({ normalizedProvider, trimmedModelId, templateIds, modelRegistry, }); } function resolveAnthropicOpus46ForwardCompatModel( provider: string, modelId: string, modelRegistry: ModelRegistry, ): Model | undefined { return resolveAnthropic46ForwardCompatModel({ provider, modelId, modelRegistry, dashModelId: ANTHROPIC_OPUS_46_MODEL_ID, dotModelId: ANTHROPIC_OPUS_46_DOT_MODEL_ID, dashTemplateId: "claude-opus-4-5", dotTemplateId: "claude-opus-4.5", fallbackTemplateIds: ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS, }); } function resolveAnthropicSonnet46ForwardCompatModel( provider: string, modelId: string, modelRegistry: ModelRegistry, ): Model | undefined { return resolveAnthropic46ForwardCompatModel({ provider, modelId, modelRegistry, dashModelId: ANTHROPIC_SONNET_46_MODEL_ID, dotModelId: ANTHROPIC_SONNET_46_DOT_MODEL_ID, dashTemplateId: "claude-sonnet-4-5", dotTemplateId: "claude-sonnet-4.5", fallbackTemplateIds: ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS, }); } // gemini-3.1-pro-preview / gemini-3.1-flash-preview are not present in pi-ai's built-in // google-gemini-cli catalog yet. Clone the nearest gemini-3 template so users don't get // "Unknown model" errors when Google Gemini CLI gains new minor-version models. function resolveGoogleGeminiCli31ForwardCompatModel( provider: string, modelId: string, modelRegistry: ModelRegistry, ): Model | undefined { if (normalizeProviderId(provider) !== "google-gemini-cli") { return undefined; } const trimmed = modelId.trim(); const lower = trimmed.toLowerCase(); let templateIds: readonly string[]; if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) { templateIds = GEMINI_3_1_PRO_TEMPLATE_IDS; } else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) { templateIds = GEMINI_3_1_FLASH_TEMPLATE_IDS; } else { return undefined; } return cloneFirstTemplateModel({ normalizedProvider: "google-gemini-cli", trimmedModelId: trimmed, templateIds: [...templateIds], modelRegistry, patch: { reasoning: true }, }); } // Z.ai's GLM-5 may not be present in pi-ai's built-in model catalog yet. // When a user configures zai/glm-5 without a models.json entry, clone glm-4.7 as a forward-compat fallback. function resolveZaiGlm5ForwardCompatModel( provider: string, modelId: string, modelRegistry: ModelRegistry, ): Model | undefined { if (normalizeProviderId(provider) !== "zai") { return undefined; } const trimmed = modelId.trim(); const lower = trimmed.toLowerCase(); if (lower !== ZAI_GLM5_MODEL_ID && !lower.startsWith(`${ZAI_GLM5_MODEL_ID}-`)) { return undefined; } for (const templateId of ZAI_GLM5_TEMPLATE_MODEL_IDS) { const template = modelRegistry.find("zai", templateId) as Model | null; if (!template) { continue; } return normalizeModelCompat({ ...template, id: trimmed, name: trimmed, reasoning: true, } as Model); } return normalizeModelCompat({ id: trimmed, name: trimmed, api: "openai-completions", provider: "zai", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: DEFAULT_CONTEXT_TOKENS, maxTokens: DEFAULT_CONTEXT_TOKENS, } as Model); } export function resolveForwardCompatModel( provider: string, modelId: string, modelRegistry: ModelRegistry, ): Model | undefined { return ( resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) ?? resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveAnthropicSonnet46ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveGoogleGeminiCli31ForwardCompatModel(provider, modelId, modelRegistry) ); }