Skip to content

[Provider] Route OpenCode Go GPT-5.6 Luna through Responses with bounded terminal delivery #1482

Description

@novelKR

Client or integration

Codex CLI

Provider or upstream service

OpenCode Go

OpenCodex version

dev@849ab5e35fdacddb4d47416c00a315111c78f97e (inspected on 2026-08-12)

Endpoint or capability

POST /v1/responses, model-specific wire selection, streaming terminal delivery, function-tool calls, and tool-result continuations for opencode-go/gpt-5.6-luna.

Current behaviour

OpenCode Go documents a model-specific protocol matrix. GPT 5.6 Luna is assigned to https://opencode.ai/zen/go/v1/responses with @ai-sdk/openai, while the same provider exposes other models through Chat Completions or Anthropic Messages.

At the inspected dev commit, however, OpenCodex's built-in opencode-go registry entry is provider-wide adapter: "openai-chat" and has no modelWireDefaults entry for gpt-5.6-luna. Unless a user adds an explicit modelAdapters override, the built-in route therefore selects the Chat Completions adapter and posts to /chat/completions instead of Luna's documented OpenCode Go Responses endpoint.

There is a second, distinct compatibility gap after correcting that mapping. The motivating reproduction registered the same OpenCode Go origin as a custom openai-responses provider and selected gpt-5.6-luna. In affected rich-history/tool turns:

  • the upstream request returned HTTP 200;
  • OpenCodex observed response.completed and reported usage within a few seconds;
  • Native Codex did not commit a usable terminal event;
  • the same logical turn was sampled again near the 300-second stream-idle boundary or was manually interrupted.

The failure was conditional: fresh minimal turns and some function-call continuations could complete. A control path in which Native Codex connected directly to the same OpenCode Go Responses upstream, without OpenCodex or the deployment relay, completed consecutive turns and tool round-trips.

The two gaps should therefore remain separate in diagnosis:

  1. the built-in opencode-go preset selects the wrong wire for Luna;
  2. once Luna is correctly routed through Responses, OpenCodex's streaming relay/client-delivery path can still fail to deliver a terminal that Native Codex commits.

This report does not claim that OpenAI's GPT-5.6 Luna universally lacks Chat Completions support. Official OpenAI documentation lists both Chat Completions and Responses. The provider-specific mismatch is that OpenCode Go itself documents its Luna route as Responses, and official OpenAI model guidance recommends Responses for reasoning, tool-calling, and multi-turn workflows.

Expected behaviour

  • Built-in opencode-go/gpt-5.6-luna selects the OpenAI Responses wire and reaches /zen/go/v1/responses.
  • Other OpenCode Go models retain their documented existing wires.
  • An explicit user modelAdapters override retains the current precedence and remains able to opt out.
  • Function and custom tool calls preserve response IDs, item IDs, call_id, names, argument strings, usage, and the matching function_call_output continuation.
  • A client stream:true request always receives a valid terminal Responses lifecycle that Native Codex can commit.
  • OpenCodex does not replay a model request, switch to another wire after dispatch, or execute a tool more than once.
  • Malformed, oversized, stalled, or protocol-mismatched upstream responses fail closed.

Minimal redacted request or reproduction

# OpenCodex listens on loopback. The OpenCode Go key is configured through the
# normal provider credential flow and is intentionally omitted here.

curl --no-buffer http://127.0.0.1:10100/v1/responses \
  -H 'content-type: application/json' \
  --data-binary '{
    "model": "opencode-go/gpt-5.6-luna",
    "store": false,
    "stream": true,
    "input": [{
      "role": "user",
      "content": [{
        "type": "input_text",
        "text": "Call diagnostic_echo exactly once with marker LUNA_RESPONSES_TOOL_42."
      }]
    }],
    "tools": [{
      "type": "function",
      "name": "diagnostic_echo",
      "description": "Return the marker unchanged.",
      "parameters": {
        "type": "object",
        "properties": {
          "marker": { "type": "string" }
        },
        "required": ["marker"],
        "additionalProperties": false
      }
    }],
    "tool_choice": "required"
  }'

# Capture the returned response id and function call_id, execute the tool once,
# then submit the result through the same conversation. Native Codex normally
# performs this continuation automatically.

