feat(providers): add Claude Pro/Max subscription OAuth (PKCE-localhost)
27th registry variant, 2nd subscription-OAuth provider. Log in with a Claude
Pro/Max subscription via PKCE authorization-code + S256 (loopback callback on
127.0.0.1:53692, with a manual code-paste fallback), then use it against
api.anthropic.com — reusing the existing Anthropic Messages wire + Anthropic
listing + the multi-provider OAuth foundation (dbce6bf). Sourced from Pi
(earendil-works/pi auth/oauth/anthropic.ts): client 9d1c250a..., authorize
claude.ai/oauth/authorize, token platform.claude.com/v1/oauth/token, scope
'…user:inference user:sessions:claude_code…'.
New machinery (foundation handles token routing — claude-pro-max is a
uses_oauth platform so its bearer/refresh/api_key already route to its own
pooled manager, never Kimi):
- OAuthConfig gains flow{DeviceCode|PkceLocalhost} + token_host + token_body
{Form|JSON}; xai/kimi rows unchanged (DeviceCode/Form).
- auth/oauth_pkce.rs: PKCE S256 wire — loopback listener with STRICT state
validation (CSRF, fail-closed), manual-paste fallback, JSON code→token
exchange + rotating-refresh. Never logs code/verifier/tokens.
- Messages OAuth adaptation gated on SamplerConfig.anthropic_oauth (true only
for a claude-pro-max managed key): Authorization: Bearer + anthropic-beta
oauth + user-agent claude-cli + x-app cli, and the required 'You are Claude
Code' system prefix. API-key anthropic/minimax Messages requests are
BYTE-IDENTICAL (regression-guarded).
- Live /models under the OAuth Bearer + oauth-beta headers (Anthropic listing,
enriched from models.dev anthropic); persistent 401 → 0 models + WARN, NO
hardcoded fallback list (honest failure).
Adversarial review: no blocking findings (secret handling, CSRF/state, the
anthropic_oauth gate, token routing, non-regression all CONFIRMED). Full gate
green. Registry at 27; picker updated. Residual (unverifiable without a real
Claude Pro/Max account): whether GET /v1/models accepts the OAuth bearer, and
the real endpoint's acceptance of the OAuth Messages request.
This commit is contained in:
@@ -849,6 +849,7 @@ async fn set_session_model_invalidates_byok_memo_for_same_model_id() {
|
||||
api_backend: crate::sampling::ApiBackend::ChatCompletions,
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
anthropic_oauth: false,
|
||||
extra_headers: Default::default(),
|
||||
context_window: 256_000,
|
||||
force_http1: false,
|
||||
|
||||
@@ -46,6 +46,7 @@ async fn persist_ack_waits_for_disk_flush_before_success() {
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
anthropic_oauth: false,
|
||||
extra_headers: Default::default(),
|
||||
context_window: 100_000,
|
||||
force_http1: false,
|
||||
@@ -342,6 +343,7 @@ async fn first_turn_memory_injection_persists_to_chat_history() {
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
anthropic_oauth: false,
|
||||
context_window: 100_000,
|
||||
force_http1: false,
|
||||
max_retries: None,
|
||||
@@ -472,6 +474,7 @@ async fn first_turn_memory_injection_disabled_does_not_persist_to_chat_history()
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
anthropic_oauth: false,
|
||||
context_window: 100_000,
|
||||
force_http1: false,
|
||||
max_retries: None,
|
||||
@@ -1740,6 +1743,7 @@ async fn cancel_propagates_to_sampler_handle_so_no_further_emission() {
|
||||
api_backend: kigi_sampler::ApiBackend::Responses,
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
anthropic_oauth: false,
|
||||
extra_headers: Default::default(),
|
||||
context_window: 100_000,
|
||||
force_http1: false,
|
||||
|
||||
Reference in New Issue
Block a user