fix(opencode-zen): add 'opencode' provider alias and sync model list with live API

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.
This commit is contained in:
Automation
2026-05-21 20:03:45 +02:00
parent 1ea91b6c71
commit 07df7c8e43
5 changed files with 87 additions and 12 deletions

View File

@@ -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;
}

View File

@@ -1244,23 +1244,71 @@ export const REGISTRY: Record<string, RegistryEntry> = {
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",

View File

@@ -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(),

View File

@@ -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/<model>` 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 = {

View File

@@ -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");