mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
Add support for OpenAI, Anthropic, Ollama, and OpenAI-compatible endpoints alongside the existing NEAR AI backend. Users can now bring their own API keys via environment variables (LLM_BACKEND, OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) while NEAR AI remains the default. Co-authored-by: Claude Opus 4.6 <[email protected]>
125 lines
4.2 KiB
Rust
125 lines
4.2 KiB
Rust
//! Per-model cost lookup table for multi-provider LLM support.
|
|
//!
|
|
//! Returns (input_cost_per_token, output_cost_per_token) as Decimal pairs.
|
|
//! Ollama and other local models return zero cost.
|
|
|
|
use rust_decimal::Decimal;
|
|
use rust_decimal_macros::dec;
|
|
|
|
/// Look up known per-token costs for a model by its identifier.
|
|
///
|
|
/// Returns `Some((input_cost, output_cost))` for known models, `None` otherwise.
|
|
pub fn model_cost(model_id: &str) -> Option<(Decimal, Decimal)> {
|
|
// Normalize: strip provider prefixes (e.g., "openai/gpt-4o" -> "gpt-4o")
|
|
let id = model_id
|
|
.rsplit_once('/')
|
|
.map(|(_, name)| name)
|
|
.unwrap_or(model_id);
|
|
|
|
match id {
|
|
// OpenAI models -- prices per token (USD)
|
|
"gpt-4o" | "gpt-4o-2024-11-20" | "gpt-4o-2024-08-06" => {
|
|
Some((dec!(0.0000025), dec!(0.00001)))
|
|
}
|
|
"gpt-4o-mini" | "gpt-4o-mini-2024-07-18" => Some((dec!(0.00000015), dec!(0.0000006))),
|
|
"gpt-4-turbo" | "gpt-4-turbo-2024-04-09" => Some((dec!(0.00001), dec!(0.00003))),
|
|
"gpt-4" | "gpt-4-0613" => Some((dec!(0.00003), dec!(0.00006))),
|
|
"gpt-3.5-turbo" | "gpt-3.5-turbo-0125" => Some((dec!(0.0000005), dec!(0.0000015))),
|
|
"o1" | "o1-2024-12-17" => Some((dec!(0.000015), dec!(0.00006))),
|
|
"o1-mini" | "o1-mini-2024-09-12" => Some((dec!(0.000003), dec!(0.000012))),
|
|
"o3-mini" | "o3-mini-2025-01-31" => Some((dec!(0.0000011), dec!(0.0000044))),
|
|
|
|
// Anthropic models
|
|
"claude-3-5-sonnet-20241022" | "claude-3-5-sonnet-latest" | "claude-sonnet-4-20250514" => {
|
|
Some((dec!(0.000003), dec!(0.000015)))
|
|
}
|
|
"claude-3-5-haiku-20241022" | "claude-3-5-haiku-latest" => {
|
|
Some((dec!(0.0000008), dec!(0.000004)))
|
|
}
|
|
"claude-3-opus-20240229" | "claude-3-opus-latest" | "claude-opus-4-20250514" => {
|
|
Some((dec!(0.000015), dec!(0.000075)))
|
|
}
|
|
"claude-3-haiku-20240307" => Some((dec!(0.00000025), dec!(0.00000125))),
|
|
|
|
// Ollama / local models -- free
|
|
_ if is_local_model(id) => Some((Decimal::ZERO, Decimal::ZERO)),
|
|
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Default cost for unknown models.
|
|
pub fn default_cost() -> (Decimal, Decimal) {
|
|
// Conservative estimate: roughly GPT-4o pricing
|
|
(dec!(0.0000025), dec!(0.00001))
|
|
}
|
|
|
|
/// Heuristic to detect local/self-hosted models (Ollama, llama.cpp, etc.).
|
|
fn is_local_model(model_id: &str) -> bool {
|
|
let lower = model_id.to_lowercase();
|
|
lower.starts_with("llama")
|
|
|| lower.starts_with("mistral")
|
|
|| lower.starts_with("mixtral")
|
|
|| lower.starts_with("phi")
|
|
|| lower.starts_with("gemma")
|
|
|| lower.starts_with("qwen")
|
|
|| lower.starts_with("codellama")
|
|
|| lower.starts_with("deepseek")
|
|
|| lower.starts_with("starcoder")
|
|
|| lower.starts_with("vicuna")
|
|
|| lower.starts_with("yi")
|
|
|| lower.contains(":latest")
|
|
|| lower.contains(":instruct")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_known_model_costs() {
|
|
let (input, output) = model_cost("gpt-4o").unwrap();
|
|
assert!(input > Decimal::ZERO);
|
|
assert!(output > input);
|
|
}
|
|
|
|
#[test]
|
|
fn test_claude_costs() {
|
|
let (input, output) = model_cost("claude-3-5-sonnet-20241022").unwrap();
|
|
assert!(input > Decimal::ZERO);
|
|
assert!(output > input);
|
|
}
|
|
|
|
#[test]
|
|
fn test_local_model_free() {
|
|
let (input, output) = model_cost("llama3").unwrap();
|
|
assert_eq!(input, Decimal::ZERO);
|
|
assert_eq!(output, Decimal::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ollama_tagged_model_free() {
|
|
let (input, output) = model_cost("mistral:latest").unwrap();
|
|
assert_eq!(input, Decimal::ZERO);
|
|
assert_eq!(output, Decimal::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn test_unknown_model_returns_none() {
|
|
assert!(model_cost("some-totally-unknown-model-xyz").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_cost_nonzero() {
|
|
let (input, output) = default_cost();
|
|
assert!(input > Decimal::ZERO);
|
|
assert!(output > Decimal::ZERO);
|
|
}
|
|
|
|
#[test]
|
|
fn test_provider_prefix_stripped() {
|
|
// "openai/gpt-4o" should resolve to same as "gpt-4o"
|
|
assert_eq!(model_cost("openai/gpt-4o"), model_cost("gpt-4o"));
|
|
}
|
|
}
|