diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index efaef7729..62203e285 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -94,7 +94,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | `modelReasoningEfforts?` | `Record` | Per-model labels. An empty list hides effort control. As with `reasoningEfforts`, each configured `google`-adapter ladder asserts `thinkingLevel` capability; direct and Vertex non-image requests use the flat Gemini path, while Cloud Code Assist sends it under its request envelope. | | `modelSupportsReasoningSummaries?` | `Record` | Set a model to `false` to stop advertising summaries and strip summary-delivery fields. | | `modelReasoningSummaryDelivery?` | `Record` | Per-model Responses delivery enum; rewrites an existing delivery field. | -| `modelAdapters?` | `Record` | Per-model `openai-chat` or `openai-responses` wire override for mixed-wire gateways. Explicit entries beat registry defaults; DeepSeek's preset can select native Responses for `deepseek-v4-flash`, and GitHub Copilot declares Responses-only defaults for its GPT-5 family (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) because those models reject `/chat/completions` for agent traffic. Models without a built-in default (for example `gpt-5.4-nano`) can be opted in here. Single-wire upstream pins and canonical ChatGPT forward reject overrides. | +| `modelAdapters?` | `Record` | Per-model `openai-chat` or `openai-responses` wire override for mixed-wire gateways. Explicit entries beat registry defaults. The OpenCode Go preset selects Responses for `gpt-5.6-luna` while leaving sibling models on their documented wires; DeepSeek can select native Responses for `deepseek-v4-flash`; and GitHub Copilot declares Responses-only defaults for its GPT-5 family (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) because those models reject `/chat/completions` for agent traffic. Models without a built-in default (for example `gpt-5.4-nano`) can be opted in here. Single-wire upstream pins and canonical ChatGPT forward reject overrides. | | `modelPreferHostedTools?` | `Record` | Exact-model opt-in for non-forward Responses gateways that reserve a hosted-tool namespace. Currently accepts only `["image_generation"]`; a matching model must use the `openai-responses` wire and support that hosted tool. It removes colliding client `image_gen` declarations and rewrites their selectors to preserve caller tool choice. For OpenAI API virtual `-pro` models, the selected public ID is matched first and the resolved base wire-model ID is a fallback. `modelAdapters` resolves the public ID first, then the base ID; the second resolution determines the final wire. Other models retain normal alias behavior. | | `reasoningEffortMap?` | `Record` | Provider-wide wire aliases for reasoning labels. | | `modelReasoningEffortMap?` | `Record>` | Per-model wire aliases for reasoning labels. | diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 54c623027..b94483259 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1177,6 +1177,15 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1", authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code", jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…", + /* [Decision Log] + - 목적과 의도: Route GPT 5.6 Luna to the Responses endpoint that OpenCode Go documents for that exact model. + - 기존 구현 및 제약 조건: The provider is mixed-wire but its provider-wide `openai-chat` adapter sent Luna to `/chat/completions`; explicit user `modelAdapters` entries must remain authoritative. + - 검토한 주요 대안: Change the whole provider to Responses; infer the wire from model-family names; add one registry-only exact-model default. + - 선택한 방식: Declare only `gpt-5.6-luna` as `openai-responses` through the existing registry default mechanism. + - 다른 대안 대신 이 방식을 선택한 이유: OpenCode Go documents sibling models on Chat or Anthropic endpoints, and an exact registry default preserves both those routes and explicit opt-out precedence. + - 장점, 단점 및 영향: Luna reaches `/responses` from every inbound surface without changing siblings; a future upstream endpoint change requires an evidence-backed registry update. + */ + modelWireDefaults: { "gpt-5.6-luna": "openai-responses" }, modelContextWindows: { "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW }, modelInputModalities: { "kimi-k3": ["text", "image"] }, modelReasoningEfforts: { diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index e3b052d72..6c88a61d0 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -33,6 +33,29 @@ within their route; neither route falls through to the other. See and before the `/v1/*` guard. Unknown `/v1/*` paths return JSON 404 errors instead of falling through to GUI static serving. +### Mixed-wire provider defaults + +Registry `modelWireDefaults` select an evidence-backed upstream protocol for an exact model without +changing the provider-wide adapter. Explicit, allowed `modelAdapters` configuration always wins, +including an entry that opts the model back into the provider-wide wire. Defaults are applied only +while the configured provider still matches the registry transport, so reusing a preset name for a +different custom destination does not inherit its upstream assumptions. + +OpenCode Go documents `gpt-5.6-luna` on `/zen/go/v1/responses` while sibling models use its Chat or +Anthropic endpoints. The built-in preset therefore selects `openai-responses` only for Luna and +keeps the provider-wide `openai-chat` default for other non-pinned models. This endpoint correction +does not set `modelResponsesUpstreamStreaming`: client `stream: true` remains real upstream +streaming until a current-runtime reproduction justifies a separate bounded-JSON compatibility +policy. + +[Decision Log] +- 목적과 의도: Match OpenCode Go's model-specific Luna endpoint without changing sibling model behavior. +- 기존 구현 및 제약 조건: The preset had one Chat default even though the upstream publishes a mixed Chat, Responses, and Anthropic matrix; operators must retain explicit override precedence. +- 검토한 주요 대안: Move the whole preset to Responses; infer from the model name; declare one exact registry default; also force bounded JSON from an older conditional terminal report. +- 선택한 방식: Use one exact Luna wire default and leave upstream streaming unchanged. +- 다른 대안 대신 이 방식을 선택한 이유: The endpoint mismatch is reproducible from current code and upstream documentation, whereas a current-dev live canary has not established the separate terminal-delivery policy. +- 장점, 단점 및 영향: Luna reaches its documented endpoint across inbound surfaces and explicit opt-out still works; any future stream workaround remains a separately reviewed compatibility decision. + ### Passthrough SSE stream shapes (#314) Native passthrough SSE has TWO shapes, selected per request in diff --git a/tests/opencode-go-luna-wire.test.ts b/tests/opencode-go-luna-wire.test.ts new file mode 100644 index 000000000..72e383abd --- /dev/null +++ b/tests/opencode-go-luna-wire.test.ts @@ -0,0 +1,85 @@ +/** + * OpenCode Go is a mixed-wire provider. Its endpoint matrix assigns GPT 5.6 Luna + * to Responses while its provider-wide default and most sibling models use Chat. + * These tests protect both the exact-model correction and the explicit override + * boundary that lets operators opt out if the upstream changes. + */ +import { afterEach, describe, expect, test } from "bun:test"; +import { providerConfigSeed } from "../src/providers/derive"; +import { getProviderRegistryEntry } from "../src/providers/registry"; +import { resolveWireProtocolOverride } from "../src/server/adapter-resolve"; +import { handleResponses } from "../src/server/responses/core"; +import type { OcxConfig, OcxProviderConfig } from "../src/types"; + +const MODEL = "gpt-5.6-luna"; + +function opencodeGo(overrides: Partial = {}): OcxProviderConfig { + const entry = getProviderRegistryEntry("opencode-go"); + if (!entry) throw new Error("missing opencode-go registry fixture"); + return { ...providerConfigSeed(entry), apiKey: "test-key", ...overrides }; +} + +describe("OpenCode Go GPT 5.6 Luna wire selection (#1482)", () => { + test("uses Responses from every inbound surface", () => { + for (const inbound of ["responses", "chat", "anthropic"] as const) { + expect(resolveWireProtocolOverride("opencode-go", MODEL, opencodeGo(), inbound).adapter) + .toBe("openai-responses"); + } + }); + + test("an explicit Chat override wins from every inbound surface", () => { + const provider = opencodeGo({ modelAdapters: { [MODEL]: "openai-chat" } }); + for (const inbound of ["responses", "chat", "anthropic"] as const) { + expect(resolveWireProtocolOverride("opencode-go", MODEL, provider, inbound).adapter) + .toBe("openai-chat"); + } + }); + + test("other OpenCode Go models keep their existing wire", () => { + for (const inbound of ["responses", "chat", "anthropic"] as const) { + expect(resolveWireProtocolOverride("opencode-go", "glm-5.2", opencodeGo(), inbound).adapter) + .toBe("openai-chat"); + } + }); +}); + +describe("OpenCode Go Luna Responses route (#1482)", () => { + const originalFetch = globalThis.fetch; + afterEach(() => { globalThis.fetch = originalFetch; }); + + test("handleResponses sends Luna to the documented /responses endpoint", async () => { + const requests: Array<{ url: string; body: Record }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + requests.push({ + url: String(input), + body: JSON.parse(String(init?.body ?? "{}")) as Record, + }); + return Response.json({ + id: "resp_opencode_go_luna", + object: "response", + status: "completed", + output: [], + }); + }) as typeof fetch; + + const config = { + providers: { "opencode-go": opencodeGo() }, + } as unknown as OcxConfig; + const response = await handleResponses( + new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: `opencode-go/${MODEL}`, input: "ping", stream: true }), + }), + config, + { model: "", provider: "" }, + { inboundWire: "responses" }, + ); + + expect(response.status).toBe(200); + expect(requests).toHaveLength(1); + expect(requests[0]?.url).toBe("https://opencode.ai/zen/go/v1/responses"); + // The endpoint fix does not silently impose the separate bounded-JSON policy. + expect(requests[0]?.body.stream).toBe(true); + }); +});