Skip to content

Open Responses parser rejects "namespace": null on function_call items ("Invalid … stream event") #55557

Closed

Description

@ijbaird

Description

Streaming from an OpenAI-Responses-compatible gateway that serializes the (optional) namespace field of a function_call item as JSON null fails on the first tool call:

AI.Error.InvalidProviderOutput: Invalid lunaroute/openai-responses stream event
  [cause]: SchemaError: Expected string | undefined
    at ["item"]["namespace"]

The session errors out (Failed to drain Session), so any tool call, including a subagent fan-out, is unusable with such a provider. Plain text turns work.

Cause: StreamItem in packages/ai/src/protocols/open-responses.ts decodes namespace as Schema.optional(Schema.String) (line 337 at v2.0.25, unchanged on dev and v2), while the neighbouring encrypted_content already uses optionalNull(...) and Event.item itself is optionalNull(StreamItem). Every Responses-based route shares this schema (openai-responses.ts reuses OpenResponses.protocol.stream.event), so @opencode/ai/providers/openai, …/openai-compatible/responses and the host-specific */responses packages are all affected.

Proposed fix: namespace: optionalNull(Schema.String) (and the same on the function_call item schema around line 208), treating null as absent where item.namespace is read (ToolStream.start / toolInputStart). Happy to send a PR if that is the preferred shape.

Related

Not a duplicate as far as I can tell. Nearest reports are about other parsers or frame classes: #40690 (chat parser rejected tool-call deltas, fixed in #40798), #50314 (data: null frames, fixed by dropping non-object payloads), #54102 (mixed LF/CRLF, fixed by the effect bump), #54330 (injected metadata frames, v1). #53405 recently widened OpenResponsesUsage to keep gateway fields, which is the same direction as this request.

Plugins

@lunaroute/opencode-extension (dev version for compatibility with opencode v2)

OpenCode version

2.0.25 (Homebrew)

Steps to reproduce

  1. Run a minimal Responses-style server that streams one function call whose item carries "namespace": null:

    // fake.cjs — node fake.cjs
    require("node:http").createServer((req, res) => {
      if (req.url === "/v1/models") return res.end(JSON.stringify({ data: [{ id: "m" }] }))
      const fc = { type: "function_call", id: "fc_1", call_id: "call_1", name: "glob", namespace: null, arguments: '{"pattern":"*.md"}' }
      const events = [
        { type: "response.created", response: { id: "r1", output: [] } },
        { type: "response.output_item.added", output_index: 0, item: { ...fc, arguments: "" } },
        { type: "response.function_call_arguments.done", item_id: "fc_1", output_index: 0, arguments: fc.arguments },
        { type: "response.output_item.done", output_index: 0, item: fc },
        { type: "response.completed", response: { id: "r1", output: [fc], usage: { input_tokens: 1, output_tokens: 1 } } },
      ]
      res.writeHead(200, { "content-type": "text/event-stream" })
      for (const e of events) res.write(`event: ${e.type}\ndata: ${JSON.stringify(e)}\n\n`)
      res.end()
    }).listen(4599)
  2. Point a provider at it in opencode.json:

    {
      "provider": {
        "fake": {
          "name": "Fake",
          "package": "@opencode/ai/providers/openai",
          "settings": { "baseURL": "http://127.0.0.1:4599/v1", "apiKey": "x" },
          "models": { "m": { "name": "m" } }
        }
      }
    }
  3. Send any prompt to fake/m. The first turn fails with the error above; the server log shows SchemaError: Expected string | undefined at ["item"]["namespace"].

  4. Delete namespace: null from the item (or change it to a string) and the same stream parses, the tool runs, and the session continues.

Screenshot and/or share link

n/a (reproduced headlessly via opencode serve + the HTTP API; log excerpt above).

Operating System

macOS 27.0.1 (arm64)

Terminal

n/a (server-side parsing)

