Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
157 commits
Select commit Hold shift + click to select a range
d8e353f
feat(copilot): authentic @github/copilot CLI identity + Claude contex…
arminanton Jun 21, 2026
8666fd7
fix(desktop): preserve other providers' hide-all in model visibility …
Jun 16, 2026
461fcc0
test(desktop): harden model-visibility toggle + dedupe default expansion
kshitijk4poor Jun 21, 2026
8ca38d3
Merge pull request #50100 from kshitijk4poor/salvage/model-visibility…
kshitijk4poor Jun 21, 2026
472c068
fix(mcp): detect 'unknown method' phrasing in ping keepalive fallback
xxxigm Jun 21, 2026
7b9a0b3
test(mcp): cover 'unknown method' ping keepalive fallback (#50028)
xxxigm Jun 21, 2026
04730f3
fix(cli): warn when in-session model switch will preflight-compress
tuancookiez-hub Jun 20, 2026
1ca2972
fix(cli): log instead of swallow preflight-warning errors; consistent…
kshitijk4poor Jun 21, 2026
44d552e
Merge pull request #50115 from NousResearch/salvage/model-switch-pref…
kshitijk4poor Jun 21, 2026
51a338a
feat(gateway): track active_agents in runtime status on turn boundaries
benbarclay Jun 21, 2026
0ee7546
feat(dashboard): surface gateway busy/drainable on /api/status
benbarclay Jun 21, 2026
b577f25
refactor(gateway): dedupe drain-timeout resolution + share active_age…
kshitijk4poor Jun 21, 2026
4d7bb38
refactor(gateway): route all active_agents coercion through parse_act…
kshitijk4poor Jun 21, 2026
5aec00f
Merge pull request #50131 from kshitijk4poor/salvage/gateway-busy-rea…
kshitijk4poor Jun 21, 2026
1965d56
fix(agent): scale tool-output budget to the model context window (#23…
kshitijk4poor Jun 21, 2026
1e0b3a2
fix(agent): reset stale token calibration on model switch (#23767)
kshitijk4poor Jun 21, 2026
fd45714
style: add explicit utf-8 encoding to copilot CLI bundle read (PLW1514)
arminanton Jun 21, 2026
796f618
fix(telegram): keep chunk markers outside code fences
whoislikemiha Jun 21, 2026
9f67ba1
fix(agent): guard finalize_turn cleanup chain so it never drops the r…
teknium1 Jun 21, 2026
99233fa
fix(cli): persist sessions before shutdown
May 16, 2026
e499d69
feat(api-server): configurable concurrent-run cap to prevent DoS (#50…
teknium1 Jun 21, 2026
c7e8854
fix(tui): persist session messages on force-quit / signal shutdown
bogerman1 May 9, 2026
6984026
fix(browser): enable SSRF guard when terminal runs in container
liuhao1024 Jun 4, 2026
ed8f789
Merge pull request #50136 from NousResearch/fix/context-aware-tool-bu…
kshitijk4poor Jun 21, 2026
c6a0929
Merge pull request #50137 from NousResearch/fix/reset-calibration-on-…
kshitijk4poor Jun 21, 2026
3509be7
fix(compression): auto-compression triggers at minimum context length…
teknium1 Jun 21, 2026
03563da
fix(gateway): raise session-hygiene hard message limit 400 → 5000 (#5…
teknium1 Jun 21, 2026
31e59fe
fix(telegram): preserve newlines in rich slash-command output (#46070)
Tranquil-Flow Jun 21, 2026
a966932
fix(telegram): exempt tables from rich newline hard-breaks
teknium1 Jun 21, 2026
8a506ed
fix(auth): make load_pool() non-destructive for env-seeded credentials
yeyitech Jun 21, 2026
2f4f23f
fix(codex): bridge app-server item/started events to Telegram tool-pr…
teknium1 Jun 21, 2026
6369374
fix(copilot): make Copilot ACP auxiliary client awaitable for async c…
arminanton Jun 21, 2026
65a477f
feat(desktop): add Update now button to About panel (#50186)
OutThisLife Jun 21, 2026
ea056b0
fix(telegram): avoid rich messages for CJK text
tt-a1i Jun 21, 2026
9078b4b
fix(file): harden read_file device alias blocking
egilewski May 29, 2026
6f5f58e
fix: keep poll read-only for notify_on_complete watcher
de1tydev Apr 29, 2026
41ba90f
fix(process): keep CLI drain dedup after poll goes read-only (#10156)
teknium1 Jun 21, 2026
296b290
chore(release): add AUTHOR_MAP entry for de1tydev (#10158)
teknium1 Jun 21, 2026
b6a4638
fix(compressor): treat empty-content summary response as failure, not…
teknium1 Jun 21, 2026
3e354b6
fix(agent): preserve copilot routed headers
konsisumer Jun 4, 2026
41e0c10
fix(agent): route repeated-compression warning through _emit_status (…
teknium1 Jun 21, 2026
b17180d
fix(session): finalize owned SQLite session rows on AIAgent.close()
yeyitech Jun 21, 2026
9e4fe32
fix(session): opt the background-review fork out of session finalization
teknium1 Jun 21, 2026
d0de460
fix(tui): /compress shows a before/after summary (#46686)
teknium1 Jun 21, 2026
7bc6f18
fix(hindsight): skip local_embedded daemon when running as root
LehaoLin Jun 21, 2026
e0498bd
fix(bedrock): price Claude prompt-cache tokens in /usage (#50307)
teknium1 Jun 21, 2026
a18bae6
fix(config): redact api_key in config show/set output (#50245) (#50313)
teknium1 Jun 21, 2026
71274f2
fix(file): reject read_file line-numbered writeback
Beandon13 May 4, 2026
16899ae
test(file): update guard assertions for unified display-text message
teknium1 Jun 21, 2026
93ea9b0
fix(gateway): cap inbound media download size to prevent memory exhau…
sgaofen Jun 21, 2026
d19aabb
fix(gateway): persist in-flight transcript on restart/shutdown drain …
teknium1 Jun 21, 2026
3b56d3a
fix(security): redact secrets in kanban tool payloads before persistence
steveonjava May 24, 2026
6183e8c
fix(telegram): make Bot API 10.1 rich messages opt-in (default off)
0x0sec Jun 21, 2026
7a8c4fe
chore(release): add AUTHOR_MAP entry for #48422 salvage
teknium1 Jun 21, 2026
587b5b9
fix(backup): capture memory-provider state stored outside HERMES_HOME…
teknium1 Jun 21, 2026
e581740
fix(kanban): single-writer dispatch lock to prevent orphan-dispatcher…
teknium1 Jun 21, 2026
56255f8
fix(agent): stop delegate cascade from deleting the parent session
jackjin1997 Jun 19, 2026
7a131f7
fix(api-server): stop silently promising async delivery on stateless …
teknium1 Jun 21, 2026
2a45423
fix(photon): classify Envoy overflow errors as retryable; add typing …
JoaoMarcos44 Jun 21, 2026
9578e52
fix(photon): detect unexpected sidecar death and trigger reconnect
JoaoMarcos44 Jun 21, 2026
e267237
test(photon): cover overflow retry, typing cooldown, sidecar-crash de…
teknium1 Jun 21, 2026
def3f63
fix(file): anchor device symlink guard to task cwd
egilewski Jun 21, 2026
5e3e89c
feat(hindsight): configurable embedded daemon health grace timeout (#…
teknium1 Jun 21, 2026
475e81d
fix(web_server): use run_in_executor for gateway pre-warm and drain-t…
JoaoMarcos44 Jun 21, 2026
e580706
test(web_server): add integration tests for desktop boot handshake fix
JoaoMarcos44 Jun 21, 2026
6bbacc2
fix(desktop): make cold-start port-announcement deadline tolerant
teknium1 Jun 21, 2026
c0409a8
feat(gateway): typed send-error classification (SendResult.error_kind…
teknium1 Jun 21, 2026
565b7c8
fix(telegram): stop typing indicator lingering after final reply
Jun 21, 2026
7d9f6a2
chore(release): add AUTHOR_MAP entry for #48678 salvage
teknium1 Jun 21, 2026
9d883ac
feat(plugins): add ctx.profile_name for session-agnostic profile acce…
teknium1 Jun 21, 2026
e217fd4
feat(kanban): add task lifecycle plugin hooks (claimed/completed/bloc…
teknium1 Jun 21, 2026
b6d1072
fix(cli): branch new worktrees from the fresh remote tip, not stale l…
teknium1 Jun 21, 2026
9630ec6
fix(kanban): pin worker TERMINAL_CWD to the task workspace (#50348)
teknium1 Jun 21, 2026
84ba83b
fix(kanban): bound the cross-process init lock so connect() can't han…
teknium1 Jun 21, 2026
1f4c5ae
fix(kanban): honor kanban.auto_decompose toggle live, without a gatew…
teknium1 Jun 21, 2026
ae46699
fix(security): validate snapshot_id and file paths in restore_quick_s…
Apr 13, 2026
87615f4
test(backup): add regression tests for restore_quick_snapshot path tr…
memosr Jun 5, 2026
d164ed0
fix(kanban): make reclaim claim-lock-aware to stop task/run status de…
teknium1 Jun 21, 2026
d7737bf
docs(ui-tui): fix file paths, add billing command, update file map
alelpoan Jun 21, 2026
b6f03ab
docs(ui-tui): add billing.step_up.verification event + perfPane.tsx t…
teknium1 Jun 21, 2026
8e4d2fd
docs(plugins): document acting from hooks via ctx.profile_name + disp…
teknium1 Jun 21, 2026
7502d38
fix(windows): prefer cmd npm shim on PATH fallback
helix4u Jun 21, 2026
5b45fb2
fix(security): sanitize kanban markdown html
zapabob Jun 5, 2026
537ad9e
fix(cli): guard missing ui-tui workspace before TUI launch
konsisumer Jun 19, 2026
db097fb
fix(cli): auto-restore a deleted ui-tui workspace from git before TUI…
teknium1 Jun 21, 2026
6902eb3
fix(cli): make ZIP-update directory replace atomic so it can't delete…
teknium1 Jun 21, 2026
09a96ba
fix(gateway): pause Telegram typing before stream finalize
LeonSGP43 Jun 21, 2026
f726908
fix(desktop/windows): stop in-app update from cascading into a backen…
teknium1 Jun 21, 2026
a4b1554
fix(whatsapp): normalize bare phone targets to JIDs before bridge send
sgaofen Jun 21, 2026
ed3d12a
fix(security): fail-closed when WebSocket peer is empty in loopback mode
memosr Jun 5, 2026
99f3072
fix(model-switch): a failed in-place swap must be a no-op, not a dead…
teknium1 Jun 21, 2026
bb77a8b
fix(gateway): respawn unmapped Windows gateways after update (#50090)…
teknium1 Jun 21, 2026
824c9d3
fix(config): alias model.api_base -> model.base_url for custom provid…
teknium1 Jun 21, 2026
a9c8025
fix(approval): honor interrupt in blocking gateway approval wait (#8697)
panghuer023 Jun 21, 2026
4cff036
test(approval): regression for interrupt-unblocks-approval; AUTHOR_MAP
teknium1 Jun 21, 2026
b7f6cb9
fix(email): resolve IMAP/SMTP host from config and validate before co…
devorun Jun 20, 2026
e921c4f
chore(release): map devorun salvage author email
teknium1 Jun 21, 2026
f79e0a7
fix(email): mark missing-config as non-retryable + reject blank env v…
teknium1 Jun 21, 2026
b5b8a4c
fix(gateway): respect adapter decline of fresh-final to prevent doubl…
liuhao1024 Jun 21, 2026
ed96669
fix(security): handle IPv6 scope IDs in URL safety checks to prevent …
sprmn24 May 25, 2026
87ab373
test(url-safety): cover IPv6 scope-ID strip + fail-closed in URL guards
teknium1 Jun 21, 2026
6f0ecf3
fix(redact): mask all Authorization schemes and x-api-key style headers
devorun Jun 21, 2026
bb59075
Merge pull request #50398 from helix4u/fix/windows-npm-path-fallback
ethernet8023 Jun 21, 2026
c8eb7cf
fix: V-009 security vulnerability
orbisai0security Apr 14, 2026
3a6a43c
fix(V-009): reject path traversal in SessionEntry.from_dict and harde…
orbisai0security Jun 5, 2026
aa2aac6
fix(V-009): reject Windows drive-letter paths in session field valida…
orbisai0security Jun 5, 2026
4d4ba08
refactor(session): simplify traversal guard to a helper + logger, har…
teknium1 Jun 21, 2026
624580e
fix(browser): verify daemon identity before orphan reaper kills a PID…
teknium1 Jun 21, 2026
745c4db
feat(desktop/windows): show update-in-progress feedback before the de…
teknium1 Jun 21, 2026
7785655
fix(desktop): keep the floating composer in-bounds so it can't be los…
OutThisLife Jun 21, 2026
c11ae82
fix(codex): seed app-server sessions with configured cwd
jplew Jun 4, 2026
8fcb813
fix(security): harden smart approval guard against prompt injection
Dusk1e Jun 21, 2026
242ec45
fix(gateway): don't lazy-install SDKs for unconfigured platforms on s…
xxxigm Jun 21, 2026
29176ff
test(gateway): cover no eager platform install on startup sweep
xxxigm Jun 21, 2026
8baa4e9
feat(cli): add native Antigravity OAuth provider
pmos69 May 20, 2026
b7a912e
fix(antigravity): bake in public OAuth client + default project fallback
teknium1 Jun 21, 2026
37c37c9
fix(antigravity): register google-antigravity ProviderProfile + AUTHO…
teknium1 Jun 21, 2026
c768c4b
fix(antigravity): move model flow to model_setup_flows + stop bare-al…
teknium1 Jun 21, 2026
16aeba1
fix(desktop): clamp composer peel-off under cursor
OutThisLife Jun 21, 2026
bef1d3e
fix(desktop): filter undefined entries in AttachmentList to prevent r…
liuhao1024 Jun 21, 2026
84fcbbf
fix(security): quote HERMES_TIMEZONE in remote code execution to prev…
Dusk1e Apr 12, 2026
13ce811
fix: show desktop approval fallback (#46548)
itsflownium Jun 21, 2026
1ec4fcf
Merge pull request #50466 from NousResearch/bb/composer-popout-bounds
OutThisLife Jun 21, 2026
1f6994d
chore(release): add AUTHOR_MAP entry for #45205 salvage (EtherAura)
teknium1 Jun 21, 2026
e5e2583
fix(desktop): relaunch on Linux after in-app update instead of hangin…
EtherAura Jun 21, 2026
84e1d31
refactor(kanban): fold worker/orchestrator skills into injected guida…
teknium1 Jun 22, 2026
e447723
fix(process-registry): re-validate PID identity before killing host p…
valentt Jun 10, 2026
77fdbbf
fix(whatsapp): validate bridge PID identity before killing stale pidf…
valentt Jun 10, 2026
069ab40
fix(whatsapp): only kill LISTENers when freeing the bridge port, neve…
valentt Jun 10, 2026
615a8e6
fix(whatsapp): add missing re import + fix test import path after ada…
teknium1 Jun 21, 2026
0fb3b13
chore: add valentt to AUTHOR_MAP for #43846 salvage
teknium1 Jun 21, 2026
1cefc2a
test(whatsapp): fix port-spares-client test race (listen before annou…
teknium1 Jun 21, 2026
012f40c
fix(status): cross-platform start-time fingerprint via psutil fallback
teknium1 Jun 22, 2026
b6d2ac1
feat(mem0): add self-hosted support via MEM0_HOST / host config
Apr 21, 2026
452a725
fix(mem0): address PR review — restore docstrings, keep api_key required
May 4, 2026
73340d8
chore: add buihongduc132 to AUTHOR_MAP for mem0 salvage
teknium1 Jun 22, 2026
2b3a4f0
fix(agent): strip stale reasoning_content when falling back to a stri…
teknium1 Jun 22, 2026
0a7ae28
fix(compressor): remove logging.basicConfig from library class __init__
annguyenNous Jun 22, 2026
9bf9a9f
fix(swe-runner): move logging.basicConfig out of Runner __init__ into…
teknium1 Jun 22, 2026
7726ce3
fix(security): close hermes-0day MCP-persistence attack surface
teknium1 Jun 22, 2026
eb51c18
fix(docker): replace dashboard --insecure with basic-auth provider
teknium1 Jun 22, 2026
f45ace9
feat(security): startup security posture audit (warn-on-load)
teknium1 Jun 22, 2026
41fe086
style(security-audit): add explicit encoding to read_text calls (ruff…
teknium1 Jun 22, 2026
8cecaf0
feat(process): escalate SIGTERM->SIGKILL on host-pid termination afte…
teknium1 Jun 22, 2026
8cbb34b
chore: map tkwong co-author email for #15008 SIGKILL-escalation credit
teknium1 Jun 22, 2026
8cfcbd3
fix(process): SIGKILL the whole tree on escalation, not just wait_pro…
teknium1 Jun 22, 2026
5bf23ff
fix(banner): don't advertise toolsets/skills the agent wasn't given (…
teknium1 Jun 22, 2026
7130d60
feat(providers): remove google-gemini-cli + google-antigravity OAuth …
teknium1 Jun 22, 2026
0768ed3
docs(agents): fix stale platform adapter path in token-lock note
TutkuEroglu Jun 21, 2026
4c1934d
docs: repoint remaining stale gateway/platforms adapter refs to plugi…
teknium1 Jun 22, 2026
b0a2598
fix(terminal): make hermes install dir reachable in subshell PATH (#5…
teknium1 Jun 22, 2026
95d53c3
feat(cli): /reasoning full — show complete thinking, not 10-line clam…
teknium1 Jun 22, 2026
9e96e70
feat(cli): /prompt — compose your next prompt in $EDITOR (#50509)
teknium1 Jun 22, 2026
e448b21
feat(dashboard): interactive auth setup on no-provider non-loopback b…
teknium1 Jun 22, 2026
1ad961f
Merge branch '_c_50064' into z50064
arminanton Jun 22, 2026
415cbb1
test(copilot): restore dropped @patch decorator on canonical-header test
arminanton Jun 22, 2026
ce4162b
fix(copilot): keep hermes_cli/inventory.py (out-of-scope deletion bro…
arminanton Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,7 @@ automatically scope to the active profile.
a unique credential (bot token, API key), call `acquire_scoped_lock()` from
`gateway.status` in the `connect()`/`start()` method and `release_scoped_lock()` in
`disconnect()`/`stop()`. This prevents two profiles from using the same credential.
See `gateway/platforms/telegram.py` for the canonical pattern.
See `plugins/platforms/irc/adapter.py` for the canonical pattern.

6. **Profile operations are HOME-anchored, not HERMES_HOME-anchored** — `_get_profiles_root()`
returns `Path.home() / ".hermes" / "profiles"`, NOT `get_hermes_home() / "profiles"`.
Expand Down
4 changes: 4 additions & 0 deletions acp_adapter/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,10 @@ def _make_agent(

_register_task_cwd(session_id, cwd)
agent = AIAgent(**kwargs)
# Codex app-server sessions are spawned lazily on the first turn. Stamp
# the ACP workspace onto the agent so the Codex runtime starts from the
# editor/session cwd instead of the Hermes daemon's process cwd.
agent.session_cwd = cwd
# ACP stdio transport requires stdout to remain protocol-only JSON-RPC.
# Route any incidental human-readable agent output to stderr instead.
agent._print_fn = _acp_stderr_print
Expand Down
10 changes: 10 additions & 0 deletions agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,8 @@ def init_agent(
# _custom_headers; older/mocked clients may expose
# _default_headers instead.
_routed_headers = getattr(_routed_client, "_custom_headers", None)
if not _routed_headers:
_routed_headers = getattr(_routed_client, "default_headers", None)
if not _routed_headers:
_routed_headers = getattr(_routed_client, "_default_headers", None)
if _routed_headers:
Expand Down Expand Up @@ -861,6 +863,8 @@ def init_agent(
if _provider_timeout is not None:
client_kwargs["timeout"] = _provider_timeout
_fb_headers = getattr(_fb_client, "_custom_headers", None)
if not _fb_headers:
_fb_headers = getattr(_fb_client, "default_headers", None)
if not _fb_headers:
_fb_headers = getattr(_fb_client, "_default_headers", None)
if _fb_headers:
Expand Down Expand Up @@ -1096,6 +1100,12 @@ def init_agent(
agent._parent_session_id = parent_session_id
agent._last_flushed_db_idx = 0 # tracks DB-write cursor to prevent duplicate writes
agent._session_db_created = False # DB row deferred to run_conversation()
# Most agents own their session row and should finalize it on close().
# Some temporary helper agents (manual compression / session-hygiene /
# background-review forks) rotate or share the session forward to a
# continuation row that must remain open after the helper is torn down;
# those callers explicitly set this flag to False.
agent._end_session_on_close = True
agent._session_init_model_config = {
"max_iterations": agent.max_iterations,
"reasoning_config": reasoning_config,
Expand Down
120 changes: 69 additions & 51 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1378,22 +1378,6 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
agent._client_log_context(),
)
return client
if agent.provider == "google-gemini-cli" or str(client_kwargs.get("base_url", "")).startswith("cloudcode-pa://"):
from agent.gemini_cloudcode_adapter import GeminiCloudCodeClient

# Strip OpenAI-specific kwargs the Gemini client doesn't accept
safe_kwargs = {
k: v for k, v in client_kwargs.items()
if k in {"api_key", "base_url", "default_headers", "project_id", "timeout"}
}
client = GeminiCloudCodeClient(**safe_kwargs)
_ra().logger.info(
"Gemini Cloud Code Assist client created (%s, shared=%s) %s",
reason,
shared,
agent._client_log_context(),
)
return client
if agent.provider == "gemini":
from agent.gemini_native_adapter import GeminiNativeClient, is_native_gemini_base_url

Expand Down Expand Up @@ -2187,25 +2171,36 @@ def copy_reasoning_content_for_api(agent, source_msg: dict, api_msg: dict) -> No
if source_msg.get("role") != "assistant":
return

# 1. Explicit reasoning_content already set — preserve it verbatim
# (includes DeepSeek/Kimi's own space-placeholder written at creation
# time, and any valid reasoning content from the same provider).
needs_thinking_pad = agent._needs_thinking_reasoning_pad()

# 1. Explicit reasoning_content already set.
#
# When the active provider enforces the thinking-mode echo-back
# (DeepSeek / Kimi / MiMo), preserve it verbatim — that includes their
# own space-placeholder written at creation time and any valid reasoning
# from the same provider. Sessions persisted BEFORE #17341 have
# empty-string placeholders pinned at creation time; DeepSeek V4 Pro
# rejects those with HTTP 400, so upgrade "" → " " on replay.
#
# Exception: sessions persisted BEFORE #17341 have empty-string
# placeholders pinned at creation time. DeepSeek V4 Pro rejects
# those with HTTP 400. When the active provider enforces the
# thinking-mode echo, upgrade "" → " " on replay so stale history
# doesn't 400 the user on the next turn.
# When the active provider does NOT enforce echo-back, strip the field
# entirely. Strict OpenAI-compatible providers (Mistral, Cerebras, Groq,
# SambaNova, …) reject ANY reasoning_content key in input messages with
# HTTP 400/422 ("Extra inputs are not permitted"), even an empty string
# or a single-space pad. This is the cross-provider fallback case: a
# reasoning primary (DeepSeek/Kimi/MiMo) pads history with " ", then a
# fallback to a strict provider replays that pad and 422s. Stripping
# here covers the rebuild path; reapply_reasoning_echo_for_provider()
# covers the already-built api_messages path. Refs #45655.
existing = source_msg.get("reasoning_content")
if isinstance(existing, str):
if existing == "" and agent._needs_thinking_reasoning_pad():
if not needs_thinking_pad:
api_msg.pop("reasoning_content", None)
elif existing == "":
api_msg["reasoning_content"] = " "
else:
api_msg["reasoning_content"] = existing
return

needs_thinking_pad = agent._needs_thinking_reasoning_pad()

# 2. Cross-provider poisoned history (#15748): on DeepSeek/Kimi,
# if the source turn has tool_calls AND a 'reasoning' field but no
# 'reasoning_content' key, the 'reasoning' text was written by a
Expand All @@ -2231,9 +2226,13 @@ def copy_reasoning_content_for_api(agent, source_msg: dict, api_msg: dict) -> No
# for providers that use the internal 'reasoning' key.
# This must happen before the unconditional empty-string fallback so
# genuine reasoning content is not overwritten (#15812 regression in
# PR #15478).
# PR #15478). Only promote for providers that enforce echo-back —
# strict providers reject the field (refs #45655).
if isinstance(normalized_reasoning, str) and normalized_reasoning:
api_msg["reasoning_content"] = normalized_reasoning
if needs_thinking_pad:
api_msg["reasoning_content"] = normalized_reasoning
else:
api_msg.pop("reasoning_content", None)
return

# 4. DeepSeek / Kimi thinking mode: all assistant messages need
Expand All @@ -2254,34 +2253,53 @@ def copy_reasoning_content_for_api(agent, source_msg: dict, api_msg: dict) -> No


def reapply_reasoning_echo_for_provider(agent, api_messages: list) -> int:
"""Re-pad assistant turns with reasoning_content for the active provider.
"""Re-pad (or strip) assistant turns' reasoning_content for the active provider.

``api_messages`` is built once, before the retry loop, while the *primary*
provider is active. If a mid-conversation fallback then switches to a
require-side provider (DeepSeek / Kimi / MiMo thinking mode), assistant
turns that were built when the prior provider did NOT need the echo-back go
out without ``reasoning_content`` and the new provider rejects them with
HTTP 400 ("The reasoning_content in the thinking mode must be passed back").

Calling this immediately before building the request kwargs re-applies the
pad against the *current* provider. It is idempotent and a no-op unless
``_needs_thinking_reasoning_pad()`` is True for the active provider, so it
is safe to call every iteration and covers every fallback path.

Returns the number of assistant turns that gained reasoning_content.
provider is active. A mid-conversation fallback can then switch providers,
so the reasoning fields baked into ``api_messages`` are shaped for the
*prior* provider and must be reconciled against the *current* one:

* Switching TO a require-side provider (DeepSeek / Kimi / MiMo thinking
mode): assistant turns built when the prior provider did NOT need the
echo-back go out without ``reasoning_content`` and the new provider
rejects them with HTTP 400 ("The reasoning_content in the thinking mode
must be passed back"). Re-apply the pad.

* Switching TO a strict provider that rejects the field (Mistral,
Cerebras, Groq, SambaNova, …): assistant turns built under a reasoning
primary carry a ``reasoning_content`` pad (often a single space ``" "``),
and the strict provider rejects it with HTTP 400/422 ("Extra inputs are
not permitted"). Strip the field. This is the exact cross-provider
fallback bug from #45655 — a DeepSeek primary pads history with ``" "``,
the request falls back to Mistral, and Mistral 422s on the stale pad.

Calling this immediately before building the request kwargs reconciles the
fields against the *current* provider. It is idempotent and safe to call
every iteration; it covers every fallback path.

Returns the number of assistant turns whose reasoning_content was added or
removed.
"""
if not agent._needs_thinking_reasoning_pad():
return 0
padded = 0
needs_pad = agent._needs_thinking_reasoning_pad()
changed = 0
for api_msg in api_messages:
if api_msg.get("role") != "assistant":
continue
if api_msg.get("reasoning_content"):
continue
copy_reasoning_content_for_api(agent, api_msg, api_msg)
if api_msg.get("reasoning_content"):
padded += 1
return padded
if needs_pad:
if api_msg.get("reasoning_content"):
continue
copy_reasoning_content_for_api(agent, api_msg, api_msg)
if api_msg.get("reasoning_content"):
changed += 1
else:
# Strict provider — strip any stale reasoning_content pad left
# over from a reasoning primary so the fallback request doesn't
# 400/422 on it.
if "reasoning_content" in api_msg:
api_msg.pop("reasoning_content", None)
changed += 1
return changed


def _iter_pool_sockets(client: Any):
Expand Down
127 changes: 125 additions & 2 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,16 @@ def _supports_fast_mode(model: str) -> bool:
_COMMON_BETAS = [
"interleaved-thinking-2025-05-14",
"fine-grained-tool-streaming-2025-05-14",
# Added 2026-06-04 from Worker-A RE of github.copilot-chat 0.52.2026060402.
# The VS Code Copilot Chat extension historically sent these on every
# /v1/messages call. Hermes sends them under its single Copilot CLI identity
# (`Copilot-Integration-Id: copilot-developer-cli`).
# `advanced-tool-use-2025-11-20` enables the newer tool-call envelopes
# that Claude 4.7+ emits; `context-management-2025-06-27` enables the
# `context_management: [{type: "clear_tool_results_20250919", ...}]`
# body field that opus-4.6+ requires for clean 1M-context rolls.
"advanced-tool-use-2025-11-20",
"context-management-2025-06-27",
]
# MiniMax's Anthropic-compatible endpoints fail tool-use requests when
# the fine-grained tool streaming beta is present. Omit it so tool calls
Expand Down Expand Up @@ -551,7 +561,38 @@ def _base_url_needs_context_1m_beta(base_url: str | None) -> bool:
normalized = _normalize_base_url_text(base_url).lower()
if not normalized:
return False
return "azure.com" in normalized
return "azure.com" in normalized or "githubcopilot.com" in normalized


def _is_copilot_base_url(base_url: Optional[str]) -> bool:
if not base_url:
return False
try:
from urllib.parse import urlparse
host = (urlparse(base_url).hostname or "").lower()
except Exception:
return False
return host == "api.githubcopilot.com"


def _request_messages_have_image_parts(messages: Any) -> bool:
"""Return True when any message carries image content.

Matches both OpenAI-style parts (``image_url`` / ``input_image``): the
shape ``build_anthropic_kwargs`` receives before ``convert_messages_to_anthropic``
runs, and already-converted Anthropic blocks (``{"type": "image"}``), so the
check is robust regardless of where it is called in the pipeline.
"""
def _contains_image(value: Any) -> bool:
if isinstance(value, dict):
if value.get("type") in {"image_url", "input_image", "image"}:
return True
return any(_contains_image(v) for v in value.values())
if isinstance(value, list):
return any(_contains_image(v) for v in value)
return False

return isinstance(messages, list) and any(_contains_image(m) for m in messages)


def _is_minimax_anthropic_endpoint(base_url: str | None) -> bool:
Expand Down Expand Up @@ -616,8 +657,41 @@ def _common_betas_for_base_url(
betas = list(_COMMON_BETAS)
if _base_url_needs_context_1m_beta(base_url) and not drop_context_1m_beta:
betas.append(_CONTEXT_1M_BETA)

if _is_copilot_base_url(base_url):
# 2026-06-04 (Worker-G RE of github.copilot-chat 0.52.2026060402):
# The official VS Code Copilot Chat extension only ever sends THREE
# Anthropic betas on the /v1/messages path:
# interleaved-thinking-2025-05-14
# context-management-2025-06-27
# advanced-tool-use-2025-11-20
# The historical "Master Probe triplet" of
# cli-internal-2026-02-09 + context-1m-2025-08-07 + task-budgets-2026-03-13
# does NOT exist anywhere in the extension bundle (grep on the 32MB
# beautified JS returned 0 hits for each string). Sending those
# non-existent betas was identified by Worker G as a likely
# contributor to historical "Context length exceeded → snap-back to
# 168k" loops on accounts not pre-advertising context-1m. 1M context
# is unlocked by the `Copilot-Integration-Id` we send
# (`copilot-developer-cli`, the official CLI's id) + the server-side
# account entitlement, never by any beta.
#
# All three real betas already live in _COMMON_BETAS, so this block
# is intentionally a no-op now \u2014 kept as a docstring marker so
# future readers don't reintroduce the fictional triplet.
pass

if _is_minimax_anthropic_endpoint(base_url):
_stripped = {_TOOL_STREAMING_BETA, _CONTEXT_1M_BETA}
# MiniMax's Anthropic-compatible endpoint rejects most vendor-specific
# betas. Strip everything except interleaved-thinking which it does
# honor. The two 2026-06-04 additions (advanced-tool-use,
# context-management) are also unrecognized by MiniMax.
_stripped = {
_TOOL_STREAMING_BETA,
_CONTEXT_1M_BETA,
"advanced-tool-use-2025-11-20",
"context-management-2025-06-27",
}
return [b for b in betas if b not in _stripped]
if drop_context_1m_beta:
return [b for b in betas if b != _CONTEXT_1M_BETA]
Expand Down Expand Up @@ -782,6 +856,45 @@ def build_anthropic_client(
"User-Agent": "claude-code/0.1.0",
**( {"anthropic-beta": ",".join(common_betas)} if common_betas else {} )
}
elif _is_copilot_base_url(normalized_base_url):
# GitHub Copilot's Anthropic deployment (POST /v1/messages) is the ONLY
# Copilot endpoint that serves Claude at the real 1,000,000-token input
# window; /chat/completions clamps it and returns a misleading
# "exceeds the limit of 168000" error (see probe/FINDINGS.md §2). It
# requires:
# 1. Authorization: Bearer *** (NOT Anthropic x-api-key)
# 2. The full VS Code Copilot identity header set
# (Editor-Version, User-Agent: rest-book, Copilot-Integration-Id:
# vscode-chat, X-GitHub-Api-Version, x-initiator) PLUS the
# X-Copilot-Agent-Slug: copilot-1m-context unlock.
# 3. anthropic-beta: cli-internal + context-1m + task-budgets
# (already computed in common_betas for this base_url).
# This is the exact header set proved working in
# probe/live_transport.py that unlocked opus-4.8 → 992,497 input
# tokens on /v1/messages. Checked BEFORE _requires_bearer_auth and the
# x-api-key fallthrough so the Copilot token is sent as Bearer, not as
# an Anthropic API key.
kwargs["auth_token"] = api_key
_copilot_headers: dict[str, str] = {}
try:
from hermes_cli.copilot_auth import copilot_request_headers
# Single Copilot CLI identity (copilot-developer-cli + CLI User-Agent),
# the same builder used on the inference path, so /v1/messages and
# /chat/completions present one consistent identity.
_copilot_headers = copilot_request_headers(
is_agent_turn=True, model="claude"
)
except Exception:
# Never block client construction on header enrichment; the
# bearer token alone still authenticates, just without the
# 1M unlock slug.
_copilot_headers = {}
# The proven anthropic-beta triplet (computed in common_betas) is
# authoritative; apply it last so it always wins over any value the
# identity header set may carry.
if common_betas:
_copilot_headers["anthropic-beta"] = ",".join(common_betas)
kwargs["default_headers"] = _copilot_headers
elif _requires_bearer_auth(normalized_base_url):
# Some Anthropic-compatible providers (e.g. MiniMax) expect the API key in
# Authorization: Bearer *** for regular API keys. Route those endpoints
Expand Down Expand Up @@ -2494,6 +2607,16 @@ def _to_oauth_wire_name(name: str) -> str:
betas.append(_FAST_MODE_BETA)
kwargs["extra_headers"] = {"anthropic-beta": ",".join(betas)}

# Copilot's /v1/messages proxy only processes image input when the
# Copilot-Vision-Request header is set. The persistent Anthropic client is
# built with is_vision=False (no such header), so image-bearing turns to
# Copilot otherwise return an empty content block (HTTP 200) that the loop
# fails as "invalid response". Add it per-request only when an image is
# present, gated to the Copilot endpoint so it never leaks to
# api.anthropic.com or Bedrock-hosted Anthropic.
if _is_copilot_base_url(base_url) and _request_messages_have_image_parts(messages):
kwargs.setdefault("extra_headers", {})["Copilot-Vision-Request"] = "true"

return kwargs


Expand Down
Loading