Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c2adfce
Delete dead typed Codex build_result path
Finesssee Oct 10, 2026
63fdd22
Share the Codex reset observation store reader
Finesssee Oct 10, 2026
1e7f003
Pin Codex named-window routing
Finesssee Oct 10, 2026
c340721
Collapse Codex API request and cache layers
Finesssee Oct 10, 2026
65db4cc
Pin every Codex weekly-reset diagnostic code
Finesssee Oct 10, 2026
37b7f8b
Fold Codex weekly-reset rejections into one admission check
Finesssee Oct 10, 2026
de87ae1
Share Codex API test fixtures
Finesssee Oct 10, 2026
0c76b42
Name the Codex named-window test row type
Finesssee Oct 10, 2026
65e2af1
Share Codex weekly-reset test helpers
Finesssee Oct 10, 2026
dbb5cb6
Share the Copilot request tail and quota slots
Finesssee Oct 10, 2026
a0997d1
Share Copilot snapshot test helpers
Finesssee Oct 10, 2026
690830c
Pin Claude CLI error markers
Finesssee Oct 10, 2026
367cfa9
Inline one-caller Claude CLI wrappers into an error table
Finesssee Oct 10, 2026
5eeff08
Pin Claude CLI label-section windows
Finesssee Oct 10, 2026
334dd2d
Share the Claude CLI label-section scan
Finesssee Oct 10, 2026
de5a23a
Pin that rendered Claude CLI text carries no escapes
Finesssee Oct 10, 2026
bd66fb6
Drop dead Claude escape stripping and wrappers
Finesssee Oct 10, 2026
dc699c7
Pin Claude Admin API model and cost ranking
Finesssee Oct 10, 2026
a91fbe5
Pin Claude reset date forms and observation store loading
Finesssee Oct 10, 2026
bfb9a05
Share small Claude fold and window helpers
Finesssee Oct 10, 2026
ae3183f
Pin Claude web request error mapping
Finesssee Oct 10, 2026
a50d8fe
Share the Claude web GET and status checks
Finesssee Oct 10, 2026
4f918f4
Pin claude-swap envelope checks for both commands
Finesssee Oct 10, 2026
89b2368
Share the claude-swap envelope and termination call
Finesssee Oct 10, 2026
7c2e5d7
Re-export only the claude-swap items used outside it
Finesssee Oct 10, 2026
d6734eb
Table-drive the Claude CLI parse and error-state tests
Finesssee Oct 10, 2026
42eaec8
Table-drive the Claude web API tests
Finesssee Oct 10, 2026
241d28b
Table-drive the Claude OAuth and claude-swap sanitize tests
Finesssee Oct 10, 2026
70333ac
Share Claude test builders for quota history, claude-swap and junctions
Finesssee Oct 10, 2026
7942da8
Move claude-swap parser tests into parser_tests.rs
Finesssee Oct 10, 2026
8c489b1
Move Claude web API tests into web_api_tests.rs
Finesssee Oct 10, 2026
950c284
Move Copilot API tests into api_tests.rs
Finesssee Oct 10, 2026
f036d67
Split Codex API client into credentials, parse and reset-credits modules
Finesssee Oct 10, 2026
c858406
Split Claude CLI probe, binary lookup and text helpers out of mod.rs
Finesssee Oct 10, 2026
5e6fc65
Move the cswap list doc back above parse_account_list
Finesssee Oct 10, 2026
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
34 changes: 18 additions & 16 deletions rust/src/providers/claude/accounts/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,24 +416,30 @@ mod tests {
assert!(!removable.exists());
}

#[cfg(windows)]
fn make_junction(link: &Path, target: &Path, failure: &str) {
use std::os::windows::process::CommandExt;
let output = Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", "New-Item -ItemType Junction -Path $env:CODEXBAR_TEST_LINK -Target $env:CODEXBAR_TEST_TARGET | Out-Null"])
.env("CODEXBAR_TEST_LINK", link).env("CODEXBAR_TEST_TARGET", target)
.creation_flags(0x0800_0000).output().unwrap();
assert!(output.status.success(), "{failure}");
}

