bitrouter 1.0.0-alpha.18

BitRouter: an LLM API router. CLI + TUI + assembly library.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
//! App-layer routing glue for the Claude Code **subscription** provider
//! (`claude-code`).
//!
//! Two daemon-side responsibilities, both keyed on the `claude-code` provider
//! id and the Claude Code agent-profile beta:
//!
//! - [`ClaudeCodeRouter`] — an ingress [`PromptTransform`] that detects genuine
//!   Claude Code traffic by its `anthropic-beta: claude-code-…` header and
//!   routes it to the subscription provider by provider-prefixing the model
//!   (`claude-code:<model>`).
//! - [`enable_if_logged_in`] — auto-add the `claude-code` provider to the
//!   in-memory `providers:` map when the OAuth credential store holds a
//!   `claude-code` credential (mirrors [`crate::cloud::enable_in_zero_config`]).
//!
//! These live in the app layer (not `bitrouter-providers`) because the routing
//! decision needs the ingress request (the parsed [`Prompt`] plus the inbound
//! headers), which only exists above the SDK ingress seam, and because the
//! enable step mutates the assembled [`Config`].

use bitrouter_providers::oauth::credential_store::{Credential, CredentialStore, DEFAULT_LABEL};
use bitrouter_sdk::HeaderMap;
use bitrouter_sdk::PromptTransform;
use bitrouter_sdk::config::{Config, ProviderConfig};
use bitrouter_sdk::language_model::types::Prompt;

/// Provider id of the Claude Pro/Max subscription provider. Must match the id
/// the [`bitrouter_providers::claude_code::ClaudeCodeAuthApplier`] is registered
/// under and the id used in the explicit-provider route prefix below.
const PROVIDER_ID: &str = "claude-code";

/// Ingress [`PromptTransform`] that routes genuine Claude Code traffic to the
/// Claude Pro/Max **subscription** provider.
///
/// Genuine Claude Code traffic carries the Claude Code agent-profile beta —
/// `anthropic-beta: claude-code-…` — the same marker the Pro/Max subscription
/// endpoint keys on. It's sent identically by the CLI, the Agent SDK, and
/// `bitrouter spawn`, and is stable across releases (unlike the
/// version-dependent system-prompt text the older detection relied on). Such a
/// request also targets a bare Claude model id (e.g. `claude-opus-4-8`).
///
/// When both hold, the transform rewrites `prompt.model` to
/// `claude-code:<model>`, which sends the request to the subscription provider
/// via the explicit-provider route. Everything else is left untouched:
/// non-Claude-Code traffic, non-Claude models, and already-prefixed models all
/// route wherever they already pointed (the pay-as-you-go `anthropic` provider
/// for bare Claude models, or the explicit provider).
///
/// The transform **only reads** the marker — it never adds it. The subscription
/// applier separately *requires* the beta to be present and refuses to
/// fabricate it, so this transform cannot be used to spoof arbitrary traffic as
/// Claude Code.
pub struct ClaudeCodeRouter;

impl PromptTransform for ClaudeCodeRouter {
    fn apply(&self, _prompt: &mut Prompt) {
        // Detection needs the inbound `anthropic-beta` header, so all the work
        // is in `apply_with_headers` (which the HTTP server always calls). With
        // no headers there is nothing to decide, so this is a no-op.
    }

    fn apply_with_headers(&self, prompt: &mut Prompt, headers: &HeaderMap) {
        // Genuine Claude Code carries the agent-profile beta
        // (`anthropic-beta: claude-code-…`) — the same marker the subscription
        // endpoint keys on, stable across Claude Code's CLI / Agent-SDK /
        // `bitrouter spawn` shapes (unlike the version-dependent system prompt
        // text). When it's present and the request targets a bare `claude-*`
        // model, route to the subscription provider by prefixing the model.
        // Non-Claude-Code traffic is left untouched (it falls to the
        // pay-as-you-go `anthropic` provider). The transform only READS the
        // marker — it never adds it, so it can't be used to spoof.
        let is_cc = headers_indicate_claude_code(headers);
        if is_cc && prompt.model.starts_with("claude") && !prompt.model.starts_with("claude-code:")
        {
            prompt.model = format!("claude-code:{}", prompt.model);
        }
    }
}

/// Whether the inbound headers carry the Claude Code agent-profile beta. The
/// `anthropic-beta` header is a comma-joined list; any token whose name starts
/// with `claude-code` counts, so the match is stable across the dated suffix.
fn headers_indicate_claude_code(headers: &HeaderMap) -> bool {
    headers
        .get_all("anthropic-beta")
        .iter()
        .filter_map(|v| v.to_str().ok())
        .any(|v| v.split(',').any(|b| b.trim().starts_with("claude-code")))
}

