lean_ctx/core/context_kernel/
provider_parity.rs1use serde_json::Value;
4
5use super::token_envelope::{ProviderKind, TokenEnvelope};
6
7const SUPPORTED: [ProviderKind; 7] = [
8 ProviderKind::OpenAi,
9 ProviderKind::Anthropic,
10 ProviderKind::Gemini,
11 ProviderKind::OpenRouter,
12 ProviderKind::Bedrock,
13 ProviderKind::Azure,
14 ProviderKind::Local,
15];
16
17pub fn detect_provider(base_url: &str) -> ProviderKind {
19 let url = base_url.to_ascii_lowercase();
20 if url.contains("api.openai.com") {
21 ProviderKind::OpenAi
22 } else if url.contains("api.anthropic.com") {
23 ProviderKind::Anthropic
24 } else if url.contains("generativelanguage.googleapis.com")
25 || url.contains("aiplatform.googleapis.com")
26 {
27 ProviderKind::Gemini
28 } else if url.contains("bedrock-runtime") && url.contains("amazonaws.com") {
29 ProviderKind::Bedrock
30 } else if url.contains("openai.azure.com")
31 || url.contains("services.ai.azure.com")
32 || url.contains("cognitiveservices.azure.com")
33 {
34 ProviderKind::Azure
35 } else if url.contains("openrouter.ai") {
36 ProviderKind::OpenRouter
37 } else if url.contains("localhost") || url.contains("127.0.0.1") || url.contains("0.0.0.0") {
38 ProviderKind::Local
39 } else {
40 ProviderKind::Unknown
41 }
42}
43
44fn token(usage: &Value, path: &[&str]) -> usize {
45 path.iter()
46 .try_fold(usage, |value, key| value.get(key))
47 .and_then(Value::as_u64)
48 .and_then(|value| usize::try_from(value).ok())
49 .unwrap_or(0)
50}
51
52pub fn envelope_from_usage(provider: ProviderKind, model: &str, usage: &Value) -> TokenEnvelope {
54 let (input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens) =
55 match provider {
56 ProviderKind::Anthropic => (
57 token(usage, &["input_tokens"]),
58 token(usage, &["output_tokens"]),
59 token(usage, &["cache_read_input_tokens"]),
60 token(usage, &["cache_creation_input_tokens"]),
61 0,
62 ),
63 ProviderKind::Gemini => (
64 token(usage, &["promptTokenCount"]),
65 token(usage, &["candidatesTokenCount"]),
66 token(usage, &["cachedContentTokenCount"]),
67 0,
68 0,
69 ),
70 ProviderKind::Bedrock => (
71 token(usage, &["inputTokens"]),
72 token(usage, &["outputTokens"]),
73 0,
74 0,
75 0,
76 ),
77 ProviderKind::OpenAi
78 | ProviderKind::Azure
79 | ProviderKind::OpenRouter
80 | ProviderKind::Local
81 | ProviderKind::Unknown => (
82 token(usage, &["prompt_tokens"]),
83 token(usage, &["completion_tokens"]),
84 token(usage, &["prompt_tokens_details", "cached_tokens"]),
85 0,
86 token(usage, &["completion_tokens_details", "reasoning_tokens"]),
87 ),
88 };
89
90 TokenEnvelope {
91 model: model.to_owned(),
92 provider,
93 input_tokens,
94 output_tokens,
95 cache_read_tokens,
96 cache_write_tokens,
97 reasoning_tokens,
98 cost_usd: None,
99 tokens_saved: 0,
100 is_retry: false,
101 }
102}
103
104pub const fn provider_display_name(kind: ProviderKind) -> &'static str {
106 match kind {
107 ProviderKind::OpenAi => "OpenAI",
108 ProviderKind::Anthropic => "Anthropic",
109 ProviderKind::Gemini => "Gemini",
110 ProviderKind::Bedrock => "Bedrock",
111 ProviderKind::Azure => "Azure",
112 ProviderKind::OpenRouter => "OpenRouter",
113 ProviderKind::Local => "Local",
114 ProviderKind::Unknown => "Unknown",
115 }
116}
117
118pub const fn all_supported() -> &'static [ProviderKind] {
120 &SUPPORTED
121}
122
123#[cfg(test)]
124mod tests {
125 use serde_json::json;
126
127 use super::{detect_provider, envelope_from_usage};
128 use crate::core::context_kernel::token_envelope::ProviderKind;
129
130 macro_rules! detect_test {
131 ($name:ident, $url:expr, $kind:expr) => {
132 #[test]
133 fn $name() {
134 assert_eq!(detect_provider($url), $kind);
135 }
136 };
137 }
138
139 detect_test!(
140 detect_openai,
141 "https://api.openai.com/v1",
142 ProviderKind::OpenAi
143 );
144 detect_test!(
145 detect_anthropic,
146 "https://api.anthropic.com",
147 ProviderKind::Anthropic
148 );
149 detect_test!(
150 detect_bedrock,
151 "https://bedrock-runtime.us-east-1.amazonaws.com",
152 ProviderKind::Bedrock
153 );
154 detect_test!(
155 detect_azure_foundry,
156 "https://westus.services.ai.azure.com",
157 ProviderKind::Azure
158 );
159 detect_test!(
160 detect_azure_classic,
161 "https://tenant.openai.azure.com",
162 ProviderKind::Azure
163 );
164 detect_test!(
165 detect_gemini,
166 "https://generativelanguage.googleapis.com",
167 ProviderKind::Gemini
168 );
169 detect_test!(
170 detect_localhost,
171 "http://localhost:11434",
172 ProviderKind::Local
173 );
174 detect_test!(detect_unknown, "https://example.com", ProviderKind::Unknown);
175
176 #[test]
177 fn envelope_openai() {
178 let value = json!({"prompt_tokens": 100, "completion_tokens": 50});
179 let envelope = envelope_from_usage(ProviderKind::OpenAi, "gpt", &value);
180 assert_eq!((envelope.input_tokens, envelope.output_tokens), (100, 50));
181 }
182
183 #[test]
184 fn envelope_anthropic() {
185 let value = json!({
186 "input_tokens": 100,
187 "output_tokens": 50,
188 "cache_read_input_tokens": 20
189 });
190 let envelope = envelope_from_usage(ProviderKind::Anthropic, "claude", &value);
191 assert_eq!(envelope.input_tokens, 100);
192 assert_eq!(envelope.output_tokens, 50);
193 assert_eq!(envelope.cache_read_tokens, 20);
194 }
195
196 #[test]
197 fn envelope_empty_safe() {
198 let envelope = envelope_from_usage(ProviderKind::Unknown, "unknown", &json!({}));
199 assert_eq!(envelope.input_tokens, 0);
200 assert_eq!(envelope.output_tokens, 0);
201 assert_eq!(envelope.cache_read_tokens, 0);
202 assert_eq!(envelope.cache_write_tokens, 0);
203 assert_eq!(envelope.reasoning_tokens, 0);
204 }
205}