fix(opencode-zen): add 'opencode' provider alias + resolve conflicts (#2508)

This commit is contained in:
diegosouzapw
2026-05-21 16:02:25 -03:00
5 changed files with 87 additions and 12 deletions

Submodule .claude/worktrees/fix-batch-v3.8.2-r3 added at f4534c12e6

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