Commit graph

139 commits

Author SHA1 Message Date
srojk34
3ea35d6711 fix(vertex,moa): register vertex in PROVIDER_REGISTRY and HERMES_OVERLAYS
The Vertex AI provider (added same-day, commit c73e74386) was never added to
either of the two provider registries that agent/auxiliary_client.py and the
MoA slot-resolution chain depend on, breaking Vertex outside the main
conversation loop:

1. hermes_cli/auth.py::PROVIDER_REGISTRY had no "vertex" entry. The
   plugin-auto-extend loop that normally fills gaps explicitly skips
   non-api_key auth types (`if _pp.auth_type != "api_key": continue`), and
   Vertex was never hand-declared like "bedrock" is. Because
   resolve_provider_client() in agent/auxiliary_client.py gates everything
   on `pconfig = PROVIDER_REGISTRY.get(provider)` and returns (None, None)
   immediately when pconfig is None, its `elif pconfig.auth_type == "vertex"`
   branch was permanently dead code — every auxiliary Vertex call (vision,
   title generation, reflection, context compression, MoA reference/
   aggregator slots) failed outright, not just a MoA-specific edge case.

2. hermes_cli/providers.py::HERMES_OVERLAYS also had no "vertex" entry, so
   hermes_cli.providers.get_provider("vertex") returned None. This backs
   _preserve_provider_with_base_url() in agent/auxiliary_client.py, which a
   MoA slot's resolved (base_url, api_key) pair needs to keep its "vertex"
   identity instead of silently collapsing to "custom" — losing the
   identity _refresh_provider_credentials() needs to re-mint an expired
   OAuth2 token (~1h lifetime) on a 401, and permanently breaking every
   subsequent call in that MoA preset for the rest of the session.

Fix mirrors the existing "bedrock"/aws_sdk entries in both registries
exactly, plus adds a "vertex" branch to _refresh_provider_credentials() (it
had branches for openai-codex/nous/anthropic/xai-oauth but not vertex,
so a 401 fell through to `return False` without evicting the stale cached
client).

- hermes_cli/auth.py: hand-declared vertex ProviderConfig(auth_type="vertex")
  in PROVIDER_REGISTRY, matching bedrock's shape.
- hermes_cli/providers.py: vertex HermesOverlay(auth_type="vertex") in
  HERMES_OVERLAYS + "Google Vertex AI" label override.
- agent/auxiliary_client.py: vertex branch in _refresh_provider_credentials
  that re-mints the token via get_vertex_config() and evicts the stale
  cached client.
- 8 new regression tests across tests/hermes_cli/test_vertex_provider.py and
  tests/agent/test_auxiliary_client.py: registry membership, end-to-end
  resolve_provider_client("vertex", ...) building a working client (proving
  the previously-dead branch is now reachable), and the 401-refresh/cache-
  eviction path.