/// Insert the `claude-code` provider into `config.providers` when the OAuth
/// credential store holds a `claude-code` credential (subscription marker or
/// stored OAuth token) and the entry is not already present.
///
/// The inserted entry is an empty [`ProviderConfig::default()`]; the registry
/// merge fills its `api_base` / `api_protocol` / auth from the fetched
/// `claude-code` registry entry, the
/// [`bitrouter_providers::claude_code::ClaudeCodeAuthApplier`] authenticates it,
/// and `claude-code`'s `access: local_oauth` keeps it active even though it
/// declares no canonical models.
///
/// Best-effort: a missing or unreadable store is a no-op — the user simply
/// hasn't signed in to their Claude subscription yet. Mirrors
/// [`crate::cloud::enable_in_zero_config`].
pub fn enable_if_logged_in(config: &mut Config) {
    // Adopt any legacy #590 subscription marker (stored under `anthropic`)
    // before reading the store, so a running daemon picks up the move to
    // `claude-code` on a serve / reload config-build pass.
    migrate_legacy_anthropic_marker_default();
    let Ok(store) = CredentialStore::default_path() else {
        return;
    };
    enable_if_logged_in_with_store(config, &store);
}

/// Provider id of the legacy platform / pay-as-you-go Anthropic provider. A
/// pre-split (#590) user signed in to their Claude subscription stored the
/// [`Credential::ClaudeCodeCli`] marker here; the migration moves it to
/// [`PROVIDER_ID`].
const LEGACY_PROVIDER_ID: &str = "anthropic";

/// Move a pre-split (#590) Claude subscription marker from the legacy
/// `anthropic` provider to the dedicated `claude-code` provider, using the
/// default credential-store path. Best-effort: an unresolvable / unreadable
/// store is a silent no-op. See [`migrate_legacy_anthropic_marker`].
pub fn migrate_legacy_anthropic_marker_default() {
    let Ok(store) = CredentialStore::default_path() else {
        return;
    };
    let _ = migrate_legacy_anthropic_marker(store.path().to_path_buf());
}

/// Move a pre-split (#590) Claude subscription marker from the legacy
/// `anthropic` provider to the dedicated `claude-code` provider.
///
/// #590 stored the [`Credential::ClaudeCodeCli`] subscription marker under
/// `anthropic`. Now that `anthropic` is platform / pay-as-you-go (`x-api-key`)
/// and the subscription is its own `claude-code` provider, an existing user
/// would otherwise be stranded. For every label under `anthropic` whose
/// credential is the `ClaudeCodeCli` marker, this re-keys it to `claude-code`
/// (set under `claude-code`, removed from `anthropic`).
///
/// Strictly scoped to the marker:
/// - A pasted [`Credential::ApiKey`] (or any OAuth credential) under `anthropic`
///   is **left untouched** — that is `anthropic`'s own platform key.
/// - If `claude-code` already holds a credential for that label, the `anthropic`
///   marker is left as-is (no clobber).
///
/// Best-effort: an unreadable store is a silent no-op. Takes the store path so
/// callers / tests can inject it.
pub fn migrate_legacy_anthropic_marker(store_path: std::path::PathBuf) -> bool {
    let Ok(mut store) = CredentialStore::load(store_path) else {
        return false;
    };
    // Snapshot the labels first — the move mutates the store as it goes.
    let labels: Vec<String> = store
        .labels(LEGACY_PROVIDER_ID)
        .into_iter()
        .map(str::to_string)
        .collect();
    let mut moved_any = false;
    for label in labels {
        // Only the tokenless subscription marker migrates; an `ApiKey` or any
        // other credential is anthropic's platform credential and stays put.
        if !matches!(
            store.get_any(LEGACY_PROVIDER_ID, &label),
            Some(Credential::ClaudeCodeCli)
        ) {
            continue;
        }
        // Don't clobber an existing `claude-code` credential for this label.
        if store.get_any(PROVIDER_ID, &label).is_some() {
            continue;
        }
        if store
            .set(PROVIDER_ID, &label, Credential::ClaudeCodeCli)
            .is_err()
        {
            // A failed write leaves the legacy marker in place; try again next
            // time rather than removing the only copy.
            continue;
        }
        let _ = store.remove(LEGACY_PROVIDER_ID, &label);
        moved_any = true;
    }
    moved_any
}