curl --no-buffer http://127.0.0.1:10100/v1/responses \
  -H 'content-type: application/json' \
  --data-binary '{
    "model": "opencode-go/gpt-5.6-luna",
    "store": false,
    "stream": true,
    "previous_response_id": "<REDACTED_RESPONSE_ID>",
    "input": [{
      "type": "function_call_output",
      "call_id": "<SAME_REDACTED_CALL_ID>",
      "output": "LUNA_RESPONSES_TOOL_42"
    }]
  }'

# Static route check on the current built-in preset:
# expected upstream path for Luna: /zen/go/v1/responses
# current preset path without an explicit override: /zen/go/v1/chat/completions

For the original client-level reproduction, run the equivalent request in one persistent Native Codex conversation for four turns, including one forced tool turn and one later turn that references both prior text and the tool result.

Actual response or error

# Redacted OpenCodex-side outcome from an affected rich-history turn:
HTTP status:          200
upstream terminal:    completed
usage:                reported
upstream duration:    approximately 2-7 seconds

# Client-side outcome:
committed terminal:   not observed
next sampling/retry:  approximately 302-304 seconds later
final result:         retried, remained pending, or was manually interrupted

No prompt body, model output, API key, Authorization header, account identifier, host name, request ID, conversation ID, or exact timestamp is included.

The control path completed four consecutive Native Codex turns against the same OpenCode Go Responses upstream without OpenCodex. The external compatibility relay described below also completed local and external four-turn/tool scenarios without an extra request or duplicate tool execution. These controls narrow the failure boundary but do not prove one specific Bun primitive as the sole root cause.

Upstream documentation

Suggested mapping or implementation notes

Required: correct the built-in model wire

The smallest model-specific registry correction is:

modelWireDefaults: {
  "gpt-5.6-luna": "openai-responses",
},

This should use the existing mixed-wire resolution contract. It must not change the provider-wide default for Grok, GLM, Kimi, DeepSeek, MiMo, MiniMax, or Qwen models, and an explicit allowed modelAdapters entry must continue to win.

Strongly recommended initial compatibility policy

Correcting only the endpoint exposes Luna to the Responses relay path where the conditional terminal-delivery failure was observed. For the initial built-in rollout, connect Luna to the existing bounded upstream policy:

modelResponsesUpstreamStreaming: {
  "gpt-5.6-luna": false,
},

OpenCodex already has the relevant primitives on current dev:

  • resolve the effective per-model Responses policy;
  • preserve the client's original stream:true intent;
  • send the upstream Responses request once with stream:false;
  • read the completed JSON through the existing total-size and stall bounds;
  • validate and apply existing client-facing item/model repairs;
  • reframe it as canonical client SSE:
    response.created -> response.output_item.done* ->
    response.completed | response.failed | response.incomplete -> [DONE];
  • provide the equivalent event framing for Responses WebSocket clients.

This is structurally the same workaround proven in the deployment-specific Go relay:

exact Luna policy
  -> client stream:true
  -> one upstream Responses request with stream:false
  -> bounded capture
  -> strict completed-JSON validation
  -> canonical terminal SSE
  -> Native Codex

The Go relay also enforced no model-request replay, no automatic local/external fallback, exact-model opt-in, bounded request/response storage, and exact preservation of tool identifiers and arguments. Its local and external four-turn canaries both produced the expected logical request sequence 1 -> 2 -> 4 -> 5, one tool execution, zero retries, one terminal per logical request, and idle resource counters afterward. This is supporting compatibility evidence, not an upstream merge requirement.

The web-search/tool iteration path fixed in #1143 must continue to honor the same effective upstream-streaming decision. A hosted tool loop must not reintroduce stream:true after the model policy selected bounded JSON.

Acceptance criteria

  • Built-in Luna reaches /responses and never /chat/completions unless the user explicitly overrides the model adapter.
  • Other OpenCode Go models remain unchanged.
  • HTTP/SSE and Responses WebSocket both terminate correctly.
  • A four-turn persistent conversation preserves prior text and tool results.
  • A forced function call executes exactly once and its function_call_output continues with the same call_id.
  • Native Codex retry count is zero and exactly one terminal event is emitted per logical request.
  • Hosted web search preserves the bounded upstream decision.
  • Unexpected SSE after stream:false, invalid JSON, unknown terminal status, oversize, timeout, and cancellation fail once without replay or alternate-wire fallback.

