Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,14 @@ def _supports_adaptive_thinking(model: str) -> bool:
# Beta headers for enhanced features (sent with ALL auth types)
_COMMON_BETAS = [
"interleaved-thinking-2025-05-14",
"fine-grained-tool-streaming-2025-05-14",
]

# Additional beta headers required for OAuth/subscription auth.
# Matches what Claude Code (and pi-ai / OpenCode) send.
_OAUTH_ONLY_BETAS = [
"claude-code-20250219",
"oauth-2025-04-20",
"context-1m-2025-08-07",
]

# Claude Code identity — required for OAuth requests to be routed correctly.
Expand Down Expand Up @@ -231,6 +231,7 @@ def build_anthropic_client(api_key: str, base_url: str = None):
# not use Anthropic's sk-ant-api prefix and would otherwise be misread as
# Anthropic OAuth/setup tokens.
kwargs["auth_token"] = api_key
kwargs["api_key"] = None # Prevent SDK from reading ANTHROPIC_API_KEY env var
if _COMMON_BETAS:
kwargs["default_headers"] = {"anthropic-beta": ",".join(_COMMON_BETAS)}
elif _is_third_party_anthropic_endpoint(base_url):
Expand All @@ -247,9 +248,15 @@ def build_anthropic_client(api_key: str, base_url: str = None):
# without Claude Code's fingerprint, requests get intermittent 500s.
all_betas = _COMMON_BETAS + _OAUTH_ONLY_BETAS
kwargs["auth_token"] = api_key
# Explicitly set api_key=None to prevent the SDK from reading
# ANTHROPIC_API_KEY from the environment. When both auth_token and
# api_key are set, the SDK sends both X-Api-Key and Authorization
# headers — the empty X-Api-Key from .env overrides the valid Bearer
# token, causing Anthropic to reject the request as unauthenticated.
kwargs["api_key"] = None
kwargs["default_headers"] = {
"anthropic-beta": ",".join(all_betas),
"user-agent": f"claude-cli/{_get_claude_code_version()} (external, cli)",
"User-Agent": f"claude-cli/{_get_claude_code_version()} (external, cli)",
"x-app": "cli",
}
else:
Expand All @@ -258,7 +265,14 @@ def build_anthropic_client(api_key: str, base_url: str = None):
if _COMMON_BETAS:
kwargs["default_headers"] = {"anthropic-beta": ",".join(_COMMON_BETAS)}

return _anthropic_sdk.Anthropic(**kwargs)
client = _anthropic_sdk.Anthropic(**kwargs)
# When using Bearer auth (auth_token), ensure api_key is None so the SDK
# does not also send an X-Api-Key header. The SDK's constructor reads
# ANTHROPIC_API_KEY from the environment when api_key is not passed,
# which can produce an empty-string api_key that overrides valid OAuth.
if kwargs.get("auth_token") and not kwargs.get("api_key"):
client.api_key = None
return client


def read_claude_code_credentials() -> Optional[Dict[str, Any]]:
Expand Down
1 change: 1 addition & 0 deletions agent/error_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def is_transient(self) -> bool:
"exceeded your current quota",
"account is deactivated",
"plan does not include",
"out of extra usage",
]

# Patterns that indicate rate limiting (transient, will resolve)
Expand Down
12 changes: 6 additions & 6 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,23 +150,23 @@ def _strip_yaml_frontmatter(content: str) -> str:
"that prevents the user from having to correct or remind you again. "
"User preferences and recurring corrections matter more than procedural task details.\n"
"Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO "
"state to memory; use session_search to recall those from past transcripts. "
"state to memory; use the search_sessions tool to recall those from past transcripts. "
"If you've discovered a new way to do something, solved a problem that could be "
"necessary later, save it as a skill with the skill tool."
)

SESSION_SEARCH_GUIDANCE = (
"When the user references something from a past conversation or you suspect "
"relevant cross-session context exists, use session_search to recall it before "
"asking them to repeat themselves."
"relevant cross-session context exists, use the search_sessions tool to recall it "
"before asking them to repeat themselves."
)

SKILLS_GUIDANCE = (
"After completing a complex task (5+ tool calls), fixing a tricky error, "
"or discovering a non-trivial workflow, save the approach as a "
"skill with skill_manage so you can reuse it next time.\n"
"skill with the manage_skills tool so you can reuse it next time.\n"
"When using a skill and finding it outdated, incomplete, or wrong, "
"patch it immediately with skill_manage(action='patch') — don't wait to be asked. "
"patch it immediately with manage_skills(action='patch') — don't wait to be asked. "
"Skills that aren't maintained become liabilities."
)

Expand Down Expand Up @@ -731,7 +731,7 @@ def build_skills_system_prompt(
"## Skills (mandatory)\n"
"Before replying, scan the skills below. If one clearly matches your task, "
"load it with skill_view(name) and follow its instructions. "
"If a skill has issues, fix it with skill_manage(action='patch').\n"
"If a skill has issues, fix it with manage_skills(action='patch').\n"
"After difficult/iterative tasks, offer to save as a skill. "
"If a skill you loaded was missing steps, had wrong commands, or needed "
"pitfalls you discovered, update it before finishing.\n"
Expand Down
8 changes: 4 additions & 4 deletions tests/agent/test_anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ def test_setup_token_uses_auth_token(self):
assert "oauth-2025-04-20" in betas
assert "claude-code-20250219" in betas
assert "interleaved-thinking-2025-05-14" in betas
assert "fine-grained-tool-streaming-2025-05-14" in betas
assert "api_key" not in kwargs
assert "context-1m-2025-08-07" in betas
assert kwargs.get("api_key") is None

def test_api_key_uses_api_key(self):
with patch("agent.anthropic_adapter._anthropic_sdk") as mock_sdk:
Expand Down Expand Up @@ -90,9 +90,9 @@ def test_minimax_anthropic_endpoint_uses_bearer_auth_for_regular_api_keys(self):
)
kwargs = mock_sdk.Anthropic.call_args[1]
assert kwargs["auth_token"] == "minimax-secret-123"
assert "api_key" not in kwargs
assert kwargs.get("api_key") is None
assert kwargs["default_headers"] == {
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
"anthropic-beta": "interleaved-thinking-2025-05-14"
}


Expand Down