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,
ThirdParty,
Unknown,
}
impl EgressTier {
pub fn label(self) -> &'static str {
match self {
Self::Local => "local",
Self::ThirdParty => "third-party",
Self::Unknown => "unknown",
}
}
pub fn admits_local(self) -> bool {
matches!(self, Self::Local)
}
}
#[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::Unknown),
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::Codebuff | AgentKind::Custom => {
("unknown", MeteringShape::Unknown)
}
};
(ProviderId::new(id), shape)
}
pub fn egress_for_provider(provider: &ProviderId) -> EgressTier {
if provider.is_unknown() {
EgressTier::Unknown
} else {
EgressTier::ThirdParty
}
}
pub fn egress_for_cli(cli: AgentKind) -> EgressTier {
let (provider, _) = provider_for_cli(cli);
egress_for_provider(&provider)
}
pub fn egress_for_base_url(base_url: &str) -> EgressTier {
let trimmed = base_url.trim();
if trimmed.is_empty() {
return EgressTier::Unknown;
}
match loopback_host(trimmed) {
Some(true) => EgressTier::Local,
Some(false) => EgressTier::ThirdParty,
None => EgressTier::Unknown,
}
}
fn loopback_host(base_url: &str) -> Option<bool> {
let host = host_from_base_url(base_url)?;
let host = host.trim_matches(|c| c == '[' || c == ']').to_ascii_lowercase();
Some(host == "localhost" || host == "127.0.0.1" || host == "::1")
}
fn host_from_base_url(base_url: &str) -> Option<&str> {
let rest = base_url
.split_once("://")
.map(|(_, after)| after)
.unwrap_or(base_url);
let authority = rest.split('/').next().unwrap_or(rest);
if authority.is_empty() {
return None;
}
let hostport = authority.rsplit('@').next().unwrap_or(authority);
if hostport.starts_with('[') {
return hostport.split(']').next().map(|h| h.trim_start_matches('['));
}
Some(hostport.split(':').next().unwrap_or(hostport)).filter(|h| !h.is_empty())
}
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;