Activity

  1. opencode-agent commented on Oct 14, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the minimal server. I reproduced this on 2.0.25, and it still happens on the latest dev (3943b84).

    What I did:

    • I ran a fake Responses server that streams one glob function_call item with "namespace": null, then replies with text once the tool result comes back.
    • I pointed a @opencode/ai/providers/openai provider at it and ran opencode run -m fake/m "list markdown files".
    • The first turn fails with Invalid fake/openai-responses stream event, the tool never runs, and the server log shows Failed to drain Session.
    • Without the field, the same stream works: the tool runs and the follow-up turn completes.
    • Decoding that single event with OpenResponses.protocol.stream.event on dev gives the same error as yours: Expected string | undefined at ["item"]["namespace"].

    The script below reproduces it. Run it as is for the released version, or with OC_DEV=<checkout> to run from source; NS=omit sends the stream without the field as a control.

    One small note on the steps: in v2 the config key is providers, not provider. With provider you get an unrelated "Unsupported package" error instead.

    Related Responses-compatibility issues, not duplicates: #50314 (data: null frames), #43732 (a gateway sending "truncation": ""), #55357 (replayed reasoning items sent without id).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for anomalyco/opencode#55557: Responses stream with function_call item "namespace": null.
    # Usage: ./repro.sh                 (uses `opencode` from PATH)
    #        OC_DEV=/path/to/checkout ./repro.sh   (runs from source)
    # Set NS=omit to send the same stream without the namespace field (control).
    set -u
    WORK=$(mktemp -d)
    PORT=${PORT:-4599}
    NS=${NS:-null}
    cat > "$WORK/fake.cjs" <<EOF
    let n = 0
    require("node:http").createServer((req, res) => {
      if (req.url === "/v1/models") return res.end(JSON.stringify({ data: [{ id: "m" }] }))
      let body = ""
      req.on("data", (c) => (body += c))
      req.on("end", () => {
        n++; console.error("[fake] request", n, body.includes("function_call_output") ? "(has tool output)" : "(no tool output)")
        res.writeHead(200, { "content-type": "text/event-stream" })
        let events
        if (!body.includes("function_call_output")) {
          const fc = { type: "function_call", id: "fc_1", call_id: "call_1", name: "glob", arguments: '{"pattern":"*.md"}' }
          if ("$NS" === "null") fc.namespace = null
          events = [
            { type: "response.created", response: { id: "r1", output: [] } },
            { type: "response.output_item.added", output_index: 0, item: { ...fc, arguments: "" } },
            { type: "response.function_call_arguments.done", item_id: "fc_1", output_index: 0, arguments: fc.arguments },
            { type: "response.output_item.done", output_index: 0, item: fc },
            { type: "response.completed", response: { id: "r1", output: [fc], usage: { input_tokens: 1, output_tokens: 1 } } },
          ]
        } else {
          const msg = { type: "message", id: "msg_1", role: "assistant", status: "completed", content: [{ type: "output_text", text: "DONE_AFTER_TOOL", annotations: [] }] }
          events = [
            { type: "response.created", response: { id: "r2", output: [] } },
            { type: "response.output_item.added", output_index: 0, item: { ...msg, status: "in_progress", content: [] } },
            { type: "response.output_text.delta", item_id: "msg_1", output_index: 0, content_index: 0, delta: "DONE_AFTER_TOOL" },
            { type: "response.output_item.done", output_index: 0, item: msg },
            { type: "response.completed", response: { id: "r2", output: [msg], usage: { input_tokens: 1, output_tokens: 1 } } },
          ]
        }
        for (const e of events) res.write("event: " + e.type + "\ndata: " + JSON.stringify(e) + "\n\n")
        res.end()
      })
    }).listen($PORT)
    EOF
    node "$WORK/fake.cjs" & SRV=$!
    trap 'kill $SRV 2>/dev/null; rm -rf "$WORK"' EXIT
    mkdir -p "$WORK/proj" && cd "$WORK/proj" && git init -q && echo hi > README.md
    mkdir -p "$WORK/config/opencode"; cat > "$WORK/config/opencode/opencode.json" <<EOF
    {
      "providers": {
        "fake": {
          "name": "Fake",
          "package": "@opencode/ai/providers/openai",
          "settings": { "baseURL": "http://127.0.0.1:$PORT/v1", "apiKey": "x" },
          "models": { "m": { "name": "m" } }
        }
      }
    }
    EOF
    export OPENCODE_PASSWORD=repro-pass XDG_DATA_HOME="$WORK/data" XDG_CONFIG_HOME="$WORK/config" XDG_STATE_HOME="$WORK/state" XDG_CACHE_HOME="$WORK/cache"
    sleep 1
    if [ -n "${OC_DEV:-}" ]; then
      OC=(bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts")
    else
      OC=(opencode)
    fi
    echo "version: $("${OC[@]}" --version)  namespace=$NS"
    CONN=(--standalone)
    if [ -n "${OC_DEV:-}" ]; then
      # From source, the auto-spawned standalone server misses the preload, so start `serve` separately.
      "${OC[@]}" serve --stdio --port 0 < <(sleep 600) > "$WORK/serve.out" 2> "$WORK/serve.err" & SERVE=$!
      trap 'kill $SRV $SERVE 2>/dev/null; rm -rf "$WORK"' EXIT
      for i in $(seq 60); do grep -q url "$WORK/serve.out" 2>/dev/null && break; sleep 1; done
      URL=$(grep -o 'http://[^"]*' "$WORK/serve.out" | head -1)
      CONN=(--server "$URL")
      sleep 1
    fi
    timeout 90 "${OC[@]}" run "${CONN[@]}" --auto --print-logs -m fake/m "list markdown files" 2>&1 | grep -E "\[fake\]|namespace|Invalid|SchemaError|DONE_AFTER_TOOL|Error" | cut -c1-400 | head -20
    [ -n "${OC_DEV:-}" ] && grep -E "Invalid|SchemaError|namespace" "$WORK/serve.err" | cut -c1-400 | head -5

    Run it: bash repro.sh

  2. added a commit that references this issue on Oct 15, 2026
    5b1fe94
  3. rekram1-node commented on Oct 15, 2026

    @rekram1-node
    Collaborator

    merged a fix, will prolly loosen some more areas that are too strict

  4. self-assigned this
    on Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions