From 3a791cd0a496f5d048822bd71345a993df818401 Mon Sep 17 00:00:00 2001 From: Anthony Gallon <64985097+AntzCode@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:17:06 +1300 Subject: [PATCH 1/2] fix(opencode-go): route GPT-5.6+ models through Responses API --- .../types/src/__tests__/opencode-go.test.ts | 31 +++++++++--- packages/types/src/providers/opencode-go.ts | 48 +++++++++++++------ .../providers/__tests__/opencode-go.spec.ts | 9 +++- .../fetchers/__tests__/opencode-go.spec.ts | 30 ++++++++++++ src/api/providers/fetchers/opencode-go.ts | 29 +++++++++-- src/api/providers/opencode-go.ts | 22 +++++---- 6 files changed, 133 insertions(+), 36 deletions(-) diff --git a/packages/types/src/__tests__/opencode-go.test.ts b/packages/types/src/__tests__/opencode-go.test.ts index 7a253d156f..701045eb31 100644 --- a/packages/types/src/__tests__/opencode-go.test.ts +++ b/packages/types/src/__tests__/opencode-go.test.ts @@ -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, @@ -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", () => { @@ -142,6 +138,15 @@ 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-5.5-pro")).toBe(false) + expect(isOpencodeGoResponsesFormatModel("gpt-oss-20b")).toBe(false) + }) + it("contains exactly the Responses-only models", () => { expect([...OPENCODE_GO_RESPONSES_FORMAT_MODELS].sort()).toEqual([...responsesFormatModels].sort()) }) @@ -189,6 +194,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) diff --git a/packages/types/src/providers/opencode-go.ts b/packages/types/src/providers/opencode-go.ts index 17120858bc..f74b3b666d 100644 --- a/packages/types/src/providers/opencode-go.ts +++ b/packages/types/src/providers/opencode-go.ts @@ -658,6 +658,27 @@ export const opencodeGoModels: Record = { 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"], + reasoningEffort: "medium", + inputPrice: 0.1, + outputPrice: 0.5, + cacheWritesPrice: 0.13, + cacheReadsPrice: 0.01, + longContextPricing: { + thresholdTokens: 272_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 1.5, + cacheWritesPriceMultiplier: 2, + cacheReadsPriceMultiplier: 2, + }, + description: "GPT-6 Luna via the OpenCode Go Responses API.", + }, } /** @@ -694,27 +715,21 @@ 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. 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([ - // --- 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, +] + /** * Returns `true` when the given Go-plan model ID must be requested via the * Anthropic Messages format (`/v1/messages`) rather than the OpenAI-compatible @@ -732,7 +747,10 @@ export function isOpencodeGoAnthropicFormatModel(modelId: string): boolean { * 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)) + ) } /** diff --git a/src/api/providers/__tests__/opencode-go.spec.ts b/src/api/providers/__tests__/opencode-go.spec.ts index 2f859a8c43..4000d71c6b 100644 --- a/src/api/providers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/__tests__/opencode-go.spec.ts @@ -40,8 +40,9 @@ 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 () { @@ -49,6 +50,7 @@ vitest.mock("../fetchers/modelCache", () => ({ "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), @@ -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) diff --git a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts index b39e4e772c..3754e13948 100644 --- a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts @@ -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" }] } }) @@ -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", @@ -298,6 +315,19 @@ 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("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) diff --git a/src/api/providers/fetchers/opencode-go.ts b/src/api/providers/fetchers/opencode-go.ts index 68ac3d2d45..1e140e3be9 100644 --- a/src/api/providers/fetchers/opencode-go.ts +++ b/src/api/providers/fetchers/opencode-go.ts @@ -2,7 +2,7 @@ import axios from "axios" 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" @@ -32,6 +32,19 @@ const opencodeGoModelsResponseSchema = z.object({ 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 = { + maxTokens: 128_000, + contextWindow: 1_050_000, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", +} + /** * Maps a raw Opencode Go model entry to the internal {@link ModelInfo} shape. * @@ -47,8 +60,8 @@ const opencodeGoModelsResponseSchema = z.object({ * 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. @@ -71,6 +84,16 @@ export const parseOpencodeGoModel = (model: OpencodeGoModel): ModelInfo => { } } + 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, diff --git a/src/api/providers/opencode-go.ts b/src/api/providers/opencode-go.ts index 46ea52a2be..7f45e62778 100644 --- a/src/api/providers/opencode-go.ts +++ b/src/api/providers/opencode-go.ts @@ -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 is not supported for format oa-compat`, so this @@ -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 @@ -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 @@ -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. From 821fff95eda0b9c27fed348c03844f7ee01ca530 Mon Sep 17 00:00:00 2001 From: Anthony Gallon <64985097+AntzCode@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:14:05 +1300 Subject: [PATCH 2/2] fix(opencode-go): route GPT-5.6+ models through Responses API Address CodeRabbit and Codecov findings from PR #1988 Fixes #1979 --- .../types/src/__tests__/opencode-go.test.ts | 8 +++++++ .../fetchers/__tests__/opencode-go.spec.ts | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/types/src/__tests__/opencode-go.test.ts b/packages/types/src/__tests__/opencode-go.test.ts index 701045eb31..ec66750bdb 100644 --- a/packages/types/src/__tests__/opencode-go.test.ts +++ b/packages/types/src/__tests__/opencode-go.test.ts @@ -143,7 +143,15 @@ describe("opencode-go registry", () => { 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) }) diff --git a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts index 3754e13948..a229d20906 100644 --- a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts @@ -328,6 +328,27 @@ describe("Opencode Go Fetchers", () => { }) }) + 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)