Commit Graph

14240 Commits

Author SHA1 Message Date
Teknium 850f576f3d feat(moa): add every_n fanout cadence with cached-guidance reuse
Extends the fanout enum with 'every_n:<N>' (N >= 2): advisors run on the
first iteration of each user turn and every Nth tool iteration after it;
off-cadence iterations REUSE the cached guidance from the last on-cadence
run via the same cache mechanism the user_turn fanout uses, so the
aggregator still gets advice on every step. The cadence counter is scoped
per user turn (resets on a new user message) and only advances when the
advisory state actually changes, so streaming retries never consume a
cadence slot. Mapping form {mode: every_n, n: N} normalizes to the
canonical string. Unknown/degenerate values fall back to per_iteration.

Addresses issue #63393 (advisor fan-out multiplies turn latency/cost by
the tool-iteration count). Redesigned from PR #63448: the submitted shape
skipped references entirely on off-cadence iterations (aggregator ran
advice-less); this version keeps the last advice in play, credited for
the idea and cadence framing.

Config-gated, default-off (default fanout remains per_iteration).

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-07-23 17:50:40 -07:00
Teknium 74a56b76b0 test(moa): regression for aggregator-model thought_signature resolution (#66212)
Adds test_moa_gemini_aggregator_sanitize_uses_real_model: drives a full MoA
tool-call turn (virtual-provider mode) with a Gemini aggregator and asserts
the strict-API sanitize pass is invoked with the resolved aggregator model
(gemini-3-pro-preview), never the virtual preset name once a slot is
resolved — the exact path that stripped extra_content/thought_signature and
made Gemini aggregators 400 (#65092).

Writing the test surfaced a gap in the salvaged #66212 fix: in virtual-
provider MoA mode (provider=moa, no moa_config threaded through
run_conversation) the conversation-loop branch never fired because it only
consulted moa_config. Extend it to fall back to the facade's
last_aggregator_slot — the same source the handle_max_iterations fix uses —
so both MoA entry modes resolve the real aggregator model.

Also adds the contributors/emails mapping for the #15676 credit base.
2026-07-23 17:26:24 -07:00
Teknium 16950a4568 fix(moa): normalize Copilot aliases + carry x-initiator through retry rebuilds (#60293)
Follow-ups to the salvaged core of #60293:

- Gate the x-initiator header on _normalize_aux_provider() instead of a
  literal 'copilot' string compare, so slot configs spelled github /
  github-copilot / github-models / copilot-acp / mixed case all get the
  user-turn attribution.
- Thread extra_headers through _retry_same_provider_sync/_async so the
  credential-refresh and pool-rotation retry rebuilds don't silently drop
  the header (the rebuilt kwargs previously started from scratch).
- Add a transport-boundary test asserting the header reaches the SDK
  client's create() kwargs (no call_llm mocking), an alias-spelling
  matrix test, and a retry-rebuild preservation test.
2026-07-23 17:26:24 -07:00
dyreckt 4c66307c36 fix(moa): pass Copilot initiator header to advisors 2026-07-23 17:26:24 -07:00
Teknium 0749cac7a1 fix(moa): share facade factory so restore/recover keep reference relay (#53802)
Follow-up to the salvaged core of #53802: a naive MoAClient(preset) rebuild
restores a working facade but silently drops the reference_callback relay
wired in agent_init, so moa.reference / moa.aggregating display events stop
reaching every frontend for the rest of the session.

Introduce agent.moa_loop.build_moa_facade(agent, preset) as the single
construction point for the MoA facade and use it at:
- initial client construction (agent_init.py)
- turn-start fallback restore (restore_primary_runtime)
- transient transport recovery (try_recover_primary_transport — previously
  fell through to _create_openai_client with MoA's empty client_kwargs and
  died with 'api_key client option must be set')
- mid-session model switches (switch_model)

The relay reads agent.tool_progress_callback at emit time, so callbacks
attached after construction are picked up automatically.

Adds test_moa_restored_facade_still_emits_reference_events covering event
delivery through a restored facade.
2026-07-23 17:26:24 -07:00
kosta 5501187847 fix(moa): restore virtual runtime after fallback 2026-07-23 17:26:24 -07:00
Dineth Hettiarachchi 8d14e19f9a fix(agent): close MoA stream on interrupt 2026-07-23 17:26:24 -07:00
Idris Almalki 8d119832b4 fix(gemini): emit thoughtSignature sentinel for cross-provider tool_calls in native adapter
When Hermes fails over from a non-Gemini provider (xAI, Anthropic, etc.) to
Gemini mid-conversation, the existing assistant tool_calls in history carry
no Gemini ``extra_content.google.thought_signature`` (the originating provider
never emits one).  The native adapter's ``_translate_tool_call_to_gemini``
omitted ``thoughtSignature`` entirely in that case, so Gemini 3 thinking
models rejected every replayed turn with::

    HTTP 400 INVALID_ARGUMENT
    Function call is missing a thought_signature in functionCall parts.
    Additional data, function call default_api:<tool_name>, position N.

The Cloud Code Assist sibling adapter already handles this exact case by
emitting a sentinel ``"skip_thought_signature_validator"`` (see
``agent/gemini_cloudcode_adapter.py:106``, originally added in #11270 and
documented as matching ``opencode-gemini-auth``'s approach).  This change
mirrors that fallback in the native adapter so the two paths behave
identically when replaying cross-provider history.

Verified live against ``generativelanguage.googleapis.com/v1beta`` with
``gemini-3-pro-preview``: synthetic 2-turn conversation with no real
``thoughtSignature`` returns 400 without the sentinel and 200 with it.

Test added: ``test_build_native_request_emits_sentinel_for_cross_provider_tool_call``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-23 17:26:24 -07:00
Teknium d43cc2ca80 fix(compress): gate N-user tail guarantee to actionable turns, behavior-preserving default
Follow-up fixes on top of the salvaged #22566 mechanism:

- N-collector now counts only REAL actionable user turns via
  _is_actionable_user_turn + _is_synthetic_compression_user_turn —
  the same filter pair _find_last_user_message_idx uses post-#69291.
  The contributor's bare role=='user' + _is_context_summary_content
  check let blank platform echoes and continuation/todo rows consume
  N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
  measured to change the tail cut on transcripts whose budget covers
  only the last turn. min_tail_user_messages=1 delegates to the
  existing single-user anchor; N>1 is opt-in, and the call site is
  gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
  floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
  DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
  N; tool-call/result pairs never split by the N-boundary (no-orphan
  both directions); N-guarantee wins over tail_token_budget and the
  _MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
  parity pin; DEFAULT_CONFIG pin.
2026-07-23 17:03:49 -07:00
Jerry a9c868225e feat(compress): preserve recent N user messages during context compression
Add _ensure_last_n_user_messages_in_tail to guarantee the last N user
messages survive compression in the uncompressed tail, with surrounding
assistant/tool context preserved.

- Add min_tail_user_messages parameter (default 3) to ContextCompressor
- New _ensure_last_n_user_messages_in_tail method generalizes single-user protection
- Skip context-summary handoff banners when counting user messages
- User messages are clean boundaries — skip _align_boundary_backward
- Wire through cli.py, agent_init.py, and gateway cache busting keys

Config:
  compression:
    min_tail_user_messages: 3

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 17:03:49 -07:00
Teknium 69365109b3 fix(compression): mark raw skill_view bodies summarized away, not only pre-pruned rows
_collect_ghosted_skill_names() covers both ghost-skill shapes in the
compressed middle window: rows already demoted to a [SKILL_PRUNED: ...]
marker AND raw skill_view bodies (> _SKILL_VIEW_PRUNE_MIN_CHARS) that
survived Phase-1 inside an earlier protected tail and then aged into the
compression window — the summarizer paraphrases those instructions away
too. Shared threshold constant between the emit site and the scan.
Pinned by a live-probe-shaped test (real compress(), mocked aux LLM).
2026-07-23 16:58:06 -07:00
Teknium 28f73d32e9 test(compression): ghost-skill defense suite — marker round-trip, protected prune, real-compress survival
21 tests pinning the salvaged #44166 behavior:
- marker emit + extractor round trip (patterns adapted from PR #32375
  by @LeonSGP43, with credit)
- no-duplicate re-injection when the canonical marker survived (the
  original PR's presence-check defect)
- Phase-1 protection for just-loaded / user-referenced skills, and the
  Pass-4 pressure override that keeps #61932 fixed
- deterministic marker survival through a REAL compress() with a mocked
  aux LLM: drop → re-injected, keep → not duplicated, static-fallback
  path, iterative re-compression via rehydrated handoff
- markers never classify as handoff content (classify_summary_content /
  _strip_context_summary_handoff_message untouched)
- SKILLS_GUIDANCE Skill Safety Rule renders with real newlines
2026-07-23 16:58:06 -07:00
Teknium df051c17cc fix(vertex): surface vertex in the /model picker — credential gate + curated model list
Community verification of #56688 (zmack12344321) found two follow-up gaps
that kept Vertex invisible in the /model menu even after registry
registration:

1. hermes_cli/model_switch.py: list_authenticated_providers() had a
   credential gate hard-coded to API keys (with an aws_sdk special case
   only) — add a vertex branch using has_vertex_credentials(), mirroring
   the aws_sdk shape.
2. hermes_cli/models.py: Vertex's OpenAI-compatible endpoint has no
   /models listing route, so without a curated _PROVIDER_MODELS entry the
   picker only ever showed the current model — add a Gemini curated list.

Follow-up to #56688.
2026-07-23 16:55:41 -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
iniak a7d78ad685 fix: filter invalid MoA slot providers 2026-07-23 16:55:41 -07:00
liuhao1024 d4c6ae7b11 fix(moa): preserve save_traces/trace_dir on GUI config save
MoaConfigPayload does not declare save_traces or trace_dir, so
set_moa_models() overwrites cfg["moa"] with a dict that lacks these
hand-edited keys.  Use dict.update() to merge instead of replace.

Fixes #58819
2026-07-23 16:55:41 -07:00
Teknium 85b2d52b71 test(moa): regression tests for JSON-string reference_models parsing (follow-up to #59497) 2026-07-23 16:55:41 -07:00
liuhao1024 c1f5f0f911 fix(doctor): recognise 'moa' as a valid internal provider
MoA (Mixture of Agents) is a legitimate internal provider used by
Diagnosis presets and multi-model aggregation. When a MoA preset sets
model.provider to 'moa', hermes doctor incorrectly reports it as
'unrecognised' and suggests changing it, which would break the MoA setup.

Add 'moa' to the known_providers set alongside 'openrouter', 'custom',
and 'auto' so doctor recognises it as valid.

Fixes #58759
2026-07-23 16:55:41 -07:00
0xDevNinja d661886c90 test(cli): assert HermesCLI.__init__ wires moa:<preset> to the moa provider
The existing tests cover _normalize_moa_model() in isolation and a local
precedence expression, but not the __init__ wiring itself. Add two
init-level regression tests: constructing HermesCLI(model='moa:strategy')
strips the prefix to model='strategy' and forces requested_provider='moa',
and the moa: prefix wins over an explicit --provider. Both fail if the
override is dropped from the requested_provider resolution.

Refs #56828
2026-07-23 16:55:41 -07:00
0xDevNinja 8d72845399 fix(cli): resolve moa:<preset> model in non-interactive mode
hermes chat -Q -m moa:strategy failed with 'model moa:strategy is not
supported' (HTTP 401/400): the raw model string was passed straight to
the real provider. The MoA virtual provider only got wired up through the
interactive /moa command and the model picker, never through the -Q
one-shot startup path.

resolve_runtime_provider already handles requested_provider == 'moa', and
agent_init builds the MoAClient off provider == 'moa' (surface-agnostic).
The only gap was mapping the moa:<preset> model string to that provider.

Add _normalize_moa_model() and apply it in HermesCLI.__init__ before
provider resolution: a moa:<preset> model sets requested_provider='moa'
and model=<preset>, so the existing MoA path runs in non-interactive mode
too. The moa: prefix wins over an explicit --provider (previously
--provider deepseek -m moa:strategy silently dropped MoA).

Fixes #56828
2026-07-23 16:55:41 -07:00
Teknium b7a05b6b6f fix: re-anchor summary-input bound to current main + bound iterative path
Follow-ups on top of the cherry-picked #27748 mechanism:
- move the cap constant to module level with full rationale comment
  (class attribute aliases it so subclasses/tests can override)
- bound the iterative-update path too: the PREVIOUS SUMMARY block is
  passed through _bound_summary_input so a pathological rehydrated
  handoff cannot blow up the prompt (previous summary + new turns each
  capped)
- extra regression tests: byte-identical small-input passthrough
  (identity), direct bound+marker unit check, bound-after-per-message-
  truncation shape (hundreds of under-_CONTENT_MAX turns), iterative
  path bounded, marker vs classify_summary_content non-collision
- contributor email mapping for @robgfl45
2026-07-23 16:44:53 -07:00
Cluster2 80ece3867b fix: bound compression summary input 2026-07-23 16:44:53 -07:00
Teknium fa4800414c feat(compression): prompt-cache reclaim gate + hardened wiring for proactive prune
Follow-ups on top of the cherry-picked #62644 mechanism, porting it to
current main and closing the salvage-review requirements:

- proactive_prune_min_reclaim_tokens (default 4096): a prune only COMMITS
  when it reclaims a meaningful token batch, measured on the pruned output.
  A committed prune rewrites already-sent history and invalidates the
  provider prompt-cache prefix; this hysteresis gate keeps those breaks
  episodic/amortized (like a compression boundary) instead of firing every
  tool iteration. 0 disables the gate. (Design point credited to the
  #62389 review cycle's prune_minimum_tokens.)
- Standard no-op caller contract: every skip path returns the INPUT list
  object; the loop commits only on 'result is not messages' + non-zero count.
- Loop call is getattr+callable guarded (plugin engines predating the hook,
  SimpleNamespace test doubles) and exception-swallowed at debug level.
- Config parse follows the compression.max_attempts hardened semantics:
  booleans rejected, fractional floats rejected, integral floats/numeric
  strings accepted; negative trigger = disabled.
- cli-config.yaml.example documented (all three keys) and gateway
  _CACHE_BUSTING_CONFIG_KEYS extended so hot-reload rebuilds the agent.
- Tests: min-reclaim gate both directions, input-object no-op contract,
  no-orphan tool_call_id pairing in BOTH directions (#69830 pin rule),
  default-off zero-behavior-change pin, config parse seam, and behavioral
  loop-wiring tests (consulted/commit/no-op/absent-method/raising).
2026-07-23 16:44:12 -07:00
Kolektori cb481e2f2b feat(compression): proactive tool-result pruning for large-window models
The phase-1 tool-result prune only runs inside compress(), which fires
near 50% of the context window, so it never triggers on large-window
models; old tool outputs then ride in history and are re-sent every turn.

Add prune_tool_results_only(): the same no-LLM prune on a separate, low
proactive_prune_tokens trigger, run as an elif to the compression branch.
Opt-in (default 0), protects the recent tail by message count.

Add the method to the ContextEngine base as a no-op default so pluggable
engines inherit it safely (the post-tool-call path never AttributeErrors on
a non-built-in engine); the built-in compressor supplies the real prune.
Register both keys under the top-level compression config with defaults and
document them.
2026-07-23 16:44:12 -07:00
izumi0uu 17a81ac89e fix(context_compression): roll back interrupted preflight state pollution
Interrupted turns can seed a speculative display token count before the provider receives the request. Restore that display-only seed when interruption wins the race, while preserving completed post-compaction state and treating a successful provider response independently of optional usage metadata.

Constraint: #54776 remains reproducible on current main, while review #4702305384 identifies anti-thrashing rollback as stale and usage receipt as an unreliable response-completion signal.
Rejected: Restore anti-thrashing counters from a preflight snapshot | current main derives their verdict from real provider usage after a completed compaction boundary.
Confidence: high
Scope-risk: narrow
Directive: Keep interrupted preflight rollback display-only, and never infer provider completion from the presence of usage metadata.
Tested: ./.venv/bin/python -m pytest -q tests/run_agent/test_413_compression.py (29 passed); turn-finalizer/conversation-loop tests (31 passed); context-compressor targeted tests (12 passed); infinite-compaction targeted tests (3 passed); ruff; git diff --check.
Not-tested: End-to-end interactive interrupt through CLI or gateway transport.
2026-07-23 16:38:06 -07:00
root 34678d2f2e fix(compression): skip empty post-handoff summary windows 2026-07-23 16:27:06 -07:00
Teknium eebc2286fc fix(gateway): retry-next-message semantics for compression_deferred + regression suite
Gateway half of the #49874 salvage: pass compression_deferred through
both _run_agent_inner result dicts and guard the compression-exhausted
auto-reset block with it — a lock-contended defer keeps the session
intact (the concurrent compressor is actively shrinking it) instead of
wiping it via reset_session.

Regression tests:
- tests/run_agent/test_compression_lock_defer.py — provider-mock 413 and
  400-overflow turns whose compression pass lost the lock end as
  compression_deferred (failed=False, no compression_exhausted); flag
  unset keeps the terminal exhaustion path byte-identical; type-pin
  tests vs MagicMock agents and junk flag values; cap=1 e2e proving the
  refunded pre-API defer leaves the budget for the provider-proven
  413 retry.
- tests/agent/test_preflight_lock_defer.py — a lock-skipped preflight
  pass stops the loop WITHOUT arming preflight_compression_blocked;
  plain no-op still arms it; MagicMock junk does not defer.
- tests/gateway/test_compression_deferred_soft_result.py — AST pin that
  the deferred branch guards the auto-reset chain and performs no
  session mutation (mirrors test_35809_auto_reset_clean_context.py).
2026-07-23 16:23:57 -07:00
Teknium fdefb2d38c fix(compression): prefer psutil.pid_exists for lease liveness probe; add same-pid self-reclaim guard
Hardening on top of the salvaged dead-PID lease reclamation from PR #65775
(@the3asic):

- Probe via psutil.pid_exists (hard dependency; CONTRIBUTING.md critical
  rule #1) with the contributor's os.kill(pid, 0) POSIX probe retained
  only as a scaffold-phase fallback when psutil is missing.
- Same-process holders (pid == os.getpid()) are never probed and never
  self-reclaimed — another thread's live lease is owned by the lease
  refresher/release path.
- Any probe doubt (exceptions, permission errors) conservatively keeps
  the lease until normal TTL expiry; Windows stays TTL-only.
- Tests: psutil-first dead-pid reclaim (probe call pinned), os.kill
  fallback path, probe-doubt keeps lease, same-pid no self-reclaim,
  legacy holder + Windows paths assert NO probe via either API.
2026-07-23 16:21:19 -07:00
3ASiC 6ab8428b88 fix(compression): keep PID probing POSIX-only 2026-07-23 16:21:19 -07:00
3ASiC 8cd49c496f fix(compression): reclaim locks from dead processes 2026-07-23 16:21:19 -07:00
golldyck 69339aab91 fix(compaction): skip compression when it can't reduce tokens
compress_trajectory (and _async) replaced the compressible middle region with
a [CONTEXT SUMMARY] turn without checking that the region is actually larger
than the summary. When a large protected system prompt dominates the budget,
the compressible middle can be tiny; replacing e.g. a 2-token middle with a
~60-token summary GROWS the trajectory (tokens_saved negative), marks it
was_compressed, and still spends a summarization call — the opposite of the
intent, on exactly the hard over-budget cases.

Add a net-savings guard mirroring the code's own comment (net_savings =
region_tokens - summary_target_tokens): if the safely-compressible region is
no larger than summary_target_tokens, return the trajectory unchanged. Applied
to both the sync and async paths. Add sync+async regression tests.
2026-07-23 16:21:03 -07:00
Rain bc7212cf93 feat(moa): per-reference-model max_tokens override
MoA reference_max_tokens is preset-level — one cap for all reference
models. When mixing a verbose model with a terse one, a single cap is
either too tight for the terse model or too loose for the verbose one.

Now each reference slot can optionally carry its own max_tokens:

  reference_models:
    - provider: openrouter
      model: deepseek/deepseek-v4-pro
      max_tokens: ***        # per-slot cap, overrides preset-level
    - provider: openai-codex
      model: gpt-5.5
      # no max_tokens → falls back to preset-level reference_max_tokens

_clean_slot (moa_config.py) preserves an optional max_tokens field on
the slot dict, coerced via _coerce_int_or_none. _run_reference
(moa_loop.py) reads slot-level max_tokens first, falling back to the
preset-level cap passed by the caller. Slots without the field are
unaffected — backward compatible.

Type hints on slot-handling functions updated from dict[str, str] to
dict[str, Any] to reflect the now-heterogeneous slot shape.
2026-07-23 16:17:27 -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 cc1725cbe5 fix(moa): stop reference_max_tokens from also capping the aggregator
aggregate_moa_context's single max_tokens parameter was applied to
both the reference fan-out (_run_references_parallel) and the
aggregator's own synthesis call_llm. #53580 explicitly removed a
hardcoded cap from the aggregator call because it truncated long
aggregator syntheses; #56756 (reference_max_tokens, added to speed up
the advisor fan-out) reintroduced the same shared cap by passing it to
both calls, silently regressing #53580's fix.

Rename the parameter to reference_max_tokens (matching the caller's
own moa_config key) and stop forwarding it to the aggregator's
call_llm invocation, which now always runs uncapped as intended.
2026-07-23 16:17:27 -07:00
Alex Fournier 502e4d63a2 perf(relay): avoid disabled mark initialization
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:31:12 -07:00
Alex Fournier 5a5743188b fix(relay): close failed chat streams eagerly
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:25:14 -07:00
Alex Fournier a3ef27ab70 fix(relay): preserve intentional request removals
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:22:49 -07:00
Alex Fournier 06ed41aa1f fix(relay): preserve managed cancellation signals
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:21:37 -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
Alex Fournier ad6fb26681 fix(relay): fence bypassed stream chunks
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:09:33 -07:00
Alex Fournier 5e1b68ce16 test(telemetry): mock profile consent source
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:06:33 -07:00
Alex Fournier d2e179c54f test(lifecycle): document finalize coverage owner
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:05:36 -07:00
Alex Fournier 43d994986e fix(telemetry): keep consent profile-owned
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:04:07 -07:00
Alex Fournier bba63d8786 fix(telemetry): prune expired local metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 14:01:41 -07:00
Alex Fournier 31387a4621 test(tools): verify hook ownership behaviorally
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:57:30 -07:00
Alex Fournier 4788994bd2 fix(relay): preserve managed callback context
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:53:59 -07:00
Alex Fournier e9b5e4c8f4 fix(relay): protect turn initialization cleanup
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-23 13:48:52 -07:00