Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions packages/types/src/__tests__/opencode-go.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
OPENCODE_GO_DEFAULT_TEMPERATURE,
OPENCODE_GO_ANTHROPIC_FORMAT_MODELS,
OPENCODE_GO_RESPONSES_FORMAT_MODELS,
OPENCODE_GO_RESPONSES_FORMAT_REGEX,
isOpencodeGoAnthropicFormatModel,
isOpencodeGoResponsesFormatModel,
getOpencodeGoModelInfo,
Expand Down Expand Up @@ -37,12 +38,7 @@ describe("opencode-go registry", () => {
"omen-alpha",
"grok-4.5",
]
const responsesFormatModels = [
"gpt-5.6-luna",
"grok-4.6",
"muse-spark-1.3-contributor",
"muse-spark-1.2-contributor",
]
const responsesFormatModels = ["grok-4.6", "muse-spark-1.3-contributor", "muse-spark-1.2-contributor"]

describe("isOpencodeGoAnthropicFormatModel", () => {
it("classifies Qwen and MiniMax models as Anthropic-format", () => {
Expand Down Expand Up @@ -142,6 +138,23 @@ describe("opencode-go registry", () => {
})

describe("OPENCODE_GO_RESPONSES_FORMAT_MODELS", () => {
it("classifies later numeric GPT models through the Responses regex", () => {
expect(OPENCODE_GO_RESPONSES_FORMAT_REGEX.some((regex) => regex.test("gpt-5.6-luna"))).toBe(true)
expect(OPENCODE_GO_RESPONSES_FORMAT_REGEX.some((regex) => regex.test("gpt-6-luna"))).toBe(true)
expect(isOpencodeGoResponsesFormatModel("gpt-6-luna")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("gpt-7-luna")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("gpt-8")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("gpt-6.1-mini")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("gpt-5.10-foo")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("gpt-10-foo")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("GPT-6-Luna")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("gpt-5.5-pro")).toBe(false)
expect(isOpencodeGoResponsesFormatModel("gpt-5")).toBe(false)
expect(isOpencodeGoResponsesFormatModel("gpt-4.1")).toBe(false)
expect(isOpencodeGoResponsesFormatModel("gpt-6o")).toBe(false)
expect(isOpencodeGoResponsesFormatModel("gpt-oss-20b")).toBe(false)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("contains exactly the Responses-only models", () => {
expect([...OPENCODE_GO_RESPONSES_FORMAT_MODELS].sort()).toEqual([...responsesFormatModels].sort())
})
Expand Down Expand Up @@ -189,6 +202,20 @@ describe("opencode-go registry", () => {
})
})

it("curates gpt-6-luna with its Go pricing and capabilities", () => {
expect(getOpencodeGoModelInfo("gpt-6-luna")).toMatchObject({
maxTokens: 128_000,
supportsMaxTokens: true,
contextWindow: 1_050_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.1,
outputPrice: 0.5,
cacheWritesPrice: 0.13,
cacheReadsPrice: 0.01,
})
})

it("is disjoint from the Anthropic-format set", () => {
for (const id of OPENCODE_GO_RESPONSES_FORMAT_MODELS) {
expect(OPENCODE_GO_ANTHROPIC_FORMAT_MODELS.has(id)).toBe(false)
Expand Down
48 changes: 33 additions & 15 deletions packages/types/src/providers/opencode-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,27 @@
description:
"Muse Spark 1.2 Contributor is Meta's multimodal coding model with a 1M context window. Available via the Opencode Go plan.",
},
"gpt-6-luna": {
maxTokens: 128_000,
contextWindow: 1_050_000,
supportsImages: true,
supportsPromptCache: true,
supportsMaxTokens: true,
supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"],

Check warning on line 667 in packages/types/src/providers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/opencode-go.ts:667: 6 mutation test gaps; example: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
reasoningEffort: "medium",

Check warning on line 668 in packages/types/src/providers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/opencode-go.ts:668: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
inputPrice: 0.1,
outputPrice: 0.5,
cacheWritesPrice: 0.13,

@WebMad WebMad Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the OpenCode documentation, the correct price is $0.125:

https://opencode.ai/docs/go/#usage-limits

cacheReadsPrice: 0.01,
longContextPricing: {

Check warning on line 673 in packages/types/src/providers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/opencode-go.ts:673: Survived ObjectLiteral mutant (replacement: {}). See the job summary for the complete list and resolution guidance.
thresholdTokens: 272_000,
inputPriceMultiplier: 2,
outputPriceMultiplier: 1.5,
cacheWritesPriceMultiplier: 2,
cacheReadsPriceMultiplier: 2,
},
description: "GPT-6 Luna via the OpenCode Go Responses API.",

Check warning on line 680 in packages/types/src/providers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/opencode-go.ts:680: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
},
}

/**
Expand Down Expand Up @@ -694,27 +715,21 @@
* (`/v1/responses`), not the OpenAI-compatible Chat Completions endpoint
* (`/v1/chat/completions`).
*
* 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).
*
* Drive routing from this set rather than from the model ID string so the
* gateway's protocol contract stays explicit, testable, and easy to extend
* when the next Responses-only model lands. Unknown model IDs default to the
* OpenAI-compatible chat completions format.
* The Go gateway maps known non-GPT Responses models explicitly, while GPT-5.6
* and later numeric GPT generations are routed by pattern. The separate `gpt-oss`
* family remains on Chat Completions.
*/
export const OPENCODE_GO_RESPONSES_FORMAT_MODELS = new Set<string>([
// --- OpenAI ---
"gpt-5.6-luna",
// --- xAI ---
"grok-4.6",
// --- Meta ---
"muse-spark-1.3-contributor",
"muse-spark-1.2-contributor",
])

export const OPENCODE_GO_RESPONSES_FORMAT_REGEX: RegExp[] = [
// gpt-5.6 and above are routed by pattern for automatic discovery
/^gpt-(?:5\.(?:[6-9]|\d{2,})|[6-9]\d*(?:[.-]|$)|\d{2,}(?:[.-]|$))/i,

Check warning on line 730 in packages/types/src/providers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/opencode-go.ts:730: 2 mutation test gaps; example: Survived Regex mutant (replacement: /gpt-(?:5\.(?:[6-9]|\d{2,})|[6-9]\d*(?:[.-]|$)|\d{2,}(?:[.-]|$))/i). See the job summary for the complete list and resolution guidance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regex infers the API protocol from the GPT version, but Go documents endpoints per model—not a general rule for all future GPT generations. It also accepts malformed IDs such as gpt-5.6o and gpt-00. For this fix, I’d keep explicit Responses routing for GPT 5.6 Luna and GPT 6 Luna, and handle future-model discovery separately once there is a documented gateway contract

]

/**
* Returns `true` when the given Go-plan model ID must be requested via the
* Anthropic Messages format (`/v1/messages`) rather than the OpenAI-compatible
Expand All @@ -732,7 +747,10 @@
* format, matching the gateway's default routing.
*/
export function isOpencodeGoResponsesFormatModel(modelId: string): boolean {
return OPENCODE_GO_RESPONSES_FORMAT_MODELS.has(modelId)
return (
OPENCODE_GO_RESPONSES_FORMAT_MODELS.has(modelId) ||
OPENCODE_GO_RESPONSES_FORMAT_REGEX.some((regex) => regex.test(modelId))

Check warning on line 752 in packages/types/src/providers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/opencode-go.ts:752: Survived MethodExpression mutant (replacement: OPENCODE_GO_RESPONSES_FORMAT_REGEX.every(regex => regex.test(modelId))). See the job summary for the complete list and resolution guidance.
)
}

/**
Expand Down
9 changes: 7 additions & 2 deletions src/api/providers/__tests__/opencode-go.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ vitest.mock("../fetchers/modelCache", () => ({
"glm-5.1": { ...opencodeGoModels["glm-5.1"] },
// Anthropic-format model used to exercise the /v1/messages path.
"qwen3.7-max": { ...opencodeGoModels["qwen3.7-max"] },
// Responses-format model (Zoo-Code-Org/Zoo-Code#1431).
// Responses-format models (Zoo-Code-Org/Zoo-Code#1431 and #1979).
"gpt-5.6-luna": { ...opencodeGoModels["gpt-5.6-luna"] },
"gpt-6-luna": { ...opencodeGoModels["gpt-6-luna"] },
})
}),
refreshModels: vitest.fn().mockImplementation(function () {
return Promise.resolve({
"glm-5.1": { ...opencodeGoModels["glm-5.1"] },
"qwen3.7-max": { ...opencodeGoModels["qwen3.7-max"] },
"gpt-5.6-luna": { ...opencodeGoModels["gpt-5.6-luna"] },
"gpt-6-luna": { ...opencodeGoModels["gpt-6-luna"] },
})
}),
getModelsFromCache: vitest.fn().mockReturnValue(undefined),
Expand Down Expand Up @@ -1362,8 +1364,11 @@ describe("OpencodeGoHandler", () => {
}).rejects.toThrow("Opencode Go completion error: internal server error")
})

it("classifies documented Responses models as Responses-format and other models as not", () => {
it("classifies documented and numeric Responses models, excluding gpt-oss", () => {
expect(isOpencodeGoResponsesFormatModel("gpt-5.6-luna")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("gpt-6-luna")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("gpt-5.4-nano")).toBe(false)
expect(isOpencodeGoResponsesFormatModel("gpt-oss-20b")).toBe(false)
expect(isOpencodeGoResponsesFormatModel("grok-4.5")).toBe(false)
expect(isOpencodeGoResponsesFormatModel("grok-4.6")).toBe(true)
expect(isOpencodeGoResponsesFormatModel("muse-spark-1.3-contributor")).toBe(true)
Expand Down
51 changes: 51 additions & 0 deletions src/api/providers/fetchers/__tests__/opencode-go.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ describe("Opencode Go Fetchers", () => {
})
})

it("uses Responses defaults for an uncurated numeric gpt model", async () => {
mockedAxios.get.mockResolvedValue({ data: { data: [{ id: "gpt-7-luna" }] } })

const models = await getOpencodeGoModels("k")

expect(models["gpt-7-luna"]).toMatchObject({
contextWindow: 1_050_000,
maxTokens: 128_000,
supportsMaxTokens: true,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"],
reasoningEffort: "medium",
})
})

it("falls back to default context/max tokens for an unknown model when metadata is absent", async () => {
mockedAxios.get.mockResolvedValue({ data: { data: [{ id: "some-unknown-model" }] } })

Expand Down Expand Up @@ -213,6 +229,7 @@ describe("Opencode Go Fetchers", () => {
"hy3",
"hy3-preview",
"gpt-5.6-luna",
"gpt-6-luna",
"grok-4.5",
"grok-4.6",
"muse-spark-1.3-contributor",
Expand Down Expand Up @@ -298,6 +315,40 @@ describe("Opencode Go Fetchers", () => {
expect(info.cacheReadsPrice).toBe(0.26)
})

it("uses Responses defaults when parsing an uncurated numeric gpt model", () => {
const info = parseOpencodeGoModel({ id: "gpt-7-foo" })
expect(info).toMatchObject({
contextWindow: 1_050_000,
maxTokens: 128_000,
supportsMaxTokens: true,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"],
reasoningEffort: "medium",
})
})

it("overrides Responses defaults with live metadata for an uncurated numeric gpt model", () => {
const info = parseOpencodeGoModel({
id: "gpt-7-foo",
context_length: 2_000_000,
max_output_tokens: 64_000,
supports_images: false,
description: "Live GPT model description",
})

expect(info).toMatchObject({
contextWindow: 2_000_000,
maxTokens: 64_000,
supportsImages: false,
description: "Live GPT model description",
supportsMaxTokens: true,
supportsPromptCache: true,
supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"],
reasoningEffort: "medium",
})
})

it("falls back to defaults for an unknown model with no cache pricing", () => {
const info = parseOpencodeGoModel({ id: "x", context_window: 100000, max_tokens: 8000 })
expect(info.supportsPromptCache).toBe(false)
Expand Down
29 changes: 26 additions & 3 deletions src/api/providers/fetchers/opencode-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { z } from "zod"

import type { ModelInfo } from "@roo-code/types"
import { opencodeGoDefaultModelInfo, getOpencodeGoModelInfo } from "@roo-code/types"
import { getOpencodeGoModelInfo, isOpencodeGoResponsesFormatModel, opencodeGoDefaultModelInfo } from "@roo-code/types"

import { throwIfAborted } from "../utils/abort-signal"

Expand Down Expand Up @@ -32,6 +32,19 @@
data: z.array(opencodeGoModelSchema),
})

// Capability defaults for uncurated Responses-format models. This lets newly
// discovered GPT models route and expose their output-token control without
// inventing model-specific pricing; prices remain available only for curated IDs.
const opencodeGoResponsesModelDefaults: ModelInfo = {

Check warning on line 38 in src/api/providers/fetchers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/api/providers/fetchers/opencode-go.ts:38: Survived ObjectLiteral mutant (replacement: {}). See the job summary for the complete list and resolution guidance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new opencodeGoResponsesModelDefaults field sets properties like context, image support, reasoning effort, and others by default.

However, OpenCodeGo supports many different models, and some of them may not support reasoning effort, images, or other capabilities.

Could we reduce the number of default properties, or avoid using defaults here altogether?

maxTokens: 128_000,
contextWindow: 1_050_000,
supportsImages: true,

Check warning on line 41 in src/api/providers/fetchers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/api/providers/fetchers/opencode-go.ts:41: Survived BooleanLiteral mutant (replacement: false). See the job summary for the complete list and resolution guidance.
supportsPromptCache: true,

Check warning on line 42 in src/api/providers/fetchers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/api/providers/fetchers/opencode-go.ts:42: Survived BooleanLiteral mutant (replacement: false). See the job summary for the complete list and resolution guidance.
supportsMaxTokens: true,

Check warning on line 43 in src/api/providers/fetchers/opencode-go.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/api/providers/fetchers/opencode-go.ts:43: Survived BooleanLiteral mutant (replacement: false). See the job summary for the complete list and resolution guidance.
supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"],
reasoningEffort: "medium",
}

/**
* Maps a raw Opencode Go model entry to the internal {@link ModelInfo} shape.
*
Expand All @@ -47,8 +60,8 @@
* 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.
* 3. Use Responses-specific capability defaults for uncurated Responses
* models; otherwise use {@link opencodeGoDefaultModelInfo} for unknowns.
*
* @param model - Validated model entry from the `/models` response.
* @returns Normalised model metadata suitable for the model picker.
Expand All @@ -71,6 +84,16 @@
}
}

if (isOpencodeGoResponsesFormatModel(model.id)) {
return {
...opencodeGoResponsesModelDefaults,
...(liveContextWindow !== undefined && { contextWindow: liveContextWindow }),
...(liveMaxTokens !== undefined && { maxTokens: liveMaxTokens }),
...(liveSupportsImages !== undefined && { supportsImages: liveSupportsImages }),
description: model.description ?? model.name,
}
}

return {
maxTokens: liveMaxTokens ?? opencodeGoDefaultModelInfo.maxTokens,
contextWindow: liveContextWindow ?? opencodeGoDefaultModelInfo.contextWindow,
Expand Down
22 changes: 12 additions & 10 deletions src/api/providers/opencode-go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ type OpencodeGoFormat = "anthropic" | "openai" | "responses"
* - Anthropic Messages (`/v1/messages`) — used by Qwen (qwen3.8-max,
* qwen3.7-max, qwen3.7-plus, qwen3.6-plus) and MiniMax (minimax-m3,
* minimax-m2.7, minimax-m2.5) models.
* - OpenAI Responses (`/v1/responses`) — used by gpt-5.6-luna, whose
* chat-completions adapter fails with an opaque HTTP 500
* (Zoo-Code-Org/Zoo-Code#1431).
* - OpenAI Responses (`/v1/responses`) — used by numeric GPT models from
* GPT-5.6 onward, such as `gpt-5.6-luna` and `gpt-6-luna`. Earlier numeric
* GPT models and the separate `gpt-oss` family use chat completions.
*
* Sending an Anthropic-format model to the chat completions endpoint is
* rejected with `401 Model <id> is not supported for format oa-compat`, so this
Expand Down Expand Up @@ -182,10 +182,10 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio
* reasoning, partial tool calls, and token usage.
*
* Anthropic-format models (Qwen/MiniMax) are streamed via
* {@link streamAnthropicMessage} against `/v1/messages`; Responses-format
* models (gpt-5.6-luna) are streamed via {@link streamResponsesMessage}
* against `/v1/responses`; all other models use the OpenAI-compatible chat
* completions endpoint.
* {@link streamAnthropicMessage} against `/v1/messages`; numeric GPT models
* from GPT-5.6 onward, excluding `gpt-oss`, use
* {@link streamResponsesMessage} against `/v1/responses`; all other models use
* the OpenAI-compatible chat completions endpoint.
*
* For OpenAI-format models that require reasoning_content to be passed back
* during multi-turn tool calls (`preserveReasoning`), messages are
Expand Down Expand Up @@ -292,7 +292,8 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio

/**
* Streams an OpenAI Responses-format completion for Go models that only
* accept the `/v1/responses` endpoint (currently gpt-5.6-luna).
* accept the `/v1/responses` endpoint (numeric GPT model IDs from GPT-5.6
* onward, excluding `gpt-oss`).
*
* Follows the focused xAI handler pattern: the conversation is converted
* with the shared {@link convertToResponsesApiInput} transform, the system
Expand Down Expand Up @@ -690,8 +691,9 @@ 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 via `/v1/responses`; all other
* models use the OpenAI-compatible chat completions endpoint.
* numeric GPT models from GPT-5.6 onward, excluding `gpt-oss`, use
* `/v1/responses`; all other models use the OpenAI-compatible chat completions
* endpoint.
*
* @param prompt - The user prompt to send as a single user message.
* @returns The model's reply text, or an empty string if no content is returned.
Expand Down
Loading