diff --git a/packages/types/src/__tests__/opencode-go.test.ts b/packages/types/src/__tests__/opencode-go.test.ts index c65de6165c..ecba7d5324 100644 --- a/packages/types/src/__tests__/opencode-go.test.ts +++ b/packages/types/src/__tests__/opencode-go.test.ts @@ -13,6 +13,7 @@ import { describe("opencode-go registry", () => { const anthropicFormatModels = [ "qwen3.8-max", + "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", @@ -33,6 +34,13 @@ describe("opencode-go registry", () => { "deepseek-v4-pro", "deepseek-v4-flash", ] + const responsesFormatModels = [ + "gpt-5.6-luna", + "grok-4.5", + "grok-4.6", + "muse-spark-1.3-contributor", + "muse-spark-1.2-contributor", + ] describe("isOpencodeGoAnthropicFormatModel", () => { it("classifies Qwen and MiniMax models as Anthropic-format", () => { @@ -133,11 +141,13 @@ describe("opencode-go registry", () => { describe("OPENCODE_GO_RESPONSES_FORMAT_MODELS", () => { it("contains exactly the Responses-only models", () => { - expect([...OPENCODE_GO_RESPONSES_FORMAT_MODELS].sort()).toEqual(["gpt-5.6-luna"]) + expect([...OPENCODE_GO_RESPONSES_FORMAT_MODELS].sort()).toEqual([...responsesFormatModels].sort()) }) - it("classifies gpt-5.6-luna as Responses-format", () => { - expect(isOpencodeGoResponsesFormatModel("gpt-5.6-luna")).toBe(true) + it("classifies every Responses-only model as Responses-format", () => { + for (const id of responsesFormatModels) { + expect(isOpencodeGoResponsesFormatModel(id)).toBe(true) + } }) it("classifies Anthropic-format and OpenAI-compatible models as non-Responses-format", () => { @@ -156,6 +166,7 @@ describe("opencode-go registry", () => { it("curates gpt-5.6-luna with its Go Responses capabilities", () => { expect(getOpencodeGoModelInfo("gpt-5.6-luna")).toMatchObject({ + supportsMaxTokens: true, maxTokens: 128_000, contextWindow: 1_050_000, supportsImages: true, @@ -184,6 +195,195 @@ describe("opencode-go registry", () => { }) describe("opencodeGoModels registry invariants", () => { + it.each([ + { + id: "glm-5.3-flash", + expected: { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["low", "high", "max"], + inputPrice: 0.075, + outputPrice: 0.25, + cacheReadsPrice: 0.015, + }, + }, + { + id: "kimi-k2.7-code", + expected: { + maxTokens: 262_144, + contextWindow: 262_144, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + inputPrice: 0.95, + outputPrice: 4, + cacheReadsPrice: 0.19, + }, + }, + { + id: "longcat-2.0", + expected: { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: false, + supportsReasoningBinary: true, + inputPrice: 0.3, + outputPrice: 1.2, + cacheReadsPrice: 0.006, + }, + }, + { + id: "mimo-v2-pro", + expected: { + maxTokens: 128_000, + contextWindow: 1_048_576, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 1, + outputPrice: 3, + cacheReadsPrice: 0.2, + longContextPricing: { + thresholdTokens: 256_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 2, + cacheReadsPriceMultiplier: 2, + }, + }, + }, + { + id: "mimo-v2-omni", + expected: { + maxTokens: 128_000, + contextWindow: 262_144, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.4, + outputPrice: 2, + cacheReadsPrice: 0.08, + }, + }, + { + id: "qwen3.5-plus", + expected: { + maxTokens: 65_536, + contextWindow: 262_144, + supportsImages: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + inputPrice: 0.2, + outputPrice: 1.2, + cacheReadsPrice: 0.02, + cacheWritesPrice: 0.25, + }, + }, + { + id: "qwen3.8-flash", + expected: { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: true, + supportsMaxTokens: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + inputPrice: 0.15, + outputPrice: 0.47, + cacheReadsPrice: 0.016, + cacheWritesPrice: 0.2, + }, + }, + { + id: "deepseek-v4-flash-vision-exp", + expected: { + maxTokens: 384_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsReasoningEffort: ["disable", "low", "high", "max"], + inputPrice: 0.22, + outputPrice: 0.66, + cacheReadsPrice: 0.007, + }, + }, + { + id: "hy4-preview", + expected: { + maxTokens: 64_000, + contextWindow: 1_024_000, + supportsImages: false, + supportsReasoningEffort: ["disable", "high"], + inputPrice: 0.834, + outputPrice: 2.501, + cacheReadsPrice: 0.042, + }, + }, + ...(["hy3", "hy3-preview"] as const).map((id) => ({ + id, + expected: { + maxTokens: 64_000, + contextWindow: 256_000, + supportsImages: false, + supportsReasoningEffort: ["disable", "low", "high"], + inputPrice: 0.0175, + outputPrice: 0.0725, + cacheReadsPrice: 0.004375, + }, + })), + { + id: "grok-4.5", + expected: { + maxTokens: 500_000, + contextWindow: 500_000, + supportsImages: true, + supportsReasoningEffort: ["low", "medium", "high"], + inputPrice: 2, + outputPrice: 6, + cacheReadsPrice: 0.3, + longContextPricing: { + thresholdTokens: 200_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 2, + cacheReadsPriceMultiplier: 2, + }, + }, + }, + { + id: "grok-4.6", + expected: { + maxTokens: 500_000, + contextWindow: 500_000, + supportsImages: true, + supportsReasoningEffort: ["low", "medium", "high", "xhigh"], + inputPrice: 2, + outputPrice: 6, + cacheReadsPrice: 0.5, + longContextPricing: { + thresholdTokens: 200_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 2, + cacheReadsPriceMultiplier: 2, + }, + }, + }, + ...(["muse-spark-1.3-contributor", "muse-spark-1.2-contributor"] as const).map((id) => ({ + id, + expected: { + maxTokens: 131_072, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh"], + inputPrice: 0.1, + outputPrice: 0.2, + cacheReadsPrice: 0.002, + }, + })), + ])("keeps independently asserted metadata for $id", ({ id, expected }) => { + expect(getOpencodeGoModelInfo(id)).toMatchObject(expected) + }) + it("every entry has a positive maxTokens and contextWindow", () => { for (const [id, info] of Object.entries(opencodeGoModels)) { expect(info.maxTokens).toBeGreaterThan(0) diff --git a/packages/types/src/providers/opencode-go.ts b/packages/types/src/providers/opencode-go.ts index 10d6bcbc50..eb32dbcac5 100644 --- a/packages/types/src/providers/opencode-go.ts +++ b/packages/types/src/providers/opencode-go.ts @@ -3,9 +3,9 @@ import type { ModelInfo } from "../model.js" // Opencode "Go" plan โ€” OpenAI-compatible gateway. // https://opencode.ai/docs/go/ ยท base URL: https://opencode.ai/zen/go/v1 // -// The full model list (and metadata) is fetched dynamically from -// `https://opencode.ai/zen/go/v1/models`, so models can be switched on the fly. -// The values below are only a fallback used before the live list resolves. +// Model IDs are fetched dynamically from `https://opencode.ai/zen/go/v1/models` +// so models can be switched on the fly. The endpoint currently omits metadata, +// so Zoo Code supplies limits and capabilities from the registries below. export const opencodeGoDefaultModelId = "glm-5.2" export const opencodeGoDefaultModelInfo: ModelInfo = { @@ -16,7 +16,7 @@ export const opencodeGoDefaultModelInfo: ModelInfo = { // Pricing is intentionally omitted: ModelInfoView renders a `0` field as "$0.00 / 1M tokens" // (implying the service is free), so we leave it unknown โ€” consistent with the dynamically // fetched models, which also leave price fields absent. See PR #319 review. - description: "Opencode Go plan model. Available models and metadata are resolved dynamically from /v1/models.", + description: "Opencode Go plan model. Model IDs are fetched dynamically; metadata uses Zoo Code's model registry.", } export const OPENCODE_GO_DEFAULT_TEMPERATURE = 0 @@ -102,6 +102,21 @@ export const opencodeGoModels: Record = { description: "GLM-5.3 is Zhipu's flagship coding and agent model with a 1M context window, 128k max output, and always-on reasoning with configurable effort (Low/High/Max). Available via the Opencode Go plan.", }, + "glm-5.3-flash": { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["low", "high", "max"], + reasoningEffort: "max", + preserveReasoning: true, + inputPrice: 0.075, + outputPrice: 0.25, + cacheReadsPrice: 0.015, + description: + "GLM-5.3 Flash is Zhipu's fast multimodal coding and agent model with a 1M context window and configurable reasoning effort. Available via the Opencode Go plan.", + }, "glm-5.2": { maxTokens: 131_072, contextWindow: 1_000_000, @@ -163,6 +178,36 @@ export const opencodeGoModels: Record = { description: "Kimi K2.6 is Moonshot AI's native multimodal agentic MoE model with a 256k context window, built for long-horizon coding and tool use. Available via the Opencode Go plan.", }, + "kimi-k2.7-code": { + maxTokens: 262_144, + contextWindow: 262_144, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + preserveReasoning: true, + defaultTemperature: 1.0, + inputPrice: 0.95, + outputPrice: 4.0, + cacheReadsPrice: 0.19, + description: + "Kimi K2.7 Code is Moonshot AI's coding model for long-context programming tasks, with multimodal input and a 256k context window. Available via the Opencode Go plan.", + }, + + // --- Meituan LongCat --- + "longcat-2.0": { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningBinary: true, + preserveReasoning: true, + inputPrice: 0.3, + outputPrice: 1.2, + cacheReadsPrice: 0.006, + description: + "LongCat 2.0 is Meituan's long-context reasoning and coding model with a 1M context window. Available via the Opencode Go plan.", + }, // --- Xiaomi MiMo --- "mimo-v2.5": { @@ -201,6 +246,36 @@ export const opencodeGoModels: Record = { description: "MiMo V2.5 Pro - Xiaomi's flagship reasoning model with 1M context, deep thinking, and tool calling. Available via the Opencode Go plan.", }, + "mimo-v2-pro": { + maxTokens: 128_000, + contextWindow: 1_048_576, + supportsImages: false, + supportsPromptCache: false, + preserveReasoning: true, + inputPrice: 1.0, + outputPrice: 3.0, + cacheReadsPrice: 0.2, + longContextPricing: { + thresholdTokens: 256_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 2, + cacheReadsPriceMultiplier: 2, + }, + description: + "MiMo V2 Pro is Xiaomi's text reasoning and coding model with a 1M context window. Available via the Opencode Go plan.", + }, + "mimo-v2-omni": { + maxTokens: 128_000, + contextWindow: 262_144, + supportsImages: true, + supportsPromptCache: false, + preserveReasoning: true, + inputPrice: 0.4, + outputPrice: 2.0, + cacheReadsPrice: 0.08, + description: + "MiMo V2 Omni is Xiaomi's multimodal reasoning model with a 256k context window. Available via the Opencode Go plan.", + }, // --- MiniMax --- "minimax-m2.5": { @@ -255,6 +330,21 @@ export const opencodeGoModels: Record = { }, // --- Alibaba Qwen --- + "qwen3.5-plus": { + maxTokens: 65_536, + contextWindow: 262_144, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + preserveReasoning: true, + inputPrice: 0.2, + outputPrice: 1.2, + cacheReadsPrice: 0.02, + cacheWritesPrice: 0.25, + description: + "Qwen3.5 Plus is Alibaba's multimodal reasoning model with a 256k context window. Available via the Opencode Go plan.", + }, "qwen3.6-plus": { maxTokens: 65_536, contextWindow: 1_000_000, @@ -321,6 +411,22 @@ export const opencodeGoModels: Record = { description: "Qwen3.8 Max - Alibaba's flagship multimodal reasoning model with a 1M context window, 128k max output, and long-horizon coding and agentic capabilities. Available via the Opencode Go plan.", }, + "qwen3.8-flash": { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + preserveReasoning: true, + inputPrice: 0.15, + outputPrice: 0.47, + cacheReadsPrice: 0.016, + cacheWritesPrice: 0.2, + description: + "Qwen3.8 Flash is Alibaba's fast multimodal reasoning model with a 1M context window. Available via the Opencode Go plan.", + }, // --- DeepSeek --- "deepseek-v4-pro": { @@ -359,6 +465,70 @@ export const opencodeGoModels: Record = { description: "DeepSeek-V4-Flash is DeepSeek's fast, cost-efficient V4 model supporting thinking and non-thinking modes. Available via the Opencode Go plan.", }, + "deepseek-v4-flash-vision-exp": { + maxTokens: 384_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["disable", "low", "high", "max"], + reasoningEffort: "high", + preserveReasoning: true, + inputPrice: 0.22, + outputPrice: 0.66, + cacheReadsPrice: 0.007, + description: + "DeepSeek V4 Flash Vision Experimental is a fast multimodal reasoning model with a 1M context window. Available via the Opencode Go plan.", + }, + + // --- Tencent Hunyuan --- + "hy4-preview": { + maxTokens: 64_000, + contextWindow: 1_024_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["disable", "high"], + reasoningEffort: "high", + preserveReasoning: true, + inputPrice: 0.834, + outputPrice: 2.501, + cacheReadsPrice: 0.042, + description: + "Hunyuan 4 Preview is Tencent's long-context reasoning and coding model with a 1M context window. Available via the Opencode Go plan.", + }, + hy3: { + maxTokens: 64_000, + contextWindow: 256_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["disable", "low", "high"], + reasoningEffort: "high", + preserveReasoning: true, + inputPrice: 0.0175, + outputPrice: 0.0725, + cacheReadsPrice: 0.004375, + description: + "Hunyuan 3 is Tencent's reasoning and coding model with a 256k context window. Available via the Opencode Go plan.", + }, + "hy3-preview": { + // The live endpoint still lists this historical alias; it shares HY3's limits and pricing. + maxTokens: 64_000, + contextWindow: 256_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["disable", "low", "high"], + reasoningEffort: "high", + preserveReasoning: true, + inputPrice: 0.0175, + outputPrice: 0.0725, + cacheReadsPrice: 0.004375, + description: + "Hunyuan 3 Preview is Tencent's preview reasoning and coding model with a 256k context window. Available via the Opencode Go plan.", + }, + // --- OpenAI Responses --- // Luna is curated here because the Go gateway's model catalogue does not // currently provide its capability metadata. These values intentionally @@ -368,6 +538,7 @@ export const opencodeGoModels: Record = { contextWindow: 1_050_000, supportsImages: true, supportsPromptCache: true, + supportsMaxTokens: true, supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], reasoningEffort: "medium", inputPrice: 0.2, @@ -381,7 +552,76 @@ export const opencodeGoModels: Record = { cacheWritesPriceMultiplier: 2, cacheReadsPriceMultiplier: 2, }, - description: "GPT-5.6 Luna via the OpenCode Go Responses API.", + description: + "GPT-5.6 Luna is OpenAI's fast reasoning model with a 1M context window. Available via the Opencode Go plan.", + }, + "grok-4.5": { + maxTokens: 500_000, + contextWindow: 500_000, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["low", "medium", "high"], + reasoningEffort: "high", + inputPrice: 2.0, + outputPrice: 6.0, + cacheReadsPrice: 0.3, + longContextPricing: { + thresholdTokens: 200_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 2, + cacheReadsPriceMultiplier: 2, + }, + description: + "Grok 4.5 is xAI's multimodal reasoning and agent model with a 500k context window. Available via the Opencode Go plan.", + }, + "grok-4.6": { + maxTokens: 500_000, + contextWindow: 500_000, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["low", "medium", "high", "xhigh"], + reasoningEffort: "high", + inputPrice: 2.0, + outputPrice: 6.0, + cacheReadsPrice: 0.5, + longContextPricing: { + thresholdTokens: 200_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 2, + cacheReadsPriceMultiplier: 2, + }, + description: + "Grok 4.6 is xAI's multimodal reasoning and agent model with a 500k context window. Available via the Opencode Go plan.", + }, + "muse-spark-1.3-contributor": { + maxTokens: 131_072, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh"], + reasoningEffort: "medium", + inputPrice: 0.1, + outputPrice: 0.2, + cacheReadsPrice: 0.002, + description: + "Muse Spark 1.3 Contributor is Meta's multimodal reasoning model for coding and agentic workflows. Available via the Opencode Go plan.", + }, + "muse-spark-1.2-contributor": { + maxTokens: 131_072, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh"], + reasoningEffort: "medium", + inputPrice: 0.1, + outputPrice: 0.2, + cacheReadsPrice: 0.002, + description: + "Muse Spark 1.2 Contributor is Meta's multimodal coding model with a 1M context window. Available via the Opencode Go plan.", }, } @@ -404,6 +644,7 @@ export const opencodeGoModels: Record = { export const OPENCODE_GO_ANTHROPIC_FORMAT_MODELS = new Set([ // --- Alibaba Qwen --- "qwen3.8-max", + "qwen3.8-flash", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", @@ -418,10 +659,9 @@ export const OPENCODE_GO_ANTHROPIC_FORMAT_MODELS = new Set([ * (`/v1/responses`), not the OpenAI-compatible Chat Completions endpoint * (`/v1/chat/completions`). * - * The Go gateway maps every model to exactly one wire format. Some models - * (currently only `gpt-5.6-luna`) are Responses-only and are also explicitly - * curated in `opencodeGoModels`: the gateway's - * `/v1/chat/completions` adapter for them fails with an opaque HTTP 500 + * The Go gateway maps every model to exactly one wire format. Responses-only + * models are explicitly curated in `opencodeGoModels`: the gateway's + * `/v1/chat/completions` adapter for these models can fail with an opaque HTTP 500 * (`{"type":"error","error":{"type":"error","message":"Internal server error"}}`), * while `/v1/responses` succeeds (Zoo-Code-Org/Zoo-Code#1431). * @@ -433,6 +673,12 @@ export const OPENCODE_GO_ANTHROPIC_FORMAT_MODELS = new Set([ export const OPENCODE_GO_RESPONSES_FORMAT_MODELS = new Set([ // --- OpenAI --- "gpt-5.6-luna", + // --- xAI --- + "grok-4.5", + "grok-4.6", + // --- Meta --- + "muse-spark-1.3-contributor", + "muse-spark-1.2-contributor", ]) /** diff --git a/src/api/providers/__tests__/opencode-go.spec.ts b/src/api/providers/__tests__/opencode-go.spec.ts index 7415ea73b1..964299a154 100644 --- a/src/api/providers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/__tests__/opencode-go.spec.ts @@ -1362,8 +1362,12 @@ describe("OpencodeGoHandler", () => { }).rejects.toThrow("Opencode Go completion error: internal server error") }) - it("classifies gpt-5.6-luna as Responses-format and other models as not", () => { + it("classifies documented Responses models as Responses-format and other models as not", () => { expect(isOpencodeGoResponsesFormatModel("gpt-5.6-luna")).toBe(true) + expect(isOpencodeGoResponsesFormatModel("grok-4.5")).toBe(true) + expect(isOpencodeGoResponsesFormatModel("grok-4.6")).toBe(true) + expect(isOpencodeGoResponsesFormatModel("muse-spark-1.3-contributor")).toBe(true) + expect(isOpencodeGoResponsesFormatModel("muse-spark-1.2-contributor")).toBe(true) expect(isOpencodeGoResponsesFormatModel("glm-5.3")).toBe(false) expect(isOpencodeGoResponsesFormatModel("qwen3.7-max")).toBe(false) expect(isOpencodeGoResponsesFormatModel("some-unknown-model")).toBe(false) @@ -1372,6 +1376,7 @@ describe("OpencodeGoHandler", () => { describe("isOpencodeGoAnthropicFormatModel", () => { it("classifies Qwen and MiniMax Go models as Anthropic-format", () => { + expect(isOpencodeGoAnthropicFormatModel("qwen3.8-flash")).toBe(true) expect(isOpencodeGoAnthropicFormatModel("qwen3.7-max")).toBe(true) expect(isOpencodeGoAnthropicFormatModel("qwen3.7-plus")).toBe(true) expect(isOpencodeGoAnthropicFormatModel("qwen3.6-plus")).toBe(true) @@ -1385,6 +1390,7 @@ describe("OpencodeGoHandler", () => { expect(isOpencodeGoAnthropicFormatModel("kimi-k2.6")).toBe(false) expect(isOpencodeGoAnthropicFormatModel("deepseek-v4-pro")).toBe(false) expect(isOpencodeGoAnthropicFormatModel("mimo-v2.5")).toBe(false) + expect(isOpencodeGoAnthropicFormatModel("qwen3.5-plus")).toBe(false) }) it("defaults unknown model IDs to the OpenAI-compatible format", () => { diff --git a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts index 20040ffb22..6d886819b3 100644 --- a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts @@ -87,6 +87,38 @@ describe("Opencode Go Fetchers", () => { }) }) + it("uses native model info when the endpoint returns only an OpenCode Go model ID", async () => { + mockedAxios.get.mockResolvedValue({ + data: { + data: [ + { id: "gpt-5.6-luna" }, + { id: "hy4-preview" }, + { id: "deepseek-v4-flash-vision-exp" }, + { id: "muse-spark-1.3-contributor" }, + ], + }, + }) + + const models = await getOpencodeGoModels("k") + + expect(models["gpt-5.6-luna"]).toMatchObject({ contextWindow: 1_050_000, maxTokens: 128_000 }) + expect(models["hy4-preview"]).toMatchObject({ contextWindow: 1_024_000, maxTokens: 64_000 }) + expect(models["deepseek-v4-flash-vision-exp"]).toMatchObject({ + contextWindow: 1_000_000, + maxTokens: 384_000, + }) + expect(models["muse-spark-1.3-contributor"]).toMatchObject({ + contextWindow: 1_048_576, + maxTokens: 131_072, + supportsImages: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["minimal", "low", "medium", "high", "xhigh"], + inputPrice: 0.1, + outputPrice: 0.2, + cacheReadsPrice: 0.002, + }) + }) + it("returns an empty map on network error", async () => { mockedAxios.get.mockRejectedValue(new Error("network")) expect(await getOpencodeGoModels("k")).toEqual({}) @@ -121,6 +153,59 @@ describe("Opencode Go Fetchers", () => { }) describe("parseOpencodeGoModel", () => { + it("has complete model info for every model returned by OpenCode Go", () => { + const modelIds = [ + "minimax-m3", + "minimax-m2.7", + "minimax-m2.5", + "kimi-k3", + "kimi-k2.7-code", + "kimi-k2.6", + "longcat-2.0", + "kimi-k2.5", + "glm-5.2", + "glm-5.3-flash", + "glm-5.3", + "glm-5.1", + "glm-5", + "deepseek-v4-pro", + "deepseek-v4-flash", + "deepseek-v4-flash-vision-exp", + "qwen3.7-max", + "qwen3.8-max", + "qwen3.8-flash", + "qwen3.7-plus", + "qwen3.6-plus", + "qwen3.5-plus", + "mimo-v2-pro", + "mimo-v2-omni", + "mimo-v2.5-pro", + "mimo-v2.5", + "hy4-preview", + "hy3", + "hy3-preview", + "gpt-5.6-luna", + "grok-4.5", + "grok-4.6", + "muse-spark-1.3-contributor", + "muse-spark-1.2-contributor", + ] + expect(Object.keys(opencodeGoModels).sort()).toEqual([...modelIds].sort()) + + for (const modelId of modelIds) { + const info = getOpencodeGoModelInfo(modelId) + expect(info, modelId).toBeDefined() + expect(info?.contextWindow, modelId).toBeGreaterThan(0) + expect(info?.maxTokens, modelId).toBeGreaterThan(0) + expect(info?.supportsImages, modelId).toEqual(expect.any(Boolean)) + expect(info?.supportsPromptCache, modelId).toEqual(expect.any(Boolean)) + expect(info?.inputPrice, modelId).toEqual(expect.any(Number)) + expect(info?.outputPrice, modelId).toEqual(expect.any(Number)) + expect(info?.cacheReadsPrice, modelId).toEqual(expect.any(Number)) + expect(info?.description, modelId).toBeTruthy() + } + }) + it("merges live endpoint values over the native registry for a curated model", () => { const info = parseOpencodeGoModel({ id: "glm-5.1", context_window: 150000, max_output_tokens: 8000 }) // Live values win for volatile fields. diff --git a/src/api/providers/fetchers/opencode-go.ts b/src/api/providers/fetchers/opencode-go.ts index d28b8872f0..6b2ad361b2 100644 --- a/src/api/providers/fetchers/opencode-go.ts +++ b/src/api/providers/fetchers/opencode-go.ts @@ -41,15 +41,12 @@ const opencodeGoModelsResponseSchema = z.object({ * slider, and accurate cost reporting. * * Resolution order for a fully-populated {@link ModelInfo}: - * 1. Start from the native registry ({@link getOpencodeGoModelInfo}) when the - * model ID is curated โ€” this supplies correct context lengths, max tokens, - * capability flags, and pricing sourced from vendor specs. - * 2. Override `contextWindow`, `maxTokens`, and `supportsImages` with values - * from the live `/models` payload when present, so the gateway stays the - * source of truth for those volatile fields. - * 3. Fall back to {@link opencodeGoDefaultModelInfo} for any field still - * missing on an unknown (non-curated) model, ensuring downstream consumers - * always receive a fully-populated object. + * 1. Use the native registry ({@link getOpencodeGoModelInfo}) when the model + * is curated, including its capabilities and pricing. + * 2. Override static limits and image support with live `/models` values when + * present, keeping the gateway authoritative for volatile fields. + * 3. Fall back to {@link opencodeGoDefaultModelInfo} for an unknown model, + * ensuring downstream consumers always receive a fully-populated object. * * @param model - Validated model entry from the `/models` response. * @returns Normalised model metadata suitable for the model picker. diff --git a/src/api/providers/opencode-go.ts b/src/api/providers/opencode-go.ts index bbfa63780a..46ea52a2be 100644 --- a/src/api/providers/opencode-go.ts +++ b/src/api/providers/opencode-go.ts @@ -690,7 +690,7 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio * Performs a non-streaming chat completion and returns the full response text. * * Anthropic-format models are completed via the `/v1/messages` endpoint; - * Responses-format models (gpt-5.6-luna) via `/v1/responses`; all other + * Responses-format models via `/v1/responses`; all other * models use the OpenAI-compatible chat completions endpoint. * * @param prompt - The user prompt to send as a single user message.