diff --git a/packages/types/src/__tests__/opencode-go.test.ts b/packages/types/src/__tests__/opencode-go.test.ts index 7a253d156f..2f434939a1 100644 --- a/packages/types/src/__tests__/opencode-go.test.ts +++ b/packages/types/src/__tests__/opencode-go.test.ts @@ -39,6 +39,7 @@ describe("opencode-go registry", () => { ] const responsesFormatModels = [ "gpt-5.6-luna", + "gpt-6-luna", "grok-4.6", "muse-spark-1.3-contributor", "muse-spark-1.2-contributor", @@ -189,6 +190,30 @@ describe("opencode-go registry", () => { }) }) + it("curates gpt-6-luna with its documented Responses capabilities and pricing", () => { + expect(getOpencodeGoModelInfo("gpt-6-luna")).toMatchObject({ + maxTokens: 128_000, + supportsMaxTokens: true, + supportsTemperature: false, + contextWindow: 1_050_000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", + inputPrice: 0.1, + outputPrice: 0.5, + cacheWritesPrice: 0.125, + cacheReadsPrice: 0.01, + longContextPricing: { + thresholdTokens: 272_000, + inputPriceMultiplier: 2, + outputPriceMultiplier: 1.5, + cacheWritesPriceMultiplier: 2, + cacheReadsPriceMultiplier: 2, + }, + }) + }) + 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..49f190ccd6 100644 --- a/packages/types/src/providers/opencode-go.ts +++ b/packages/types/src/providers/opencode-go.ts @@ -565,8 +565,8 @@ export const opencodeGoModels: Record = { }, // --- OpenAI Responses --- - // Luna is curated here because the Go gateway's model catalogue does not - // currently provide its capability metadata. These values intentionally + // Luna models are curated here because the Go gateway's model catalogue does + // not currently provide their capability metadata. These values intentionally // describe the Go Responses route, not the OpenAI-native or Codex routes. "gpt-5.6-luna": { maxTokens: 128_000, @@ -590,6 +590,30 @@ export const opencodeGoModels: Record = { description: "GPT-5.6 Luna is OpenAI's fast reasoning 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, + // The OpenCode Go Responses model silently drops `temperature`, so the + // UI temperature control would otherwise render as a dead control. + supportsTemperature: false, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", + inputPrice: 0.1, + outputPrice: 0.5, + cacheWritesPrice: 0.125, + 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.", + }, "grok-4.5": { maxTokens: 500_000, contextWindow: 500_000, @@ -708,6 +732,7 @@ export const OPENCODE_GO_ANTHROPIC_FORMAT_MODELS = new Set([ export const OPENCODE_GO_RESPONSES_FORMAT_MODELS = new Set([ // --- OpenAI --- "gpt-5.6-luna", + "gpt-6-luna", // --- xAI --- "grok-4.6", // --- Meta --- diff --git a/src/api/providers/__tests__/opencode-go.spec.ts b/src/api/providers/__tests__/opencode-go.spec.ts index 2f859a8c43..26fcf98398 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 issue #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), @@ -833,7 +835,7 @@ describe("OpencodeGoHandler", () => { }) }) - describe("Responses-format models (gpt-5.6-luna)", () => { + describe("Responses-format models (gpt-5.6-luna, gpt-6-luna)", () => { // gpt-5.6-luna is Responses-only on the Go gateway: its chat-completions // adapter fails with an opaque HTTP 500 (Zoo-Code-Org/Zoo-Code#1431), // so the handler must route it through /v1/responses and never fall @@ -842,6 +844,10 @@ describe("OpencodeGoHandler", () => { opencodeGoApiKey: "test-key", opencodeGoModelId: "gpt-5.6-luna", } + const gpt6LunaOptions: ApiHandlerOptions = { + opencodeGoApiKey: "test-key", + opencodeGoModelId: "gpt-6-luna", + } beforeEach(() => { mockResponsesCreate.mockImplementation(async () => @@ -982,6 +988,21 @@ describe("OpencodeGoHandler", () => { expect(mockAnthropicCreate).not.toHaveBeenCalled() }) + it("routes gpt-6-luna streaming through responses.create only", async () => { + const handler = new OpencodeGoHandler(gpt6LunaOptions) + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + + const chunks = await collectStream(handler.createMessage("sys", messages)) + + expect(chunks).toContainEqual({ type: "text", text: "Hello" }) + expect(mockResponsesCreate).toHaveBeenCalledWith( + expect.objectContaining({ model: "gpt-6-luna", stream: true }), + expect.anything(), + ) + expect(mockCreate).not.toHaveBeenCalled() + expect(mockAnthropicCreate).not.toHaveBeenCalled() + }) + it("streams text and reasoning chunks from the Responses event stream", async () => { const handler = new OpencodeGoHandler(lunaOptions) const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] @@ -1315,6 +1336,20 @@ describe("OpencodeGoHandler", () => { expect(callArgs.temperature).toBeUndefined() }) + it("completePrompt routes gpt-6-luna through responses.create only", async () => { + mockResponsesCreate.mockResolvedValue({ output_text: "Luna 6 response" }) + const handler = new OpencodeGoHandler(gpt6LunaOptions) + + expect(await handler.completePrompt("ping")).toBe("Luna 6 response") + + expect(mockResponsesCreate).toHaveBeenCalledWith( + expect.objectContaining({ model: "gpt-6-luna", store: false }), + expect.anything(), + ) + expect(mockCreate).not.toHaveBeenCalled() + expect(mockAnthropicCreate).not.toHaveBeenCalled() + }) + it("forwards Responses-specific max_output_tokens and reasoning in completePrompt", async () => { mockResponsesCreate.mockResolvedValue({ output_text: "Hello!" }) const handler = new OpencodeGoHandler({ ...lunaOptions, includeMaxTokens: true, modelMaxTokens: 7_500 }) @@ -1364,6 +1399,7 @@ describe("OpencodeGoHandler", () => { it("classifies documented Responses models as Responses-format and other models as not", () => { expect(isOpencodeGoResponsesFormatModel("gpt-5.6-luna")).toBe(true) + expect(isOpencodeGoResponsesFormatModel("gpt-6-luna")).toBe(true) 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..e51a21f115 100644 --- a/src/api/providers/fetchers/__tests__/opencode-go.spec.ts +++ b/src/api/providers/fetchers/__tests__/opencode-go.spec.ts @@ -75,6 +75,30 @@ describe("Opencode Go Fetchers", () => { }) }) + it("uses curated gpt-6-luna metadata when the live model omits metadata", async () => { + mockedAxios.get.mockResolvedValue({ data: { data: [{ id: "gpt-6-luna" }] } }) + + const models = await getOpencodeGoModels("k") + + expect(models["gpt-6-luna"]).toMatchObject({ + contextWindow: 1_050_000, + maxTokens: 128_000, + supportsImages: true, + supportsPromptCache: true, + supportsMaxTokens: true, + // The Opencode Go Responses gateway rejects `temperature` for + // `gpt-6-luna`, so the curated registry must opt out so the UI + // hides the dead control rather than rendering it. + supportsTemperature: false, + supportsReasoningEffort: ["none", "low", "medium", "high", "xhigh", "max"], + reasoningEffort: "medium", + inputPrice: 0.1, + outputPrice: 0.5, + cacheWritesPrice: 0.125, + cacheReadsPrice: 0.01, + }) + }) + 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 +237,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", @@ -248,6 +273,20 @@ describe("Opencode Go Fetchers", () => { expect(info.inputPrice).toBe(1.4) }) + it("keeps gpt-6-luna live limits authoritative over its curated metadata", () => { + const info = parseOpencodeGoModel({ + id: "gpt-6-luna", + context_window: 1_000_000, + max_output_tokens: 64_000, + }) + + expect(info.contextWindow).toBe(1_000_000) + expect(info.maxTokens).toBe(64_000) + expect(info.supportsImages).toBe(true) + expect(info.inputPrice).toBe(0.1) + expect(info.cacheWritesPrice).toBe(0.125) + }) + it("uses native registry defaults when the live payload omits volatile fields", () => { const info = parseOpencodeGoModel({ id: "deepseek-v4-flash" }) const native = getOpencodeGoModelInfo("deepseek-v4-flash")! diff --git a/src/api/providers/opencode-go.ts b/src/api/providers/opencode-go.ts index 46ea52a2be..6fcb78d865 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 `gpt-5.6-luna` and + * `gpt-6-luna`, whose chat-completions adapters are not supported by the + * Go gateway. * * Sending an Anthropic-format model to the chat completions endpoint is * rejected with `401 Model is not supported for format oa-compat`, so this @@ -183,7 +183,7 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio * * 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} + * models (`gpt-5.6-luna` and `gpt-6-luna`) are streamed via {@link streamResponsesMessage} * against `/v1/responses`; all other models use the OpenAI-compatible chat * completions endpoint. * @@ -292,7 +292,7 @@ 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 (`gpt-5.6-luna` and `gpt-6-luna`). * * Follows the focused xAI handler pattern: the conversation is converted * with the shared {@link convertToResponsesApiInput} transform, the system