Commit graph

2052 commits

Author SHA1 Message Date
ethernet
18ca0e862c
Merge pull request #66471 from NousResearch/ethie/typescript-lsp
fix(lsp): never report stale diagnostics — version-gated freshness for slow servers
2026-07-20 14:16:50 -04:00
Teknium
086a56a028 fix(matrix): correct platform hint over-claims, add regression tests + docs
Follow-up to the salvaged #52552 and #53083 commits:

- Rework the Matrix PLATFORM_HINTS entry around what the adapter actually
  emits: headings, numbered lists, blockquotes, strikethrough-free markdown
  all render (the adapter converts them to sanctioned HTML). Keep the
  genuinely valuable guidance: no Markdown tables (Element X / Beeper /
  mobile clients don't render HTML tables — cells collapse into one line),
  no spoilers/checkboxes/~~strikethrough~~ (not converted by
  python-markdown), prefer descriptive link text.
- Regression test: hint must steer models away from tables.
- Fix test_long_response_split_preserves_thread_context to derive its
  payload size from the adapter's configurable limit instead of assuming
  the old hardcoded 4000.
- Document matrix.max_message_length in the Matrix docs page.
- Contributor mapping for RKelln.
2026-07-20 11:10:49 -07:00
Ryan Kelln
08626c18be fix(prompt_builder): improve Matrix PLATFORM_HINTS with tested formatting rules
- Replace outdated brief hint with comprehensive, tested rules
- Document exactly what renders on Matrix and what does not
- Add critical linebreak semantics (two trailing spaces = soft break)
- Add link formatting guidance (descriptive text, never bare URLs)
2026-07-20 11:10:49 -07:00
PRATHAMESH75
acc1b6e76a fix(agent): inject empty required array on Moonshot object schemas
Moonshot/Kimi's tool-parameter validator rejects object schemas that omit
the required key with HTTP 400 ("required must be an array"), even though
standard JSON Schema allows omitting it. Any Hermes tool with zero required
parameters (browser_back, delegate_task, project_list, several MCP list_*
tools, etc.) tripped this when routed to a Moonshot endpoint.

Add a Rule 4 to the sanitizer: every object schema gets a required array,
defaulting to []. Existing lists are preserved but pruned to names that
actually appear in properties (dangling entries are also rejected upstream).
Applied recursively to nested object schemas and to the coerced/empty
top-level fallback.

Fixes #66835
2026-07-20 11:05:52 -07:00
ethernet
eb09df6ec8 refactor(lsp): version-tagged _DocState replaces timestamp freshness tracking
The staleness fix (f9b1fd799) bolted two wall-clock dicts (_changed_at,
_pulled_at) onto a client that already scattered per-document state
across six parallel dicts (_files, _push_diagnostics, _pull_diagnostics,
_published, _published_version, _first_push_seen) — eight maps kept in
sync by hand.

Collapse all of it into one _DocState per path, and use the LSP document
version as the freshness token instead of clocks:

- didChange bumps doc.version; stored push/pull results carry the
  version they describe (push_version from the server's echoed version,
  or the current version at receipt for servers that don't echo one;
  pull_version captured at request send so an in-flight pull that a
  didChange races past is stale on arrival).
- fresh == tag >= version. Invalidation is implicit in the bump — no
  store-clearing, no clock comparisons, no race windows.
- _has_fresh_push/_has_fresh_pull helpers dissolve into two one-line
  _DocState methods; diagnostics_for(fresh_only=True) becomes a
  three-liner.

Semantics are unchanged from f9b1fd799 (same tests pass, one test
updated off private internals); net -15 lines.
2026-07-20 13:43:36 -04:00
ethernet
a632e68a01 fix(lsp): never report stale diagnostics — wait for fresh post-edit data
Slow language servers (tsserver on large projects especially) publish
diagnostics long after an edit. The client's wait/report path had three
holes that together surfaced the PREVIOUS edit's errors as if they were
current ("ghost diagnostics"), sending the agent chasing errors it had
already fixed:

1. open_file only cleared the diagnostic stores on first open — on the
   didChange path (every subsequent edit) stale push/pull entries
   survived.
2. wait_for_diagnostics' predicates were satisfiable by that leftover
   state (`path in _published`, `path in _pull_diagnostics`), so the
   "wait" often returned instantly with old data.
3. diagnostics_for merged the stale push store unconditionally, so even
   a fresh clean pull got the old error merged back in.

Fix: anchor freshness on a per-file didChange timestamp.

- Pull results record their request send-time and are dropped when a
  didChange raced past them; the pull store is invalidated on every
  change, not just first open.
- wait_for_diagnostics now returns bool (fresh data vs timeout), only
  counts pushes published at/after the change (and version >= ours when
  the server echoes versions), and accepts an explicit timeout — the
  user's lsp.wait_timeout config now actually controls the inner wait
  budget instead of only the outer thread-join.
- diagnostics_for(fresh_only=True) excludes stores that predate the
  latest change; all manager report paths use it.
- On timeout the manager returns [] ("no data") instead of stale
  state, logs a WARNING via eventlog, and does NOT mark the server
  broken — slow is not dead.
- seed-on-first-push no longer marks the file published, so the TS
  seed push can't satisfy a waiter.

Tests: new "stale" and "slow_push" mock-server scripts model the slow
tsserver, plus client- and service-level regression tests
(tests/agent/lsp/test_stale_diagnostics.py).
2026-07-20 13:43:36 -04:00
Teknium
25eafd7d71 fix(models): complete kimi-k3 rollout across Kimi-direct catalog surfaces
Follow-up widening for salvaged PRs #67115, #67685, #67620:

- _PROVIDER_MODELS: add kimi-k3 atop kimi-coding / moonshot / opencode-go
  curated lists (kimi-coding-cn covered by cherry-picked #67620)
- setup.py _DEFAULT_PROVIDER_MODELS: kimi-k3 for kimi-coding(-cn) + opencode-go
- model_metadata: align DEFAULT_CONTEXT_LENGTHS kimi-k3 entry to 1,048,576
  (matches endpoint-scoped override, models.dev, and OpenRouter live metadata)
- anthropic_adapter: classify the bare Coding Plan slug 'k3' (and k3.x/k3-*)
  as Kimi family so adaptive thinking applies on proxied endpoints
- moonshot_schema: is_moonshot_model matches bare 'k3' so tool-schema
  sanitization runs on the chat-completions path
- contributor mappings for githubespresso407, datachainsystems, Punyko8

Tests: 582 passed across 11 targeted files; hermetic E2E verifies picker
order (kimi-k3 first), no dupes, and 1M context resolution.
2026-07-20 08:47:55 -07:00
datachainsystems
54c39c0301 fix: add Kimi K3 1M context window to DEFAULT_CONTEXT_LENGTHS
Kimi K3 ships with a 1M-token context window (verified against
platform.kimi.ai/docs/overview) but was falling through to the generic
'kimi': 262144 catch-all. Added 'kimi-k3': 1_000_000 before the catch-all
so longest-key-first substring matching resolves K3 to 1M while older
Kimi models still hit the 256K default.

Added matching test_kimi_k3_context_1m test covering native,
vendor-prefixed (kimi/, moonshotai/), and older model fallback.
2026-07-20 08:47:55 -07:00
githubespresso407
77aa026ca6 Resolve kimi-k3 context length to 1M on canonical Kimi Coding endpoints
Kimi Coding serves K3 under the bare slug 'k3', but users can also
configure or select the public-facing aliases 'kimi-k3' and
'kimi-k3-cot'. The endpoint-scoped 1M context window was only keyed
on the bare 'k3' slug, so selecting 'kimi-k3' fell through to the
generic 'kimi' catch-all (262k).

Extend the guard in _endpoint_scoped_context_length to also recognize
'kimi-k3' and 'kimi-k3-cot', while keeping the endpoint check that
limits the 1M value to https://api.kimi.com/coding (legacy Moonshot
endpoints still fall back to 262k). Update the existing test to cover
all three aliases.

Fixes: context window limited to 262k when using kimi-k3 via kimi-coding.
2026-07-20 08:47:55 -07:00
ethernet
c363db81e0
fix(desktop, ink): don't wipe messages before final message (#65919)
* fix(desktop): preserve interim assistant text wiped at message.complete

When the agent emits interim text (commentary alongside tool calls, or the
attempted final answer before a verify-on-stop nudge), all UI surfaces
streamed it live but then wiped it at message.complete — keeping only the
final response. The user saw text appear during inference, then disappear.

This is the complete fix across all three layers: agent core, gateway
transport, and all UI surfaces (desktop + Ink TUI).

The verify-on-stop and pre_verify paths flagged the assistant's attempted
final answer as _verification_stop_synthetic, suppressing it from both
state.db and the UI. The user only saw the terse post-verification reply.

Now the assistant response is real content: it's persisted to state.db and
emitted as an interim message via _emit_interim_assistant_message(force_display=True)
before the verification loop runs. Only the synthetic nudge messages keep
the synthetic flags. The turn finalizer drops nudges from live history and
compares content (not just role) to avoid duplicating a published candidate.
Message sequence repair collapses verification candidates in the
consecutive-assistant merge.

Wire agent.interim_assistant_callback both at construction (_agent_cbs())
and per-turn (defense-in-depth), emitting a new message.interim event with
{text, already_streamed}. Gated on display.interim_assistant_messages
(default true). Cleared in the finally block so a stale closure can't
fire on a later turn.

Add message.interim to the GatewayEventName union (apps/shared) and a
typed payload to the TUI's GatewayEvent discriminated union.

The TUI already had the segment-anchoring machinery (flushStreamingSegment +
finalTail) but had no handler for message.interim. Added recordInterimMessage
+ interimBoundaryIndex to seal segments mid-turn, and updated
recordMessageComplete to only dedupe segments after the interim boundary.

Replaced the fragile sealed-set approach with a proper interimBoundaryPending
state flag on ClientSessionState. finalizeInterimAssistantMessage finalizes
the streaming bubble in place (or creates a standalone one), rotates the
stream ID so next deltas create a new bubble, and sets the flag. When the
final text equals an already-sealed interim, they stay as distinct messages.

Extracted mergeFinalAssistantText() as a pure function in chat-messages.ts,
used by both completeAssistantMessage and finalizeInterimAssistantMessage.
Split the bidirectional dedup predicate: reasoning is a restatement only when
the final FULLY covers it. A short final ("Done.") no longer swallows a
longer reasoning block that merely starts with it.

Honor display.interim_assistant_messages (default true) across all layers:
the tui_gateway gates the callback, the desktop wires it to a nanostores
atom via use-hermes-config. Updated hermes_cli/config.py and
cli-config.yaml.example comments to document the Desktop behavior.

_split_segment_tokens now accepts posix=False and _find_ad_hoc_match tries
both posix modes so ad-hoc verification scripts with Windows backslash
paths are matched correctly. (response_previewed forwarding from #53553
is not included — our emit-interim + persist approach makes it unnecessary
since the attempted answer is now surfaced before the verification loop.)

- tsc: clean (desktop + TUI + shared)
- vitest desktop: 73/73 pass (7 interim-sealing + 5 mergeFinalAssistantText + 4 config atom)
- vitest TUI: 83/83 pass (4 new message.interim tests)
- python: 390 tests pass (340 tui_gateway + 33 verification/finalizer + 6 config gating + 3 evidence + 8 continuation budget)

Co-authored-by: Liam Zhang <yingliang-zhang@users.noreply.github.com>
Co-authored-by: Lucas D'Alessandro <lucasfdale@users.noreply.github.com>
Co-authored-by: Eric Manganaro <superposition@users.noreply.github.com>
Co-authored-by: sweetcornna <sweetcornna@users.noreply.github.com>
Co-authored-by: DECK6 <DECK6@users.noreply.github.com>
Co-authored-by: matantsevs <matantsevs@users.noreply.github.com>
Co-authored-by: gitcommit90 <gitcommit90@users.noreply.github.com>

* fix: prefix-match interim streamed content to avoid benign duplicate bubbles

_interim_content_was_streamed used exact equality (streamed == visible_content),
so a final response that was the streamed text plus a trailing delta — or a
partial stream before the verify nudge fired — failed the match and left
_response_was_previewed false. The turn then showed two bubbles (interim +
identical final) instead of settling the interim in place.

Relax to a prefix check (visible_content.startswith(streamed)) in both the
core match and the desktop's settle-in-place gate. The TUI already used
prefix matching via finalTail. The reverse direction (streamed longer than
final) is intentionally not matched — that could suppress a needed resend
in the gateway path where already_streamed=True calls on_segment_break().

* test(desktop): add partial-stream-then-nudge dedup edge case

Third edge case for the interim-sealing dedup: model streams part of its
answer via message.delta, verify nudge fires, interim seals the streamed
prefix, then the final response is the same text plus a trailing delta.
Asserts one bubble (not two) containing the full final text.

Acceptance protocol #2 — covers all three dedup edges:
  1. interim == final (existing)
  2. interim = strict prefix of final (existing)
  3. partial-stream-then-nudge (this commit)

---------

Co-authored-by: Liam Zhang <yingliang-zhang@users.noreply.github.com>
Co-authored-by: Lucas D'Alessandro <lucasfdale@users.noreply.github.com>
Co-authored-by: Eric Manganaro <superposition@users.noreply.github.com>
Co-authored-by: sweetcornna <sweetcornna@users.noreply.github.com>
Co-authored-by: DECK6 <DECK6@users.noreply.github.com>
Co-authored-by: matantsevs <matantsevs@users.noreply.github.com>
Co-authored-by: gitcommit90 <gitcommit90@users.noreply.github.com>
2026-07-20 11:42:29 -04:00
Teknium
58391436f7 fix: reconcile probe cache with stale-entry invalidation + stale test fixtures
- The #44861 stale-cache guard invalidated any cached value that differed
  from the static table, which would have discarded legitimate
  probe-derived windows larger than the table. Treat the table as a
  FLOOR: only drop under-reporting cache entries.
- Update probe test fixtures that predated the 4.6+ 1M table flip
  (opus-4-6 fallback expectations 200K -> 1M).
2026-07-20 05:49:44 -07:00
kubolko
6be4944bc0 fix(bedrock): probe real context window instead of stale static table
Bedrock models resolved their context window from a hardcoded table
(BEDROCK_CONTEXT_LENGTHS) keyed by longest-substring match. AWS ships
new model versions faster than the table tracks, so a new model like
claude-opus-4-8 (1M-token window) silently matched the older
"anthropic.claude-opus-4" entry and got capped at 200K — wasting 80%
of the available context.

Bedrock exposes the real window nowhere in metadata: get-foundation-model
omits it, Converse usage metrics omit it, CountTokens is unsupported on
several models. The only authoritative source is the ValidationException
raised when a prompt exceeds the window:

    "prompt is too long: 1300032 tokens > 1000000 maximum"

Length validation runs before inference, so an oversized request is
rejected immediately and cheaply (no tokens generated, no input
processed). This adds probe_bedrock_context_length(): pad a request just
past a tier, parse the reported maximum, return it. get_bedrock_context_length()
now probes first and falls back to the static table only when the probe
can't run (missing creds, network error, unparseable error). The static
table stays as a safety net.

get_model_context_length() caches the probe result per model+region, so
the network cost is paid once, not every turn. probe=False / empty region
disables probing for offline/display paths — backward compatible with the
single-arg callers.

Verified E2E against live Bedrock (eu-central-1): claude-opus-4-8 resolves
to 1000000. Unit tests cover error parsing, unparseable errors, missing
client, probe-beats-table, and table fallback.
2026-07-20 05:49:44 -07:00
Avi Fenesh
77ba81f75f fix(bedrock): add Fable + Claude 4.6/4.7/4.8 1M entries to context table, drop stale cached values
BEDROCK_CONTEXT_LENGTHS was missing entries for current 1M-context Claude
models, and the resolution path in get_model_context_length() short-circuits
to that table (step 1b) before DEFAULT_CONTEXT_LENGTHS is ever consulted, so
the catalog's correct values could never apply on Bedrock:

- claude-fable-5 (no entry at all) fell through to
  BEDROCK_DEFAULT_CONTEXT_LENGTH and reported 128K for a 1M model.
- opus-4-7 / opus-4-8 substring-matched the generic 'anthropic.claude-opus-4'
  key and reported 200K.
- opus-4-6 / sonnet-4-6 had explicit 200K entries predating their 1M windows.

The practical symptom: the agent compresses context prematurely (at ~128K or
~200K of a 1M window) on every Bedrock-hosted current Claude model.

Fixing the table alone is not enough for existing installs: a previously
persisted 128K/200K value in the context-length cache wins at step 1 and
masks the corrected table forever. Step 1 now reconciles Bedrock-context
cache hits against the static table (the table is authoritative for Bedrock
— there is no live probe to reconcile against), invalidating stale entries
so existing users converge to the right window without manual cache surgery.

Tests cover the new table entries (incl. inference-profile and versioned ID
forms), the 128K-default regression for Fable, the stale-cache invalidation
path, and that pre-4.6 models keep their 200K entries.
2026-07-20 05:38:55 -07:00
Teknium
45f8ba0b9b fix: widen geo-prefix parity to all Bedrock ID normalization sites
The au./apac. additions from #46297 and #65973 covered
is_anthropic_bedrock_model and _normalize_bedrock_model_name; the same
prefix lists exist at two more sibling sites that would still miss
au./ca./sa./me./af. profiles:
- anthropic_adapter._looks_like_bedrock_model_id
- chat_completion_helpers (reasoning stale-timeout floor resolution)

All four sites now share the same 11-prefix set (global/us/eu/apac/ap/
au/jp/ca/sa/me/af, longest-first so apac. wins over ap.). The Bedrock
picker's BEDROCK_GEO_PREFIXES is deliberately untouched: au. absent
there fails open (profile shown), and adding it requires a region-to-geo
remap to avoid hiding Sydney profiles.
2026-07-20 05:38:45 -07:00
Drexuxux
487574c5b6 fix(pricing): strip apac./au. Bedrock region prefixes so cost isn't unknown
_normalize_bedrock_model_name stripped ("us.", "global.", "eu.", "ap.",
"jp.") before the pricing lookup, but AWS Bedrock's Asia-Pacific
cross-region inference profiles are prefixed "apac." (and Australia
"au."), not "ap.". A bare "ap." never matches an "apac.*" id
(str.startswith stops at the 'a' where "ap." expects '.'), so
"apac.anthropic.claude-*" and "au.anthropic.claude-*" fell through with
the prefix intact, missed the bare "anthropic.claude-*" pricing key, and
every Asia-Pacific / Australia Bedrock session priced as "unknown" — no
cost estimate or tracking for two whole geographies, while us./eu./global.
worked.

Add "apac." and "au." to the strip list (mirrors the same fix landing in
bedrock_adapter.is_anthropic_bedrock_model via #46297, which covers the
prompt-caching capability gate but not this duplicated cost-lookup copy).

Extends the existing cross-region pricing test to cover apac./au.; without
the fix it fails with scoped == None for "apac.".
2026-07-20 05:38:45 -07:00
Jake Tracey
4e4904c379 fix(bedrock): recognise au./apac. inference profiles to enable prompt caching
is_anthropic_bedrock_model() strips a regional prefix before checking for
"anthropic.claude" to route Claude through the AnthropicBedrock SDK path
(prompt caching, thinking budgets) instead of the Converse path. The prefix
list was missing "au." and "ap." does not match "apac.", so AU/APAC Claude
inference profiles silently lost prompt caching. Add "apac." and "au.".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 05:38:45 -07:00
polyhistor
a7e911150c fix(bedrock): address review — route list-string items through _safe_text, update stale placeholder assertions
Addresses hermes-sweeper review on PR #66167:

1. _convert_content_to_converse() still emitted {"text": part} directly
   for plain-string items inside a content list (as opposed to
   {"type": "text"} dicts), bypassing _safe_text() entirely. A
   whitespace-only string item (e.g. ["   "]) could still reach Bedrock
   as a blank block. Now routed through _safe_text().

2. tests/agent/test_bedrock_adapter.py::TestEmptyTextBlockFix asserted
   the pre-fix behavior (whitespace -> literal space " "), contradicting
   the new _safe_text()/_EMPTY_TEXT_PLACEHOLDER behavior added in
   4618095a. Updated assertions to expect the non-whitespace placeholder,
   plus added a regression test for the list-string-item case above.
2026-07-20 03:27:38 -07:00
polyhistor
9172048a2f fix(bedrock): use non-whitespace placeholder for empty text blocks
Bedrock Converse rejects text content blocks that are empty OR
whitespace-only (ValidationException: "text content blocks must
contain non-whitespace text"). The prior fix attempt substituted a
single space (" ") for missing content -- but a lone space IS
whitespace, so it was rejected by the exact same validation rule it
was meant to satisfy. This caused a deterministic, unrecoverable
retry-loop failure once any blank/whitespace assistant, tool, or user
turn entered history (most commonly via context-compaction rewriting
a turn to a blank string).

Adds _safe_text()/_EMPTY_TEXT_PLACEHOLDER ("(empty)") and applies it
everywhere a blank text block could reach the wire: user/assistant
content conversion, tool results, the assistant-empty-turn fallback,
and the first/last-message user-alternation padding. System-prompt
blocks are the one exception: blank parts are dropped entirely rather
than placeholder-filled, since a system prompt block should never
carry meaningless placeholder text.

Adds tests/agent/test_bedrock_empty_text_blocks.py (11 tests, was
already present uncommitted -- codifies the exact failing history
from issue #9486 and asserts no blank block ever reaches Bedrock).

Verified against the actual failed request dump from this session
(27-message payload) -- replaying it through the fixed converter now
produces zero blank/whitespace-only blocks.
2026-07-20 03:27:38 -07:00
iizotov
81d4b3654a fix(bedrock): map Claude opus/sonnet 4.6+ to 1M context window
The Bedrock static context table only had entries up to the 4-6
generation and capped all Claude models at 200K. Newer model IDs
(opus/sonnet 4-7, 4-8) silently inherited 200K via the generic
"anthropic.claude-opus-4" / "...-sonnet-4" substring fallback,
contradicting the native Anthropic table in model_metadata.py which
already maps these to 1M.

Map opus/sonnet 4-6/4-7/4-8 to 1_000_000 on Bedrock. Haiku 4.5,
sonnet 4.5, legacy Claude 4 and 3.x stay at 200K (no 1M window).

Add opus-4-8 coverage and haiku/sonnet-4-5 200K guards.
2026-07-20 03:27:28 -07:00
Patrick Muller
3cf6ee7355 fix(bedrock): correct Sonnet 4.5 and Haiku 4.5 context to 200K per Anthropic docs
The previous commit in this branch bumped claude-sonnet-4-5 and
claude-haiku-4-5 to 1_000_000 on the assumption the context-1m-2025-08-07
beta enabled 1M on all Claude 4.x models. Verification against Anthropic's
own documentation shows that is incorrect:

- Claude Haiku 4.5 is a standard 200K model per
  https://platform.claude.com/docs/en/about-claude/models/overview
  (the 'Latest models comparison' table shows '200k tokens' for Haiku 4.5).

- Claude Sonnet 4.5 had its 1M beta retired on April 30, 2026 per
  https://platform.claude.com/docs/en/release-notes/overview:
  'We've retired the 1M token context window beta (context-1m-2025-08-07)
  for Claude Sonnet 4.5 and Claude Sonnet 4. The beta header now has no
  effect on these models, and requests exceeding the standard 200k-token
  context window return an error.'

Revert both entries to 200_000. Opus 4.7, Opus 4.6, and Sonnet 4.6
remain at 1_000_000 — those three have 1M generally available with no
beta header required per the same source.

Also updates the header comment to cite the Anthropic models overview
and the April 30 2026 release note so future readers have an upstream
source of truth.
2026-07-20 03:27:28 -07:00
Patrick Muller
c02466d5e8 fix(bedrock): raise Claude 4.x context window to 1M and add opus-4-7
Claude 4.x models on Bedrock support a 1M-token context window via the
context-1m-2025-08-07 beta header, which Hermes already injects
automatically in build_anthropic_bedrock_client
(agent/anthropic_adapter.py). However, BEDROCK_CONTEXT_LENGTHS in
agent/bedrock_adapter.py still reported 200K for opus-4-6, sonnet-4-6,
sonnet-4-5, and haiku-4-5, and had no entry at all for opus-4-7 (which
falls back via substring match to the 200K opus-4 entry).

This caused Hermes to display a 200K window, compress conversations
earlier than necessary (compression.threshold * 200K instead of * 1M),
and generally under-utilize the full 1M context users are paying for.

The fix is metadata-only — the Bedrock API and beta header already
support 1M end-to-end. agent/model_metadata.py's DEFAULT_CONTEXT_LENGTHS
table already lists claude-opus-4-7 / -4-6 / sonnet-4-6 at 1M for the
non-Bedrock paths, so this change brings the Bedrock table into
alignment.

Changes:
- Add anthropic.claude-opus-4-7 at 1_000_000
- Bump anthropic.claude-opus-4-6 from 200_000 to 1_000_000
- Bump anthropic.claude-sonnet-4-6 from 200_000 to 1_000_000
- Bump anthropic.claude-sonnet-4-5 from 200_000 to 1_000_000
- Bump anthropic.claude-haiku-4-5 from 200_000 to 1_000_000
- Add explanatory comment pointing readers at the beta-header injection
  site in agent/anthropic_adapter.py
2026-07-20 03:27:28 -07:00
Teknium
3441b80f4f feat(pricing): add Bedrock rows for Opus 4.8/4.7, correct Opus 4.6 to $5/$25
Adds current-gen Claude Opus pricing rows on Bedrock keyed to Anthropic's
published list price, which commercial Bedrock on-demand mirrors. Also
corrects the existing Opus 4.6 row: it carried Claude-3-era Opus pricing
($15/$75); Opus 4.5+ list at $5/$25 with cache write 1.25x / read 0.1x.

The AWS Price List API had not published these SKUs machine-readably as
of 2026-07, so these are commercial-list snapshots pending an
authoritative machine source.

Reapplied from PR #62327 (commit authored under a placeholder identity,
so cherry-pick was not usable; sonnet-5 row from that PR already landed
via #67932).

Co-authored-by: pgregg88 <4943027+pgregg88@users.noreply.github.com>
2026-07-20 03:27:16 -07:00
Osraka
e101bbaebb fix(pricing): restrict Bedrock profile normalization 2026-07-20 03:27:16 -07:00
Osraka
54418a888e fix(pricing): resolve versioned Bedrock profile IDs 2026-07-20 03:27:16 -07:00
Jan-Stefan Janetzky
766c617e83 fix(compression): detect semantic no-op results 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
75af6dc57c fix(redaction): normalize URL credential key aliases 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
46e4891c64 fix(compression): close post-dispatch lock scope 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
62a00a7391 fix(redaction): cover strict URL reference forms 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
a48315e322 fix(compression): guard lock refresher startup 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
34bab1c6ac fix(compression): harden provider context handoff 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
192ef93ad5 fix(agent): harden pre-compress context handoff 2026-07-20 02:25:57 -07:00
Tranquil-Flow
ad8c533cc7 fix(agent): capture on_pre_compress return value and pass to compressor
The MemoryProvider.on_pre_compress() hook returns text that providers
want preserved in the compression summary, but run_agent.py discarded
the return value. Additionally, compress() and _generate_summary() had
no mechanism to accept this context.

- Capture on_pre_compress() return value in run_agent.py
- Add memory_context parameter to compress() and _generate_summary()
- Inject memory provider insights into summarization prompts

Fixes all MemoryProvider plugins that return context from
on_pre_compress() (currently silently broken for every plugin).
2026-07-20 02:25:57 -07:00
Teknium
f6abfc05be fix: keep Sonnet 5 intro pricing over duplicate standard-rate entry
PR #55848 and #60410 both added an (anthropic, claude-sonnet-5) pricing
key; the later duplicate (/$15 standard rate) would silently win in
the dict literal. Keep the intro pricing entry ($2/$10 through
2026-08-31 per Anthropic docs) which carries the reversion note.
2026-07-20 02:25:44 -07:00
Daniel Brunsdon
f44074df7f feat(pricing): add Claude Sonnet 5 intro pricing entry
Sonnet 5 launched 2026-06-30 with introductory pricing ($2/$10 per
MTok input/output) through 2026-08-31, after which it reverts to
$3/$15. The model had no entry in the official-docs pricing snapshot,
so any session on claude-sonnet-5 was tracked as cost_status=unknown
with $0 estimated cost -- silently hiding real spend from
hermes insights and any downstream cost sync.

Source: https://platform.claude.com/docs/en/about-claude/pricing
2026-07-20 02:25:44 -07:00
Ariel Bravy
e2561466c7 feat(models): add Claude Sonnet 5 support 2026-07-20 02:25:44 -07:00
kshitijk4poor
98cadadd84 refactor: extract _build_partial_stream_stub helper
Deduplicates the SimpleNamespace stub-response construction that was
copy-pasted between the tool-call-drop guard and the text-only-drop
guard (both in interruptible_streaming_api_call).  The third site
(error-handler at ~L3775) has a structurally different shape (different
role/reasoning/model/usage sources + _content_filter_terminated tag)
and is left inline with an existing comment.
2026-07-20 13:31:02 +05:30
ajzrva-sys
65bb16c8ce fix(streaming): detect text-only stream drops with no finish_reason (#32086)
When a streaming response ends cleanly (HTTP 200) with no finish_reason
after delivering text but no tool calls, the chunk collector silently
stamps finish_reason='stop' and the conversation loop presents truncated
text as a complete response.

Three stream-drop paths now exist after chunk collection:

1. Zero-chunk → EmptyStreamError, retried (existing)
2. Tool-call in progress, no finish_reason → partial-stream-stub (existing)
3. Text-only, no finish_reason → partial-stream-stub (NEW — this fix)

Path 3 routes through the same PARTIAL_STREAM_STUB_ID + FINISH_REASON_LENGTH
machinery as path 2. The conversation loop shows 'Stream interrupted —
requesting continuation' and injects a continue prompt, giving the model
a chance to resume where the stream dropped.

Observed with DeepSeek provider where CloudFront drops SSE streams
mid-response after delivering partial text.
2026-07-20 13:31:02 +05:30
kshitijk4poor
86e603e7d6 fix(compression): verify cached prompt embeds current memory before retaining
The salvaged retention check compared the built-in memory snapshot
before vs after the disk reload. That holds for a long-lived CLI agent,
but on fresh-agent surfaces (gateway per-turn agents, TUI) the cached
prompt is restored from the session DB and can predate mid-session
memory writes that the fresh MemoryStore already absorbed at init: the
snapshot is then identical on both sides of the reload while the prompt
itself is stale, so compression would retain (and re-persist via
update_system_prompt) a prompt missing the new memory for the life of
the session.

Replace the equality check with a containment check
(_cached_prompt_reflects_builtin_memory): retain the cached prompt only
when the freshly-reloaded rendered blocks appear verbatim inside it,
and rebuild when a leftover block header remains for a target whose
entries have since been emptied or disabled. Block headers are shared
via MEMORY_BLOCK_HEADERS in tools/memory_tool.py so the check stays in
lockstep with MemoryStore._render_block.

Adds regression guards for the gateway stale-restore path and the
emptied-memory leftover-block path; verified with a real-MemoryStore
E2E matrix (9 scenarios) against a temp HERMES_HOME.
2026-07-20 13:13:02 +05:30
konsisumer
54c3f589ad fix(compression): retain prompt cache when memory is unchanged 2026-07-20 13:13:02 +05:30
Teknium
ad86b8f469 fix: add qwen3.7-plus to alibaba list + qwen3-max context fallback
Follow-up to the salvaged #66083/#42792 commits:
- alibaba (Qwen Cloud coding-intl) gets qwen3.7-plus too — same platform
  allowlist as alibaba-coding-plan (issue #44662 comment by @coder-movers)
- qwen3-max substring context entry (262144) so the newly-listed
  qwen3-max-2026-01-23 snapshot doesn't fall to the generic 131072 qwen
  fallback
2026-07-20 00:41:01 -07:00
asscan
e0a27690d3 fix: add qwen3.7-plus context length (1M)
Add qwen3.7-plus to DEFAULT_CONTEXT_LENGTHS with 1M context window.
Without this entry, the model falls back to the generic 'qwen' entry
(128K), causing premature context compression at 50% (64K tokens)
instead of the correct 500K threshold.

Official docs: https://help.aliyun.com/zh/model-studio/developer-reference/
2026-07-20 00:41:01 -07:00
Drexuxux
1157c636c5 fix(compression): stop the progress floor from splitting a tool group
_find_tail_cut_by_tokens aligns cut_idx away from tool-call/result
boundaries (_align_boundary_backward), and both tail anchors re-align after
moving it. The final statement then raised the result to head_end + 1 so
compression always claims at least one message — without that floor the
caller's compress_start >= compress_end guard turns the pass into a no-op
that re-runs forever.

That raise discarded the alignment. When the floor landed inside a tool
group, the parent assistant(tool_calls) fell in the summarised region while
its tool results started the tail, and _sanitize_tool_pairs dropped those
orphans outright — so the tool output was neither summarised nor kept. It
vanished. That is exactly the silent loss _align_boundary_backward's own
docstring says the alignment exists to prevent.

Two back-to-back tool calls are enough to trigger it on default settings
(protect_first_n=3):

    system, assistant(call_1), tool, tool, assistant(call_2), tool

    aligned cut          = 4   (keeps call_2's group together)
    returned cut         = 5   (floor overrode it)
    summarised region    = [assistant(call_2)]
    tail                 = [tool(call_2)]  -> orphan -> dropped

Sweeping every well-formed block layout up to length 6 (21840 transcripts),
5623 of them — 26% — split a call/result pair this way.

Re-align FORWARD after applying the floor. Forward, never backward: pulling
back would hand return the message the floor just claimed and reopen the
no-op loop. Sliding forward instead moves the cut past the end of the group,
so the whole call/result pair is summarised together and nothing is
orphaned. The same sweep reports 0 violations after the change, and the
progress guarantee is pinned by its own test.
2026-07-20 11:58:50 +05:30
Brooklyn Nicholson
b0f60622aa style(agent): tighten request-estimate comment 2026-07-19 19:54:31 -05:00
Brooklyn Nicholson
c1c4e56e7e perf(agent): drop per-call base64 re-serialization from request-size estimate
Every API iteration computed `total_chars = sum(len(str(msg)) ...)`, which
str()-serializes the ENTIRE history — including base64 images and large tool
results — just to take its length, then called estimate_request_tokens_rough,
which walked the messages a SECOND time (it re-runs estimate_messages_tokens_rough
internally, already computed one line above).

Now derive both from one image-stripped message estimate:
  approx_tokens = estimate_messages_tokens_rough(api_messages)   # once
  request_pressure_tokens = approx_tokens + tools_tokens          # == old value
  total_chars = approx_tokens * 4                                 # log/metric only

request_pressure_tokens is byte-identical to the old
estimate_request_tokens_rough(api_messages, tools=agent.tools or None) (no
system_prompt arg → messages + tools). total_chars only feeds a verbose log and
the pre-api-request hook's request_char_count, so a rough proxy is fine and it no
longer balloons on image turns. On the TTFT critical path for every call.

tests/agent/test_model_metadata.py + test_compressor_image_tokens.py green.
2026-07-19 19:48:12 -05:00
Teknium
9b428ddd08
feat(x_search): default model grok-4.20-reasoning -> grok-4.5 (#67719)
grok-4.5 is xAI's newest release (their versioning is non-monotonic:
4.5 > 4.20) and is the model xAI's own docs use for the server-side
x_search tool. Users who explicitly pinned x_search.model keep their
choice; everyone else picks up the new default via the config
deep-merge — no _config_version bump needed.

- tools/x_search_tool.py: DEFAULT_X_SEARCH_MODEL
- hermes_cli/config.py: DEFAULT_CONFIG x_search.model + comment
- agent/reasoning_timeouts.py: 300s stale-timeout floor entry for
  grok-4.5 (grok-4.20-reasoning entry kept for pinned users)
- docs: x-search.md en + zh-Hans (config sample + troubleshooting)
- tests: default-model assertion + timeout-floor positive case
2026-07-19 16:32:20 -07:00
liqiping
60811ced37 feat(agent): adaptive thinking for Kimi-family Anthropic endpoints
Kimi's Anthropic-compatible endpoints (api.moonshot.cn/anthropic,
api.kimi.com/coding) implement the adaptive thinking contract — they
accept thinking.type=adaptive + output_config.effort (all of low,
medium, high, xhigh, max verified live) and return thinking blocks, and
the replay-validation 400s that originally motivated dropping the
parameter (#13848) no longer occur.

_supports_adaptive_thinking() now returns True for Kimi-family models,
so they get thinking={type: adaptive, display: summarized} +
output_config.effort via ADAPTIVE_EFFORT_MAP instead of nothing, and
the blanket drop of the thinking parameter for Kimi-family endpoints is
removed. MiniMax and other non-adaptive third parties keep the manual
budget_tokens path; Claude behavior is unchanged.
2026-07-19 12:21:59 -07:00
Soju06
c0c76a4715 perf(gateway): byte-stable session context prompts
The per-message ephemeral context prompt re-renders every turn, and
any byte change (Discord auto-thread rename, reset notes, voice
channel state) both breaks the provider prompt-cache prefix at the
head of every request and changes the gateway agent-cache signature,
forcing a full agent rebuild per message. Pin the rendered block per
session keyed by a hash of exactly the fields it renders, so only a
real input change (rename, topic edit, /sethome, redact_pii flip)
re-renders; deliver one-shot per-turn facts (auto-reset note,
first-contact intro, voice-channel changes) on the current user
message via the api_content sidecar instead of the system prompt; sort
get_connected_platforms for byte-stable ordering.
2026-07-19 14:58:59 +05:30
fanyu
ddd81e9352 fix(anthropic): preserve thinking blocks on Kimi-family endpoints on replay
_manage_thinking_signatures treated every Kimi-family endpoint with the
#13848-era contract: strip signed Anthropic thinking blocks from replayed
history, assuming the upstream cannot validate Anthropic signatures.

Live probing shows that contract is outdated for the whole Kimi family:

- Kimi For Coding (api.kimi.com/coding) issues AND validates its own
  thinking signatures (K3+): both verbatim and content-mutated signed
  blocks replay with HTTP 200;
- Moonshot's Anthropic surface (api.moonshot.cn/anthropic) accepts signed
  blocks the same way (200 on both verbatim and mutated);
- every other harness that replays signed blocks to KFC (Claude Code, pi,
  Kilo Code) round-trips fine.

Stripping signed blocks there silently discarded the model's prior
chain-of-thought in multi-turn conversations — e.g. a two-turn recall
probe loses the reasoning between turns while the text answer survives
(agent.log: turn-2 input ≈ turn-1 input + a few dozen tokens instead of
+thinking).  With this change, the same probe recalls the exact hidden
values from turn-1 thinking (+230 tokens on turn 2).

So: on _is_kimi_family_endpoint, keep signed and unsigned thinking blocks
unchanged on replay — one uniform rule for the whole Kimi family, no
/coding-vs-Moonshot split.  DeepSeek keeps the #16748 contract (strip
signed, preserve unsigned).  Third-party and direct-Anthropic behavior is
untouched.

Add tests/agent/test_anthropic_kimi_signed_thinking_replay.py pinning the
unified behavior (Kimi /coding + Moonshot keep signed and unsigned) and
the unchanged neighbors (DeepSeek strips, direct Anthropic keeps).
2026-07-19 01:53:01 -07:00
teknium1
14add28785 fix: exempt persist-disabled review forks from the session-scoped tripwire
Background-review forks share the live parent's session_id for prompt-cache
warmth but are _persist_disabled — they can never write to the transcript.
Without this, every review fork on an active session would (a) trip a false
cross-agent overlap warning against the parent's real turn, sending the
#64934 route investigation the wrong way, and (b) pop the parent's in-flight
slot at its own persist, making a real overlap right after a review go
unreported. Both legs now skip persist-disabled agents symmetrically.
+2 tests.
2026-07-19 01:09:29 -07:00
hotragn
8c6627638e fix(agent): catch cross-agent turn overlaps in the tripwire (#64934)
note_turn_start kept its in-flight marker on the agent object, but the
gateway caches agents per routing key (_agent_cache) while transcripts
are owned by session_id — and switch_session (/resume from a second
surface, CLI-continuity rebinding, async-delegation pinning,
topic-binding tip-walks) maps multiple routing keys onto one session_id
without any cross-key check. Two keys mapped to one session run
concurrent turns on two different agent objects, so the per-agent
tripwire could never fire for exactly the dispatch route #64934 is
waiting to identify.

Add a module-level session_id-keyed in-flight registry alongside the
per-agent marker. Same philosophy as the original tripwire: log-only,
takes ownership on overlap, under-reports rather than double-reports
(a same-agent overlap warns once, not twice). The persist-time clear
pops the session id stamped at turn start, so a mid-turn compression
rotation of agent.session_id cannot strand the slot.
2026-07-19 01:09:29 -07:00