use serde::Serialize;
use super::AgentKind;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct ProviderId(String);
impl ProviderId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn unknown() -> Self {
Self("unknown".to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_unknown(&self) -> bool {
self.0 == "unknown"
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum EgressTier {
Local,
PrivateNetwork,
ThirdParty,
Unknown,
}
impl EgressTier {
pub fn label(self) -> &'static str {
match self {
Self::Local => "local",
Self::PrivateNetwork => "private-network",
Self::ThirdParty => "third-party",
Self::Unknown => "unknown",
}
}
pub fn admits_local(self) -> bool {
matches!(self, Self::Local)
}
pub fn admits_private_network(self) -> bool {
matches!(self, Self::Local | Self::PrivateNetwork)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum MeteringShape {
AccountPool,
PerModelFamily,
SpendBudget,
Subscription,
None,
Unknown,
}
pub fn provider_for_cli(cli: AgentKind) -> (ProviderId, MeteringShape) {
let (id, shape) = match cli {
AgentKind::Codex => ("openai-chatgpt-plan", MeteringShape::AccountPool),
AgentKind::Qwen => ("alibaba-modelstudio-token-plan", MeteringShape::AccountPool),
AgentKind::Antigravity => ("google-antigravity-individual", MeteringShape::PerModelFamily),
AgentKind::OpenCode => ("opencode-zen", MeteringShape::SpendBudget),
AgentKind::CommandCode => ("commandcode.ai", MeteringShape::Unknown),
AgentKind::Oz => ("warp", MeteringShape::AccountPool),
AgentKind::Droid => ("factory", MeteringShape::AccountPool),
AgentKind::Grok => ("xai", MeteringShape::AccountPool),
AgentKind::Cursor => ("cursor-subscription", MeteringShape::Subscription),
AgentKind::Claude => ("anthropic", MeteringShape::Unknown),
AgentKind::Gemini => ("google-genai", MeteringShape::Unknown),
AgentKind::Copilot => ("github-copilot", MeteringShape::Subscription),
AgentKind::Kilo | AgentKind::MiMoCode | AgentKind::Custom => {
("unknown", MeteringShape::Unknown)
}
};
(ProviderId::new(id), shape)
}
pub fn provider_for_custom(
declared_provider: Option<&str>,
declared_metering: Option<&str>,
) -> (ProviderId, MeteringShape) {
let provider = match declared_provider.map(str::trim).filter(|s| !s.is_empty()) {
Some(id) => ProviderId::new(id),
None => ProviderId::unknown(),
};
let metering = declared_metering
.map(str::trim)
.filter(|s| !s.is_empty())
.map(parse_metering_label)
.unwrap_or(MeteringShape::Unknown);
(provider, metering)
}
mod provider_egress;
pub use provider_egress::{egress_for_base_url, egress_for_cli, egress_for_provider};
fn parse_metering_label(label: &str) -> MeteringShape {
match label {
"account_pool" => MeteringShape::AccountPool,
"per_model_family" => MeteringShape::PerModelFamily,
"spend_budget" => MeteringShape::SpendBudget,
"subscription" => MeteringShape::Subscription,
"none" => MeteringShape::None,
"unknown" => MeteringShape::Unknown,
_ => MeteringShape::Unknown,
}
}
pub fn model_family(model: &str) -> &'static str {
let model = model.to_ascii_lowercase();
if model.starts_with("gemini") {
"gemini"
} else if model.starts_with("claude") {
"claude"
} else if model.starts_with("gpt") {
"gpt-oss"
} else {
"other"
}
}
#[cfg(test)]
#[path = "provider_tests.rs"]
mod tests;