diff --git a/crates/cortexkit-provider-usage/Cargo.toml b/crates/cortexkit-provider-usage/Cargo.toml index a68f4bc..f4962d7 100644 --- a/crates/cortexkit-provider-usage/Cargo.toml +++ b/crates/cortexkit-provider-usage/Cargo.toml @@ -11,7 +11,7 @@ # says; this crate makes no guarantee about how a producer derived the numbers. [package] name = "cortexkit-provider-usage" -version = "0.1.0" +version = "0.2.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/crates/cortexkit-provider-usage/src/lib.rs b/crates/cortexkit-provider-usage/src/lib.rs index be7508a..b6a4d29 100644 --- a/crates/cortexkit-provider-usage/src/lib.rs +++ b/crates/cortexkit-provider-usage/src/lib.rs @@ -131,6 +131,15 @@ fn account_info_is_empty(value: &Option) -> bool { pub struct ProviderUsage { /// CodexBar provider name (e.g. "codex"), which consumers map to their own id. pub provider: String, + /// Canonical API provider identifier — the models.dev slug for the same + /// provider (e.g. "openai" when `provider == "codex"`, "anthropic" for + /// "claude", "google" for "gemini", "xai" for "grok"). Present when the + /// producer knows the canonical name; absent for providers with no models.dev + /// counterpart, where consumers fall back to `provider`. Lets every consumer + /// key on one canonical name instead of each maintaining its own + /// CodexBar-name → canonical map. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub api_provider: Option, #[serde(skip_serializing_if = "Option::is_none")] pub account: Option, /// Which retrieval path produced this (e.g. "oauth") — observability only. @@ -155,6 +164,7 @@ impl ProviderUsage { pub fn healthy(provider: &str, account: Option, source: &str, usage: Usage) -> Self { Self { provider: provider.to_string(), + api_provider: None, account, source: Some(source.to_string()), account_info: None, @@ -170,6 +180,7 @@ impl ProviderUsage { pub fn degraded(provider: &str, error: impl std::fmt::Display) -> Self { Self { provider: provider.to_string(), + api_provider: None, account: None, source: None, account_info: None, @@ -276,4 +287,20 @@ mod tests { assert!(json.contains("\"error\":\"no session\"")); assert!(!json.contains("usage")); } + + #[test] + fn api_provider_is_camel_case_present_when_set_and_omitted_when_absent() { + let mut entry = ProviderUsage::healthy("codex", None, "oauth", Usage::default()); + let json = serde_json::to_string(&entry).unwrap(); + assert!( + !json.contains("apiProvider"), + "absent api_provider must be omitted" + ); + + entry.api_provider = Some("openai".to_string()); + let json = serde_json::to_string(&entry).unwrap(); + assert!(json.contains("\"apiProvider\":\"openai\"")); + let back: ProviderUsage = serde_json::from_str(&json).unwrap(); + assert_eq!(back, entry); + } }