Files
Kigi-CLI/crates/codegen/kigi-shell/tests/common/mod.rs
T
ZacharyZhang-NY 5a9183b08b 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.
2026-07-22 02:53:25 -04:00

63 lines
2.2 KiB
Rust

//! Shared helpers for kigi-shell integration tests.
use kigi_shell::sampling::{ApiBackend, Client, SamplerConfig};
/// Create a sampling client configured for a mock server. Shared by the
/// integration tests so the ~30-field `SamplerConfig` literal lives in one
/// place (`SamplerConfig` has no `Default`).
pub fn create_test_client(base_url: &str, api_backend: ApiBackend) -> Client {
create_test_client_with_extra_headers(base_url, api_backend, &[])
}
/// Like [`create_test_client`] but seeds `SamplerConfig::extra_headers`, so a
/// test can assert that session-injected headers reach the wire.
pub fn create_test_client_with_extra_headers(
base_url: &str,
api_backend: ApiBackend,
extra_headers: &[(&str, &str)],
) -> Client {
Client::new(test_sampler_config(base_url, api_backend, extra_headers)).unwrap()
}
/// The shared mock-server `SamplerConfig`; tests needing a non-default field
/// (e.g. `doom_loop_recovery`) mutate the returned value before building the
/// client themselves.
pub fn test_sampler_config(
base_url: &str,
api_backend: ApiBackend,
extra_headers: &[(&str, &str)],
) -> SamplerConfig {
// Shell `Client` is `kigi_sampler::SamplingClient`, which takes a
// `SamplerConfig` directly. Construct one inline here.
SamplerConfig {
api_key: Some("test-api-key".to_string()),
base_url: base_url.to_string(),
model: "test-model".to_string(),
max_completion_tokens: Some(1000),
temperature: Some(0.7),
top_p: None,
api_backend,
auth_scheme: Default::default(),
anthropic_oauth: false,
chat_compat: Default::default(),
extra_headers: extra_headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
context_window: 256_000,
force_http1: false,
max_retries: None,
stream_tool_calls: false,
idle_timeout_secs: None,
reasoning_effort: None,
origin_client: None,
attribution_callback: None,
bearer_resolver: None,
supports_backend_search: false,
compactions_remaining: None,
compaction_at_tokens: None,
doom_loop_recovery: None,
header_injector: None,
}
}