/// Inner form taking the credential store explicitly so unit tests can drive
/// the logic without touching the user's real store.
fn enable_if_logged_in_with_store(config: &mut Config, store: &CredentialStore) {
    if config.providers.contains_key(PROVIDER_ID) {
        return;
    }
    let logged_in = !store.labels(PROVIDER_ID).is_empty()
        || store.get_any(PROVIDER_ID, DEFAULT_LABEL).is_some();
    if !logged_in {
        return;
    }
    config
        .providers
        .insert(PROVIDER_ID.to_string(), ProviderConfig::default());
}

#[cfg(test)]
mod tests {
    use super::*;
    use bitrouter_providers::oauth::credential_store::Credential;
    use bitrouter_sdk::language_model::types::{GenerationParams, ProviderMetadata};

    /// A representative `anthropic-beta` value as genuine Claude Code sends it:
    /// the agent-profile beta first, then feature betas.
    const CC_BETA: &str = "claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07";

    fn prompt(model: &str) -> Prompt {
        Prompt {
            model: model.to_string(),
            system: None,
            system_provider_metadata: ProviderMetadata::new(),
            messages: Vec::new(),
            tools: Vec::new(),
            params: GenerationParams::default(),
            response_format: None,
            tool_choice: None,
            stream: false,
        }
    }

    /// Drive the router with an optional inbound `anthropic-beta` header value,
    /// returning the (possibly rewritten) model.
    fn route(model: &str, anthropic_beta: Option<&str>) -> String {
        let mut p = prompt(model);
        let mut headers = HeaderMap::new();
        if let Some(beta) = anthropic_beta {
            headers.insert("anthropic-beta", beta.parse().unwrap());
        }
        ClaudeCodeRouter.apply_with_headers(&mut p, &headers);
        p.model
    }

    #[test]
    fn claude_code_beta_and_bare_claude_model_is_prefixed() {
        assert_eq!(
            route("claude-opus-4-8", Some(CC_BETA)),
            "claude-code:claude-opus-4-8"
        );
    }

    #[test]
    fn claude_code_beta_and_non_claude_model_is_untouched() {
        assert_eq!(route("gpt-5", Some(CC_BETA)), "gpt-5");
    }

    #[test]
    fn no_claude_code_beta_leaves_claude_model_for_payg() {
        // The key guarantee: non-Claude-Code traffic for a Claude model is NOT
        // routed to the subscription — it falls to the pay-as-you-go provider.
        assert_eq!(route("claude-opus-4-8", None), "claude-opus-4-8");
        // A beta header that lacks the claude-code agent profile also doesn't route.
        assert_eq!(
            route(
                "claude-opus-4-8",
                Some("oauth-2025-04-20,context-1m-2025-08-07")
            ),
            "claude-opus-4-8"
        );
    }

    #[test]
    fn already_prefixed_claude_model_is_untouched() {
        assert_eq!(
            route("claude-code:claude-opus-4-8", Some(CC_BETA)),
            "claude-code:claude-opus-4-8"
        );
    }

    #[test]
    fn idempotent_on_already_claude_code_prefixed_model() {
        // Running the transform twice must not double-prefix.
        let mut p = prompt("claude-opus-4-8");
        let mut headers = HeaderMap::new();
        headers.insert("anthropic-beta", CC_BETA.parse().unwrap());
        ClaudeCodeRouter.apply_with_headers(&mut p, &headers);
        ClaudeCodeRouter.apply_with_headers(&mut p, &headers);
        assert_eq!(p.model, "claude-code:claude-opus-4-8");
    }

    #[test]
    fn apply_without_headers_is_noop() {
        // The header-less path can't detect Claude Code, so it must not route.
        let mut p = prompt("claude-opus-4-8");
        ClaudeCodeRouter.apply(&mut p);
        assert_eq!(p.model, "claude-opus-4-8");
    }

