Skip to content

fix(agent): parse OpenRouter/Nous "X in the output" output-cap errors - #39175

Closed
ssiweifnag wants to merge 5 commits into
NousResearch:mainfrom
ssiweifnag:fix/parse-output-cap-openrouter-format
Closed

fix(agent): parse OpenRouter/Nous "X in the output" output-cap errors#39175
ssiweifnag wants to merge 5 commits into
NousResearch:mainfrom
ssiweifnag:fix/parse-output-cap-openrouter-format

Conversation

@ssiweifnag

Copy link
Copy Markdown
Contributor

Summary

parse_available_output_tokens_from_error() in agent/model_metadata.py recognized only Anthropic's "... = available_tokens: N" shape. OpenRouter and Nous Research return the same output-cap condition in a different shape:

maximum context length is 256000 tokens. However, you requested about 281093 tokens (5683 of text input, 13410 of tool input, 262000 in the output).

For these providers the parser returned None, so the conversation loop classified the error as input-overflow and tried to compress history. On a fresh session there's nothing to compress, the gateway auto-reset, and the same max_tokens config value produced the same error on the next message — infinite loop. /new doesn't help because the trigger is config, not session history.

This adds the OpenRouter/Nous shape recognition (guarded by three structural anchors — text input, tool input, in the output — to avoid false positives) and extracts available_output = context_length - text_input - tool_input. All 11 existing Anthropic-format tests still pass.

Test plan

Closes #38652

🤖 Generated with Claude Code

…esent

_check_via_local_git() hard-coded 'origin' for both the fetch and the
rev-list ref, so users on a fork (where 'origin' is their own clone)
saw a 'behind' count measured against their fork's main rather than the
canonical source. The fork itself was always ahead of local by 0-3
commits, so the message was permanently misleading.

Add a small _detect_canonical_remote() helper that returns 'upstream'
when that remote exists, else 'origin', and use it in
_check_via_local_git. This mirrors the convention already used by
cmd_update (see _has_upstream_remote, _sync_with_upstream_if_needed)
so the update-check banner and the 'hermes update' flow now agree
on what 'behind' means for fork users.

Also update the docstring of check_for_updates() to say 'canonical
remote' instead of 'origin/main'.
Adds regression coverage for the canonical-remote detection in
_check_via_local_git. Without these tests, a future refactor that
re-hardcodes "origin" would silently break fork users (the "behind"
banner would measure against the fork's main, which is normally 0-3
commits ahead, producing permanent false positives).

Two test groups:
- TestDetectCanonicalRemote: exercises the detector against real git
  repos in tmp_path (4 scenarios: upstream present, no upstream, not a
  git repo, only upstream).
- TestCheckViaLocalGitUsesCanonicalRemote: pins the contract that the
  checker fetches+rev-lists whatever the detector returns, by mocking
  _detect_canonical_remote directly so subprocess.run only sees the
  intended fetch/rev-list calls.
The error returned by _validate_cron_script_path for absolute / ~ /
Windows paths told users to "Place scripts in ~/.hermes/scripts/" — but
the actual resolution uses get_hermes_home() / "scripts", so on Docker
or any non-default HERMES_HOME setup the error message points users (and
the agent) at the wrong directory.

The fix computes scripts_dir once and uses it in both the rejection
message and the (unchanged) containment check.

Closes NousResearch#38693
…Exit

The memory plugin loader used to glob every *.py in a user-installed
provider directory and exec_module it, with a bare `except Exception`
around the call. Two compounding problems:

1. Files that are not real submodules — `setup.py`, `conftest.py`,
   `pyproject.py`, `test_*.py`, `*_test.py` — get executed. A `setup.py`
   next to a plugin will call setuptools, which parses sys.argv and
   `sys.exit()`s on bad subcommand.
2. `sys.exit()` raises SystemExit, which inherits from BaseException —
   not Exception. The bare `except Exception` does not catch it, so
   SystemExit propagates and crashes the whole Hermes process.

Fix: skip the known non-submodule files in the glob, and explicitly
re-raise KeyboardInterrupt/SystemExit in the load path so they cannot
be swallowed by future `except` clauses either.

Closes NousResearch#38674
The conversation loop's recovery path uses
parse_available_output_tokens_from_error() to detect when the request
failed because max_tokens (the *output* cap) is too large relative to
the context window, vs. the *input* itself being too long. The two
require different recovery strategies — output-cap errors reduce
max_tokens for the next call, while input-overflow errors compress
history.

The parser previously recognized only Anthropic's
"... = available_tokens: N" shape. OpenRouter and Nous Research
return the same condition in a different shape:

  "maximum context length is 256000 tokens. However, you requested
   about 281093 tokens (5683 of text input, 13410 of tool input,
   262000 in the output)."

For OpenRouter/Nous users, the parser returned None, so Hermes
classified the error as a prompt-overflow and tried to compress
history. On a fresh session with 1 message there's nothing to
compress, so the gateway auto-reset the session — and on the next
message the same max_tokens config value produced the same error,
looping forever.

The new guard recognizes the OpenRouter shape by its three
structural anchors (text input, tool input, "in the output") and
extracts the available output as context_length - text_input -
tool_input. All existing Anthropic-format tests still pass.

Closes NousResearch#38652
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/openrouter OpenRouter aggregator provider/nous Nous Research API (OAuth) labels Jun 4, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #38659 (filed first) — both add OpenRouter/Nous "N in the output" recognition to parse_available_output_tokens_from_error() in agent/model_metadata.py to fix the #38652 infinite auto-reset loop. This is now the 4th competing fix in the cluster (#38659, #38667, #38689). Recommend consolidating on one.

@teknium1

Copy link
Copy Markdown
Contributor

This looks implemented on current main by a later consolidated fix. This is an automated hermes-sweeper review.

  • agent/model_metadata.py:1054 now recognizes the OpenRouter/Nous output-cap wording with maximum context length + in the output.
  • agent/model_metadata.py:1086 parses (A of text input, B of tool input, C in the output) and computes available output as context_length - text_input - tool_input.
  • The fix landed in 1fb99b1f229a700f8ee1a3e90ff1d44f85e963d4 (fix(stream+output-cap): guard empty streams and parse OpenRouter output-cap errors (#40589)), which is contained in v2026.6.19.
  • Prior discussion on this PR also identified it as part of the duplicate [Bug]: parse_available_output_tokens_from_error() misses OpenRouter/Nous "in the output" format — causes infinite auto-reset loop #38652 fix cluster.

Thanks for the report and patch; the specific agent-side output-cap parser fix this PR targets is now on main.

@teknium1 teknium1 closed this Jun 21, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P1 High — major feature broken, no workaround provider/nous Nous Research API (OAuth) provider/openrouter OpenRouter aggregator sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: parse_available_output_tokens_from_error() misses OpenRouter/Nous "in the output" format — causes infinite auto-reset loop

3 participants