hermes - 💡(How to fix) Fix [Bug]: HTTP 400 empty body on all OpenRouter requests (Hermes 0.9.0); curl works with same key/model [3 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#16804Fetched 2026-04-29 06:39:00
View on GitHub
Comments
3
Participants
2
Timeline
8
Reactions
0
Author
Participants
Timeline (top)
labeled ×4commented ×3subscribed ×1

Hermes 0.9.0 returns HTTP 400 BadRequestError with empty response body for all OpenRouter API calls, regardless of model. Direct curl to OpenRouter with the same API key and model returns HTTP 200 and a valid completion. hermes doctor's own API Connectivity check confirms the failure (✗ OpenRouter API (HTTP 400)). This appears to be two related bugs in Hermes's request construction and error handling for OpenRouter.

Error Message

API call failed (attempt 1/3): BadRequestError [HTTP 400] Provider: openrouter Model: openai/gpt-5.4-nano Endpoint: https://openrouter.ai/api/v1 Error: HTTP 400: Error code: 400 Non-retryable error (HTTP 400) — trying fallback...

Root Cause

Hermes surfaces this to the user as a generic HTTP 400 BadRequestError with empty body, and discards the helpful error message body. This hid the actual root cause for several hours of debugging.

Fix Action

Fix / Workaround

Workaround tested

No user-side workaround has been found; appears to require an upstream code fix.

  1. Configure Hermes 0.9.0 with OpenRouter as the provider and any model (tested: anthropic/claude-sonnet-4.5, openai/gpt-5.4-nano, anthropic/claude-sonnet-4.6, openrouter/elephant-alpha — all fail identically).
  2. Ensure the OpenRouter account has a positive credit balance.
  3. Run hermes to start a chat session.
  4. Send any short prompt, e.g. "Hello in one sentence."
  5. Observe: HTTP 400 BadRequestError with empty response body. Request dump written to /opt/data/sessions/request_dump_*.json shows extra_body.reasoning.enabled: true and max_tokens: 64000 being sent.
  6. Verify the upstream is healthy: a direct curl to OpenRouter with the same API key and same model returns HTTP 200 with a valid completion.
  7. Confirm via Hermes's own diagnostics: hermes doctor reports ✗ OpenRouter API (HTTP 400) under API Connectivity.
  8. Tested workarounds (none resolved): lowering agent.max_tokens to 4096; reducing toolset from 17/19 → 1/19 enabled (request payload shrunk from ~4000 tokens to ~1095 tokens — same error).

Code Example

HTTP 402
{"error":{"message":"Insufficient credits. This account never purchased credits...","code":402}}

---

curl -s -w "\nHTTP_STATUS:%{http_code}\n" \
  https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer sk-or-v1-XXX" \
  -H "Content-Type: application/json" \
  -d '{"model":"openai/gpt-5.4-nano","messages":[{"role":"user","content":"Say hello from curl."}]}'

---

HTTP_STATUS:200
{"id":"...","choices":[{"message":{"content":"..."}}],...}

---

hermes
> Hello in one sentence.

---

API call failed (attempt 1/3): BadRequestError [HTTP 400]
Provider: openrouter  Model: openai/gpt-5.4-nano
Endpoint: https://openrouter.ai/api/v1
Error: HTTP 400: Error code: 400
Non-retryable error (HTTP 400) — trying fallback...

---

API Connectivity
OpenRouter API (HTTP 400)

---

{
  "model": "openai/gpt-5.4-nano",
  "messages": [...],
  "max_tokens": 64000,
  "extra_body": {
    "reasoning": {
      "enabled": true,
      "effort": "medium"
    }
  },
  "tools": [... 28 tools ...]
}

---

"error": {
  "type": "BadRequestError",
  "message": "Error code: 400",
  "status_code": 400,
  "body": "",
  "response_status": 400,
  "response_text": ""
}

---

Debug report bundle generated via `hermes debug share` on 2026-04-28:

- Report: https://paste.rs/20475
- agent.log: https://paste.rs/qh9Ls

(paste.rs links may be ephemeral — uploaded the same day this issue is filed.)

Additional request dump path inside the container, in case more dumps are needed:
/opt/data/sessions/request_dump_*.json

---

Request payload excerpt (from /opt/data/sessions/request_dump_*.json):

{
  "model": "openai/gpt-5.4-nano",
  "max_tokens": 64000,
  "extra_body": {
    "reasoning": {
      "enabled": true,
      "effort": "medium"
    }
  },
  "tools": [... 28 tool definitions ...],
  "messages": [...]
}

Response error from same dump:

"error": {
  "type": "BadRequestError",
  "message": "Error code: 400",
  "status_code": 400,
  "body": "",
  "response_status": 400,
  "response_text": ""
}

Note the empty `body` and `response_text`OpenRouter's actual error message is being discarded by Hermes's error handling. Direct curl to OpenRouter with the same key+model on the same container returns HTTP 200 with valid JSON; this rules out the upstream provider, key, account, and model availability as the cause.
RAW_BUFFERClick to expand / collapse

Bug Description

Summary

Hermes 0.9.0 returns HTTP 400 BadRequestError with empty response body for all OpenRouter API calls, regardless of model. Direct curl to OpenRouter with the same API key and model returns HTTP 200 and a valid completion. hermes doctor's own API Connectivity check confirms the failure (✗ OpenRouter API (HTTP 400)). This appears to be two related bugs in Hermes's request construction and error handling for OpenRouter.

Environment

  • Hermes version: 0.9.0 (build 2026.4.13)
  • Install method: Docker container, Hostinger VPS template
  • Image: ghcr.io/hostinger/hvps-hermes-agent:latest
  • Container name: hermes-agent-9cex-hermes-agent-1
  • Python: 3.13.5
  • OpenAI SDK: 2.31.0
  • Provider: OpenRouter (via API key in /opt/data/.env)

Bug 1: HTTP 402 mapped to HTTP 400 with empty body (error-handling bug)

When the OpenRouter account has zero credits, OpenRouter correctly returns:

HTTP 402
{"error":{"message":"Insufficient credits. This account never purchased credits...","code":402}}

Hermes surfaces this to the user as a generic HTTP 400 BadRequestError with empty body, and discards the helpful error message body. This hid the actual root cause for several hours of debugging.

Reproducer:

  1. Configure Hermes with OpenRouter, any model
  2. Ensure the OpenRouter account has zero credits
  3. Run hermes chat with any prompt
  4. Observe: HTTP 400 BadRequestError with body: "" in the request dump

Expected: Hermes should propagate the upstream HTTP status (402) and the error body (insufficient credits message) so users can fix the underlying issue.

Bug 2: Request-construction incompatibility persists after credits added

Even after credits were added and curl confirmed end-to-end success, Hermes calls to the same model still fail with HTTP 400 empty body.

Reproducer:

  1. OpenRouter account has credits available (verified via dashboard and curl test)
  2. Hermes configured with provider=openrouter, model=openai/gpt-5.4-nano
  3. Direct curl works:
curl -s -w "\nHTTP_STATUS:%{http_code}\n" \
  https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer sk-or-v1-XXX" \
  -H "Content-Type: application/json" \
  -d '{"model":"openai/gpt-5.4-nano","messages":[{"role":"user","content":"Say hello from curl."}]}'

Returns:

HTTP_STATUS:200
{"id":"...","choices":[{"message":{"content":"..."}}],...}
  1. Same key, same model, via Hermes:
hermes
> Hello in one sentence.

Returns:

API call failed (attempt 1/3): BadRequestError [HTTP 400]
Provider: openrouter  Model: openai/gpt-5.4-nano
Endpoint: https://openrouter.ai/api/v1
Error: HTTP 400: Error code: 400
Non-retryable error (HTTP 400) — trying fallback...
  1. hermes doctor reports:
API Connectivity
  ✗ OpenRouter API (HTTP 400)

Models tested (all failed identically)

ModelResult
anthropic/claude-sonnet-4.6HTTP 400 empty body
anthropic/claude-sonnet-4.5HTTP 400 empty body
openrouter/elephant-alphaHTTP 400 empty body
openai/gpt-5.4-nanoHTTP 400 empty body

All four models confirmed callable via direct curl from the same container with the same key. So the failure is not model-specific or vendor-specific.

Variables tested (none changed the result)

  • Lowering agent.max_tokens from 64000 → 4096 via hermes config set agent.max_tokens 4096
  • Reducing toolset from 17/19 → 1/19 enabled (only Terminal & Processes)
    • Request payload shrunk from ~4,000 tokens to ~1,095 tokens — same HTTP 400
  • API key rotation (rotated for security; new key confirmed working via curl)

Suspected cause

Request dumps show Hermes injecting extra_body.reasoning.enabled: true and max_tokens: 64000 into all OpenRouter requests, regardless of whether the upstream provider accepts those fields. After the model-and-toolset variables were eliminated, the most likely remaining culprit is the request payload extras (extra_body.reasoning and possibly other defaults) being included on requests to providers/models that don't advertise support for them.

Excerpt from request dump (/opt/data/sessions/request_dump_*.json):

{
  "model": "openai/gpt-5.4-nano",
  "messages": [...],
  "max_tokens": 64000,
  "extra_body": {
    "reasoning": {
      "enabled": true,
      "effort": "medium"
    }
  },
  "tools": [... 28 tools ...]
}

Response dump:

"error": {
  "type": "BadRequestError",
  "message": "Error code: 400",
  "status_code": 400,
  "body": "",
  "response_status": 400,
  "response_text": ""
}

Debug share artifacts

(Generated by hermes debug share on 2026-04-28.)

Suggested fix direction

  1. Gate extra_body.reasoning.enabled injection on whether the target model/provider advertises support for it (per OpenRouter's supported_parameters field on the model).
  2. Propagate upstream HTTP status codes and error bodies through to the user instead of mapping all 4xx to a generic 400 with empty body.
  3. Consider a config flag to opt out of extra_body injection entirely for users on minimal request shapes.

Workaround tested

  • Reducing toolset to 1/19 (Terminal only): does not resolve the bug.
  • Lowering agent.max_tokens to 4096: does not resolve the bug.
  • Switching models within OpenRouter: does not resolve the bug.
  • Direct curl from the same container/key: works (HTTP 200).

No user-side workaround has been found; appears to require an upstream code fix.

Steps to Reproduce

  1. Configure Hermes 0.9.0 with OpenRouter as the provider and any model (tested: anthropic/claude-sonnet-4.5, openai/gpt-5.4-nano, anthropic/claude-sonnet-4.6, openrouter/elephant-alpha — all fail identically).
  2. Ensure the OpenRouter account has a positive credit balance.
  3. Run hermes to start a chat session.
  4. Send any short prompt, e.g. "Hello in one sentence."
  5. Observe: HTTP 400 BadRequestError with empty response body. Request dump written to /opt/data/sessions/request_dump_*.json shows extra_body.reasoning.enabled: true and max_tokens: 64000 being sent.
  6. Verify the upstream is healthy: a direct curl to OpenRouter with the same API key and same model returns HTTP 200 with a valid completion.
  7. Confirm via Hermes's own diagnostics: hermes doctor reports ✗ OpenRouter API (HTTP 400) under API Connectivity.
  8. Tested workarounds (none resolved): lowering agent.max_tokens to 4096; reducing toolset from 17/19 → 1/19 enabled (request payload shrunk from ~4000 tokens to ~1095 tokens — same error).

Expected Behavior

Hermes should successfully complete the API call. Direct curl to OpenRouter with the same key and model returns HTTP 200, so the failure is in Hermes's request construction or error handling, not the provider.

Specifically:

  1. Hermes's request payload should be accepted by OpenRouter for any model the user has selected, matching the success of an equivalent direct curl call.
  2. When upstream calls do fail (e.g., insufficient credits returning HTTP 402), Hermes should propagate the actual HTTP status code and error body to the user rather than mapping all 4xx errors to a generic HTTP 400 with empty body. The current behavior hides the upstream error message and made the original cause (insufficient credits, returned by OpenRouter as 402) invisible to the user for hours.
  3. Suspected fix direction: gate extra_body.reasoning injection by whether the target model/provider advertises support for it via OpenRouter's supported_parameters field.

Actual Behavior

Hermes returns HTTP 400 BadRequestError with empty response body for every chat completion request to OpenRouter, regardless of model or toolset configuration.

Full error output from the chat session:API call failed (attempt 1/3): BadRequestError [HTTP 400] Provider: openrouter Model: openai/gpt-5.4-nano Endpoint: https://openrouter.ai/api/v1 Error: HTTP 400: Error code: 400 Elapsed: 0.26s Context: 2 msgs, ~1,095 tokens Non-retryable error (HTTP 400) — trying fallback... Request debug dump written to: /opt/data/sessions/request_dump_*.json Non-retryable error (HTTP 400): HTTP 400: Error code: 400 Non-retryable client error (HTTP 400). Aborting. This type of error won't be fixed by retrying. Hermes Error: Error code: 400

The request dump shows Hermes injecting extra_body.reasoning.enabled: true and max_tokens: 64000 into the payload. The response body is empty, so OpenRouter's actual error message is not surfaced to the user.

hermes doctor independently reports the same: ✗ OpenRouter API (HTTP 400) under API Connectivity.

A direct curl with the same key and model returns HTTP 200 with a valid completion, proving the upstream provider, account, key, credits, and endpoint are all healthy. The failure is in Hermes's request construction or error handling, not the provider.

Affected Component

Agent Core (conversation loop, context compression, memory)

Messaging Platform (if gateway-related)

No response

Debug Report

Debug report bundle generated via `hermes debug share` on 2026-04-28:

- Report: https://paste.rs/20475
- agent.log: https://paste.rs/qh9Ls

(paste.rs links may be ephemeral — uploaded the same day this issue is filed.)

Additional request dump path inside the container, in case more dumps are needed:
/opt/data/sessions/request_dump_*.json

Operating System

Ubuntu 24.04.4 LTS (Hostinger VPS host) — Hermes runs inside Docker container hermes-agent-9cex-hermes-agent-1

Python Version

3.13.5

Hermes Version

0.9.0 (build 2026.4.13)

Additional Logs / Traceback (optional)

Request payload excerpt (from /opt/data/sessions/request_dump_*.json):

{
  "model": "openai/gpt-5.4-nano",
  "max_tokens": 64000,
  "extra_body": {
    "reasoning": {
      "enabled": true,
      "effort": "medium"
    }
  },
  "tools": [... 28 tool definitions ...],
  "messages": [...]
}

Response error from same dump:

"error": {
  "type": "BadRequestError",
  "message": "Error code: 400",
  "status_code": 400,
  "body": "",
  "response_status": 400,
  "response_text": ""
}

Note the empty `body` and `response_text` — OpenRouter's actual error message is being discarded by Hermes's error handling. Direct curl to OpenRouter with the same key+model on the same container returns HTTP 200 with valid JSON; this rules out the upstream provider, key, account, and model availability as the cause.

Root Cause Analysis (optional)

No response

Proposed Fix (optional)

No response

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

extent analysis

TL;DR

The most likely fix involves modifying Hermes to conditionally include extra_body.reasoning.enabled and max_tokens in the request payload based on the target model/provider's supported parameters.

Guidance

  • Review the OpenRouter API documentation to confirm the supported parameters for each model, focusing on reasoning and max_tokens.
  • Update Hermes's request construction logic to gate the inclusion of extra_body.reasoning.enabled and max_tokens based on the model's supported parameters.
  • Modify Hermes's error handling to propagate the actual HTTP status code and error body from the upstream provider, rather than mapping all 4xx errors to a generic HTTP 400 with an empty body.
  • Consider adding a configuration flag to allow users to opt out of extra_body injection entirely for minimal request shapes.

Example

// Example request payload with conditional extra_body
{
  "model": "openai/gpt-5.4-nano",
  "messages": [...],
  "max_tokens": 64000,
  "extra_body": {
    "reasoning": {
      "enabled": true,
      "effort": "medium"
    }
  }
  // Only include extra_body if the model supports it
  // if (model.supportsReasoning) {
  //   "extra_body": { ... }
  // }
}

Notes

The proposed fix assumes that the issue is indeed caused by the inclusion of unsupported parameters in the request payload. Further investigation may be necessary to confirm this hypothesis.

Recommendation

Apply a workaround by modifying the request payload to exclude unsupported parameters until a permanent fix is implemented. This can be done by manually crafting the request payload or by using a temporary configuration flag to opt out of extra_body injection.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix [Bug]: HTTP 400 empty body on all OpenRouter requests (Hermes 0.9.0); curl works with same key/model [3 comments, 2 participants]