Skip to content
Closed
6 changes: 6 additions & 0 deletions agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ def init_agent(
checkpoint_max_total_size_mb: int = 500,
checkpoint_max_file_size_mb: int = 10,
pass_session_id: bool = False,
fast_auto_on_seconds: float = 60.0,
):
"""
Initialize the AI Agent.
Expand Down Expand Up @@ -629,6 +630,11 @@ def init_agent(
agent.max_tokens = max_tokens # None = use model default
agent.reasoning_config = reasoning_config # None = use default (medium for OpenRouter)
agent.service_tier = service_tier
from agent.fast_mode import normalize_fast_auto_on_seconds
agent.fast_auto_on_seconds = normalize_fast_auto_on_seconds(fast_auto_on_seconds)
agent._fast_mode_turn_started_at = None
agent._fast_mode_turn_eligible = False
agent._fast_mode_turn_mode = None
agent.request_overrides = dict(request_overrides or {})
agent.prefill_messages = prefill_messages or [] # Prefilled conversation turns
agent._force_ascii_payload = False
Expand Down
89 changes: 71 additions & 18 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,69 @@ def _supports_fast_mode(model: str) -> bool:
# See https://platform.claude.com/docs/en/build-with-claude/fast-mode
_FAST_MODE_BETA = "fast-mode-2026-02-01"


def _apply_fast_mode_to_kwargs(
kwargs: Dict[str, Any],
*,
enabled: bool,
model: str,
base_url: str | None,
is_oauth: bool,
drop_context_1m_beta: bool = False,
) -> Dict[str, Any]:
"""Return kwargs with Anthropic fast-mode metadata applied or removed.

