use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::LazyLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CloudflareModel {
DeepSeekV4Flash,
DeepSeekV4Pro,
Gemma4,
Glm53Flash,
Glm53,
GptOss120B,
KimiK27Code,
Qwen330BA3B,
}
#[derive(Debug, Clone)]
pub struct ModelInfo {
pub model_id: &'static str,
pub display_name: &'static str,
pub max_context_length: u32,
pub max_output_length: Option<u32>,
pub supports_tools: bool,
pub supports_multimodal: bool,
pub supports_streaming: bool,
pub input_cost_per_million: f64,
pub output_cost_per_million: f64,
}
static MODEL_CONFIGS: LazyLock<HashMap<&'static str, ModelInfo>> = LazyLock::new(|| {
let mut configs = HashMap::new();
for (id, name, context, input, output) in [
(
"@cf/deepseek-ai/deepseek-v4-flash-0731",
"deepseek-v4-flash-0731",
1048576,
0.44,
1.32,
),
(
"@cf/deepseek-ai/deepseek-v4-pro-0813",
"deepseek-v4-pro-0813",
1048576,
1.32,
3.96,
),
(
"@cf/google/gemma-4-26b-a4b-it",
"gemma-4-26b-a4b-it",
256000,
0.1,
0.3,
),
(
"@cf/zai-org/glm-5.3-flash",
"glm-5.3-flash",
1048576,
0.15,
0.5,
),
("@cf/zai-org/glm-5.3", "glm-5.3", 1048576, 1.4, 4.4),
(
"@cf/openai/gpt-oss-120b",
"gpt-oss-120b",
128000,
0.35,
0.75,
),
(
"@cf/moonshotai/kimi-k2.7-code",
"kimi-k2.7-code",
262144,
0.95,
4.0,
),
(
"@cf/qwen/qwen3-30b-a3b-fp8",
"qwen3-30b-a3b-fp8",
32768,
0.0509,
0.335,
),
] {
configs.insert(
id,
ModelInfo {
model_id: id,
display_name: name,
max_context_length: context,
max_output_length: None,
supports_tools: true,
supports_multimodal: matches!(
id,
"@cf/google/gemma-4-26b-a4b-it"
| "@cf/zai-org/glm-5.3-flash"
| "@cf/moonshotai/kimi-k2.7-code"
),
supports_streaming: true,
input_cost_per_million: input,
output_cost_per_million: output,
},
);
}
configs
});
pub fn get_model_info(model_id: &str) -> Option<&'static ModelInfo> {
let model_id = model_id.strip_prefix("cloudflare/").unwrap_or(model_id);
MODEL_CONFIGS.get(model_id)
}
pub fn get_available_models() -> Vec<&'static str> {
MODEL_CONFIGS.keys().copied().collect()
}
pub fn calculate_cost(model_id: &str, input_tokens: u32, output_tokens: u32) -> Option<f64> {
get_model_info(model_id).map(|m| {
(f64::from(input_tokens) * m.input_cost_per_million
+ f64::from(output_tokens) * m.output_cost_per_million)
/ 1_000_000.0
})
}
impl CloudflareModel {
pub fn model_id(&self) -> &'static str {
match self {
Self::DeepSeekV4Flash => "@cf/deepseek-ai/deepseek-v4-flash-0731",
Self::DeepSeekV4Pro => "@cf/deepseek-ai/deepseek-v4-pro-0813",
Self::Gemma4 => "@cf/google/gemma-4-26b-a4b-it",
Self::Glm53Flash => "@cf/zai-org/glm-5.3-flash",
Self::Glm53 => "@cf/zai-org/glm-5.3",
Self::GptOss120B => "@cf/openai/gpt-oss-120b",
Self::KimiK27Code => "@cf/moonshotai/kimi-k2.7-code",
Self::Qwen330BA3B => "@cf/qwen/qwen3-30b-a3b-fp8",
}
}
pub fn info(&self) -> Option<&'static ModelInfo> {
get_model_info(self.model_id())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retired_and_unverified_legacy_models_are_not_advertised() {
for id in [
"@cf/meta/llama-3-8b-instruct",
"@cf/meta/llama-2-7b-chat-int8",
"@cf/mistral/mistral-7b-instruct-v0.1",
"@cf/microsoft/phi-2",
] {
assert!(get_model_info(id).is_none());
assert!(!get_available_models().contains(&id));
assert!(calculate_cost(id, 1000, 500).is_none());
}
assert!(get_model_info("@cf/meta/llama-3-70b-instruct").is_none());
assert!(get_model_info("@cf/google/gemma-7b-it").is_none());
}
#[test]
fn current_model_enum_resolves_to_verified_cards_and_round_trips() {
for model in [
CloudflareModel::DeepSeekV4Flash,
CloudflareModel::DeepSeekV4Pro,
CloudflareModel::Gemma4,
CloudflareModel::Glm53Flash,
CloudflareModel::Glm53,
CloudflareModel::GptOss120B,
CloudflareModel::KimiK27Code,
CloudflareModel::Qwen330BA3B,
] {
let info = model.info().expect("current model has a card");
assert_eq!(
get_model_info(&format!("cloudflare/{}", model.model_id()))
.unwrap()
.model_id,
info.model_id
);
assert!(info.supports_streaming && info.supports_tools);
assert!(info.input_cost_per_million > 0.0);
let json = serde_json::to_string(&model).unwrap();
assert_eq!(
serde_json::from_str::<CloudflareModel>(&json).unwrap(),
model
);
}
}
#[test]
fn prices_use_published_rates_before_account_allowances() {
assert!(
(calculate_cost(CloudflareModel::GptOss120B.model_id(), 1_000_000, 1_000_000).unwrap()
- 1.1)
.abs()
< 1e-12
);
assert_eq!(
calculate_cost(CloudflareModel::GptOss120B.model_id(), 0, 0),
Some(0.0)
);
assert!(calculate_cost("unknown", 100, 50).is_none());
}
#[test]
fn vision_and_limits_follow_model_cards() {
assert!(CloudflareModel::Gemma4.info().unwrap().supports_multimodal);
assert!(
CloudflareModel::KimiK27Code
.info()
.unwrap()
.supports_multimodal
);
assert!(
!CloudflareModel::GptOss120B
.info()
.unwrap()
.supports_multimodal
);
assert_eq!(
CloudflareModel::Glm53.info().unwrap().max_context_length,
1_048_576
);
assert_eq!(
CloudflareModel::Glm53.info().unwrap().max_output_length,
None
);
}
}