Skip to content

Commit 8c3ae1e

Browse files
feat(opencode-go): native model params, anthropic-format routing, and context-token fix (#652)
* opencode go * Update default to GLM 5.2 * Use Andthropic messages for minmax and qwen * fix context calculation with minmax * Update coverage * add opencode test * fix typescript error * fix(opencode-go): address PR #652 review feedback - Type streamAnthropicMessage's info param as ModelInfo and drop the force-cast so calculateApiCostAnthropic can no longer silently return /bin/sh when pricing fields are absent. - Wrap pre-stream Anthropic-format errors (401/429/network) with the 'Opencode Go completion error:' prefix for consistency with completePrompt. - Clarify the registry doc: supportsPromptCache controls client-side cache_control injection (Anthropic path) only; OA-compat models price server-side cached_tokens via cacheReadsPrice regardless of the flag (MiMo stays false, matching the dedicated mimo provider). - Add cacheWritesPrice (0.375) to minimax-m3 so its cache writes are billed, matching M2.5/M2.7. - Add supportsMaxTokens to DeepSeek V4 models so the max-output slider is exposed like GLM. - Strengthen the streaming cost test to assert totalCost > 0, and add registry invariants for MiniMax cache-write pricing and DeepSeek supportsMaxTokens plus a streaming error-wrapping test.
1 parent b747d56 commit 8c3ae1e

9 files changed

Lines changed: 1687 additions & 43 deletions

File tree

.changeset/add-glm-5-2-support.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
---
44

55
Add GLM-5.2 support with High/Max `reasoning_effort` tiers. The default effort is High (deep reasoning stays opt-in), Max is selected only when the user explicitly picks it, and the parameter is omitted entirely when reasoning is disabled.
6+
7+
Also refines the Opencode Go provider per review: bill MiniMax M3 cache writes (`cacheWritesPrice`), expose the max-output slider for DeepSeek V4 models (`supportsMaxTokens`), wrap pre-stream Anthropic-format errors with the provider prefix, and type the Anthropic streaming path's model info as `ModelInfo` so cost calculation can no longer silently return `$0`.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import {
2+
opencodeGoDefaultModelId,
3+
opencodeGoDefaultModelInfo,
4+
opencodeGoModels,
5+
OPENCODE_GO_DEFAULT_TEMPERATURE,
6+
OPENCODE_GO_ANTHROPIC_FORMAT_MODELS,
7+
isOpencodeGoAnthropicFormatModel,
8+
getOpencodeGoModelInfo,
9+
} from "../providers/opencode-go.js"
10+
11+
describe("opencode-go registry", () => {
12+
const anthropicFormatModels = [
13+
"qwen3.7-max",
14+
"qwen3.7-plus",
15+
"qwen3.6-plus",
16+
"minimax-m3",
17+
"minimax-m2.7",
18+
"minimax-m2.5",
19+
]
20+
const openaiFormatModels = [
21+
"glm-5",
22+
"glm-5.1",
23+
"glm-5.2",
24+
"kimi-k2.5",
25+
"kimi-k2.6",
26+
"mimo-v2.5",
27+
"mimo-v2.5-pro",
28+
"deepseek-v4-pro",
29+
"deepseek-v4-flash",
30+
]
31+
32+
describe("isOpencodeGoAnthropicFormatModel", () => {
33+
it("classifies Qwen and MiniMax models as Anthropic-format", () => {
34+
for (const id of anthropicFormatModels) {
35+
expect(isOpencodeGoAnthropicFormatModel(id)).toBe(true)
36+
}
37+
})
38+
39+
it("classifies GLM/Kimi/MiMo/DeepSeek models as OpenAI-compatible", () => {
40+
for (const id of openaiFormatModels) {
41+
expect(isOpencodeGoAnthropicFormatModel(id)).toBe(false)
42+
}
43+
})
44+
45+
it("defaults unknown model IDs to the OpenAI-compatible format", () => {
46+
expect(isOpencodeGoAnthropicFormatModel("some-future-model")).toBe(false)
47+
expect(isOpencodeGoAnthropicFormatModel("")).toBe(false)
48+
})
49+
})
50+
51+
describe("getOpencodeGoModelInfo", () => {
52+
it("returns the native ModelInfo for a curated model", () => {
53+
const info = getOpencodeGoModelInfo("qwen3.7-max")
54+
expect(info).toBeDefined()
55+
expect(info?.maxTokens).toBe(65_536)
56+
expect(info?.contextWindow).toBe(1_000_000)
57+
expect(info?.supportsPromptCache).toBe(true)
58+
})
59+
60+
it("returns undefined for an unknown model ID", () => {
61+
expect(getOpencodeGoModelInfo("not-a-real-go-model")).toBeUndefined()
62+
})
63+
})
64+
65+
describe("OPENCODE_GO_ANTHROPIC_FORMAT_MODELS", () => {
66+
it("contains exactly the Qwen and MiniMax models", () => {
67+
expect([...OPENCODE_GO_ANTHROPIC_FORMAT_MODELS].sort()).toEqual([...anthropicFormatModels].sort())
68+
})
69+
70+
// The PR description calls out that the format-routing set must stay in
71+
// sync with the Go model table — every routed model must have a native
72+
// registry entry so capability flags and pricing resolve correctly.
73+
it("every Anthropic-format model has a native registry entry", () => {
74+
for (const id of OPENCODE_GO_ANTHROPIC_FORMAT_MODELS) {
75+
expect(opencodeGoModels[id]).toBeDefined()
76+
}
77+
})
78+
})
79+
80+
describe("opencodeGoModels registry invariants", () => {
81+
it("every entry has a positive maxTokens and contextWindow", () => {
82+
for (const [id, info] of Object.entries(opencodeGoModels)) {
83+
expect(info.maxTokens).toBeGreaterThan(0)
84+
expect(info.contextWindow).toBeGreaterThan(0)
85+
// Sanity: max output must not exceed the context window.
86+
expect(info.maxTokens).toBeLessThanOrEqual(info.contextWindow)
87+
void id
88+
}
89+
})
90+
91+
it("every entry declares supportsImages", () => {
92+
for (const info of Object.values(opencodeGoModels)) {
93+
expect(typeof info.supportsImages).toBe("boolean")
94+
}
95+
})
96+
97+
it("models with an array supportsReasoningEffort expose a non-empty allow-list", () => {
98+
for (const info of Object.values(opencodeGoModels)) {
99+
if (Array.isArray(info.supportsReasoningEffort)) {
100+
expect(info.supportsReasoningEffort.length).toBeGreaterThan(0)
101+
}
102+
}
103+
})
104+
105+
it("every Anthropic-format model with prompt-cache injection declares a cacheWritesPrice", () => {
106+
// MiniMax/Qwen route through /v1/messages with client-side
107+
// cache_control breakpoints, so cache_creation_input_tokens are
108+
// reported and billed — each must carry a cacheWritesPrice or the
109+
// write cost is silently reported as $0.
110+
for (const id of OPENCODE_GO_ANTHROPIC_FORMAT_MODELS) {
111+
const info = getOpencodeGoModelInfo(id)
112+
expect(info).toBeDefined()
113+
if (info?.supportsPromptCache) {
114+
expect(info.cacheWritesPrice).toBeDefined()
115+
expect(info.cacheReadsPrice).toBeDefined()
116+
}
117+
}
118+
})
119+
120+
it("DeepSeek entries expose supportsMaxTokens so the max-output slider is available", () => {
121+
expect(getOpencodeGoModelInfo("deepseek-v4-pro")?.supportsMaxTokens).toBe(true)
122+
expect(getOpencodeGoModelInfo("deepseek-v4-flash")?.supportsMaxTokens).toBe(true)
123+
})
124+
})
125+
126+
describe("defaults", () => {
127+
it("the default model id is a curated OpenAI-compatible model", () => {
128+
expect(opencodeGoDefaultModelId).toBe("glm-5.2")
129+
expect(opencodeGoModels[opencodeGoDefaultModelId]).toBeDefined()
130+
expect(isOpencodeGoAnthropicFormatModel(opencodeGoDefaultModelId)).toBe(false)
131+
})
132+
133+
it("exposes a fully-populated default ModelInfo fallback", () => {
134+
expect(opencodeGoDefaultModelInfo.maxTokens).toBeGreaterThan(0)
135+
expect(opencodeGoDefaultModelInfo.contextWindow).toBeGreaterThan(0)
136+
expect(opencodeGoDefaultModelInfo.supportsPromptCache).toBe(false)
137+
expect(opencodeGoDefaultModelInfo.description).toBeTruthy()
138+
})
139+
140+
it("exposes a deterministic default temperature", () => {
141+
expect(OPENCODE_GO_DEFAULT_TEMPERATURE).toBe(0)
142+
})
143+
})
144+
})

