-
Notifications
You must be signed in to change notification settings - Fork 281
Description
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: trueafter 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
-
Set a valid OpenRouter API key as an environment variable:
export OPENROUTER_API_KEY=sk-or-v1-xxxxxxxx -
Create
cagent_openrouter.yamlwith the following configuration:providers: openrouter: api_type: openai_chatcompletions base_url: https://openrouter.ai/api/v1 token_key: OPENROUTER_API_KEYmodels:
openrouter_model:
provider: openrouter
model: openai/gpt-4o-mini
max_tokens: 32768agents:
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.
-
Run cagent:
cagent run cagent_openrouter.yaml -
Type any message (e.g.
hi) and press Enter -
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![]()
: 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:
