Skip to content

provider: OpenCode Go Anthropic route masks upstream errors as "Provider request failed with HTTP 400" #51530

Open

Description

@aderamdani

Summary

With OpenCode Go, models served over the Anthropic Messages route (for example opencode-go/qwen3.8-flash) surface every upstream rejection as the opaque message Provider request failed with HTTP 400. The route returns an empty error payload, so the real upstream reason (for example a content-filter rejection) is lost and the user cannot tell what failed.

Environment

  • opencode version: 2.0.18
  • OS: Darwin 27.0.0 (darwin arm64)
  • Terminal: Kaku (TERM_PROGRAM=Kaku, TERM=xterm-256color, COLORTERM=truecolor)
  • Shell: /bin/zsh
  • Install/channel: Homebrew (opencode-v2 2.0.18), channel latest
  • Active plugins: local rtk plugin (shell command rewrite only; unrelated to provider requests)

Reproduction

  1. Connect OpenCode Go and select a model whose catalog entry uses the Anthropic runtime, e.g. opencode-go/qwen3.8-flash (package: "@opencode/ai/providers/anthropic", settings.baseURL: "https://opencode.ai/zen/go/v1").
  2. Build a session whose context the upstream rejects. In my case a long assistant text (a NetBird/DNS investigation, ~11.5k chars) trips Alibaba's Qwen content filter.
  3. Send any next prompt in that session (or call POST /api/session/:sessionID/generate).
  4. The step fails with Provider request failed with HTTP 400 and no further detail.

Network-level, the request is POST https://opencode.ai/zen/go/v1/messages and the response is HTTP 400 with body:

event: error
data: {}

An SSE error event whose payload is an empty object. With stream: false the same request returns:

{"model":"qwen3.8-flash"}

Neither body carries error.message or code.

Expected Behavior

The upstream rejection should be surfaced with its real message so the user can act on it. In this case the underlying reason is knowable: the same content sent over the OpenAI-compatible runtime (POST /chat/completions) returns

{"error":{"param":null,"type":"server_error","message":"Upstream request failed: [data_inspection_failed] <400> InternalError.Algo.DataInspectionFailed: Input text data may contain inappropriate content."}}

Actual Behavior

opencode can only report Provider request failed with HTTP 400. In opencode 2.0.18 the error mapping for provider HTTP errors falls back to Provider request failed with HTTP <status> whenever the response body has no message/code (see XC() near Provider request failed with HTTP ${e.statusCode} in the server bundle). Because the Anthropic route returns data: {}, there is nothing to surface.

Additional Context

  • The failure is deterministic for a given context, so retrying the same prompt keeps failing with the same opaque 400.
  • Workaround that restores the real message: override the model's runtime so requests use the OpenAI-compatible endpoint, which preserves the upstream error body:
{
  "providers": {
    "opencode-go": {
      "models": {
        "qwen3.8-flash": {
          "package": "@opencode/ai/providers/openai-compatible"
        }
      }
    }
  }
}

After this override the same step reports Upstream request failed: [data_inspection_failed] <400> InternalError.Algo.DataInspectionFailed: Input text data may contain inappropriate content.

  • Two possible fixes on the OpenCode side:
    1. When the Anthropic route receives an error event with an empty payload, fall back to the raw HTTP response body/status text so the message is not silently dropped.
    2. Have the Go gateway include the upstream error.message in the Anthropic error event instead of {}.