    fn fresh_tmp_store() -> CredentialStore {
        use std::sync::atomic::{AtomicU64, Ordering};
        static COUNTER: AtomicU64 = AtomicU64::new(0);
        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
        let dir = std::env::temp_dir().join(format!(
            "bitrouter-cc-router-test-{}-{id}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).unwrap();
        CredentialStore::load(dir.join("creds.json")).unwrap()
    }

    #[test]
    fn enable_inserts_when_marker_present() {
        let mut store = fresh_tmp_store();
        store
            .set(PROVIDER_ID, DEFAULT_LABEL, Credential::ClaudeCodeCli)
            .unwrap();
        let mut config = Config::default();
        enable_if_logged_in_with_store(&mut config, &store);
        assert!(
            config.providers.contains_key(PROVIDER_ID),
            "`claude-code` provider should be auto-enabled when a credential is stored"
        );
    }

    #[test]
    fn enable_noop_when_no_credential() {
        let store = fresh_tmp_store();
        let mut config = Config::default();
        enable_if_logged_in_with_store(&mut config, &store);
        assert!(
            !config.providers.contains_key(PROVIDER_ID),
            "no credential → no provider inserted"
        );
    }

    #[test]
    fn migration_moves_marker_from_anthropic_to_claude_code() {
        let mut store = fresh_tmp_store();
        let path = store.path().to_path_buf();
        // A pre-split (#590) user: the subscription marker lives under
        // `anthropic`, and `claude-code` has nothing yet.
        store
            .set(LEGACY_PROVIDER_ID, DEFAULT_LABEL, Credential::ClaudeCodeCli)
            .unwrap();
        drop(store);

        assert!(migrate_legacy_anthropic_marker(path.clone()));

        let reloaded = CredentialStore::load(&path).unwrap();
        assert!(
            matches!(
                reloaded.get_any(PROVIDER_ID, DEFAULT_LABEL),
                Some(Credential::ClaudeCodeCli)
            ),
            "the marker must now live under `claude-code`"
        );
        assert!(
            reloaded
                .get_any(LEGACY_PROVIDER_ID, DEFAULT_LABEL)
                .is_none(),
            "the legacy `anthropic` marker must be removed"
        );
    }

    #[test]
    fn migration_leaves_anthropic_api_key_untouched() {
        let mut store = fresh_tmp_store();
        let path = store.path().to_path_buf();
        // An `anthropic` platform key must stay put — it is not a subscription
        // marker.
        store
            .set(
                LEGACY_PROVIDER_ID,
                DEFAULT_LABEL,
                Credential::api_key("sk-ant-api03-platform"),
            )
            .unwrap();
        drop(store);

        assert!(!migrate_legacy_anthropic_marker(path.clone()));

        let reloaded = CredentialStore::load(&path).unwrap();
        assert_eq!(
            reloaded
                .get_any(LEGACY_PROVIDER_ID, DEFAULT_LABEL)
                .and_then(Credential::as_api_key),
            Some("sk-ant-api03-platform"),
            "the anthropic platform API key must be left in place"
        );
        assert!(
            reloaded.get_any(PROVIDER_ID, DEFAULT_LABEL).is_none(),
            "no claude-code credential should be created for an API key"
        );
    }

    #[test]
    fn migration_does_not_clobber_existing_claude_code_credential() {
        let mut store = fresh_tmp_store();
        let path = store.path().to_path_buf();
        // Both providers already hold a marker for this label (e.g. the user
        // already logged in to `claude-code`). The legacy entry must be left
        // as-is rather than overwriting the existing `claude-code` credential.
        store
            .set(LEGACY_PROVIDER_ID, DEFAULT_LABEL, Credential::ClaudeCodeCli)
            .unwrap();
        store
            .set(
                PROVIDER_ID,
                DEFAULT_LABEL,
                Credential::api_key("sk-ant-oat-existing"),
            )
            .unwrap();
        drop(store);

        assert!(!migrate_legacy_anthropic_marker(path.clone()));

        let reloaded = CredentialStore::load(&path).unwrap();
        assert!(
            matches!(
                reloaded.get_any(LEGACY_PROVIDER_ID, DEFAULT_LABEL),
                Some(Credential::ClaudeCodeCli)
            ),
            "the anthropic marker must be left as-is when claude-code already has a credential"
        );
        assert_eq!(
            reloaded
                .get_any(PROVIDER_ID, DEFAULT_LABEL)
                .and_then(Credential::as_api_key),
            Some("sk-ant-oat-existing"),
            "the pre-existing claude-code credential must not be clobbered"
        );
    }

    #[test]
    fn migration_noop_on_unreadable_store() {
        // A path that cannot be loaded as a store (it's a directory) is a
        // silent no-op, never a panic.
        let dir =
            std::env::temp_dir().join(format!("bitrouter-cc-migrate-bad-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).unwrap();
        assert!(!migrate_legacy_anthropic_marker(dir.clone()));
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn enable_noop_when_already_present() {
        let mut store = fresh_tmp_store();
        store
            .set(PROVIDER_ID, DEFAULT_LABEL, Credential::ClaudeCodeCli)
            .unwrap();
        let mut config = Config::default();
        // Pre-populate with a sentinel `api_base` to prove the existing entry
        // is not overwritten.
        config.providers.insert(
            PROVIDER_ID.to_string(),
            ProviderConfig {
                api_base: "https://example.invalid".to_string(),
                ..ProviderConfig::default()
            },
        );
        enable_if_logged_in_with_store(&mut config, &store);
        assert_eq!(
            config.providers.get(PROVIDER_ID).unwrap().api_base,
            "https://example.invalid",
            "existing entry must not be overwritten"
        );
    }
}