This is safe to call again immediately before dispatch. It preserves
unrelated ``extra_body`` fields and headers while ensuring an expired
dynamic fast window cannot retain either ``speed=fast`` or the matching
beta header assembled earlier in the request pipeline.
"""
kwargs = dict(kwargs)
extra_body = dict(kwargs.get("extra_body") or {})
extra_body.pop("speed", None)
if extra_body:
kwargs["extra_body"] = extra_body
else:
kwargs.pop("extra_body", None)

extra_headers = dict(kwargs.get("extra_headers") or {})
existing_betas = [
beta.strip()
for beta in str(extra_headers.get("anthropic-beta") or "").split(",")
if beta.strip() and beta.strip() != _FAST_MODE_BETA
]
if existing_betas:
extra_headers["anthropic-beta"] = ",".join(existing_betas)
else:
extra_headers.pop("anthropic-beta", None)
if extra_headers:
kwargs["extra_headers"] = extra_headers
else:
kwargs.pop("extra_headers", None)

if not (
enabled
and not _is_third_party_anthropic_endpoint(base_url)
and _supports_fast_mode(model)
):
return kwargs

kwargs.setdefault("extra_body", {})["speed"] = "fast"
betas = [
*existing_betas,
*_common_betas_for_base_url(
base_url,
drop_context_1m_beta=drop_context_1m_beta,
),
]
if is_oauth:
betas.extend(_OAUTH_ONLY_BETAS)
betas.append(_FAST_MODE_BETA)
kwargs.setdefault("extra_headers", {})["anthropic-beta"] = ",".join(
dict.fromkeys(betas)
)
return kwargs

# Additional beta headers required for OAuth/subscription auth.
# Matches what Claude Code (and pi-ai / OpenCode) send.
_OAUTH_ONLY_BETAS = [
Expand Down Expand Up @@ -2683,24 +2746,14 @@ def _to_oauth_wire_name(name: str) -> str:
# Opus 4.6 — Opus 4.7 and other models 400 on the speed parameter.
# Only for native Anthropic endpoints — third-party providers would
# reject the unknown beta header and speed parameter.
if (
fast_mode
and not _is_third_party_anthropic_endpoint(base_url)
and _supports_fast_mode(model)
):
kwargs.setdefault("extra_body", {})["speed"] = "fast"
# Build extra_headers with ALL applicable betas (the per-request
# extra_headers override the client-level anthropic-beta header).
betas = list(_common_betas_for_base_url(
base_url,
drop_context_1m_beta=drop_context_1m_beta,
))
if is_oauth:
betas.extend(_OAUTH_ONLY_BETAS)
betas.append(_FAST_MODE_BETA)
kwargs["extra_headers"] = {"anthropic-beta": ",".join(betas)}

return kwargs
return _apply_fast_mode_to_kwargs(
kwargs,
enabled=fast_mode,
model=model,
base_url=base_url,
is_oauth=is_oauth,
drop_context_1m_beta=drop_context_1m_beta,
)


# Keys that belong exclusively to the OpenAI Responses / Codex API shape.
Expand Down
35 changes: 29 additions & 6 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,10 @@ def _call():

def build_api_kwargs(agent, api_messages: list) -> dict:
"""Build the keyword arguments dict for the active API mode."""
from agent.fast_mode import effective_request_overrides

tools_for_api = agent.tools
request_overrides = effective_request_overrides(agent)

if agent.api_mode == "anthropic_messages":
_transport = agent._get_transport()
Expand All @@ -838,7 +841,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
preserve_dots=agent._anthropic_preserve_dots(),
context_length=ctx_len,
base_url=getattr(agent, "_anthropic_base_url", None),
fast_mode=(agent.request_overrides or {}).get("speed") == "fast",
fast_mode=request_overrides.get("speed") == "fast",
drop_context_1m_beta=bool(getattr(agent, "_oauth_1m_beta_disabled", False)),
)

Expand Down Expand Up @@ -913,7 +916,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
session_id=getattr(agent, "session_id", None),
max_tokens=agent.max_tokens,
timeout=agent._resolved_api_call_timeout(),
request_overrides=agent.request_overrides,
request_overrides=request_overrides,
is_github_responses=is_github_responses,
is_codex_backend=is_codex_backend,
is_xai_responses=is_xai_responses,
Expand Down Expand Up @@ -1015,7 +1018,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
ephemeral_max_output_tokens=_ephemeral_out,
max_tokens_param_fn=agent._max_tokens_param,
reasoning_config=agent.reasoning_config,
request_overrides=agent.request_overrides,
request_overrides=request_overrides,
session_id=getattr(agent, "session_id", None),
provider_profile=_profile,
ollama_num_ctx=agent._ollama_num_ctx,
Expand Down Expand Up @@ -1047,7 +1050,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
ephemeral_max_output_tokens=_ephemeral_out,
max_tokens_param_fn=agent._max_tokens_param,
reasoning_config=agent.reasoning_config,
request_overrides=agent.request_overrides,
request_overrides=request_overrides,
session_id=getattr(agent, "session_id", None),
model_lower=(agent.model or "").lower(),
is_openrouter=_is_or,
Expand Down Expand Up @@ -1747,6 +1750,11 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:
messages.append({"role": "user", "content": summary_request})

try:
from agent.fast_mode import (
effective_fast_mode_overrides,
revalidate_fast_mode_request,
)

# Build API messages, stripping internal-only fields
# (finish_reason, reasoning) that strict APIs like Mistral reject with 422
_needs_sanitize = agent._should_sanitize_tool_calls()
Expand Down Expand Up @@ -1836,6 +1844,7 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:
if agent.api_mode == "codex_responses":
codex_kwargs = agent._build_api_kwargs(api_messages)
codex_kwargs.pop("tools", None)
codex_kwargs = revalidate_fast_mode_request(agent, codex_kwargs)
summary_response = agent._run_codex_stream(codex_kwargs)
_ct_sum = agent._get_transport()
_cnr_sum = _ct_sum.normalize_response(summary_response)
Expand Down Expand Up @@ -1905,14 +1914,20 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:

if agent.api_mode == "anthropic_messages":
_tsum = agent._get_transport()
_summary_overrides = effective_fast_mode_overrides(agent)
_ant_kw = _tsum.build_kwargs(model=agent.model, messages=api_messages, tools=None,
max_tokens=agent.max_tokens, reasoning_config=agent.reasoning_config,
is_oauth=agent._is_anthropic_oauth,
preserve_dots=agent._anthropic_preserve_dots())
preserve_dots=agent._anthropic_preserve_dots(),
context_length=getattr(agent.context_compressor, "context_length", None),
base_url=getattr(agent, "_anthropic_base_url", None),
fast_mode=_summary_overrides.get("speed") == "fast",
drop_context_1m_beta=bool(getattr(agent, "_oauth_1m_beta_disabled", False)))
summary_response = agent._anthropic_messages_create(_ant_kw)
_summary_result = _tsum.normalize_response(summary_response, strip_tool_prefix=agent._is_anthropic_oauth)
final_response = (_summary_result.content or "").strip()
else:
summary_kwargs.update(effective_fast_mode_overrides(agent))
summary_response = agent._ensure_primary_openai_client(reason="iteration_limit_summary").chat.completions.create(**summary_kwargs)
_summary_result = agent._get_transport().normalize_response(summary_response)
final_response = (_summary_result.content or "").strip()
Expand All @@ -1929,16 +1944,22 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:
if agent.api_mode == "codex_responses":
codex_kwargs = agent._build_api_kwargs(api_messages)
codex_kwargs.pop("tools", None)
codex_kwargs = revalidate_fast_mode_request(agent, codex_kwargs)
retry_response = agent._run_codex_stream(codex_kwargs)
_ct_retry = agent._get_transport()
_cnr_retry = _ct_retry.normalize_response(retry_response)
final_response = (_cnr_retry.content or "").strip()
elif agent.api_mode == "anthropic_messages":
_tretry = agent._get_transport()
_retry_overrides = effective_fast_mode_overrides(agent)
_ant_kw2 = _tretry.build_kwargs(model=agent.model, messages=api_messages, tools=None,
is_oauth=agent._is_anthropic_oauth,
max_tokens=agent.max_tokens, reasoning_config=agent.reasoning_config,
preserve_dots=agent._anthropic_preserve_dots())
preserve_dots=agent._anthropic_preserve_dots(),
context_length=getattr(agent.context_compressor, "context_length", None),
base_url=getattr(agent, "_anthropic_base_url", None),
fast_mode=_retry_overrides.get("speed") == "fast",
drop_context_1m_beta=bool(getattr(agent, "_oauth_1m_beta_disabled", False)))
retry_response = agent._anthropic_messages_create(_ant_kw2)
_retry_result = _tretry.normalize_response(retry_response, strip_tool_prefix=agent._is_anthropic_oauth)
final_response = (_retry_result.content or "").strip()
Expand All @@ -1956,6 +1977,8 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:
if summary_extra_body:
summary_kwargs["extra_body"] = summary_extra_body

summary_kwargs.update(effective_fast_mode_overrides(agent))

summary_response = agent._ensure_primary_openai_client(reason="iteration_limit_summary_retry").chat.completions.create(**summary_kwargs)
_retry_result = agent._get_transport().normalize_response(summary_response)
final_response = (_retry_result.content or "").strip()
Expand Down
12 changes: 12 additions & 0 deletions agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from agent.conversation_compression import conversation_history_after_compression
from agent.display import KawaiiSpinner
from agent.error_classifier import FailoverReason, classify_api_error
from agent.fast_mode import begin_fast_mode_turn
from agent.iteration_budget import IterationBudget
from agent.turn_context import build_turn_context
from agent.turn_retry_state import TurnRetryState
Expand Down Expand Up @@ -566,6 +567,12 @@ def run_conversation(
Returns:
Dict: Complete conversation result with final response and message history
"""
# Dynamic fast modes are turn-local. Resolve eligibility and start the
# clock at ingress so prompt assembly, hooks, and preflight work count
# toward the same cutoff as the provider calls they precede. Cold-mode
# eligibility comes from the durable prior transcript.
begin_fast_mode_turn(agent, conversation_history)

if moa_config is None:
try:
from hermes_cli.moa_config import decode_moa_turn
Expand Down Expand Up @@ -1349,6 +1356,11 @@ def _stop_spinner():
_use_streaming = False

def _perform_api_call(next_api_kwargs):
from agent.fast_mode import revalidate_fast_mode_request

next_api_kwargs = revalidate_fast_mode_request(
agent, next_api_kwargs
)
if agent.api_mode == "codex_responses":
next_api_kwargs = agent._get_transport().preflight_kwargs(
next_api_kwargs,
Expand Down
Loading