packages/types/src/__tests__/provider-settings.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,32 @@ describe("getApiProtocol", () => {
5353
})
5454
})
5555

56+
describe("Opencode Go provider", () => {
57+
it("should return 'anthropic' for opencode-go Anthropic-format models (Qwen/MiniMax)", () => {
58+
expect(getApiProtocol("opencode-go", "qwen3.7-max")).toBe("anthropic")
59+
expect(getApiProtocol("opencode-go", "qwen3.7-plus")).toBe("anthropic")
60+
expect(getApiProtocol("opencode-go", "qwen3.6-plus")).toBe("anthropic")
61+
expect(getApiProtocol("opencode-go", "minimax-m3")).toBe("anthropic")
62+
expect(getApiProtocol("opencode-go", "minimax-m2.7")).toBe("anthropic")
63+
expect(getApiProtocol("opencode-go", "minimax-m2.5")).toBe("anthropic")
64+
})
65+
66+
it("should return 'openai' for opencode-go OpenAI-format models (GLM/DeepSeek/etc.)", () => {
67+
expect(getApiProtocol("opencode-go", "glm-5.2")).toBe("openai")
68+
expect(getApiProtocol("opencode-go", "deepseek-v4-pro")).toBe("openai")
69+
expect(getApiProtocol("opencode-go", "kimi-k2.5")).toBe("openai")
70+
expect(getApiProtocol("opencode-go", "mimo-v2.5")).toBe("openai")
71+
})
72+
73+
it("should return 'openai' for opencode-go without a model", () => {
74+
expect(getApiProtocol("opencode-go")).toBe("openai")
75+
})
76+
77+
it("should return 'openai' for opencode-go with an unknown model id", () => {
78+
expect(getApiProtocol("opencode-go", "some-future-model")).toBe("openai")
79+
})
80+
})
81+
5682
describe("Other providers", () => {
5783
it("should return 'openai' for non-anthropic providers regardless of model", () => {
5884
expect(getApiProtocol("openrouter", "claude-3-opus")).toBe("openai")

packages/types/src/provider-settings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
internationalZAiModels,
2222
minimaxModels,
2323
mimoModels,
24+
isOpencodeGoAnthropicFormatModel,
2425
} from "./providers/index.js"
2526

2627
/**
@@ -595,6 +596,17 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
595596
return "anthropic"
596597
}
597598

599+
// Opencode Go routes a subset of its models (Qwen, MiniMax) through the
600+
// Anthropic Messages wire format (`/v1/messages`), which reports usage in
601+
// Anthropic style: `input_tokens` excludes cache tokens, with separate
602+
// `cache_creation_input_tokens` / `cache_read_input_tokens` fields. These
603+
// models must use the anthropic protocol so token/cost aggregation adds the
604+
// cache tokens back into the input total — otherwise the cached prefix is
605+
// dropped from `contextTokens`, undercounting context-window usage.
606+
if (provider && provider === "opencode-go" && modelId && isOpencodeGoAnthropicFormatModel(modelId)) {
607+
return "anthropic"
608+
}
609+
598610
return "openai"
599611
}
600612

0 commit comments

Comments
 (0)