Activity

  1. github-actions commented on Sep 26, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    • #50446: OpenCode Go Qwen3.8 Anthropic-route 400s where the provider error body is not surfaced.
    • #51348: OpenCode Go /v1/messages returns a 400 with an empty error payload; the report asks for a descriptive error.
  2. a-d-gordienko commented on Sep 29, 2026

    @a-d-gordienko

    Same failure here with opencode-go/qwen3.8-flash — adding data points from a different platform, in case they help narrow it down:

    Environment

    • opencode 1.18.33 on Windows 11 (build 26200), PowerShell 7
    • Go subscription; model opencode-go/qwen3.8-flash
    • UI shows: Bad Request: event: error data: {}

    Deterministic — 5/5 attempts on 2026-09-29 (both plan and build agents):

    timestamp=2026-09-29T03:21:23.709Z level=ERROR message="stream error" providerID=opencode-go modelID=qwen3.8-flash session.id=ses_f14f124b0ffeKNd3m5ZK8V1TWp agent=plan error.error="AI_APICallError: Bad Request"
    timestamp=2026-09-29T03:34:00.450Z level=ERROR message="stream error" providerID=opencode-go modelID=qwen3.8-flash agent=plan error.error="AI_APICallError: Bad Request"
    timestamp=2026-09-29T04:27:57.036Z level=ERROR message="stream error" providerID=opencode-go modelID=qwen3.8-flash agent=plan error.error="AI_APICallError: Bad Request"
    timestamp=2026-09-29T06:42:40.440Z level=ERROR message="stream error" providerID=opencode-go modelID=qwen3.8-flash agent=build error.error="AI_APICallError: Bad Request"
    timestamp=2026-09-29T07:15:12.727Z level=ERROR message="stream error" providerID=opencode-go modelID=qwen3.8-flash agent=plan error.error="AI_APICallError: Bad Request"
    

    Control: in the same session/environment opencode-go/deepseek-v4.1-flash works fine, so the Go subscription/auth is not the issue — only qwen3.8-flash fails.

    This matches the empty event: error + data: {} payload described in the issue. Happy to run further diagnostics (fresh session, local recording endpoint for the request body) if that would help.

  3. VictorHMH18713 commented on Oct 27, 2026

    @VictorHMH18713

    Related data point on the same route, different masking shape.

    • Client: Claude Code 2.1.293, ANTHROPIC_BASE_URL=https://opencode.ai/zen/go, model deepseek-v4.1-flash, stream: true.
    • Observed 2026-10-27 07:24–07:44Z and 07:53Z UTC.
    • Instead of an SSE event: error with {}, we get HTTP 400 with a 0-byte body (curl reports Content-Length absent, 0 bytes read), which Claude Code renders as API Error: 400 status code (no body).
    • The same request body sent with stream: false returns the real upstream reason:
    Upstream request failed: [invalid_request_error] The `content[].thinking` in the thinking mode must be passed back to the API.
    (request_id: 65871f21-0908-4a0f-bc02-89f7c5fb6d1f) (request id: 202610270826085305488478268d9d6RBCXETWj)
    

    Since Claude Code does not retry 400 (408 / 409 / 429 / 5xx / x-should-retry: true only), the empty 400 is terminal there: it killed 8 of 25 workflow subagents in a 20-minute window. Any upstream error text (SSE event or plain JSON body) would at least let the client surface and report it.

  4. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I reproduced this with a local mock in place of the OpenCode Go Anthropic route. The mock returns what you and #57122 observed: HTTP 400 with event: error / data: {}. A provider using @opencode/ai/providers/anthropic pointed at it gives:

    • 2.0.18 and 2.2.0: Provider request failed with HTTP 400, with no further detail.
    • Latest dev (86ce19d): Provider request failed with HTTP 400: event: error data: {}. Since fix(ai): show provider error bodies when no message is recognized #51978 the raw response body is appended, which is your first suggested fix. Because the gateway sends an empty payload, the real upstream reason still never reaches you.

    The remaining problem looks like the Go gateway dropping the upstream error on the Anthropic route for qwen3.8 (your second suggestion). That gateway isn't in this repo, so I couldn't test it directly. Your workaround (overriding the model's package to @opencode/ai/providers/openai-compatible) is the way to see the real message for now.

    Related: #57122 (a deterministic curl reproduction of the same empty data: {} error) and #52791 (probably the same empty error frame showing up as an invalid stream event). The script below reproduces this against a mock server.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #51530 client-side: a gateway that rejects an Anthropic Messages request with
    # HTTP 400 and an empty SSE error payload (`event: error` / `data: {}`), as the OpenCode Go
    # route does for qwen3.8-flash. Shows what opencode reports to the user.
    # Usage: bash repro.sh            (uses `opencode` from PATH)
    #        OC_DEV=/path/to/repo bash repro.sh   (runs from a source checkout)
    set -u
    DIR=$(mktemp -d)
    PORT=${PORT:-48151}
    cat > "$DIR/mock.ts" <<EOF
    Bun.serve({
      port: $PORT,
      async fetch(req) {
        const url = new URL(req.url)
        const body = await req.text()
        let stream = true
        try { stream = JSON.parse(body).stream !== false } catch {}
        console.error("mock:", req.method, url.pathname, "stream=" + stream)
        if (stream) return new Response("event: error\ndata: {}\n\n", { status: 400, headers: { "content-type": "text/event-stream" } })
        return Response.json({ model: "qwen3.8-flash" }, { status: 400 })
      },
    })
    EOF
    bun "$DIR/mock.ts" & MOCK=$!
    trap 'kill $MOCK 2>/dev/null; rm -rf "$DIR"' EXIT
    sleep 1
    mkdir -p "$DIR/project" && cd "$DIR/project" && git init -q
    cat > opencode.jsonc <<EOF
    {
      "providers": {
        "mockgo": {
          "package": "@opencode/ai/providers/anthropic",
          "settings": { "baseURL": "http://127.0.0.1:$PORT/v1", "apiKey": "test" },
          "models": { "qwen3.8-flash": {} }
        }
      }
    }
    EOF
    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 "opencode version: $("${OC[@]}" --version)"
    SERVER=()
    if [ -n "${OC_DEV:-}" ]; then
      # From source, start the server with the same preload (a spawned server would miss it).
      export OPENCODE_PASSWORD=repro-pass
      "${OC[@]}" serve --port 48152 >"$DIR/serve.log" 2>&1 & SRV=$!
      trap 'kill $MOCK $SRV 2>/dev/null; rm -rf "$DIR"' EXIT
      for i in $(seq 60); do curl -s -o /dev/null http://127.0.0.1:48152/ && break; sleep 1; done
      SERVER=(--server http://127.0.0.1:48152)
    fi
    timeout 120 "${OC[@]}" run "${SERVER[@]}" --model mockgo/qwen3.8-flash "hello" 2>&1 | tail -20

    Run it: bash repro.sh

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