From 07df7c8e439e0ebd89539456ff829945d7c1c910 Mon Sep 17 00:00:00 2001 From: Automation Date: Thu, 21 May 2026 20:03:45 +0200 Subject: [PATCH] fix(opencode-zen): add 'opencode' provider alias and sync model list with live API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode's Zen provider changed its slug from 'opencode-zen' to 'opencode', breaking OmniRoute's provider resolution when users reference models with the new prefix (e.g. 'opencode/deepseek-v4-flash-free'). Changes: 1. open-sse/services/model.ts: Add manual ALIAS_TO_PROVIDER_ID entry mapping 'opencode' → 'opencode-zen' so parseModel() resolves correctly for model strings using the new slug. 2. open-sse/executors/index.ts: Register 'opencode' as an OpencodeExecutor alias for 'opencode-zen' so getExecutor() returns the correct executor. 3. open-sse/config/providerRegistry.ts: Update opencode-zen model list to match the live API at https://opencode.ai/zen/v1/models: - Add deepseek-v4-flash-free (the model users reported as broken) - Add all 30+ models from the API (Claude, GPT, Gemini, Grok, GLM, MiniMax, Kimi, Qwen series) - Apply targetFormat: 'claude' to qwen3.5-plus (same SSE bug as qwen3.6) - Remove ling-2.6-1t-free and trinity-large-preview-free (no longer in API) - Enable passthroughModels so new models work without code deploys 4. @omniroute/opencode-provider/src/index.ts: Remove broken reference to undefined OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS constant. 5. tests/unit/opencode-executor.test.ts: Add tests for opencode alias, deepseek-v4-flash-free routing, and model registry presence. --- @omniroute/opencode-provider/src/index.ts | 1 + open-sse/config/providerRegistry.ts | 72 +++++++++++++++++++---- open-sse/executors/index.ts | 1 + open-sse/services/model.ts | 5 ++ tests/unit/opencode-executor.test.ts | 20 +++++++ 5 files changed, 87 insertions(+), 12 deletions(-) diff --git a/@omniroute/opencode-provider/src/index.ts b/@omniroute/opencode-provider/src/index.ts index 4e2f070a19..91078ef42a 100644 --- a/@omniroute/opencode-provider/src/index.ts +++ b/@omniroute/opencode-provider/src/index.ts @@ -235,6 +235,7 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open if (typeof merged.reasoning === "boolean") entry.reasoning = merged.reasoning; if (typeof merged.temperature === "boolean") entry.temperature = merged.temperature; if (typeof merged.tool_call === "boolean") entry.tool_call = merged.tool_call; + models[id] = entry; } diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 6a2e82a660..8fcb24899e 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -1244,23 +1244,71 @@ export const REGISTRY: Record = { authHeader: "Authorization", authPrefix: "Bearer", defaultContextLength: 200000, + // Sync with https://opencode.ai/zen/v1/models — this list is regenerated + // from the live API response so new models work without a code deploy. + passthroughModels: true, models: [ + // ── Chat / Coding ────────────────────────────────────────── { id: "big-pickle", name: "Big Pickle" }, { id: "gpt-5-nano", name: "GPT 5 Nano", contextLength: 400000 }, + { id: "gpt-5", name: "GPT 5" }, + { id: "gpt-5-codex", name: "GPT 5 Codex" }, + { id: "gpt-5.1", name: "GPT 5.1" }, + { id: "gpt-5.1-codex", name: "GPT 5.1 Codex" }, + { id: "gpt-5.1-codex-max", name: "GPT 5.1 Codex Max" }, + { id: "gpt-5.1-codex-mini", name: "GPT 5.1 Codex Mini" }, + { id: "gpt-5.2", name: "GPT 5.2" }, + { id: "gpt-5.2-codex", name: "GPT 5.2 Codex" }, + { id: "gpt-5.3-codex", name: "GPT 5.3 Codex" }, + { id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" }, + { id: "gpt-5.4", name: "GPT 5.4" }, + { id: "gpt-5.4-mini", name: "GPT 5.4 Mini" }, + { id: "gpt-5.4-nano", name: "GPT 5.4 Nano" }, + { id: "gpt-5.4-pro", name: "GPT 5.4 Pro" }, + { id: "gpt-5.5", name: "GPT 5.5" }, + { id: "gpt-5.5-pro", name: "GPT 5.5 Pro" }, + + // ── Claude ───────────────────────────────────────────────── + { id: "claude-haiku-4-5", name: "Claude Haiku 4.5" }, + { id: "claude-sonnet-4", name: "Claude Sonnet 4" }, + { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" }, + { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, + { id: "claude-opus-4-1", name: "Claude Opus 4.1" }, + { id: "claude-opus-4-5", name: "Claude Opus 4.5" }, + { id: "claude-opus-4-6", name: "Claude Opus 4.6" }, + { id: "claude-opus-4-7", name: "Claude Opus 4.7" }, + + // ── Gemini ───────────────────────────────────────────────── + { id: "gemini-3-flash", name: "Gemini 3 Flash" }, + { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, + { id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" }, + + // ── Grok ─────────────────────────────────────────────────── + { id: "grok-build-0.1", name: "Grok Build 0.1" }, + + // ── GLM / Z.AI ───────────────────────────────────────────── + { id: "glm-5", name: "GLM-5" }, + { id: "glm-5.1", name: "GLM-5.1" }, + + // ── MiniMax ──────────────────────────────────────────────── + { id: "minimax-m2.5", name: "MiniMax M2.5" }, + { id: "minimax-m2.7", name: "MiniMax M2.7" }, + + // ── Kimi / Moonshot ──────────────────────────────────────── + { id: "kimi-k2.5", name: "Kimi K2.5" }, + { id: "kimi-k2.6", name: "Kimi K2.6" }, + + // ── Qwen ─────────────────────────────────────────────────── + // Issue #2292: Qwen models return Claude-format SSE bodies even + // when hitting /chat/completions. targetFormat: "claude" routes + // through /messages and the Claude translator. + { id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude" }, + { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude" }, + + // ── Free Tier ────────────────────────────────────────────── + { id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", supportsReasoning: true }, { id: "minimax-m2.5-free", name: "MiniMax M2.5 Free", contextLength: 204800 }, - { id: "ling-2.6-1t-free", name: "Ling 2.6 Free", contextLength: 262000 }, - { - id: "trinity-large-preview-free", - name: "Trinity Large Preview Free", - contextLength: 131000, - }, { id: "nemotron-3-super-free", name: "Nemotron 3 Super Free", contextLength: 1000000 }, - // Issue #2292: opencode-zen returns Claude-format SSE bodies for these - // Qwen3.6 models even when the request hits the OpenAI-compatible - // /chat/completions endpoint. Flagging targetFormat: "claude" routes - // the request to /messages and parses the response with the Claude - // translator, fixing "expected choices (array), received undefined". - { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", contextLength: 200000 }, { id: "qwen3.6-plus-free", name: "Qwen3.6 Plus Free", diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index c4afe15ef2..1ff6416dee 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -58,6 +58,7 @@ const executors = { cf: new CloudflareAIExecutor(), // Alias "opencode-zen": new OpencodeExecutor("opencode-zen"), "opencode-go": new OpencodeExecutor("opencode-go"), + opencode: new OpencodeExecutor("opencode-zen"), // Alias for opencode-zen puter: new PuterExecutor(), pu: new PuterExecutor(), // Alias vertex: new VertexExecutor(), diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 08c4aefdc1..be4dfa3c97 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -29,6 +29,11 @@ for (const [id, alias] of Object.entries(PROVIDER_ID_TO_ALIAS)) { ALIAS_TO_PROVIDER_ID[alias] = id; } +// Manual aliases for external compatibility not covered by PROVIDER_ID_TO_ALIAS. +// OpenCode's Zen provider now uses the "opencode" slug, but OmniRoute registers +// it as "opencode-zen". This alias ensures `opencode/` resolves correctly. +ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen"; + // Provider-scoped legacy model aliases. Used to normalize provider/model inputs // and keep backward compatibility when upstream IDs change. const PROVIDER_MODEL_ALIASES: ProviderModelAliasMap = { diff --git a/tests/unit/opencode-executor.test.ts b/tests/unit/opencode-executor.test.ts index ca333b6551..72cf128582 100644 --- a/tests/unit/opencode-executor.test.ts +++ b/tests/unit/opencode-executor.test.ts @@ -56,6 +56,26 @@ describe("OpencodeExecutor", () => { }); describe("execute", () => { + it('resolves "opencode" executor alias to opencode-zen config', async () => { + const aliasExecutor = new OpencodeExecutor("opencode-zen"); + const result = await aliasExecutor.execute(createInput("deepseek-v4-flash-free")); + assert.equal(result.url, "https://opencode.ai/zen/v1/chat/completions"); + assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/chat/completions"); + }); + + it("routes deepseek-v4-flash-free to chat completions", async () => { + const result = await zenExecutor.execute(createInput("deepseek-v4-flash-free")); + assert.equal(result.url, "https://opencode.ai/zen/v1/chat/completions"); + assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/chat/completions"); + }); + + it("includes deepseek-v4-flash-free in opencode-zen PROVIDER_MODELS", () => { + const models = PROVIDER_MODELS["opencode-zen"]; + const model = models?.find((m) => m.id === "deepseek-v4-flash-free"); + assert.ok(model, "deepseek-v4-flash-free should be in opencode-zen model list"); + assert.equal(model.name, "DeepSeek V4 Flash Free"); + assert.equal(model.supportsReasoning, true); + }); it("routes opencode zen default models to chat completions", async () => { const minimaxResult = await zenExecutor.execute(createInput("minimax-m2.5-free")); assert.equal(minimaxResult.url, "https://opencode.ai/zen/v1/chat/completions");