Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.
| `modelReasoningEfforts?` | `Record<string, string[]>` | 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<string, boolean>` | Set a model to `false` to stop advertising summaries and strip summary-delivery fields. |
| `modelReasoningSummaryDelivery?` | `Record<string, "sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | Per-model Responses delivery enum; rewrites an existing delivery field. |
| `modelAdapters?` | `Record<string, string>` | 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<string, string>` | 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<string,string[]>` | 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<string, string>` | Provider-wide wire aliases for reasoning labels. |
| `modelReasoningEffortMap?` | `Record<string, Record<string, string>>` | Per-model wire aliases for reasoning labels. |
Expand Down
9 changes: 9 additions & 0 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
23 changes: 23 additions & 0 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions tests/opencode-go-luna-wire.test.ts
Original file line number Diff line number Diff line change
@@ -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> = {}): 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<string, unknown> }> = [];
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
requests.push({
url: String(input),
body: JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>,
});
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);
});
});
Loading