Skip to content

OpenRouter Custom Provider Fails with "unexpected end of JSON input" #1956

@Raveendiran-RR

Description

@Raveendiran-RR

Bug Report: OpenRouter Custom Provider Fails with "unexpected end of JSON input"

Describe the bug

When using cagent with a custom OpenRouter provider configured via the providers: block, all model requests fail with the following error:

all models failed: error receiving from stream: unexpected end of JSON input

This occurs despite OpenRouter returning a valid, complete HTTP 200 SSE stream. The issue is not with OpenRouter, the API key, the model, or network connectivity — all of which were verified independently via curl and mitmproxy.

cagent sends Accept-Encoding: gzip in its request headers (confirmed via mitmproxy). SSE streaming and gzip compression are fundamentally incompatible: gzip requires buffering the complete payload before decompression, while SSE sends data incrementally as chunks. If cagent's HTTP client attempts to gzip-decompress the SSE stream before parsing JSON chunks, it fails immediately because the stream is not a complete gzip payload.

This explains:

  • Why the error is unexpected end of JSON input — incomplete/garbled data after failed decompression
  • Why OpenRouter shows cancelled: true after only 1 token — cagent disconnects as soon as parsing fails
  • Why the identical request works fine via curl — curl handles SSE + gzip correctly

Version affected

cagent v1.29.0

How To Reproduce

  1. Set a valid OpenRouter API key as an environment variable:

    export OPENROUTER_API_KEY=sk-or-v1-xxxxxxxx
    
  2. Create cagent_openrouter.yaml with the following configuration:

    providers:
      openrouter:
        api_type: openai_chatcompletions
        base_url: https://openrouter.ai/api/v1
        token_key: OPENROUTER_API_KEY
    

    models:
    openrouter_model:
    provider: openrouter
    model: openai/gpt-4o-mini
    max_tokens: 32768

    agents:
    root:
    model: openrouter_model
    description: AI Agent powered by OpenRouter
    instruction: |
    You are a helpful AI assistant. Use your tools effectively to complete tasks.
    Be thorough, accurate, and provide clear explanations.

  3. Run cagent:

    cagent run cagent_openrouter.yaml
    
  4. Type any message (e.g. hi) and press Enter

  5. See error:

    ❌ all models failed: error receiving from stream: unexpected end of JSON input
    

Expectation

cagent should successfully consume the SSE stream from OpenRouter and return a response to the user. The stream is valid — OpenRouter returns HTTP 200 with content-type: text/event-stream and a complete SSE payload ending with data: [DONE].

Verified working via curl with the identical payload (same model, same stream_options: {include_usage: true}, same 14 tool definitions, same parallel_tool_calls: true):

curl https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4o-mini",
    "max_tokens": 9000,
    "parallel_tool_calls": true,
    "stream_options": {"include_usage": true},
    "stream": true,
    "messages": [{"role": "user", "content": "hi"}]
  }'

Result: complete valid SSE stream, data: [DONE] received, no errors.


Screenshots

cagent debug log (key lines)

level=DEBUG  msg="OpenAI chat completion stream created successfully" model=openai/gpt-4o-mini
level=DEBUG  msg="Processing stream" agent=root model=openai/openai/gpt-4o-mini
level=DEBUG  msg="Unknown error type, not retrying"
             error="error receiving from stream: unexpected end of JSON input"
level=ERROR  msg="Non-retryable error handling stream"
             error="error receiving from stream: unexpected end of JSON input"
level=ERROR  msg="All models failed"
             error="all models failed: error receiving from stream: unexpected end of JSON input"

OpenRouter activity log

{
  "model": "openai/gpt-4o-mini",
  "streamed": true,
  "cancelled": true,
  "tokens_prompt": 38,
  "tokens_completion": 1,
  "finish_reason": null,
  "status": 200
}

cancelled: true and tokens_completion: 1 confirms cagent disconnects after receiving only 1 token.

mitmproxy intercept — actual OpenRouter response

HTTP/2.0 200
content-type: text/event-stream
cache-control: no-cache
server: cloudflare
Image

: OPENROUTER PROCESSING

