/** * OpenCode model catalog + data-driven protocol routing table (CORE FIX). * * The built-in pi-ai `opencode` / `opencode-go` providers route several models * to the WRONG wire protocol (Go Qwen → /chat/completions instead of /messages, * Go MiniMax M2.7 → oa-compat instead of Claude, …). This catalog pins the * CORRECT protocol per model, derived from the verified OmniRoute registries: * * - source/OmniRoute/.../registry/opencode/{zen,go}/index.ts (targetFormat) * - source/OmniRoute/.../executors/opencode.ts (buildUrl) * - @earendil-works/pi-ai dist/models.generated.js (pricing/ctx) * * CRITICAL GOTCHAS encoded here: * - Zen minimax-m3 → openai-completions (/chat/completions) * - Go minimax-m3 → anthropic-messages (/messages, targetFormat:'claude') * - Go qwen3.* → anthropic-messages (upstream rejects oa-compat) * - big-pickle → openai-completions + requiresReasoningContentOnAssistantMessages */ import { GO_PRICING, ZEN_PRICING } from "../../pricing.ts" import { GO_ANTHROPIC_BASE, GO_BASE, ZEN_ANTHROPIC_BASE, ZEN_BASE } from "../../config.ts" import type { ModelCost, OpenCodeModel, ProtocolType, ProviderModelConfig, } from "../../types.ts" // =========================================================================== // Effort-tier alias parsing (ported from OmniRoute OpencodeExecutor) // =========================================================================== const EFFORT_LEVELS = ["low", "medium", "high", "max"] as const /** * Base model ids that support effort-tier aliases on opencode-go, mapped to the * suffixes the upstream accepts. Used to split a suffix off a model id (e.g. * "deepseek-v4-pro-low" → base "deepseek-v4-pro" + effort "low"). */ const EFFORT_TIERS: Record = { "deepseek-v4-pro": EFFORT_LEVELS, "deepseek-v4-flash": ["high", "max"], "glm-5.2": ["high", "max"], "mimo-v2.5": ["high", "max"], "grok-4.5": ["low", "medium", "high"], hy3: ["none", "low", "high"], "kimi-k3": ["max"], "qwen3.6-plus": ["high", "max"], "qwen3.7-max": ["high", "max"], "qwen3.7-plus": ["high", "max"], } /** * Parse a model string with an effort-level suffix. * e.g. "deepseek-v4-pro-low" → { baseModel: "deepseek-v4-pro", effort: "low" } * Returns null when the id doesn't match any known effort-tier pattern. */ export function parseEffortLevel( model: string, ): { baseModel: string; effort: string } | null { const m = String(model ?? "") for (const [baseModel, levels] of Object.entries(EFFORT_TIERS)) { for (const level of levels) { if (m === `${baseModel}-${level}`) { return { baseModel, effort: level } } } } return null } /** Strip a trailing effort suffix if present, otherwise return the id unchanged. */ function stripEffortSuffix(modelId: string): string { return parseEffortLevel(modelId)?.baseModel ?? modelId } // =========================================================================== // Zen catalog // =========================================================================== export const ZEN_MODELS: readonly OpenCodeModel[] = [ // ── OpenAI Responses (GPT family) ─────────────────────────── m("gpt-5", "GPT-5", "openai-responses", { ctx: 400000, max: 128000, off: true, image: true, cost: ZEN_PRICING["gpt-5"], }), m("gpt-5-codex", "GPT-5 Codex", "openai-responses", { ctx: 400000, max: 128000, off: true, image: true, cost: ZEN_PRICING["gpt-5-codex"], }), m("gpt-5-nano", "GPT-5 Nano", "openai-responses", { ctx: 400000, max: 128000, off: true, image: true, cost: ZEN_PRICING["gpt-5-nano"], }), m("gpt-5.1", "GPT-5.1", "openai-responses", { ctx: 400000, max: 128000, off: true, image: true, cost: ZEN_PRICING["gpt-5.1"], }), m("gpt-5.1-codex", "GPT-5.1 Codex", "openai-responses", { ctx: 400000, max: 128000, off: true, image: true, cost: ZEN_PRICING["gpt-5.1-codex"], }), m("gpt-5.1-codex-max", "GPT-5.1 Codex Max", "openai-responses", { ctx: 400000, max: 128000, off: true, image: true, cost: ZEN_PRICING["gpt-5.1-codex-max"], }), m("gpt-5.1-codex-mini", "GPT-5.1 Codex Mini", "openai-responses", { ctx: 400000, max: 128000, off: true, image: true, cost: ZEN_PRICING["gpt-5.1-codex-mini"], }), m("gpt-5.2", "GPT-5.2", "openai-responses", { ctx: 400000, max: 128000, off: true, xhigh: "xhigh", image: true, cost: ZEN_PRICING["gpt-5.2"], }), m("gpt-5.2-codex", "GPT-5.2 Codex", "openai-responses", { ctx: 400000, max: 128000, off: true, xhigh: "xhigh", image: true, cost: ZEN_PRICING["gpt-5.2-codex"], }), m("gpt-5.3-codex", "GPT-5.3 Codex", "openai-responses", { ctx: 400000, max: 128000, off: true, xhigh: "xhigh", image: true, cost: ZEN_PRICING["gpt-5.3-codex"], }), m("gpt-5.3-codex-spark", "GPT-5.3 Codex Spark", "openai-responses", { ctx: 400000, max: 128000, off: true, image: true, cost: ZEN_PRICING["gpt-5.3-codex"], }), m("gpt-5.4", "GPT-5.4", "openai-responses", { ctx: 272000, max: 128000, off: true, xhigh: "xhigh", image: true, cost: ZEN_PRICING["gpt-5.4"], }), m("gpt-5.4-mini", "GPT-5.4 Mini", "openai-responses", { ctx: 400000, max: 128000, off: true, xhigh: "xhigh", image: true, cost: ZEN_PRICING["gpt-5.4-mini"], }), m("gpt-5.4-nano", "GPT-5.4 Nano", "openai-responses", { ctx: 400000, max: 128000, off: true, xhigh: "xhigh", image: true, cost: ZEN_PRICING["gpt-5.4-nano"], }), m("gpt-5.4-pro", "GPT-5.4 Pro", "openai-responses", { ctx: 1050000, max: 128000, off: true, xhigh: "xhigh", image: true, cost: ZEN_PRICING["gpt-5.4-pro"], }), m("gpt-5.5", "GPT-5.5", "openai-responses", { ctx: 1050000, max: 128000, off: true, xhigh: "xhigh", image: true, cost: ZEN_PRICING["gpt-5.5"], }), m("gpt-5.5-pro", "GPT-5.5 Pro", "openai-responses", { ctx: 1050000, max: 128000, off: true, xhigh: "xhigh", image: true, cost: ZEN_PRICING["gpt-5.5-pro"], }), // ── Anthropic Messages (Claude family) ────────────────────── m("claude-haiku-4-5", "Claude Haiku 4.5", "anthropic-messages", { ctx: 200000, max: 64000, image: true, cost: ZEN_PRICING["claude-haiku-4-5"], }), m("claude-sonnet-4", "Claude Sonnet 4", "anthropic-messages", { ctx: 200000, max: 64000, image: true, cost: ZEN_PRICING["claude-sonnet-4"], }), m("claude-sonnet-4-5", "Claude Sonnet 4.5", "anthropic-messages", { ctx: 200000, max: 64000, image: true, cost: ZEN_PRICING["claude-sonnet-4-5"], }), m("claude-sonnet-4-6", "Claude Sonnet 4.6", "anthropic-messages", { ctx: 1000000, max: 64000, image: true, compat: { forceAdaptiveThinking: true }, cost: ZEN_PRICING["claude-sonnet-4-6"], }), m("claude-opus-4-1", "Claude Opus 4.1", "anthropic-messages", { ctx: 200000, max: 32000, image: true, cost: ZEN_PRICING["claude-opus-4-1"], }), m("claude-opus-4-5", "Claude Opus 4.5", "anthropic-messages", { ctx: 200000, max: 64000, image: true, cost: ZEN_PRICING["claude-opus-4-5"], }), m("claude-opus-4-6", "Claude Opus 4.6", "anthropic-messages", { ctx: 1000000, max: 128000, image: true, compat: { forceAdaptiveThinking: true }, xhigh: "max", cost: ZEN_PRICING["claude-opus-4-6"], }), m("claude-opus-4-7", "Claude Opus 4.7", "anthropic-messages", { ctx: 1000000, max: 128000, image: true, compat: { forceAdaptiveThinking: true }, xhigh: "xhigh", cost: ZEN_PRICING["claude-opus-4-7"], }), // ── Google Generative AI (Gemini) ─────────────────────────── m("gemini-3-flash", "Gemini 3 Flash", "google-generative-ai", { ctx: 1048576, max: 65536, image: true, off: true, cost: ZEN_PRICING["gemini-3-flash"], }), m("gemini-3.1-pro", "Gemini 3.1 Pro", "google-generative-ai", { ctx: 1048576, max: 65536, image: true, off: true, tlm: { minimal: null, low: "LOW", high: "HIGH", medium: null }, cost: ZEN_PRICING["gemini-3.1-pro"], }), m("gemini-3.5-flash", "Gemini 3.5 Flash", "google-generative-ai", { ctx: 1048576, max: 65536, image: true, off: true, cost: ZEN_PRICING["gemini-3.5-flash"], }), // ── OpenAI Completions ────────────────────────────────────── // big-pickle: DeepSeek thinking-mode upstream — declare the // reasoning_content contract so follow-up turns replay it. m("big-pickle", "Big Pickle", "openai-completions", { ctx: 200000, max: 128000, compat: { requiresReasoningContentOnAssistantMessages: true }, cost: ZEN_PRICING["big-pickle"], }), m("grok-build-0.1", "Grok Build 0.1", "openai-completions", { ctx: 256000, max: 256000, image: true, cost: ZEN_PRICING["grok-build-0.1"], }), m("glm-5", "GLM-5", "openai-completions", { ctx: 204800, max: 131072, cost: ZEN_PRICING["glm-5"], }), m("glm-5.1", "GLM-5.1", "openai-completions", { ctx: 204800, max: 131072, cost: ZEN_PRICING["glm-5.1"], }), // minimax-m3 on Zen → openai-completions (NOT /messages). GOTCHA. m("minimax-m3", "MiniMax M3", "openai-completions", { ctx: 1048576, max: 131072, image: true, cost: ZEN_PRICING["minimax-m3"], }), m("minimax-m2.5", "MiniMax M2.5", "openai-completions", { ctx: 204800, max: 131072, cost: ZEN_PRICING["minimax-m2.5"], }), m("minimax-m2.7", "MiniMax M2.7", "openai-completions", { ctx: 204800, max: 131072, cost: ZEN_PRICING["minimax-m2.7"], }), m("kimi-k2.5", "Kimi K2.5", "openai-completions", { ctx: 262144, max: 65536, image: true, cost: ZEN_PRICING["kimi-k2.5"], }), m("kimi-k2.6", "Kimi K2.6", "openai-completions", { ctx: 262144, max: 65536, image: true, cost: ZEN_PRICING["kimi-k2.6"], }), m("deepseek-v4-flash-free", "DeepSeek V4 Flash Free", "openai-completions", { ctx: 200000, max: 128000, compat: { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }, tlm: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" }, cost: ZEN_PRICING["deepseek-v4-flash-free"], }), m("nemotron-3-super-free", "Nemotron 3 Super Free", "openai-completions", { ctx: 1000000, max: 128000, cost: ZEN_PRICING["nemotron-3-super-free"], }), // ── Qwen on Zen → anthropic-messages (targetFormat:'claude') ─ m("qwen3.5-plus", "Qwen3.5 Plus", "anthropic-messages", { ctx: 262144, max: 65536, cost: ZEN_PRICING["qwen3.5-plus"], }), m("qwen3.6-plus", "Qwen3.6 Plus", "anthropic-messages", { ctx: 262144, max: 65536, cost: ZEN_PRICING["qwen3.6-plus"], }), // ── Free tier ─────────────────────────────────────────────── m("minimax-m2.5-free", "MiniMax M2.5 Free", "openai-completions", { ctx: 204800, max: 65536, cost: ZEN_PRICING["minimax-m2.5"], }), m("qwen3.6-plus-free", "Qwen3.6 Plus Free", "anthropic-messages", { ctx: 200000, max: 65536, cost: ZEN_PRICING["qwen3.6-plus"], }), ] // =========================================================================== // Go catalog (static — no live fetch) // =========================================================================== export const GO_MODELS: readonly OpenCodeModel[] = [ // ── OpenAI Completions ────────────────────────────────────── m("deepseek-v4-pro", "DeepSeek V4 Pro", "openai-completions", { ctx: 1000000, max: 384000, compat: { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }, tlm: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" }, cost: GO_PRICING["deepseek-v4-pro"], }), m("deepseek-v4-pro-low", "DeepSeek V4 Pro (low effort)", "openai-completions", { ctx: 1000000, max: 384000, compat: { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }, tlm: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" }, cost: GO_PRICING["deepseek-v4-pro"], }), m("deepseek-v4-pro-medium", "DeepSeek V4 Pro (medium effort)", "openai-completions", { ctx: 1000000, max: 384000, compat: { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }, tlm: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" }, cost: GO_PRICING["deepseek-v4-pro"], }), m("deepseek-v4-pro-high", "DeepSeek V4 Pro (high effort)", "openai-completions", { ctx: 1000000, max: 384000, compat: { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }, tlm: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" }, cost: GO_PRICING["deepseek-v4-pro"], }), m("deepseek-v4-pro-max", "DeepSeek V4 Pro (max effort)", "openai-completions", { ctx: 1000000, max: 384000, compat: { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }, tlm: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" }, cost: GO_PRICING["deepseek-v4-pro"], }), m("deepseek-v4-flash", "DeepSeek V4 Flash", "openai-completions", { ctx: 1000000, max: 384000, compat: { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }, tlm: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" }, cost: GO_PRICING["deepseek-v4-flash"], }), m("deepseek-v4-flash-high", "DeepSeek V4 Flash (high effort)", "openai-completions", { ctx: 1000000, max: 384000, compat: { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }, tlm: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" }, cost: GO_PRICING["deepseek-v4-flash"], }), m("deepseek-v4-flash-max", "DeepSeek V4 Flash (max effort)", "openai-completions", { ctx: 1000000, max: 384000, compat: { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", }, tlm: { minimal: null, low: null, medium: null, high: "high", xhigh: "max" }, cost: GO_PRICING["deepseek-v4-flash"], }), m("glm-5", "GLM-5", "openai-completions", { ctx: 202752, max: 32768, cost: GO_PRICING["glm-5"], }), m("glm-5.1", "GLM-5.1", "openai-completions", { ctx: 202752, max: 32768, cost: GO_PRICING["glm-5.1"], }), m("glm-5.2", "GLM-5.2", "openai-completions", { ctx: 202752, max: 32768, cost: GO_PRICING["glm-5.2"], }), m("glm-5.2-high", "GLM-5.2 (high effort)", "openai-completions", { ctx: 202752, max: 32768, cost: GO_PRICING["glm-5.2"], }), m("glm-5.2-max", "GLM-5.2 (max effort)", "openai-completions", { ctx: 202752, max: 32768, cost: GO_PRICING["glm-5.2"], }), m("kimi-k2.5", "Kimi K2.5", "openai-completions", { ctx: 262144, max: 65536, image: true, cost: GO_PRICING["kimi-k2.5"], }), m("kimi-k2.6", "Kimi K2.6", "openai-completions", { ctx: 262144, max: 65536, image: true, cost: GO_PRICING["kimi-k2.6"], }), m("kimi-k2.7-code", "Kimi K2.7 Code", "openai-completions", { ctx: 262144, max: 65536, image: true, cost: GO_PRICING["kimi-k2.7-code"], }), m("kimi-k3", "Kimi K3", "openai-completions", { ctx: 262144, max: 65536, image: true, cost: GO_PRICING["kimi-k3"], }), m("kimi-k3-max", "Kimi K3 (max effort)", "openai-completions", { ctx: 262144, max: 65536, image: true, cost: GO_PRICING["kimi-k3"], }), m("mimo-v2.5", "MiMo-V2.5", "openai-completions", { ctx: 1000000, max: 128000, image: true, cost: GO_PRICING["mimo-v2.5"], }), m("mimo-v2.5-pro", "MiMo-V2.5-Pro", "openai-completions", { ctx: 1048576, max: 128000, cost: GO_PRICING["mimo-v2.5-pro"], }), m("mimo-v2.5-high", "MiMo-V2.5 (high effort)", "openai-completions", { ctx: 1000000, max: 128000, image: true, cost: GO_PRICING["mimo-v2.5"], }), m("mimo-v2.5-max", "MiMo-V2.5 (max effort)", "openai-completions", { ctx: 1000000, max: 128000, image: true, cost: GO_PRICING["mimo-v2.5"], }), m("grok-4.5", "Grok 4.5", "openai-completions", { ctx: 256000, max: 256000, cost: GO_PRICING["grok-4.5"], }), m("grok-4.5-low", "Grok 4.5 (low effort)", "openai-completions", { ctx: 256000, max: 256000, cost: GO_PRICING["grok-4.5"], }), m("grok-4.5-medium", "Grok 4.5 (medium effort)", "openai-completions", { ctx: 256000, max: 256000, cost: GO_PRICING["grok-4.5"], }), m("grok-4.5-high", "Grok 4.5 (high effort)", "openai-completions", { ctx: 256000, max: 256000, cost: GO_PRICING["grok-4.5"], }), m("hy3", "Hunyuan3", "openai-completions", { ctx: 256000, max: 65536, cost: GO_PRICING["hy3"], }), m("hy3-none", "Hunyuan3 (none effort)", "openai-completions", { ctx: 256000, max: 65536, cost: GO_PRICING["hy3"], }), m("hy3-low", "Hunyuan3 (low effort)", "openai-completions", { ctx: 256000, max: 65536, cost: GO_PRICING["hy3"], }), m("hy3-high", "Hunyuan3 (high effort)", "openai-completions", { ctx: 256000, max: 65536, cost: GO_PRICING["hy3"], }), m("hy3-preview", "Hunyuan3 Preview", "openai-completions", { ctx: 256000, max: 65536, cost: GO_PRICING["hy3-preview"], }), // ── Anthropic Messages (Claude-format upstreams on Go) ────── // Go minimax-* → anthropic-messages (targetFormat:'claude'). GOTCHA. m("minimax-m3", "MiniMax M3", "anthropic-messages", { ctx: 1048576, max: 65536, image: true, cost: GO_PRICING["minimax-m3"], }), m("minimax-m2.7", "MiniMax M2.7", "anthropic-messages", { ctx: 204800, max: 65536, cost: GO_PRICING["minimax-m2.7"], }), m("minimax-m2.5", "MiniMax M2.5", "anthropic-messages", { ctx: 204800, max: 65536, cost: GO_PRICING["minimax-m2.5"], }), // Go qwen3.* → anthropic-messages (upstream rejects oa-compat). GOTCHA. m("qwen3.5-plus", "Qwen3.5 Plus", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.5-plus"], }), m("qwen3.6-plus", "Qwen3.6 Plus", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.6-plus"], }), m("qwen3.6-plus-high", "Qwen3.6 Plus (high effort)", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.6-plus"], }), m("qwen3.6-plus-max", "Qwen3.6 Plus (max effort)", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.6-plus"], }), m("qwen3.7-max", "Qwen3.7 Max", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.7-max"], }), m("qwen3.7-max-high", "Qwen3.7 Max (high effort)", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.7-max"], }), m("qwen3.7-max-max", "Qwen3.7 Max (max effort)", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.7-max"], }), m("qwen3.7-plus", "Qwen3.7 Plus", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.7-plus"], }), m("qwen3.7-plus-high", "Qwen3.7 Plus (high effort)", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.7-plus"], }), m("qwen3.7-plus-max", "Qwen3.7 Plus (max effort)", "anthropic-messages", { ctx: 262144, max: 65536, cost: GO_PRICING["qwen3.7-plus"], }), ] // =========================================================================== // Helpers to build model entries concisely // =========================================================================== type ThinkingLevelMap = NonNullable interface ModelSpec { ctx: number max: number image?: boolean off?: boolean xhigh?: string | null tlm?: ThinkingLevelMap compat?: OpenCodeModel["compat"] cost?: ModelCost } /** Compact factory for OpenCodeModel entries. */ function m(id: string, name: string, api: ProtocolType, spec: ModelSpec): OpenCodeModel { const tlm: ThinkingLevelMap = spec.tlm ?? buildTlm(spec) return { id, name, api, reasoning: true, input: spec.image ? ["text", "image"] : ["text"], cost: spec.cost, contextWindow: spec.ctx, maxTokens: spec.max, thinkingLevelMap: Object.keys(tlm).length ? tlm : undefined, compat: spec.compat, } } /** Build a thinking-level map from shorthand flags. */ function buildTlm(spec: ModelSpec): ThinkingLevelMap { const tlm: ThinkingLevelMap = {} if (spec.off) tlm["off"] = null if (spec.xhigh !== undefined) tlm["xhigh"] = spec.xhigh return tlm } // =========================================================================== // Protocol routing resolution // =========================================================================== const ZEN_MODEL_MAP: ReadonlyMap = new Map( ZEN_MODELS.map((m) => [m.id, m]), ) const GO_MODEL_MAP: ReadonlyMap = new Map( GO_MODELS.map((m) => [m.id, m]), ) /** * Resolve the wire protocol for a model id on a tier. * * 1. Strip any effort-tier suffix. * 2. Look up the base id in the tier's static catalog (authoritative). * 3. Fall back to pattern matching for live/passthrough ids. */ export function getProtocolForModel(tier: "zen" | "go", modelId: string): ProtocolType { const baseId = stripEffortSuffix(String(modelId ?? "")) const table = tier === "zen" ? ZEN_MODEL_MAP : GO_MODEL_MAP const entry = table.get(baseId) if (entry) return entry.api const id = baseId.toLowerCase() if (/^claude/.test(id)) return "anthropic-messages" if (/^gemini/.test(id)) return "google-generative-ai" if (/^gpt-?5/.test(id)) return "openai-responses" if (/^qwen/.test(id)) return "anthropic-messages" if (/^minimax/.test(id)) { // Go minimax upstreams speak Claude format; Zen defaults to oa-compat. return tier === "go" ? "anthropic-messages" : "openai-completions" } return "openai-completions" } /** * The base URL a pi-ai streamer should target for a given tier+protocol. * Anthropic needs the `/v1`-less base (the SDK appends `/v1/messages`). */ export function getBaseUrlForProtocol( tier: "zen" | "go", protocol: ProtocolType, ): string { if (protocol === "anthropic-messages") { return tier === "zen" ? ZEN_ANTHROPIC_BASE : GO_ANTHROPIC_BASE } return tier === "zen" ? ZEN_BASE : GO_BASE } // =========================================================================== // Provider model config builder (for pi.registerProvider) // =========================================================================== /** * Marker `api` value stamped on every registered OpenCode model. * * It MUST equal the provider's `api` passed to `pi.registerProvider`, so that * pi's streamWith (`provider-composer.js`) routes streaming to OUR * `createOpencodeStreamSimple` (which re-derives the REAL protocol from the * catalog via `getProtocolForModel` and applies the gotcha hooks). Without this * match, pi would call pi-ai's streamer directly with the model and silently * bypass every onPayload gotcha. */ export const OPENCODE_CUSTOM_API = "opencode-custom" /** * Convert OpenCodeModel catalog entries into the ProviderModelConfig shape that * pi.registerProvider expects. * * Each registered model is stamped with the `OPENCODE_CUSTOM_API` marker (so * streaming routes through our streamSimple), while its `baseUrl` is still * derived from the REAL per-model protocol (defensive — stream.ts re-derives * both `api` and `baseUrl` from the catalog at stream time anyway). * * `providerId` is accepted for symmetry with the commandcode builder and to * reserve a hook for per-provider header tagging if needed later. */ export function buildProviderModels( tier: "zen" | "go", providerId: string, models: readonly OpenCodeModel[], ): ProviderModelConfig[] { void providerId return models.map((model): ProviderModelConfig => { const baseUrl = getBaseUrlForProtocol(tier, model.api) return { id: model.id, name: model.name, api: OPENCODE_CUSTOM_API as ProtocolType, baseUrl, reasoning: model.reasoning, thinkingLevelMap: model.thinkingLevelMap, input: model.input, cost: model.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: model.contextWindow, maxTokens: model.maxTokens, compat: model.compat as ProviderModelConfig["compat"], } }) }