Commit graph

349 commits

Author SHA1 Message Date
Alex Fournier
14bed44c8c Reapply "feat(observability): integrate NeMo Relay runtime and shared metrics"
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 21:10:51 -07:00
Jeffrey Quesnelle
841a5a744a
Revert "feat(observability): integrate NeMo Relay runtime and shared metrics" 2026-07-27 22:28:08 -04:00
Alex Fournier
147e451cc8 Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 17:54:42 -07:00
Jaaneek
5cffc53194 fix(xai): send Hermes-Agent User-Agent on chat/completions
Direct tool HTTP calls already identified as Hermes-Agent, but the main
OpenAI-SDK chat path still sent OpenAI/Python. Set Hermes-Agent/<ver> for
api.x.ai clients (xai + xai-oauth) so normal text traffic is attributed correctly.
2026-07-27 17:47:28 -07:00
Alex Fournier
8ac686fa27 Merge remote-tracking branch 'origin/main' into fix/hermes-relay-review-round3
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	agent/chat_completion_helpers.py
2026-07-27 10:11:53 -07:00
rob-maron
02d5e23085 nous portal anthropic wire 2026-07-27 11:53:48 -04:00
Alex Fournier
4fe4b0dca7 Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 08:12:37 -07:00
teknium1
39b5965569 refactor(fallback): single owner for backend identity and failure-scoped skips
Every fallback/dedup/skip decision asks one question — 'is this candidate
the same backend as the one that failed, along the axis that failure
invalidated?' — but it was re-implemented inline at six sites across four
subsystems, each comparing whatever string was locally convenient. Each
incident fixed one site while the others kept the bug: #22548, #70893,
#59561, #72468, #62984/#54250/#57584.

agent/backend_identity.py now owns the concept: BackendIdentity (provider /
model / base_url axes), FailureScope (MODEL / CREDENTIAL / ENDPOINT — each
failure class invalidates a different axis), and should_skip_candidate().
Unknown axes never manufacture a skip (over-skipping strands failover; a
wrong try costs one RTT).

Migrated sites:
- chat_completion_helpers.try_activate_fallback: replaces the provider+model
  early-exit (the #62984 bug: ignored base_url, stranding multi-endpoint
  pools) AND _fallback_entry_is_same_backend_by_base_url (deleted)
- auxiliary_client._try_configured_fallback_chain +
  _try_main_agent_model_fallback: replace label/model comparisons; auth and
  payment map to CREDENTIAL scope, keeping the #59561 carve-out