data: {"id":"gen-...","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hello"}}]}
data: {"id":"gen-...","object":"chat.completion.chunk","choices":[{"delta":{"content":"!"}}]}
data: {"id":"gen-...","object":"chat.completion.chunk","choices":[{"delta":{"content":" How can I assist you today?"}}]}
data: {"id":"gen-...","object":"chat.completion.chunk","choices":[{"delta":{"content":""},"finish_reason":"stop"}]}
data: {"id":"gen-...","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":38,"completion_tokens":9}}
data: [DONE]

The response is a perfectly valid, complete SSE stream. cagent throws the error before fully consuming it.

Request headers sent by cagent (via mitmproxy)

POST https://openrouter.ai/api/v1/chat/completions HTTP/2.0
authorization: Bearer sk-or-v1-1bc...b1b   ✅ correct
accept-encoding: gzip                        ⚠️ suspected root cause
content-type: application/json
x-stainless-runtime: go
x-stainless-runtime-version: go1.26.0
user-agent: Cagent/v1.29.0 (darwin; arm64)

OS and Terminal type

  • OS: macOS darwin arm64 (Apple M3)
  • Terminal: zsh
  • Shell: zsh 5.9

Additional context

What was ruled out during investigation:

Logs from Openrouter :

{
  "id": 0,
  "generation_id": "gen-1772780692-YjRwYlVpGpJi8fXsoiiQ",
  "provider_name": "OpenAI",
  "model": "openai/gpt-4o-mini",
  "app_id": null,
  "external_user": null,
  **"streamed": true,
  "cancelled": true,**
  "generation_time": 505,
  "latency": 505,
  "moderation_latency": null,
  "created_at": "2026-03-06T07:04:52.524Z",
  "tokens_prompt": 161,
  "tokens_completion": 3,
  "native_tokens_prompt": 161,
  "native_tokens_completion": 3,
  "native_tokens_completion_images": null,
  "native_tokens_reasoning": 0,
  "native_tokens_cached": 0,
  "num_media_prompt": null,
  "num_input_audio_prompt": null,
  "num_media_completion": 0,
  "num_search_results": null,
  "origin": "",
  "usage": 0.00002595,
  "usage_upstream": 0.00002595,
  "finish_reason": null,
  "usage_cache": null,
  "usage_data": null,
  "usage_web": null,
  "usage_file": 0,
  "byok_usage_inference": 0,
  "provider_responses": [
    {
      "endpoint_id": "77e40332-6f2a-4c48-bc14-e44596b30ce2",
      "id": "chatcmpl-DGJWvj6lcjgm3Bh6a2V9rD8QCjZyp",
      "is_byok": false,
      "latency": 453,
      "model_permaslug": "openai/gpt-4o-mini",
      "provider_name": "OpenAI",
      "status": 200
    }
  ],
  "api_type": "completions",
  "creator_user_id": "user_3AYVWzQSYIvnAnhc1SalRFlsFbu",
  "router": null,
  "is_byok": false,
  "native_finish_reason": null,
  "user_agent": "Cagent/v1.29.0 (darwin; arm64)",
  "http_referer": null
}
Hypothesis Test Result
Invalid model name Tested gpt-3.5-turbo, gpt-5-mini, gpt-5.2, gpt-4o-mini gpt-4o-mini works via curl
Wrong provider type Tested type: OpenRouter vs openai openai is correct
Missing/invalid API key curl /auth/key endpoint Key valid, credits available
Context window exceeded Checked prompt tokens (38–2107) Well within 128k limit
stream_options incompatibility curl with include_usage: true Works fine
Tool definitions causing cancel curl with 14 tools + parallel_tool_calls: true Works fine
Network/TLS issue mitmproxy intercept shows HTTP 200 OpenRouter responds correctly

Suggested fix: Disable gzip compression for SSE streams in cagent's HTTP client, or explicitly set Accept-Encoding: identity when making streaming requests.

No workaround available within current YAML schema — the following fields were attempted but rejected as unknown fields:

  • extra_headers:
  • api_key:

Metadata

Metadata

Assignees

Labels

kind/bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions