diff --git a/open-sse/executors/opencode-go.js b/open-sse/executors/opencode-go.js index 38b3f271..7bf47edb 100644 --- a/open-sse/executors/opencode-go.js +++ b/open-sse/executors/opencode-go.js @@ -4,7 +4,14 @@ import { injectReasoningContent } from "../utils/reasoningContentInjector.js"; import { ANTHROPIC_API_VERSION } from "../providers/shared.js"; // Models that use /zen/go/v1/messages (Anthropic/Claude format + x-api-key auth) -const CLAUDE_FORMAT_MODELS = new Set(["minimax-m2.5", "minimax-m2.7"]); +const MESSAGES_FORMAT_MODELS = new Set([ + "minimax-m3", + "minimax-m2.7", + "minimax-m2.5", + "qwen3.7-max", + "qwen3.7-plus", + "qwen3.6-plus", +]); const BASE = "https://opencode.ai/zen/go/v1"; @@ -16,7 +23,7 @@ export class OpenCodeGoExecutor extends BaseExecutor { // buildUrl runs before buildHeaders in BaseExecutor.execute, cache model here buildUrl(model) { this._lastModel = model; - return CLAUDE_FORMAT_MODELS.has(model) + return MESSAGES_FORMAT_MODELS.has(model) ? `${BASE}/messages` : `${BASE}/chat/completions`; } @@ -25,7 +32,7 @@ export class OpenCodeGoExecutor extends BaseExecutor { const key = credentials?.apiKey || credentials?.accessToken; const headers = { "Content-Type": "application/json" }; - if (CLAUDE_FORMAT_MODELS.has(this._lastModel)) { + if (MESSAGES_FORMAT_MODELS.has(this._lastModel)) { headers["x-api-key"] = key; headers["anthropic-version"] = ANTHROPIC_API_VERSION; } else { diff --git a/open-sse/providers/registry/opencode-go.js b/open-sse/providers/registry/opencode-go.js index 5242839d..c980cc85 100644 --- a/open-sse/providers/registry/opencode-go.js +++ b/open-sse/providers/registry/opencode-go.js @@ -23,15 +23,19 @@ export default { headers: {}, }, models: [ - { id: "kimi-k2.6", name: "Kimi K2.6" }, - { id: "kimi-k2.5", name: "Kimi K2.5" }, + { id: "glm-5.2", name: "GLM 5.2" }, { id: "glm-5.1", name: "GLM 5.1" }, - { id: "glm-5", name: "GLM 5" }, - { id: "qwen3.5-plus", name: "Qwen 3.5 Plus" }, - { id: "qwen3.6-plus", name: "Qwen 3.6 Plus" }, - { id: "mimo-v2-pro", name: "MiMo V2 Pro" }, - { id: "mimo-v2-omni", name: "MiMo V2 Omni" }, + { id: "kimi-k2.7-code", name: "Kimi K2.7 Code" }, + { id: "kimi-k2.6", name: "Kimi K2.6" }, + { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" }, + { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" }, + { id: "mimo-v2.5", name: "MiMo V2.5" }, + { id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro" }, + { id: "minimax-m3", name: "MiniMax M3", targetFormat: "claude" }, { id: "minimax-m2.7", name: "MiniMax M2.7", targetFormat: "claude" }, { id: "minimax-m2.5", name: "MiniMax M2.5", targetFormat: "claude" }, + { id: "qwen3.7-max", name: "Qwen 3.7 Max", targetFormat: "claude" }, + { id: "qwen3.7-plus", name: "Qwen 3.7 Plus", targetFormat: "claude" }, + { id: "qwen3.6-plus", name: "Qwen 3.6 Plus", targetFormat: "claude" }, ], }; diff --git a/tests/unit/opencode-go-models.test.js b/tests/unit/opencode-go-models.test.js new file mode 100644 index 00000000..10946ab1 --- /dev/null +++ b/tests/unit/opencode-go-models.test.js @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import { PROVIDER_MODELS, getModelTargetFormat } from "../../open-sse/config/providerModels.js"; +import { OpenCodeGoExecutor } from "../../open-sse/executors/opencode-go.js"; + +const CHAT_MODELS = [ + "glm-5.2", + "glm-5.1", + // OpenCode Go docs' endpoint table currently says kimi-k2.7, but its + // config example and the live API use kimi-k2.7-code. + "kimi-k2.7-code", + "kimi-k2.6", + "deepseek-v4-pro", + "deepseek-v4-flash", + "mimo-v2.5", + "mimo-v2.5-pro", +]; + +const MESSAGES_MODELS = [ + "minimax-m3", + "minimax-m2.7", + "minimax-m2.5", + "qwen3.7-max", + "qwen3.7-plus", + "qwen3.6-plus", +]; + +describe("OpenCode Go official model catalog", () => { + it("matches the documented OpenCode Go model IDs", () => { + const ids = (PROVIDER_MODELS["opencode-go"] || []).map((model) => model.id); + + expect(ids).toEqual([...CHAT_MODELS, ...MESSAGES_MODELS]); + }); + + it("marks documented Qwen and MiniMax models as Anthropic messages format", () => { + for (const model of MESSAGES_MODELS) { + expect(getModelTargetFormat("opencode-go", model)).toBe("claude"); + } + }); + + it("keeps GLM, Kimi, DeepSeek, and MiMo on OpenAI-compatible chat format", () => { + for (const model of CHAT_MODELS) { + expect(getModelTargetFormat("opencode-go", model)).toBeNull(); + } + }); +}); + +describe("OpenCode Go endpoint routing", () => { + it("routes Qwen and MiniMax models to the messages endpoint with x-api-key auth", () => { + const executor = new OpenCodeGoExecutor(); + + for (const model of MESSAGES_MODELS) { + expect(executor.buildUrl(model)).toBe("https://opencode.ai/zen/go/v1/messages"); + const headers = executor.buildHeaders({ apiKey: "sk-test" }, false); + expect(headers["x-api-key"]).toBe("sk-test"); + expect(headers["anthropic-version"]).toBeDefined(); + expect(headers.Authorization).toBeUndefined(); + } + }); + + it("routes GLM, Kimi, DeepSeek, and MiMo models to chat/completions with bearer auth", () => { + const executor = new OpenCodeGoExecutor(); + + for (const model of CHAT_MODELS) { + expect(executor.buildUrl(model)).toBe("https://opencode.ai/zen/go/v1/chat/completions"); + const headers = executor.buildHeaders({ apiKey: "sk-test" }, false); + expect(headers.Authorization).toBe("Bearer sk-test"); + expect(headers["x-api-key"]).toBeUndefined(); + expect(headers["anthropic-version"]).toBeUndefined(); + } + }); +});