diff --git a/.claude/worktrees/fix-batch-v3.8.2-r3 b/.claude/worktrees/fix-batch-v3.8.2-r3 new file mode 160000 index 0000000000..f4534c12e6 --- /dev/null +++ b/.claude/worktrees/fix-batch-v3.8.2-r3 @@ -0,0 +1 @@ +Subproject commit f4534c12e6119410c06e3f4395b89a57a5a4e77e 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 eff48b209c..1f803c180c 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");