- hermes_cli/fallback_cmd add: primary-match + duplicate checks now identity-
  aware (#54250/#57584): same provider+model on a different explicit
  base_url is a pool entry, not a duplicate

_mark_provider_unhealthy stays label-keyed deliberately: its only triggers
are confirmed 402s, which ARE credential-scoped.

Owner-level tests pin each incident's semantics by number; sabotage-verified
(removing the base_url axis fails the #62984 test).
2026-07-26 23:41:54 -07:00
teknium1
0a2c245cd6 fix(auxiliary): reach the main agent model when a sibling aux model fails on the same provider
Widen okalentiev's failed_model narrowing (#59561) to
_try_main_agent_model_fallback. The safety-net layer still skipped on a
provider-label match alone, so single-provider users whose aux compression
model and main model share one custom endpoint had ZERO fallbacks: the aux
model timing out exhausted the chain in one hop and compression aborted.

Real incident (0.19.0 debug dump): aux zai-org/glm-5.2 hung 324s and timed
out while main mindai/macaron-v1-venti on the SAME endpoint was serving
448K-token turns — the label-only skip discarded the one viable summarizer,
the session wedged over threshold, and the anti-thrash breaker tripped.

Same convention as the chain fix: model-specific failures (timeout,
connection, rate limit) pass failed_model so only the exact failed model is
skipped; provider-wide failures (auth 401 / payment 402) pass None and keep
the whole-provider skip. Both sync and async call_llm sites pass it.

Sabotage-verified: the new regression test fails on the provider-only skip.
2026-07-26 22:33:06 -07:00
Oleksii Kalentiev
83f20e07e0 fix(auxiliary): keep provider-wide skip for auth/payment failures in fallback chain
Follow-up to the same-provider fallback fix: narrowing the configured-chain
skip to the exact failed model is only correct for model-specific failures.
Auth (401) and payment (402) errors are provider-wide — every model on the
provider shares the same broken credentials/account — so trying a sibling
model can't recover and merely burns another doomed request before the
aux task fails. Worse, returning that sibling client bypasses the
main-agent-model safety net that a provider-wide skip would have reached.

Only forward failed_model to _try_configured_fallback_chain for
model-specific failures (timeout, connection, rate limit, model-incompatible,
invalid response). Auth/payment keep failed_model=None (whole-provider skip),
preserving the pre-existing safety-net behaviour for credential/billing
failures.

Adds an integration test that a timeout forwards the failed model, and
updates the payment-error test to assert failed_model=None.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 22:33:06 -07:00
Oleksii Kalentiev
e8f8b34b0c fix(auxiliary): don't skip sibling models when a configured fallback_chain reuses the same provider
_try_configured_fallback_chain skipped every fallback_chain entry whose
provider matched the one that just failed. A chain that intentionally lists
several models under the same provider (e.g. two more NVIDIA NIM models
after the primary NIM model times out) was therefore skipped wholesale,
falling straight through to the main-agent-model safety net instead of
trying the other configured models on that provider.

Add failed_model so the skip narrows to the exact (provider, model) pair
that failed. Callers that only know the provider (client-build failures,
where the whole provider is unreachable regardless of model) keep the old
provider-wide skip; the two runtime request-error call sites (call_llm,
async_call_llm) now pass the model that just failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-26 22:33:06 -07:00
Alex Fournier
45580cc93a Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-26 09:21:19 -07:00
kudi88
5121a2a20e feat(aux): force streaming for providers that reject non-stream requests
Some OpenAI-compatible endpoints — notably Tencent Copilot
(copilot.tencent.com) — only accept streaming chat requests; any
non-streaming call returns HTTP 400 (code 11101, 'Non-stream chat
request is currently not supported'). The main conversation loop already
streams, so interactive chat works, but every auxiliary task (title
generation, compression, web extraction) used the non-streaming path and
failed on each call.

_provider_requires_stream() detects stream-only endpoints
(copilot.tencent.com built in, plus user-configurable
auxiliary.stream_only_base_urls substring markers in config.yaml).
Matching sync auxiliary calls route through _create_with_progress
(force_stream=True) and async calls through the new
_acreate_with_stream, aggregating the chunk stream — including tool-call
deltas and reasoning deltas — into a complete response via the shared
_ChatStreamAccumulator.

Salvaged from PR #60686 by @kudi88 onto the progress-aware streaming
machinery from #71508, addressing both sweeper-review gaps: the async
path now consumes the stream with 'async for' (awaiting create() and
iterating synchronously raised on AsyncOpenAI streams), and tool-call
deltas are reassembled instead of dropped (MCP passes tools= through
call_llm). Under force_stream there is no silent non-streaming retry —
a stream-only provider rejects those by definition, so the original
error surfaces to the normal recovery chains.
2026-07-25 14:58:04 -07:00
Teknium
32fd9d65cf feat(compression): progress-aware timeouts — stop punishing slow summary models
The gateway's pre-agent session-hygiene compression killed the summary
call at a fixed 30s wall-clock deadline (compression.hygiene_timeout_seconds),
regardless of whether the summary model was hung or merely slow. A reasoning
model happily streaming a large summary was cut off mid-generation, the user
got '⚠️ Context compression timed out after 30.0s', and a 300s failure
cooldown left the session oversized — a doom loop for slow-but-healthy
auxiliary models.

Timeouts are now liveness-based instead of wall-clock-based:

- agent/auxiliary_client.py: new thread-local aux_progress_hook. When
  installed (only by context compression today), the primary call_llm
  attempt streams (stream=True) and aggregates chunks back into a complete
  response, ticking the hook per chunk. The configured timeout then acts
  per stream read (idle) instead of as a total budget. Providers that
  reject streaming fall back to the plain non-streaming call; auth/payment/
  rate-limit/transport errors propagate unchanged into the existing
  recovery chains. Codex Responses (per SSE event) and Anthropic Messages
  (per stream event, via the new create_anthropic_message on_stream_event
  callback) tick the same hook from inside their wire adapters.

- agent/conversation_compression.py: CompressionCommitFence gains
  touch_progress()/seconds_since_progress(); compress_context() installs
  fence.touch_progress as the progress hook around the compress call.

- gateway/run.py: the hygiene wait loop treats hygiene_timeout_seconds as
  an INACTIVITY budget — while the fence reports fresh progress the wait
  extends, bounded by the new compression.hygiene_total_ceiling_seconds
  (default 600s, clamped >= the idle budget) so a degenerate trickle
  stream still dies. The timeout warning now says the summary model
  produced no output, which is the only case that still triggers it.

- config/docs: hygiene_total_ceiling_seconds added to DEFAULT_CONFIG and
  configuration.md; hygiene_timeout_seconds documented as inactivity-based.

Tests: tests/agent/test_aux_progress_streaming.py (hook plumbing, stream
aggregation incl. tool-call deltas and reasoning deltas, rejection
fallback, ceiling kill, fence progress surface); two new gateway tests
prove a slow-but-streaming worker survives past the fixed timeout
(sabotage-verified: fails with the old fixed deadline) and a
forever-trickling worker is still cut off at the ceiling.
2026-07-25 12:26:28 -07:00
AlexFucuson9
cf35fd6de5 fix(core,cli,gateway,plugins): add encoding='utf-8' to read_text() calls
Path.read_text() without an explicit encoding uses the platform's
default encoding. On Windows this is typically cp1252 or mbcs, which
causes UnicodeDecodeError or silent data corruption when reading
UTF-8 content (JSON files, user text, config with non-ASCII chars).

This is the read-side companion to the write_text() encoding fix.
Fixed the most critical locations that read JSON data, user content,
and config files across 14 files with 31 call sites.

Pattern: .read_text() → .read_text(encoding='utf-8')
         json.loads(path.read_text()) → json.loads(path.read_text(encoding='utf-8'))
2026-07-24 17:10:39 -07:00
Israel Lot
6bc8d68ad7 fix(codex): scope 24h retention to Bedrock Mantle 2026-07-24 15:54:08 -07:00
Israel Lot
f54fa1bcb7 fix(codex): exclude models.github.ai from auxiliary cache retention 2026-07-24 15:54:08 -07:00
Israel Lot
48049a1d31 fix(codex): skip auxiliary cache retention on Codex backend 2026-07-24 15:54:08 -07:00
Israel Lot
339be21542 fix(codex): send prompt_cache_retention 24h for the GPT-5.5 family
OpenAI documents GPT-5.5 / GPT-5.5 Pro as extended-cache-only: in-memory
prompt cache retention is not available for them, and only
prompt_cache_retention: "24h" is supported. Responses requests that omit
the field see near-zero cached_tokens even with a stable prompt_cache_key
and identical prefixes (observed on an OpenAI-compatible Responses relay:
0 cached across repeated identical calls before; 97% cache reads after).

Send the field for the gpt-5.5 model family (bare and namespaced ids like
openai.gpt-5.5) on OpenAI-compatible Responses routes, mirrored in the
auxiliary Codex adapter, and pass it through preflight normalization.
Skipped for xAI, GitHub/Copilot, and the chatgpt.com Codex backend, which
reject or ignore body-level cache fields.
2026-07-24 15:54:08 -07:00
dyreckt
4c66307c36 fix(moa): pass Copilot initiator header to advisors 2026-07-23 17:26:24 -07:00
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
4ee74fa5df fix: forward max_tokens to gemini-native so MoA reference cap applies
_build_call_kwargs omitted max_tokens for every provider except
anthropic-compat endpoints and NVIDIA NIM. Gemini's native
generateContent maps max_tokens -> maxOutputTokens and, when it is
omitted, applies a fixed 65,535-token ceiling (not "the model's full
budget"), so dropping the value made MoA's reference_max_tokens a
silent no-op for gemini advisors — they ran effectively uncapped
(observed ~2900 output tokens against a configured cap of 600),
inflating per-turn MoA latency.

Forward max_tokens for the gemini-native path (provider name or native
base_url). Gemini supports maxOutputTokens, so the cap is safe here;
providers that reject max_tokens (Copilot, GPT-5 max_completion_tokens,
ZAI vision) are unaffected — they still omit it as before.
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
289fad1868 fix(auxiliary): thread task=task through _build_call_kwargs in fallback helpers 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
Alex Fournier
fb1e417367 Merge remote-tracking branch 'origin/main' into merge/relay-metrics-upstream-20260723 2026-07-23 14:10:49 -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
Alex Fournier
b4e105031a Merge upstream main into feat/hermes-relay-shared-metrics
# Conflicts:
#	MANIFEST.in
#	pyproject.toml
#	tests/test_project_metadata.py
2026-07-23 07:22:03 -07:00
wz-heng
91546b8337 fix: preserve named custom provider vision overrides 2026-07-23 17:57:33 +05:30
Alex Fournier
82d923ad5a fix(runtime): close failed auxiliary Relay calls
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-21 09:39:24 -07:00
Alex Fournier
a15b98f414 fix(runtime): complete logical LLM calls after acceptance
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-20 11:01:14 -07:00
Alex Fournier
4dedaa4237 refactor(runtime): consolidate Relay lifecycle ownership
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-19 08:59:06 -04:00
webtecnica
65bf42b669 fix(auxiliary): resolve key_env in _resolve_task_provider_model (#66641)
_resolve_task_provider_model() read api_key from the auxiliary task
config but never consulted key_env (or api_key_env). When a user
configured an auxiliary task with key_env instead of a plaintext
api_key, the resolved API key was None, causing 401 on every call.

Add the same key_env → os.getenv() resolution pattern already used in
_fallback_entry_api_key() and named custom provider resolution.

Closes #66641
2026-07-18 19:03:51 -04:00
Teknium
73057ed161 fix(auxiliary): scope runtime state to each turn 2026-07-17 09:08:30 -07:00
Krowd
c201b72f34 fix(auxiliary): sync runtime after fallback restoration 2026-07-17 09:08:30 -07:00
dfein38347g
fdc6c32d7d fix(auxiliary): isolate runtime cache by live context 2026-07-17 09:08:30 -07:00
Teknium
adb647269a fix(auxiliary): apply review fixes to #36043 — guard named-custom routing, drop dead key assignment, tighten Palantir host match
Review follow-ups on the cherry-picked #36043 commit:

1. Guard the custom:<name> passthrough with a _get_named_custom_provider
   lookup. The PR unconditionally kept the full custom:<name> string, which
   broke config-less runtime custom providers (#34777 regression — entries
   that exist only in the live runtime, not config.yaml): the named arm
   found no entry and resolution fell through to Step 2. Now custom:<name>
   only takes the named arm when a config entry actually exists; otherwise
   it collapses to the anonymous-custom arm with the runtime endpoint,
   preserving pre-PR behavior.

2. Drop the dead 'explicit_api_key = runtime_api_key' assignment (and its
   misleading comment) in the named-entry branch. resolve_provider_client's
   named-custom arm derives the key exclusively from the entry's
   api_key/key_env and never reads explicit_api_key, so the assignment was
   a no-op. Wiring precedence in was not justified: for a named custom
   provider the runtime key IS the entry's key (set_runtime_main sources it
   from the same config), so deletion is the honest option.

3. Tighten the Palantir Bearer-auth check from a loose substring match
   ('palantirfoundry' in normalized) to a hostname match via
   base_url_host_matches(..., 'palantirfoundry.com'), so path segments or
   lookalike domains containing the string no longer trigger Bearer auth.

Tests: named-custom anthropic_messages end-to-end routing (full name kept,
AnthropicAuxiliaryClient at the original /anthropic URL, no /v1 rewrite)
plus Palantir Bearer-auth positive and substring-false-positive cases.
2026-07-16 07:28:07 -07:00
antydizajn
367d3758d5 fix(auxiliary): route custom:<name> through named-provider arm + Palantir Bearer auth
When the user's main provider is a named custom_providers entry exposing an
Anthropic Messages surface (e.g. Palantir Foundry's
/api/v2/llm/proxy/anthropic, custom LiteLLM/Bedrock proxies), auxiliary
tasks (title generation, compression, web extract, session search, etc.)
returned HTTP 404 NOT_FOUND for every call.

Root cause: `_resolve_auto` collapsed any `custom:<name>` main provider
to plain `"custom"` and passed runtime_base_url as explicit_base_url.
This landed in `resolve_provider_client`'s anonymous-custom arm
(`if provider == "custom":`), which unconditionally calls
`_to_openai_base_url` — that helper strips a trailing `/anthropic` and
substitutes `/v1` (designed for MiniMax/ZAI which expose both surfaces).
The result for Palantir is `/api/v2/llm/proxy/v1`, which does not exist
on the proxy — every auxiliary call 404s. The runtime `api_mode=
anthropic_messages` flag was discarded by this arm.

Fix: split the conditional so only the literal `"custom"` provider takes
the anonymous-custom path; `custom:<name>` keeps its full `custom:<name>`
string when handed to `resolve_provider_client`, where the
named-custom-provider arm (added in earlier work) honours the entry's
`api_mode` and routes through `AnthropicAuxiliaryClient` against the
original `/anthropic` URL.

Also: extend `_requires_bearer_auth` in `anthropic_adapter.py` to
recognise palantirfoundry hosts so the SDK sends `Authorization: Bearer`
instead of the default `x-api-key` (Palantir's proxy rejects x-api-key
with 401).

Verified end-to-end against a live Palantir Foundry deployment with both
claude-4-6-opus and claude-4-7-opus models — `generate_title` returns
real titles instead of 404ing.  Regression-tested:

  - anonymous `custom` (with base_url) still routes to OpenAI wire
  - built-in NVIDIA provider unchanged
  - custom-without-base_url still falls through to Step-2 chain
2026-07-16 07:28:07 -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
bd7e480236
fix(compression): give fallback candidates their own timeout budget + escalate repeat-timeout cooldowns (#65143)
Fixes #62452. Two amplifiers turned one slow auxiliary route into a
per-turn multi-minute stall:

1. Fallback candidates inherited the exact effective_timeout the primary
   was called with. When the primary's deadline was short (tuned or
   already burned), an independently healthy fallback died on the same
   clock — the reporter's 163k-token compression needed ~90s on the
   fallback and got the primary's 30s, every turn. fallback_chain
   entries may now declare their own 'timeout' (seconds); both fallback
   candidate call sites (sync + async) resolve it via
   _fallback_entry_timeout, label-scoped so only configured-chain
   candidates are affected. No entry timeout → task-level timeout,
   preserving existing behavior.

2. A session whose transcript structurally cannot be summarized within
   the deadline re-attempted every 60s, re-burning the full timeout on
   every subsequent turn. Consecutive timeout-class failures now
   escalate the cooldown 60s → 300s → 900s (capped); any successful
   summary or session reset clears the streak. Timeout classification
   takes precedence over the streaming-closed 30s rung ('timed out'
   also matches _is_connection_error) and now recognizes the SDK's
   'Request timed out.' phrasing.

Fail-safe behavior is unchanged: all messages are preserved when every
candidate fails; the cooldown only spaces out retries.
2026-07-15 12:28:09 -07:00
Teknium
07be37d996 fix(auxiliary_client): warn once + regression tests for bootstrap version skew
Follow-up on the salvaged fallback: silent degradation is how #64333 went
unnoticed (jobs dead on arrival, only errors.log knew). Warn once with a
resync hint, and cover both the skewed and healthy paths with tests.
2026-07-15 07:47:18 -07:00
liuhao1024
f3ec79964e fix(auxiliary_client): add backward compatibility for build_keepalive_http_client import (#64333) 2026-07-15 07:47:18 -07:00
dorokuma
771571aee4 fix(auxiliary): pass reasoning_config and extra_body through to auxiliary Anthropic calls
Two related bugs in _AnthropicCompletionsAdapter.create() in
agent/auxiliary_client.py silently discard caller-supplied
reasoning_config and extra_body on the Anthropic-Messages
auxiliary-protocol path:

  * Bug A: reasoning_config=None was hardcoded at L1000, so the
    reasoning_config parameter on build_anthropic_kwargs was
    unreachable for any auxiliary task. The main agent path
    (agent/transports/anthropic.py) already reads
    reasoning_config from caller params; this PR aligns the
    auxiliary adapter with the same pattern.

  * Bug B: create(**kwargs) accepts an OpenAI-style kwargs
    payload from the caller but only forwards a hand-picked
    subset to self._client.messages.create(). Any caller-supplied
    extra_body (e.g. thinking control, metadata, service_tier,
    vendor-specific fields) was dropped on the floor. The
    codex/responses transport in the same file already merges
    extra_body; the Anthropic branch is the gap.

This unlocks the caller-supplied extra_body path so auxiliary
callers can set per-vendor request fields (including
thinking: {type: "disabled"} for Anthropic-compatible vendors
that require an explicit disable on the wire), and lets the
reasoning_config kwarg flow into build_anthropic_kwargs like the
main agent does. Both changes are backward-compatible for
callers that don't pass the affected kwargs.

Affected providers (all routed through _AnthropicCompletionsAdapter
via _maybe_wrap_anthropic): anthropic (native), minimax /
minimax-cn, kimi-coding / kimi-coding-cn, z.ai / GLM, and any
custom /anthropic-suffixed endpoint. See PR description for
related issues (#35566, #7209, #16533, #32813, #29248).
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