Skip to content

Commit 485e217

Browse files
committed
fixup! feat(anthropic): add OAuth support and handle streaming nulls
1 parent 5b48d77 commit 485e217

8 files changed

Lines changed: 1124 additions & 17 deletions

File tree

.fork/features/anthropic.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,170 @@ Verification:
88
- uv run ruff check src/rotator_library/providers/anthropic_provider.py --select F401 — passed
99

1010
Notes: Removed unused imports (asyncio, re, Path, UsageManager and TYPE_CHECKING).
11+
12+
## 2026-07-01 — Add newer Claude models to OAuth whitelist and max output tokens
13+
14+
Target: `feat(anthropic): add OAuth support and handle streaming nulls`
15+
Files:
16+
- `src/rotator_library/providers/anthropic_provider.py`
17+
18+
Changes:
19+
- Added `claude-fable-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-sonnet-4-6`
20+
to `OAUTH_MODELS` — these are current active Anthropic models available via
21+
Claude Pro/Max OAuth subscription.
22+
- Added corresponding entries to `_MODEL_MAX_OUTPUT_TOKENS`:
23+
- `claude-fable-5`: 128,000
24+
- `claude-opus-4-8`: 128,000
25+
- `claude-opus-4-7`: 128,000
26+
- `claude-sonnet-4-6`: 64,000
27+
- `claude-mythos-5` intentionally excluded (restricted to Project Glasswing participants).
28+
29+
Model IDs sourced from Anthropic's official skills catalog
30+
(anthropics/skills/skills/claude-api/shared/models.md).
31+
32+
Verification:
33+
- `uv run python3 -m py_compile src/rotator_library/providers/anthropic_provider.py` — passed
34+
- `uv run ruff check src/rotator_library/providers/anthropic_provider.py --select F401,F811,F821,E9` — passed
35+
36+
Notes:
37+
- Existing models (opus-4-6, opus-4-5, sonnet-4-5, haiku-4-5) remain in the list.
38+
- The `model_quota_groups` (5h-limit, weekly-limit, anthropic-global) automatically
39+
include the new models since they use `list(OAUTH_MODELS)`.
40+
- The max output token prefix-matching loop uses `startswith()` with `break` on
41+
first match. No prefix collisions exist between the new entries and existing ones.
42+
- Ref: b3nw/LLM-API-Key-Proxy#97
43+
44+
## 2026-07-01 — Dynamic model discovery via models.dev
45+
46+
Target: `feat(anthropic): add OAuth support and handle streaming nulls`
47+
Files:
48+
- `src/rotator_library/providers/anthropic_provider.py`
49+
- `tests/test_anthropic_models_dev.py`
50+
- `.gitignore`
51+
52+
Changes:
53+
- Added `_fetch_anthropic_models_from_models_dev()` — fetches the Anthropic model
54+
catalog from `https://models.dev/api.json` (community-maintained, no auth required).
55+
Filters out retired 3.x models and restricted mythos models. Only includes models
56+
with `tool_call: true` (required by Claude Code).
57+
- Added `_get_dynamic_models()` — module-level cache with 1-hour TTL and 3-tier
58+
fallback: fresh cache → fetch → stale cache → None (caller falls back to hardcoded
59+
`OAUTH_MODELS`). Pattern follows the Codex provider's GitHub JSON catalog fetch.
60+
- Modified `get_models()` to use dynamic list, falling back to `OAUTH_MODELS`.
61+
- Modified max output tokens lookup in `handle_oauth_completion()` to check dynamic
62+
data first (exact match), then fall back to hardcoded `_MODEL_MAX_OUTPUT_TOKENS`.
63+
- Moved `model_quota_groups` from class attribute to `__init__`, populated from
64+
dynamic model list. Override `get_model_quota_group()` to always return
65+
`"anthropic-global"` for any Anthropic model (matches Codex pattern).
66+
- Added 11 tests: fetch parsing, filtering (3x, mythos, no-tool-call), network/JSON
67+
errors, cache behavior, stale fallback, quota group override.
68+
69+
Rationale:
70+
- OAuth tokens (`sk-ant-oat-*`) cannot call Anthropic's `GET /v1/models` endpoint.
71+
models.dev provides the same data (model IDs, context windows, max output tokens)
72+
without auth. This is the approach used by pi (earendil-works/pi) and opencode
73+
(sst/opencode).
74+
- Builds on PR #99's hardcoded fallback list. Dynamic discovery augments the
75+
fallback — when models.dev is reachable, new models appear automatically.
76+
77+
Verification:
78+
- `uv run python3 -m py_compile src/rotator_library/providers/anthropic_provider.py` — passed
79+
- `uv run ruff check src/rotator_library/providers/anthropic_provider.py --select F401,F811,F821,E9` — passed
80+
- `pytest tests/test_anthropic_models_dev.py tests/test_model_alias.py -v` — 23 passed
81+
82+
Notes:
83+
- `MODELS_DEV_URL` env var allows overriding the catalog URL (e.g., for testing or
84+
self-hosting). `ANTHROPIC_MODELS_CACHE_TTL` controls the cache TTL (default 3600s).
85+
- models.dev includes `claude-sonnet-5` which was NOT in PR #99's hardcoded list —
86+
this demonstrates the value of dynamic discovery.
87+
- Ref: b3nw/LLM-API-Key-Proxy#97
88+
89+
## 2026-07-01 — Mirror pi-agent OAuth headers and tool naming
90+
91+
Target: `feat(anthropic): add OAuth support and handle streaming nulls`
92+
Files:
93+
- `src/rotator_library/providers/anthropic_provider.py`
94+
- `tests/test_anthropic_oauth_headers.py`
95+
- `.gitignore`
96+
97+
Changes:
98+
- Added `_compute_beta_header(model)` — dynamically computes the `anthropic-beta`
99+
header based on the model. Base betas now include `claude-code-20250219`
100+
(critical: tells Anthropic this is a Claude Code session), `prompt-caching-scope-2026-01-05`,
101+
and `context-management-2025-06-27`. Long-context models (opus-4-6+, sonnet-4-6+,
102+
fable-5, sonnet-5) get `context-1m-2025-08-07` and `effort-2025-11-24`. Haiku
103+
models exclude `interleaved-thinking-2025-05-14`.
104+
- Added `x-app: cli` header to OAuth request headers.
105+
- Added `_prefix_tool_name()` helper — capitalizes first letter before prefixing
106+
(e.g., `read``mcp_Read` instead of `mcp_read`). Mirrors Claude Code's
107+
PascalCase tool naming convention.
108+
- Kept `ANTHROPIC_BETA_HEADER` constant for backward compatibility (token refresh
109+
requests that don't have a model context).
110+
- 11 new tests covering beta computation (base, long-context, haiku exclusion)
111+
and tool name prefixing (lowercase, capitalized, empty, single char).
112+
113+
Rationale:
114+
- Research into pi-agent (earendil-works/pi) and @cgaravitoq/pi-claude-code-auth
115+
revealed that the proxy was missing critical protocol signals:
116+
- `claude-code-20250219` beta (identifies as Claude Code session)
117+
- `x-app: cli` header (present in both pi implementations)
118+
- PascalCase tool names (Anthropic expects `mcp_Read`, not `mcp_read`)
119+
- Skipped for now: billing header (cch), Claude Code identity system prompt
120+
injection, system prompt relocation — these are protocol emulation, not
121+
safe header additions.
122+
123+
Verification:
124+
- `uv run python3 -m py_compile` — passed
125+
- `uv run ruff check --select F401,F811,F821,E9` — passed
126+
- `pytest tests/test_anthropic_oauth_headers.py tests/test_anthropic_models_dev.py tests/test_anthropic_translator.py -v` — 63 passed
127+
128+
Notes:
129+
- `_strip_tool_prefix()` not modified — may need case-insensitive matching
130+
in a follow-up if tool result routing breaks.
131+
- The `ANTHROPIC_BETA_HEADER` constant is kept for token refresh requests
132+
that don't have model context. It uses the base betas only.
133+
- Ref: b3nw/LLM-API-Key-Proxy#97
134+
135+
## 2026-07-01 — Full Claude Code protocol emulation (billing header + identity)
136+
137+
Target: `feat(anthropic): add OAuth support and handle streaming nulls`
138+
Files:
139+
- `src/rotator_library/providers/anthropic_provider.py`
140+
- `tests/test_anthropic_oauth_headers.py`
141+
142+
Changes:
143+
- Added `_compute_billing_header(messages)` — computes the client attestation
144+
hash (cch) from the first user message text, mirroring @cgaravitoq:
145+
- cch = SHA256(first_user_message_text)[:5]
146+
- suffix = SHA256(salt + chars_at[4,7,20] + version)[:3]
147+
- salt = "59cf53e54c78"
148+
- Added `_build_claude_code_system(messages, original_system_prompt)` — builds
149+
the system prompt array with:
150+
1. Billing header as first system entry
151+
2. Claude Code identity ("You are Claude Code, Anthropic's official CLI for Claude.")
152+
as second system entry
153+
3. Original system prompt relocated to first user message (prevents 400 rejections
154+
from non-Claude Code identity in system[])
155+
- Modified `handle_oauth_completion()` to use `_build_claude_code_system()` for
156+
all OAuth requests, replacing the plain `payload["system"] = system_prompt`.
157+
- Added 11 new tests: billing header format, determinism, hash correctness,
158+
list content extraction, empty messages, assistant message skipping, system
159+
array structure, prompt relocation, no-prompt case, no-user-message case.
160+
- Configurable: `ANTHROPIC_CLI_VERSION` and `CLAUDE_CODE_ENTRYPOINT` env vars.
161+
162+
Rationale:
163+
- Safe headers alone (PR #101 initial commit) produced 429 errors in testing.
164+
- The billing header and identity prompt are required for Anthropic to treat
165+
OAuth requests as genuine Claude Code sessions with standard rate limits.
166+
- Confirmed by pi-agent (earendil-works/pi) and @cgaravitoq/pi-claude-code-auth.
167+
168+
Verification:
169+
- `uv run python3 -m py_compile` — passed
170+
- `uv run ruff check --select F401,F811,F821,E9` — passed
171+
- `pytest tests/test_anthropic_oauth_headers.py tests/test_anthropic_models_dev.py tests/test_anthropic_translator.py tests/test_model_alias.py -v` — 74 passed
172+
173+
Notes:
174+
- The billing header salt (59cf53e54c78) is hardcoded from @cgaravitoq's reverse-
175+
engineered code. If Anthropic changes the algorithm, this will need updating.
176+
- `ANTHROPIC_CLI_VERSION` should be kept in sync with the user agent version.
177+
- Ref: b3nw/LLM-API-Key-Proxy#97

.fork/features/model-routing.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,41 @@ Notes:
2222
- Fix: `return await self._execute_streaming(...)` so the coroutine resolves to the
2323
inner async generator before being returned to the caller.
2424
- Ref: https://github.com/b3nw/LLM-API-Key-Proxy/issues/58
25+
26+
## 2026-07-01 — Add default Claude model aliases for bare-ID routing
27+
28+
Target: `feat(model-routing): MODEL_ALIASES and cross-provider rotation`
29+
Files:
30+
- `src/rotator_library/model_alias_registry.py`
31+
- `tests/test_model_alias.py`
32+
33+
Changes:
34+
- Added `DEFAULT_MODEL_ALIASES` dict mapping bare Claude model IDs to
35+
`anthropic:<model_id>` targets. This enables clients like Claude Code
36+
(which send unprefixed model IDs such as `claude-opus-4-8`) to route
37+
without requiring MODEL_ALIAS_* environment variable configuration.
38+
- Modified `_load_from_env()` to load built-in defaults first, then load
39+
MODEL_ALIAS_* env vars which override defaults for the same canonical name.
40+
- Default aliases for 4-5 family target date-suffixed IDs (matching
41+
`OAUTH_MODELS`): `claude-opus-4-5-20251101`, `claude-sonnet-4-5-20250929`,
42+
`claude-haiku-4-5-20251001`.
43+
- Default aliases for newer models target bare IDs: `claude-fable-5`,
44+
`claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`.
45+
- Added 4 tests in `TestDefaultClaudeAliases` class: defaults loaded without
46+
env vars, correct target model IDs, env var override behavior, and
47+
canonical models listing.
48+
49+
Verification:
50+
- `uv run python3 -m py_compile src/rotator_library/model_alias_registry.py` — passed
51+
- `uv run ruff check src/rotator_library/model_alias_registry.py --select F401,F811,F821,E9` — passed
52+
- `pytest tests/test_model_alias.py -v` — passed (all tests including new ones)
53+
54+
Notes:
55+
- `_register_alias()` replaces (not appends) when the same canonical name is
56+
registered twice, so env vars cleanly override defaults — no duplication.
57+
- Operators who want cross-provider failover (e.g. anthropic + copilot) can
58+
still set `MODEL_ALIAS_CLAUDE_OPUS_4_8="anthropic:...,copilot:..."` to
59+
override the single-provider default.
60+
- `/v1/models` endpoint automatically includes default aliases via
61+
`get_canonical_models()`.
62+
- Ref: b3nw/LLM-API-Key-Proxy#97

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,5 @@ command_code_cookies.json
162162

163163
!tests/test_responses_compat.py
164164
!tests/test_fair_cycle.py
165+
!tests/test_anthropic_models_dev.py
166+
!tests/test_anthropic_oauth_headers.py

src/rotator_library/model_alias_registry.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@
2525
DEFAULT_RETRY_MODE = "round_robin"
2626
VALID_RETRY_MODES = {"round_robin", "exhaust"}
2727

28+
# Built-in default aliases for Claude models.
29+
#
30+
# These provide bare-ID routing out of the box so that clients like Claude Code
31+
# (which send unprefixed model IDs such as ``claude-opus-4-8``) work without
32+
# requiring the operator to set MODEL_ALIAS_* environment variables.
33+
#
34+
# Each value uses the same ``provider:model`` format as env vars.
35+
# Env vars always override these defaults when set.
36+
DEFAULT_MODEL_ALIASES: Dict[str, str] = {
37+
"claude-fable-5": "anthropic:claude-fable-5",
38+
"claude-opus-4-8": "anthropic:claude-opus-4-8",
39+
"claude-opus-4-7": "anthropic:claude-opus-4-7",
40+
"claude-opus-4-6": "anthropic:claude-opus-4-6",
41+
"claude-sonnet-4-6": "anthropic:claude-sonnet-4-6",
42+
"claude-sonnet-5": "anthropic:claude-sonnet-5",
43+
"claude-opus-4-5": "anthropic:claude-opus-4-5-20251101",
44+
"claude-sonnet-4-5": "anthropic:claude-sonnet-4-5-20250929",
45+
"claude-haiku-4-5": "anthropic:claude-haiku-4-5-20251001",
46+
}
47+
2848

2949
@dataclass
3050
class AliasTarget:
@@ -77,7 +97,22 @@ def _register_alias(self, canonical: str, alias: ModelAlias) -> None:
7797
self._lookup[canonical] = canonical
7898

7999
def _load_from_env(self) -> None:
80-
"""Load all MODEL_ALIAS_* environment variables."""
100+
"""Load built-in default aliases, then MODEL_ALIAS_* environment variables.
101+
102+
Env vars override defaults when the same canonical name is set via both.
103+
"""
104+
# Load built-in defaults first (overridden by env vars below)
105+
for canonical, value in DEFAULT_MODEL_ALIASES.items():
106+
try:
107+
alias = self._parse_alias_value(canonical, value)
108+
if alias and alias.targets:
109+
self._register_alias(canonical, alias)
110+
except Exception as e:
111+
lib_logger.warning(
112+
f"Failed to parse default alias '{canonical}': {e}"
113+
)
114+
115+
# Load MODEL_ALIAS_* env vars (override defaults)
81116
for key, value in os.environ.items():
82117
if not key.startswith("MODEL_ALIAS_"):
83118
continue

0 commit comments

Comments
 (0)