#[cfg(windows)]
#[test]
fn startup_cleanup_does_not_follow_junctions() {
use std::os::windows::process::CommandExt;
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("logins");
let outside = dir.path().join("outside");
std::fs::create_dir_all(&root).unwrap();
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(outside.join("keep.txt"), "preserved").unwrap();
let link = root.join(uuid::Uuid::new_v4().to_string());
let output = Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", "New-Item -ItemType Junction -Path $env:CODEXBAR_TEST_LINK -Target $env:CODEXBAR_TEST_TARGET | Out-Null"])
.env("CODEXBAR_TEST_LINK", &link).env("CODEXBAR_TEST_TARGET", &outside)
.creation_flags(0x0800_0000).output().unwrap();
assert!(
output.status.success(),
"Failed to create the isolated test junction."
make_junction(
&link,
&outside,
"Failed to create the isolated test junction.",
);
cleanup_login_root(&root).unwrap();
assert!(outside.join("keep.txt").exists());
Expand Down Expand Up @@ -730,18 +736,14 @@ mod tests {
#[cfg(windows)]
#[test]
fn ordinary_junction_is_not_treated_as_wsl_backed() {
use std::os::windows::process::CommandExt;
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target");
let link = dir.path().join("link");
std::fs::create_dir_all(&target).unwrap();
let output = Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", "New-Item -ItemType Junction -Path $env:CODEXBAR_TEST_LINK -Target $env:CODEXBAR_TEST_TARGET | Out-Null"])
.env("CODEXBAR_TEST_LINK", &link).env("CODEXBAR_TEST_TARGET", &target)
.creation_flags(0x0800_0000).output().unwrap();
assert!(
output.status.success(),
"Failed to create the local junction fixture."
make_junction(
&link,
&target,
"Failed to create the local junction fixture.",
);
assert!(!path_is_wsl_backed(&link));
assert!(!path_is_wsl_backed(&target));
Expand Down
115 changes: 61 additions & 54 deletions rust/src/providers/claude/admin_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,18 @@ struct CacheCreation {
total_input_tokens: Option<i64>,
}

impl MessageResult {
fn input_tokens(&self) -> i64 {
self.uncached_input_tokens.unwrap_or(0)
+ self
.cache_creation
.as_ref()
.and_then(|c| c.total_input_tokens)
.unwrap_or(0)
+ self.cache_read_input_tokens.unwrap_or(0)
}
}

fn result_from_admin_usage(
costs: &CostReportResponse,
messages: &MessagesUsageResponse,
Expand All @@ -225,14 +237,7 @@ fn result_from_admin_usage(
.data
.iter()
.flat_map(|bucket| &bucket.results)
.map(|r| {
r.uncached_input_tokens.unwrap_or(0)
+ r.cache_creation
.as_ref()
.and_then(|c| c.total_input_tokens)
.unwrap_or(0)
+ r.cache_read_input_tokens.unwrap_or(0)
})
.map(MessageResult::input_tokens)
.sum();
let output_tokens: i64 = messages
.data
Expand Down Expand Up @@ -278,26 +283,13 @@ fn result_from_admin_usage(
.with_login_method("Admin API");
usage.updated_at = now;

let mut model_tokens: HashMap<String, i64> = HashMap::new();
for result in messages.data.iter().flat_map(|bucket| &bucket.results) {
let name = result
.model
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or("Claude API");
let tokens = result.uncached_input_tokens.unwrap_or(0)
+ result
.cache_creation
.as_ref()
.and_then(|c| c.total_input_tokens)
.unwrap_or(0)
+ result.cache_read_input_tokens.unwrap_or(0)
+ result.output_tokens.unwrap_or(0);
*model_tokens.entry(name.to_string()).or_default() += tokens;
}
let mut top_models: Vec<_> = model_tokens.into_iter().collect();
top_models.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
let top_models = ranked_totals(
messages.data.iter().flat_map(|bucket| &bucket.results),
|result| result.model.as_deref(),
"Claude API",
|result| result.input_tokens() + result.output_tokens.unwrap_or(0),
|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)),
);
for (idx, (model, tokens)) in top_models.into_iter().take(3).enumerate() {
usage = usage.with_extra_rate_window(
format!("model-{idx}"),
Expand All @@ -306,19 +298,18 @@ fn result_from_admin_usage(
);
}

let mut cost_items: HashMap<String, f64> = HashMap::new();
for result in costs.data.iter().flat_map(|bucket| &bucket.results) {
let name = result
.description
.as_deref()
.or(result.cost_type.as_deref())
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or("Claude API");
*cost_items.entry(name.to_string()).or_default() += usd_from_lowest_unit(&result.amount);
}
let mut top_items: Vec<_> = cost_items.into_iter().collect();
top_items.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let top_items = ranked_totals(
costs.data.iter().flat_map(|bucket| &bucket.results),
|result| {
result
.description
.as_deref()
.or(result.cost_type.as_deref())
},
"Claude API",
|result| usd_from_lowest_unit(&result.amount),
|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal),
);
for (idx, (item, cost)) in top_items.into_iter().take(3).enumerate() {
usage = usage.with_extra_rate_window(
format!("cost-{idx}"),
Expand All @@ -342,21 +333,16 @@ fn result_from_admin_usage(
/// only when the report spans more than one workspace; a single workspace adds
/// nothing over the organization view.
fn workspace_spend_rows(costs: &CostReportResponse) -> Vec<ProviderDisplayDetail> {
let mut workspaces: HashMap<String, f64> = HashMap::new();
for result in costs.data.iter().flat_map(|bucket| &bucket.results) {
let name = result
.workspace_id
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or("Default");
*workspaces.entry(name.to_string()).or_default() += usd_from_lowest_unit(&result.amount);
}
if workspaces.len() < 2 {
let ranked = ranked_totals(
costs.data.iter().flat_map(|bucket| &bucket.results),
|result| result.workspace_id.as_deref(),
"Default",
|result| usd_from_lowest_unit(&result.amount),
|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)),
);
if ranked.len() < 2 {
return Vec::new();
}
let mut ranked: Vec<_> = workspaces.into_iter().collect();
ranked.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
ranked
.into_iter()
.take(MAX_WORKSPACE_ROWS)
Expand All @@ -372,6 +358,27 @@ fn workspace_spend_rows(costs: &CostReportResponse) -> Vec<ProviderDisplayDetail
.collect()
}

/// Sums `value` per trimmed, non-empty `name` (else `fallback`), sorted by `order`.
fn ranked_totals<'a, R: 'a, V: Copy + Default + std::ops::AddAssign>(
rows: impl Iterator<Item = &'a R>,
name: impl Fn(&'a R) -> Option<&'a str>,
fallback: &str,
value: impl Fn(&R) -> V,
order: impl FnMut(&(String, V), &(String, V)) -> std::cmp::Ordering,
) -> Vec<(String, V)> {
let mut totals: HashMap<String, V> = HashMap::new();
for row in rows {
let key = name(row)
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or(fallback);
*totals.entry(key.to_string()).or_default() += value(row);
}
let mut ranked: Vec<_> = totals.into_iter().collect();
ranked.sort_by(order);
ranked
}

fn usd_from_lowest_unit(raw: &str) -> f64 {
raw.parse::<f64>().unwrap_or(0.0) / 100.0
}
Expand Down
67 changes: 67 additions & 0 deletions rust/src/providers/claude/admin_api_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,70 @@ fn workspace_option_groups_the_existing_cost_request_only() {
cost_report_query(&start, &end, false).len() + 1
);
}

#[test]
fn messages_and_costs_fold_into_ranked_windows() {
let messages: MessagesUsageResponse = serde_json::from_str(
r#"{"data":[
{"starting_at":"2026-09-23T00:00:00Z","ending_at":"2026-09-24T00:00:00Z","results":[
{"model":"claude-b","uncached_input_tokens":10,"cache_read_input_tokens":5,"output_tokens":5},
{"model":" ","uncached_input_tokens":1,"cache_creation":{"total_input_tokens":2}},
{"model":"claude-a","uncached_input_tokens":20}]},
{"starting_at":"2026-09-24T00:00:00Z","ending_at":"2026-09-25T00:00:00Z","results":[
{"model":"claude-c","output_tokens":7},
{"uncached_input_tokens":4}]}]}"#,
)
.unwrap();
let report = costs(
r#"{"data":[
{"starting_at":"2026-09-22T00:00:00Z","ending_at":"2026-09-23T00:00:00Z","results":[
{"amount":"300","description":" Web search "},
{"amount":"100","cost_type":"tokens"},
{"amount":"200","description":"","cost_type":"session"},
{"amount":"50"}]}]}"#,
);
let result = result_from_admin_usage(&report, &messages, now(), false);
let usage = &result.usage;
let windows: Vec<_> = usage
.extra_rate_windows
.iter()
.map(|window| {
(
window.id.as_str(),
window.title.as_str(),
window.window.reset_description.as_deref().unwrap(),
)
})
.collect();
assert_eq!(
windows,
[
("input-tokens", "Input tokens", "42"),
("output-tokens", "Output tokens", "12"),
("model-0", "Model: claude-a", "20 tokens"),
("model-1", "Model: claude-b", "20 tokens"),
("model-2", "Model: Claude API", "7 tokens"),
("cost-0", "Cost: Web search", "$3.00"),
("cost-1", "Cost: Claude API", "$2.50"),
("cost-2", "Cost: tokens", "$1.00"),
]
);
assert_eq!(
usage.primary.reset_description.as_deref(),
Some("$6.50 over last 30 days")
);
assert_eq!(
usage
.secondary
.as_ref()
.unwrap()
.reset_description
.as_deref(),
Some("54 tokens")
);
assert_eq!(
usage.primary.resets_at.map(|at| at.to_rfc3339()).as_deref(),
Some("2026-09-22T00:00:00+00:00")
);
assert!(result.display_details().is_empty());
}
10 changes: 3 additions & 7 deletions rust/src/providers/claude/claude_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,13 @@ mod sanitize;
use chrono::{DateTime, Utc};
use serde::Serialize;

pub use parser::{parse_account_list, parse_switch_result, validate_switch_target};
pub use parser::parse_account_list;
pub use projection::{
ClaudeSwapAccount, ClaudeSwapAccountAction, ClaudeSwapHistoricalUsageDto,
ClaudeSwapScopedWindowDto, ClaudeSwapSpendWindowDto, ClaudeSwapUsageWindowDto,
HISTORICAL_USAGE_PROVENANCE, action_for_account, project_accounts,
action_for_account, project_accounts,
};
pub use runner::{
DEFAULT_TIMEOUT, MAX_OUTPUT_BYTES, SWITCH_TIMEOUT, list_arguments, read_account_list,
read_account_list_with_timeout, resolve_executable_path, switch_account, switch_arguments,
};
pub use sanitize::{MAX_DIAGNOSTIC_CHARS, MAX_LABEL_CHARS, sanitize_display};
pub use runner::{read_account_list, switch_account};

#[derive(Debug, thiserror::Error)]
pub enum ClaudeSwapError {
Expand Down
Loading