2026-07-23 16:55:41 -07:00
aui
ead9d7b256 test: cover gemini-native max_tokens forwarding in _build_call_kwargs
Requested in review: builder-level assertions that the gemini-native
branch forwards max_tokens (provider names and the native
generativelanguage.googleapis.com base_url, max_tokens=600), plus a
control showing gemini models on OpenAI-compatible endpoints — including
Gemini's own /openai compatibility endpoint — keep the existing omission
behavior (#34530).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:17:27 -07:00
Janig88
3dce1b967f fix(auxiliary): scope max_tokens to moa_reference only (not aggregator)
Per review feedback from teknium1: reference_max_tokens is an advisors-only
contract. The aggregator is the acting model and must not be capped by the
reference budget. Changed _is_moa from startswith('moa_') to exact match on
'moa_reference'. Added regression test proving aggregator does NOT receive
max_tokens.
2026-07-23 16:17:27 -07:00
Janig88
3616ce006a fix: use auxiliary_max_tokens_param for Copilot GPT-5 compat
Copilot review pointed out that hardcoding kwargs['max_tokens'] would
400 on models requiring max_completion_tokens (GPT-5 family, Copilot).
The existing auxiliary_max_tokens_param() helper already selects the
correct parameter name per model — use it instead of hardcoding.

Test updated to parametrize expected_key so the Copilot gpt-5.5 case
correctly asserts max_completion_tokens instead of max_tokens.

Addresses Copilot review comments on both files.
2026-07-23 16:17:27 -07:00
Janig88
32a4faa2d5 fix(auxiliary): honor max_tokens for MoA reference/aggregator tasks
PR #56756 added reference_max_tokens to cap MoA advisor output and cut
turn latency. The value is correctly threaded through five layers of MoA
code (moa_config → conversation_loop → aggregate_moa_context →
_run_references_parallel → _run_reference → call_llm(task='moa_reference',
max_tokens=800, ...)).

However, _build_call_kwargs() in auxiliary_client.py silently drops
max_tokens for all OpenAI-compatible providers (PR #34845, which fixed
endpoints and NVIDIA NIM keep it. This means reference_max_tokens never
reached the API for the vast majority of providers.

The bug affects every OpenAI-compatible MoA reference/aggregator slot:
Z.AI (coding plan), OpenRouter, OpenAI, GitHub Copilot, and local
providers. Only Anthropic-compat endpoints (MiniMax, /anthropic URLs)
worked — by coincidence, not MoA-aware design.

Fix: thread the 'task' parameter through all six _build_call_kwargs()
call sites. When task starts with 'moa_', max_tokens is always included
in the request kwargs regardless of provider. Non-MoA auxiliary tasks
(compression, titles, vision, etc.) keep PR #34845 behavior unchanged.

Verified end-to-end:
- Z.AI GLM-5.2 with max_tokens=50 → returned exactly 50 tokens
- Z.AI GLM-5.2 with max_tokens=20 → returned exactly 20 tokens
- Z.AI GLM-5.2 uncapped → returned 315 tokens
- 7 new regression tests covering 4 providers, Anthropic wire, non-MoA
  tasks, and prefix-matching boundary
- 288 auxiliary_client tests pass (was 281, +7 new), 84 MoA tests pass
- Zero regressions
2026-07-23 16:17:27 -07:00
srojk34
2962ba2b7b fix(auxiliary): treat explicit model:auto sentinel, not just cfg_model
'auto' is a sentinel meaning "inherit from main runtime / auto-detect",
not a literal model id -- already handled for cfg_model (config-derived)
in _resolve_task_provider_model, but not for the explicit `model` kwarg.

MoA reference/aggregator slots (agent/moa_loop.py's _slot_runtime) forward
a preset's `model:` field as this explicit argument rather than through
auxiliary.<task> config, so a MoA preset configured with `model: auto`
(a natural thing to try given the existing auxiliary.*.model: auto
convention) reached this function as the explicit `model` arg and took
the `model or cfg_model` branch, bypassing the cfg_model-only sentinel
check entirely -- sending the literal string "auto" to the wire as a
model id.

Normalize both the explicit `model` and `cfg_model` the same way, fixing
this at the single chokepoint every caller (MoA included) already goes
through, rather than patching moa_loop.py separately.
2026-07-23 11:20:43 -07:00
Teknium
2755bf558e fix(auxiliary): route all MoA aux resolution through one shared aggregator helper
Follow-up to srojk34's explicit-provider unwrap (PR #56691):

- Extract _resolve_moa_aggregator() as the single preset->aggregator
  resolver shared by _resolve_auto(), _resolve_task_provider_model(),
  and resolve_provider_client() so preset lookup/validation can't drift.
- When the main provider is moa, the aggregator model is now the default
  for every UNSET auxiliary model: _read_main_model_for_aux() substitutes
  the preset's acting (aggregator) model wherever fallback chains
  pre-filled from _read_main_model() (router prefill, custom-endpoint
  fallback, named-custom default, external-process default,
  _try_main_agent_model_fallback).
- Unwrap moa at the resolve_provider_client() chokepoint so direct
  callers (vision auto-detect, plugin code) can't dead-end in the
  unknown-provider branch, and unwrap the vision auto-detect main
  provider before capability probes run against the preset name.
- Real-config tests: temp HERMES_HOME + actual config.yaml exercising
  the genuine load_config()/resolve_moa_preset() boundary.
2026-07-23 07:34:24 -07:00
srojk34
cdfe562342 fix(auxiliary): unwrap explicit provider:moa to its aggregator, not the literal name
_resolve_task_provider_model() returned an explicit provider="moa" override
(from a caller-passed arg, or auxiliary.<task>.provider: moa in config.yaml)
verbatim, with no MoA-preset unwrap. Only the *implicit* "main provider is
moa" path inside _resolve_auto() unwraps to the aggregator slot (#53827) —
this function never goes through _resolve_auto() at all, so the explicit
case was never covered.

MoA is a virtual provider with no real HTTP endpoint: resolve_provider_client()
looks "moa" up in PROVIDER_REGISTRY (no such entry), falls to the
unknown-provider dead end, and call_llm surfaces a nonsensical "Provider
'moa' is set in config.yaml but no API key was found. Set the MOA_API_KEY
environment variable..." error for a provider that was never meant to be
reached over the wire.

Fix mirrors #53827's aggregator-resolution approach exactly: when either the
explicit `provider` arg or the config-derived `cfg_provider` is "moa",
resolve the named (or default) MoA preset via resolve_moa_preset() and
continue with its aggregator's real provider+model, dropping any explicit
base_url/api_key (the moa:// virtual endpoint and placeholder key belong to
the facade, not the aggregator's real provider). If the preset can't be
resolved (renamed/deleted), degrades gracefully to the pre-fix behavior
instead of raising harder.

- agent/auxiliary_client.py: _unwrap_moa_provider() helper + call sites for
  both the explicit-arg and config-derived provider="moa" cases in
  _resolve_task_provider_model(). Also tightened base_url/api_key parameter
  types to Optional[str] (matching their actual None-accepting behavior),
  which incidentally resolved 5 pre-existing ty diagnostics at call sites.
- 5 new regression tests in tests/agent/test_auxiliary_client.py: explicit
  arg unwrap, config-derived unwrap, default-preset fallback when no model
  is configured, graceful degradation on preset-resolution failure, and a
  non-moa regression guard.
2026-07-23 07:34:24 -07:00
Teknium
eb6aa03609
feat(analytics): record auxiliary model usage per task in session accounting (#65537)
* feat(analytics): record auxiliary model usage per task in session accounting

Auxiliary LLM calls (vision, compression, title_generation, web_extract,
session_search, ...) discarded their token usage, leaving dashboard
analytics blind to aux model spend (issue #23270).

- hermes_state.py: session_model_usage gains a task PK dimension
  (''=main loop) via v22 table-rebuild migration (SQLite can't alter a
  PK); record_auxiliary_usage() writes per-(model,provider,task) deltas
  WITHOUT touching sessions counters (gateway overwrites those with
  absolute main-loop totals — folding aux in would double-count or be
  clobbered). Aux rows never inherit the session's main-loop route.
- agent/aux_accounting.py: ContextVar ambient accounting context
  (mirrors the portal_tags conversation context); record_aux_usage()
  normalizes usage via usage_pricing.normalize_usage, estimates cost,
  and is strictly best-effort. moa_reference/moa_aggregator excluded —
  conversation_loop already folds MoA usage+cost into the main delta.
- agent/auxiliary_client.py: _validate_llm_response is the recording
  chokepoint — every successful non-streaming aux response passes
  through it exactly once, sync and async, including fallback paths
  (model read from the response itself stays accurate across
  fallbacks).
- run_agent.py: run_conversation publishes/resets the accounting
  context; agent/title_generator.py republishes on its bare thread.
- hermes_cli/web_server.py: /api/analytics/usage folds aux rows into
  by_model (aux-only models finally appear) and adds a by_task
  summary; /api/analytics/models surfaces aux rows on the Models page.

Design per review of PR #62850 by @eeksock (thread-local + separate
auxiliary_usage table): rebuilt on ContextVar (async-safe — thread-local
cross-attributes concurrent coroutines on one event loop) and the
existing session_model_usage table instead of a parallel accounting
path, extended beyond vision to every aux task, and wired the analytics
endpoints so the dashboard actually shows it. Credit to @eeksock for
the approach and @tboatman for the detailed root-cause analysis.

* test(moa): match _validate_llm_response mock to new accounting-hint signature

* test(aux): accept accounting-hint kwargs in remaining _validate_llm_response mocks
2026-07-16 04:23:12 -07:00
Teknium
31dcd68bfa test: cover Anthropic aux extra_body passthrough (Bug B scope + exclusions)
Five tests for the salvaged #37217 Bug B fix: vendor-field passthrough,
reasoning-key + private-key exclusion, merge-over-existing (fast-mode
speed), no-extra_body regression guard, reasoning-only adds nothing.

Live probes against api.anthropic.com informed the exclusion design:
Anthropic strictly validates the request body (unknown keys 400 with
'Extra inputs are not permitted'), so the passthrough forwards only
caller-configured fields and never the OpenAI-shaped reasoning dict
(translated natively) or _-private plumbing keys.
2026-07-15 06:25:10 -07:00
Teknium
0bb3a82c53 refactor(moa): drop auxiliary-task reasoning knob in favor of per-slot preset config
The just-merged auxiliary.<task>.reasoning_effort shorthand applied
ensemble-wide to MoA (one value for every advisor) — wrong granularity.
Per-slot preset config supersedes it:

  moa:
    presets:
      deep_review:
        reference_models:
          - {provider: ..., model: ..., reasoning_effort: low}
          - {provider: ..., model: ..., reasoning_effort: xhigh}
        aggregator:
          {provider: ..., model: ..., reasoning_effort: high}

- Remove reasoning_effort from the moa_reference/moa_aggregator
  DEFAULT_CONFIG blocks; _get_task_extra_body now warns-and-ignores the
  key on MoA tasks, pointing at the preset config
- Guard tests: MoA aux blocks must not regrow the key; task-level value
  is rejected with the pointer warning
- Docs: configuration.md notes the MoA exception and links the MoA page
2026-07-14 21:08:22 -07:00
Justin Schille
5646dbdd5b fix(moa): project slot reasoning through provider profiles 2026-07-14 21:08:22 -07:00
Justin Schille
3dca75b45c feat(moa): support per-slot reasoning effort 2026-07-14 21:08:22 -07:00
Teknium
df5700ebe3
feat(auxiliary): per-task reasoning_effort for auxiliary models (#64597)
Every auxiliary task block (vision, web_extract, compression,
title_generation, curator, background_review, moa_reference, ...) now
accepts a reasoning_effort shorthand:

  auxiliary:
    compression:
      reasoning_effort: low
    vision:
      reasoning_effort: none

_get_task_extra_body() folds it into extra_body.reasoning, which every
auxiliary wire already translates: chat.completions passes it through,
the Codex Responses adapter maps it to top-level reasoning/include, and
the Anthropic auxiliary adapter now forwards it into
build_anthropic_kwargs(reasoning_config=...) (previously hardcoded None).

An explicit extra_body.reasoning on the same task wins over the
shorthand. Invalid levels are ignored with a warning. Empty string
(the shipped default) is a no-op — zero behavior change.

Config: reasoning_effort added to all 16 auxiliary task blocks in
DEFAULT_CONFIG (no version bump — deep-merge handles new keys).
2026-07-14 14:07:43 -07:00
kshitijk4poor
22d5a35c16 fix(codex): harden Copilot replay classification
Require literal booleans for backend-specific replay policy and pin
non-default status and content preservation through both response paths.
2026-07-11 12:09:27 +05:30
joaomarcos
83b2a685cd fix(codex): also guard the auxiliary Copilot Responses adapter
_CodexCompletionsAdapter (agent/auxiliary_client.py) is a second,
independent producer of Codex Responses input — used by auxiliary
calls (context compression, flush_memories, MoA aggregation,
session_search) that route through CodexAuxiliaryClient instead of
the main agent's ResponsesApiTransport.build_kwargs. It calls
_chat_messages_to_responses_input() directly without is_github_responses,
so the previous commit's fix didn't cover it: an auxiliary call made
against a Copilot-backed session could still replay a connection-scoped
codex_message_items id and hit the same HTTP 401.

Detect the Copilot host from the adapter's own client.base_url (same
check the adapter already does further down for prompt_cache_key
opt-out) and pass is_github_responses through, closing the gap.

Still #32716.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 12:09:27 +05:30
teknium1
d42e9b1788 fix(auxiliary): recover from stale fallback-candidate credentials instead of aborting
A fallback candidate can itself carry a stale credential (e.g. an
expired ANTHROPIC_TOKEN picked up by _try_anthropic). Its 401 previously
propagated out of the fallback call site and aborted the auxiliary task
— for compression: a 60s cooldown + context marker while the session
kept growing past the context cap. Live case: mattalachia debug dump
(Jul 2026), Codex timeout → Anthropic 401 x5 → 296K 'Cannot compress
further'.

Now each fallback candidate call is wrapped: on auth error, refresh the
candidate's provider credentials and retry once; if unrefreshable, mark
the provider unhealthy and walk the discovery chain again so the next
viable candidate serves. Sync + async paths. Non-auth errors still
raise unchanged.
2026-07-06 18:12:24 -07:00
Fan Yang
f69e3aadf1 fix(auxiliary): refresh auto-routed provider credentials on 401
Infer the concrete auxiliary auth provider from the selected client base
URL so provider:auto routes can refresh Copilot/Codex/Anthropic/Nous
credentials after auth errors, instead of skipping refresh because
resolved_provider stayed 'auto'. Adds the copilot branch to
_refresh_provider_credentials and evicts the stale auto-route cache
before retrying.

Fixes #20832. Salvaged from PR #20837, reapplied surgically onto current
main (branch predated the _retry_same_provider_sync/async extraction).
2026-07-06 18:12:24 -07:00
teknium1
ede7e31636 fix(auxiliary): gate main api_key inheritance on same-host aux base_url
Follow-up to the #55911 salvage: inherit model.api_key only when the aux
base_url resolves to the same hostname as the main model's base_url
(runtime override or config). A misconfigured aux endpoint on a different
host keeps the fail-safe no-key-required placeholder instead of leaking
the main credential cross-host.
2026-07-05 17:38:06 -07:00
Tranquil-Flow
8e09afda27 fix(auxiliary): inherit model.api_key for custom endpoint when per-task key is empty (#9318)
When an auxiliary task is configured with provider=custom and an explicit
base_url but an empty api_key, the custom_key fallback chain in
resolve_provider_client() jumped straight to the no-key-required
placeholder without consulting model.api_key from config.yaml.  Users
on self-hosted gateways who share the same endpoint and credentials for
both the main model and auxiliary tasks got 401 auth errors.

Add _read_main_api_key() following the same pattern as _read_main_model()
and _read_main_provider(): checks _RUNTIME_MAIN_API_KEY (runtime override)
first, then config.yaml model.api_key.  Insert it into the fallback chain
before no-key-required so real credentials are used when available, while
local servers without auth still get the placeholder.
2026-07-05 17:38:06 -07:00
falkoro
9ad912a791 fix(agent): honor auxiliary.<task>.base_url/api_key when provider is passed explicitly
_resolve_task_provider_model returns early on an explicit provider arg,
which skips the config block that consults auxiliary.<task>.base_url /
api_key. Any caller passing provider explicitly (e.g.
resolve_vision_provider_client(provider="custom", ...)) bypasses the
configured custom endpoint and falls through to main-runtime resolution,
silently routing the task to the wrong backend.

Adopt the task's configured base_url/api_key before the early returns,
but only when no explicit base_url was given and the config targets the
same provider (or names none) — a caller forcing a *different* provider
keeps full explicit-arg priority, and an explicit base_url still wins
over config.

Fixes #58515
2026-07-05 14:41:43 -07:00
Teknium
fb403a3a73
fix(auxiliary): retry transient blips harder + isolate client cache per model (#56889)
Two related hardening fixes for auxiliary calls (which include MoA reference
advisors — a pinned-model path where provider fallback is not a meaningful
recovery):

1. Transient-transport retries: the same-provider retry on a connection reset /
   timeout / 5xx / 408 was a single attempt, then fallback. For a pinned aux
   call a second blip silently loses the call (root of the run2 double-advisor
   'Connection error' collapse — a genuine upstream blip). Now retries N times
   with exponential backoff, N = auxiliary.transient_retries (default 2 -> 3
   total attempts, clamped [0,6]). Compression-on-timeout fast-fail carve-out
   preserved.

2. Per-model client-cache isolation: _client_cache_key excluded the model, so
   two concurrent auxiliary calls to the same provider/base_url/key but
   different models (e.g. an opus + gpt-5.5 MoA fan-out) shared one cache entry
   and could race each other's client lifecycle. Model now participates in the
   key -> distinct clients, no cross-call races. Same-model reuse unchanged.

- agent/auxiliary_client.py: _transient_retry_count() + backoff loop; model in
  _client_cache_key and both call sites.
- hermes_cli/config.py: auxiliary.transient_retries default (2).
- tests: new retry/isolation tests; updated 2 stale-expectation tests to the
  corrected behavior (per-model resolve; N-retry escalation).

Backoff base is overridable (_TRANSIENT_RETRY_BACKOFF_BASE) so tests don't sleep.
2026-07-02 01:09:37 -07:00
kshitijk4poor
76be770091 test(moa): assert aux cap against model resolver, not frozen literal
Follow-up to the salvaged fix: the regression test asserted a frozen
max_tokens == 128_000 literal, coupling it to the Opus-4-8 model table.
Assert against _get_anthropic_max_output("claude-opus-4-8") plus > 2000
instead, so the test survives model-table churn while still catching a
regression to the old `or 2000` fallback.
2026-07-02 06:31:18 +05:30
helix4u
7951250947 fix(moa): lift hidden Anthropic aux output cap 2026-07-02 06:31:18 +05:30
Teknium
eae3700b16
fix(moa): raise aux timeouts to 900s and give the Codex aux path a stable prompt_cache_key (#56395)
Two independent MoA auxiliary-call fixes:

#53866 — auxiliary.moa_reference.timeout and auxiliary.moa_aggregator.timeout
were 600s while moa_agent was 120s. Raise both to 900s so a genuinely long
reference/aggregator turn (mixed providers, deep reasoning, long tool chains)
has headroom instead of being cut mid-generation.

#53735 — _CodexCompletionsAdapter (the Codex/Responses auxiliary path used by
the MoA acting-aggregator, compression, web_extract, session_search, etc.)
never set prompt_cache_key, so it stayed cache-cold while the MAIN Responses
transport (agent/transports/codex.py) was warm. Derive the same
content-addressed key via the shared _content_cache_key(instructions, tools)
helper and set it on the aux Responses request, with the same host guards the
main transport uses (xAI carries the key in extra_body; GitHub/Copilot opts out
of cache-key routing).

Tests: 5 new prompt_cache_key cases (set+prefixed, stable across identical
prefix, differs on different instructions, skipped for xai/github hosts).
tests/agent/test_auxiliary_client.py 279 pass; tests/hermes_cli/test_config.py
130 pass.
2026-07-01 06:02:40 -07:00
Harish Kukreja
01bf61c865 fix(runtime): honor NOUS_INFERENCE_BASE_URL across pool/explicit/aux paths
Upstream #52270 added `_nous_inference_env_override()` but wired it into
only `resolve_nous_runtime_credentials`. Three sibling resolution paths
still ignored the override, so a self-hosted Nous inference endpoint set
via `NOUS_INFERENCE_BASE_URL` was silently dropped whenever credentials
arrived through any of them:

- the credential-pool path (`_resolve_runtime_from_pool_entry`)
- the explicit-provider path (`_resolve_explicit_runtime`)
- the auxiliary side-LLM client (`_pool_runtime_base_url`)

Route all three through the same auth-layer reader so every
`NOUS_INFERENCE_BASE_URL` read shares one normalization path
(trailing-slash stripping, blank -> empty) and the documented
trusted-bypass intent stays in one place. The override is live-only: it
wins for the base URL returned this run but is never persisted to
auth.json or the credential pool, so an ephemeral dev/staging value
cannot poison durable auth state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 01:52:06 -07:00
Gille
a8841e2a68 fix(aux): preserve provider identity for resolved endpoints
_resolve_task_provider_model() flattened any explicit base_url to
provider=custom. Correct for bare/custom endpoints, but wrong for
provider-backed routes (anthropic, qwen-oauth, minimax-oauth,
openai-codex, etc.) whose provider branch adds auth refresh, transport,
or request shaping. MoA reference slots resolved through those providers
lost their identity before the aux call, so e.g. a Codex reference hit
chatgpt.com/backend-api/codex without its Cloudflare headers and got
HTML back (surfacing as a spurious rate-limit).

Keep first-class providers intact when paired with a resolved base_url
via _preserve_provider_with_base_url(); bare/custom/auto/unknown and the
direct openai alias still route through custom.

Co-authored-by: Hermes Agent <127238744+teknium1@users.noreply.github.com>
2026-06-30 04:23:27 -07:00
Teknium
c8376e0dc6
fix(auxiliary): stop SDK retries from multiplying compression stall (#54465) (#55544)
The auxiliary OpenAI clients were built without overriding the SDK's
default max_retries=2, so every aux call silently made up to 3 attempts
against a slow/hung endpoint — a 120s timeout could stall ~360s before
Hermes saw a single failure. On the critical compression preflight path,
Hermes then added its own same-provider timeout retry on top, roughly
doubling the user-visible stall again before fallback.

- Build both the sync (_create_openai_client) and async (_to_async_client)
  aux clients with max_retries=0 (setdefault, so explicit callers still
  override). Hermes already owns retry + provider/model fallback policy.
- For task == compression, skip the same-provider transient retry on a
  full-budget timeout and fall straight through to fallback. Fast blips
  (streaming-close, 5xx) still retry, since those are cheap.
- Add _is_timeout_error to distinguish a full-budget timeout from a fast
  connection drop.

Addresses the retry-multiplication root cause of #54465 (the resume-wedge
persistence half landed in #55499).
2026-06-30 02:54:08 -07:00
HexLab98
f1345290ed test(auxiliary): cover NVIDIA NIM max_tokens in _build_call_kwargs 2026-06-29 18:04:39 +05:30
LeonSGP43
c56b39c11e fix(auxiliary): fall back to OPENROUTER_API_KEY when credential pool exhausted
_try_openrouter() returned (None, None) whenever an OpenRouter credential
pool existed but was exhausted (_select_pool_entry -> (True, None)), making
the OPENROUTER_API_KEY env-var fallback unreachable. Auxiliary tasks
(compression, vision, web_extract) silently failed even with a valid env key.

Now the pool-present branch only returns early when it successfully builds a
client; an exhausted pool falls through to the env-var path. The final
failure (pool exhausted AND no env var) still marks the provider unhealthy.

Fixes #23452.

Co-authored-by: ambition0802 <noreply@github.com>
2026-06-27 19:09:27 -07:00
qWaitCrypto
46e18804ad fix(auxiliary): fall back on 401 auth errors in auto mode (#21165)
When the primary provider returns 401 and the auth-refresh path is
unavailable or fails, both call_llm() and async_call_llm() reached the
should_fallback gate without _is_auth_error in the condition, so the
auxiliary task (e.g. compression) was dropped silently — losing message
history. Add _is_auth_error to should_fallback (NOT is_capacity_error) in
both sync and async paths, plus an 'auth error' reason branch.

Auth stays a non-capacity error: it falls back in auto mode via the
is_auto gate, but on an explicitly-configured provider it still respects
the user's choice and raises rather than silently switching providers.
2026-06-27 19:07:04 -07:00
helix4u
063fe4f6ef fix(auxiliary): fallback on invalid provider responses 2026-06-26 13:49:46 +05:30
teknium1
0d777453fa fix(auxiliary): fall back when a route can't run the model at all (400 capability mismatch)
The salvaged context-window screen (#52392) skips fallback candidates that
are too small, and the rate-limit/403 fixes skip candidates that are at
capacity. A third hard failure remained uncovered: a fallback that builds a
client fine but returns a 400 because it structurally cannot run the model.
The canonical case is a configured openai-codex / ChatGPT-account fallback
asked to compress a glm-5.2 conversation:

    400 - {'detail': "The 'glm-5.2' model is not supported when using
    Codex with a ChatGPT account."}

This is a request-validation error, so should_fallback was False and the
explicit-provider gate blocked it — the auxiliary task (compression) aborted
every turn, dropping middle turns without a summary and churning the session,
which is exactly what destroys the prompt cache.

Adds _is_model_incompatible_error() (400 + capability phrasing, excluding
not-found and billing 400s which the sibling predicates own) and treats it as
a fallback-worthy capacity error in both sync and async call_llm, so the chain
skips the incapable route and continues to the next viable candidate.
2026-06-25 13:08:18 -07:00
Tranquil-Flow
e4d026aa3b fix(auxiliary): screen fallback chain by context window for compression (#52392)
The runtime auxiliary fallback chain (_try_configured_fallback_chain and
_try_main_fallback_chain) returned the first reachable candidate without
checking whether the candidate's context window was large enough for the
task. For task='compression' this meant a reachable but undersized
fallback (e.g. 32K) could be selected and then fail, even when a later
larger-context fallback was available.

This adds two small helpers:

  _task_minimum_context_length(task)
      Returns MINIMUM_CONTEXT_LENGTH (64K) for compression, None for
      other tasks (vision, web_extract, etc.).

  _candidate_context_window(provider, model, ...)
      Thin wrapper around get_model_context_length that returns None on
      probe failure so unknown/custom endpoints pass through unchanged
      (preserves the existing fallback surface).

Both fallback loops now skip reachable candidates whose resolved context
is below the task minimum and continue iterating. The success path
(first viable candidate wins) is unchanged. Return shape and ordering
for healthy candidates are preserved.

Six regression tests cover:
  L2 configured chain skips too-small candidate
  L2 chain continues after skipping, returns last viable
  L3 main chain skips too-small candidate
  L4 unknown-context candidate passes through
  L5 non-compression task is not filtered
  L6 minimum constant matches MINIMUM_CONTEXT_LENGTH (64K)

3/6 fail on upstream/main without the production change (verified); all
6 pass with the fix. Full test_auxiliary_client.py suite (231 tests)
and related compression tests (130 tests) remain green.
2026-06-25 13:08:18 -07:00
herbalizer404
b82c83d320 fix(auxiliary): honor fallback chain when compression provider auth is unavailable
When an explicit aux provider cannot build a client before any request is
sent (missing raw env key, exhausted/unavailable OAuth or credential-pool
auth, resolver returning (None, None)), call_llm raised a misleading
"no API key was found" error and bypassed the configured fallback_chain
entirely. A provider authenticated through Hermes auth / the credential
pool (e.g. ollama-cloud) whose pool entry is exhausted hit this path, so
compression failed instead of routing to the configured fallback.

Adds _try_configured_fallback_for_unavailable_client() and wires it into
both sync and async call_llm before the raise, and into the startup
compression feasibility check.

Salvaged from #51835 by @herbalizer404.
2026-06-25 13:08:18 -07:00
pyxl-dev
751adfa6b9 fix: include rate-limit in auxiliary capacity-error fallback gate
Rate-limit (429) errors on explicit-provider auxiliary tasks were
silently failing instead of triggering the fallback chain. The
is_capacity_error gate only checked payment and connection errors,
excluding rate limits — so when a configured provider like
openai-codex hit its rate limit, auxiliary tasks (kanban_decomposer,
vision, web_extract, approval, etc.) had zero resilience.

Add _is_rate_limit_error() to is_capacity_error at both call sites
(sync and async paths) so rate limits trigger fallback regardless
of whether the provider was auto-detected or explicitly configured.

Fixes #52228
2026-06-25 13:08:18 -07:00
herbalizer404
ff8920299c fix(auxiliary): treat 403 subscription and session-usage-limit errors as payment errors for fallback
Ollama Cloud (and similar) return 403 with bodies like "this model requires
a subscription, upgrade for access" or "you have reached your session usage
limit, upgrade for higher limits". These are capacity/billing conditions
semantically identical to credit exhaustion, but _is_payment_error() did not
recognize them (403 missing from the status set; keywords missing), so the
configured fallback_chain was never tried and compression failed outright.

Adds 403 to the status set and the subscription/session-usage keywords.

Salvaged from #49076 by @herbalizer404.
2026-06-25 13:08:18 -07:00
Shannon Sands
4b09903de5 fix Nous auth refresh for idle agents 2026-06-21 22:43:48 -07:00
tt-a1i
46f9d53468 fix(agent): aggregate anthropic aux calls via stream 2026-06-19 17:32:13 +05:30
Teknium
4858942c55
fix(auxiliary): honor main fallback chain for auto tasks (#47235) 2026-06-16 06:23:24 -07:00
achaljhawar
819def44c7 fix(agent): scope Nous tags to Nous auxiliary calls 2026-06-13 13:24:40 -07:00
Xiangji
19c07c4037 fix(params): send max_completion_tokens for newer OpenAI families on custom endpoints
Third-party OpenAI-compatible endpoints (self-hosted gateways, OpenRouter,
Azure proxies) fronting gpt-4o / gpt-4.1 / gpt-5+ / o1-o4 models silently
received max_tokens and 400'd with unsupported_parameter, because the three
kwarg-selection sites only checked base_url_hostname(...) == "api.openai.com"
and fell through to max_tokens on every other host. The constraint is
enforced server-side by the model family, not by the URL, so name-based
detection is required as a fallback.

Changes:
- utils.py: new shared helper model_forces_max_completion_tokens(model) that
  prefix-matches gpt-4o, gpt-4.1, gpt-5, o1, o3, o4 families on normalized
  (lowercased, vendor-prefix-stripped) names.
- run_agent.py: _max_tokens_param ORs the helper into the URL check.
- agent/auxiliary_client.py:
  - auxiliary_max_tokens_param gains an optional keyword-only model arg.
  - _build_call_kwargs inline branch applies the same check for both
    provider == "custom" and non-custom paths.

Tests:
- tests/test_model_forces_max_completion_tokens.py: 31 new cases covering
  positive families, negatives (classic gpt-4, claude, llama, mistral, qwen,
  deepseek), vendor prefixes, case-insensitivity, whitespace, None/empty,
  and substring-not-prefix guards.
- tests/run_agent/test_run_agent.py::TestMaxTokensParam: 5 new model-based
  cases (custom + gpt-5.4, openrouter + gpt-4o-mini, custom + o1-preview,
  classic gpt-4-turbo keeps max_tokens, llama3 keeps max_tokens).
- tests/agent/test_auxiliary_client.py::TestAuxiliaryMaxTokensParam: new
  class, 7 tests covering the URL x model matrix.
2026-06-09 23:22:10 -07:00
teknium1
02a4d66951 fix(auxiliary): retry transient transport error once before fallback (#16587)
Some checks failed
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Nix / nix (macos-latest) (push) Waiting to run
Nix / nix (ubuntu-latest) (push) Waiting to run
Tests / test (1) (push) Waiting to run
Tests / test (2) (push) Waiting to run
Tests / test (3) (push) Waiting to run
Tests / test (4) (push) Waiting to run
Tests / test (5) (push) Waiting to run
Tests / test (6) (push) Waiting to run
Tests / save-durations (push) Blocked by required conditions
Tests / e2e (push) Waiting to run
Nix Lockfile Fix / auto-fix-main (push) Has been cancelled
Nix Lockfile Fix / fix (push) Has been cancelled
A one-off transient transport failure (streaming-close / incomplete
chunked read / 5xx / 408) on an auxiliary LLM call escalated straight to
provider/model fallback (or, for context compression, dropped the summary
and entered cooldown), even when an immediate retry on the same provider
would have succeeded.

Add a single same-target retry at the top of call_llm() and
async_call_llm() — before the existing except-chain — gated on a new
_is_transient_transport_error() that reuses the canonical
_is_connection_error() detector plus a 5xx/408 status check. A second
failure (or any non-transient error: auth, other 4xx, malformed payload)
falls through to first_err and the existing fallback handling unchanged.

This lives in call_llm so every auxiliary task (compression, memory flush,
title generation, session search, vision) shares one transient-retry
surface, rather than each caller re-implementing it. The context
compressor needs no change — it calls call_llm and inherits the retry; its
existing fallback-to-main path (#18458) now composes naturally (retry the
aux model once, then fall back to main only if the retry also fails).

Co-authored-by: ARegalado1 <alberto.regalado@ymail.com>
2026-06-08 01:05:45 -07:00
Teknium
2789bf4e25 fix(auxiliary): route Codex Responses path through shared converter (#5709)
The auxiliary Codex adapter maintained its own chat->Responses conversion
loop that forwarded every non-system message's role verbatim into
Responses input[]. When flush_memories()/compression replayed session
history containing assistant tool_calls + role=tool results, those tool
messages leaked into the request and the Responses API rejected them with
HTTP 400: Invalid value: 'tool'.

Route _CodexCompletionsAdapter.create() through the same shared converter
the main agent transport uses (_chat_messages_to_responses_input), so tool
calls become function_call items and tool results become function_call_output
items with a valid call_id. Single conversion path means no future drift.

Also remove the now-dead _convert_content_for_responses() helper — its only
caller was the private conversion loop this change deletes.

Co-authored-by: ProgramCaiCai <techxacm@gmail.com>
2026-06-07 22:18:31 -07:00
teknium
153fe28474 fix(vision): use MiniMax type="video" block (not input_video) + tests
The salvaged conversion emitted type:"input_video", which MiniMax M3 rejects
just like the original video_url block. Per MiniMax's Anthropic-compat docs,
the video content block is type:"video" with an image-style source (base64 or
url). Fixes the block type, converts URL-based videos too, and adds 4 video
conversion tests (none shipped with the original PR).
2026-06-04 05:38:11 -07:00
Teknium
ab2472e692
fix(aux): self-heal Nous-routed calls when a pinned model leaves the catalog (#37732)
A long-lived process (gateway, watcher) caches the Nous Portal's
recommended-models payload and can pin a model for its whole lifetime.
When that model is later dropped from the Nous -> OpenRouter catalog,
every auxiliary call 404s with 'model does not exist in our
configuration or OpenRouter catalog' until the process restarts.

Now such a 404 force-refreshes the Portal recommendation and retries
once with the current pick (or the gemini-3-flash-preview default).
Scoped to Nous-routed calls only.

- _is_model_not_found_error(): 404/400 'not found / does not exist /
  not a valid model' predicate, excludes billing keywords so it never
  overlaps _is_payment_error.
- _refresh_nous_recommended_model(): force-refresh fetch, returns a
  model distinct from the one that failed, else the known-good default.
- Wired into both call_llm and async_call_llm error chains.
2026-06-02 17:14:36 -07:00
Teknium
2062a84000
fix(auxiliary): stop capping output with max_tokens by default (#34530) (#34845)
* fix(auxiliary): stop capping output with max_tokens by default

Auxiliary LLM calls (compression, titles, vision, etc.) no longer send
max_tokens on the OpenAI-compatible chat-completions path. Most providers
treat an omitted max_tokens as "use the model max", which is what we want;
an explicit cap only risks truncation or a wire-format 400.

This was surfaced by GitHub Copilot / GPT-5 (#34530): those models reject
max_tokens and require max_completion_tokens, so compression 400'd and fell
back to a static context marker. Omitting the param sidesteps that quirk
(and ZAI vision's error 1210) entirely.

The Anthropic Messages wire (MiniMax + /anthropic endpoints) keeps
max_tokens because it is a mandatory field there.

* test(auxiliary): update temperature-retry assertions for omitted max_tokens

The temperature-retry tests asserted retry_kwargs["max_tokens"] == 500 on an
api.openai.com endpoint. Now that auxiliary calls omit max_tokens on
OpenAI-compatible endpoints (#34530), that key is absent. Assert it's absent
in both first and retry kwargs and use model as the survives-the-retry witness.
2026-05-29 17:24:30 -07:00
Robin Fernandes
41ff6e5937 refactor(auth): Disable Nous legacy session key fallback 2026-05-29 02:24:48 -07:00
kshitijk4poor
66827f8947 chore: prune unused imports and duplicate import redefinitions
Remove unused imports (F401) and duplicate/shadowed import
redefinitions (F811) across the codebase using ruff's safe
autofixes. No behavioral changes -- imports only.

- ~1400 safe autofixes applied across 644 files (net -1072 lines)
- __init__.py re-exports preserved (excluded from F401 removal so
  public re-export surfaces stay intact)
- Re-exports that are imported or monkeypatched by tests but look
  unused in their defining module are kept with explicit # noqa:
  F401 (gateway/run.py load_dotenv; run_agent re-exports from
  agent.message_sanitization, agent.context_compressor,
  agent.retry_utils, agent.prompt_builder, agent.process_bootstrap,
  agent.codex_responses_adapter)
- Unsafe F841 (unused-variable) fixes deliberately skipped -- those
  can change behavior when the RHS has side effects
- ruff lints remain disabled in pyproject.toml (only PLW1514 is
  selected); this is a one-time cleanup, not a config change

Verification:
- python -m compileall: clean
- pytest --collect-only: all 27161 tests collect (zero import errors)
- core entry points import clean (run_agent, model_tools, cli,
  toolsets, hermes_state, batch_runner, gateway)
- static scan: every name any test imports directly from an edited
  module still resolves
2026-05-28 22:26:25 -07:00
Robin Fernandes
406901b27d feat(auth) normalise the way in which we check whether a user has free/paid access to nous portal so we can expose behaviour and error messages accordingly. 2026-05-28 00:19:31 -07:00