After a protocol-safe one-reader streaming path has been qualified for this provider/runtime, maintainers can reconsider whether the bounded policy should remain the default. That later optimization should not block correcting the documented wire now.

Additional context and attachments

  • Related diagnostic and implementation draft by the same reporter: feat(responses): add an opt-in bounded JSON fallback for custom providers #1367. It proposed an opt-in custom-provider bounded fallback and contains the original cross-platform analysis. It is related evidence, not a claim that this built-in preset issue is already fixed.
  • Luna compatibility policy bypassed by web-search loop forcing upstream streaming #1143 is a closed, related tool-loop policy propagation issue. It is not a duplicate of the missing built-in Luna wire mapping.
  • [architecture][memory] Make 32 concurrent tool-recall sessions protocol-safe and memory-bounded #820 tracks the longer-term protocol-safe, memory-bounded one-reader architecture. It is not a prerequisite for the localized built-in preset correction and bounded fallback.
  • Current dev registry inspected for this report:
    https://github.com/lidge-jun/opencodex/blob/849ab5e35fdacddb4d47416c00a315111c78f97e/src/providers/registry.ts
  • Current bounded Responses policy and JSON-to-SSE path:
    // Transport-neutral reliability policy (#875): applies to any Responses
    // upstream whose final adapter is openai-responses, not only WS turns.
    const responsesUpstreamStreaming = providerModelResponsesUpstreamStreaming(
    route.providerName,
    route.provider,
    route.modelId,
    );
    // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
    // this request will actually use (#404).
    route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
    if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
    logCtx.model = route.modelId;
    logCtx.provider = route.providerName;
    logCtx.providerAdapter = route.provider.adapter;
    logCtx.routeDecision = route.routeDecision;
    if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
    parsed.stream = false;
    if (parsed._rawBody && typeof parsed._rawBody === "object") {
    (parsed._rawBody as Record<string, unknown>).stream = false;
    }

    // #875: the transport-neutral reliability policy forced a bounded JSON
    // upstream for a client that asked for SSE. Reframe the completed JSON
    // as the canonical terminal SSE sequence (created → output_item.done →
    // terminal → [DONE]) so Codex commits the turn instead of hanging on a
    // stream that never closes. Non-streaming clients keep the plain JSON.
    if (clientRequestedStream === true
    && options.inboundTransport !== "websocket"
    && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
    && route.provider.adapter === "openai-responses") {
    let completed: Record<string, unknown> | undefined;
    try {
    const parsedCompleted = JSON.parse(clientJson) as unknown;
    if (!parsedCompleted || typeof parsedCompleted !== "object" || Array.isArray(parsedCompleted)) {
    throw new TypeError("bounded Responses JSON is not an object");
    }
    let candidate = parsedCompleted as Record<string, unknown>;
    // The bounded-JSON answer bypasses the SSE relay, so it also bypasses
    // the SSE item-id rewrite. Apply the same client-facing normalization
    // here or this policy would silently disable id repair for the very
    // providers that need it (raw record already happened above).
    if (hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)) {
    candidate = repairResponsesJsonItemIds(candidate, route.provider.responsesItemIdRepair!, translatorBudget);
    }
    completed = candidate;
    } catch {
    // Non-JSON despite content-type: fall through to the plain relay.
    }
    if (completed) {
    let stream: ReadableStream<Uint8Array>;
    try {
    stream = responsesJsonToSseStream(completed);
    } catch (error) {
    if (error instanceof RangeError) {
    return formatErrorResponse(
    502,
    "upstream_error",
    "upstream JSON response exceeded the synthesized SSE item limit",
    );
    }
    throw error;
    }
    const sseHeaders = sanitizePassthroughHeaders(headers);
    sseHeaders.set("content-type", "text/event-stream");
    sseHeaders.set("cache-control", "no-store");
    return new Response(stream, {
    status: upstreamResponse.status,
    statusText: upstreamResponse.statusText,
    headers: sseHeaders,
    });

No current-dev live canary is claimed in this issue. The current-dev mapping finding is static source evidence; the terminal-delivery and relay acceptance evidence comes from the earlier isolated live paths described above.

Checks

  • I searched existing provider and compatibility issues.
  • The request and response were redacted.
  • The expected behaviour is based on an upstream specification or a concrete client requirement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    providerProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reportsstreamingSSE, WebSocket, terminal stream framestoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions