use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::harness::model::ModelResponse;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderKind {
OpenAi,
Anthropic,
Ollama,
DeepSeek,
Groq,
Xai,
OpenRouter,
Together,
Mistral,
Compatible,
}
impl ProviderKind {
pub fn as_str(&self) -> &'static str {
match self {
ProviderKind::OpenAi => "openai",
ProviderKind::Anthropic => "anthropic",
ProviderKind::Ollama => "ollama",
ProviderKind::DeepSeek => "deepseek",
ProviderKind::Groq => "groq",
ProviderKind::Xai => "xai",
ProviderKind::OpenRouter => "openrouter",
ProviderKind::Together => "together",
ProviderKind::Mistral => "mistral",
ProviderKind::Compatible => "compatible",
}
}
pub fn infer(model: &str) -> Option<Self> {
let lower = model.to_ascii_lowercase();
if let Some((prefix, _)) = lower.split_once(':') {
return match prefix {
"openai" => Some(ProviderKind::OpenAi),
"anthropic" => Some(ProviderKind::Anthropic),
"ollama" => Some(ProviderKind::Ollama),
"deepseek" => Some(ProviderKind::DeepSeek),
"groq" => Some(ProviderKind::Groq),
"xai" => Some(ProviderKind::Xai),
"openrouter" => Some(ProviderKind::OpenRouter),
"together" => Some(ProviderKind::Together),
"mistral" | "mistralai" => Some(ProviderKind::Mistral),
_ => None,
};
}
if lower.starts_with("gpt-")
|| lower.starts_with("o1")
|| lower.starts_with("o3")
|| lower.starts_with("o4")
{
Some(ProviderKind::OpenAi)
} else if lower.starts_with("claude") {
Some(ProviderKind::Anthropic)
} else if lower.starts_with("deepseek") {
Some(ProviderKind::DeepSeek)
} else if lower.starts_with("grok") {
Some(ProviderKind::Xai)
} else if lower.starts_with("mistral") || lower.starts_with("mixtral") {
Some(ProviderKind::Mistral)
} else {
None
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderSpec {
pub kind: ProviderKind,
pub provider: String,
pub model: String,
pub base_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key_env: Option<String>,
#[serde(default)]
pub requires_api_key: bool,
}
impl ProviderSpec {
pub fn for_kind(kind: ProviderKind) -> Self {
match kind {
ProviderKind::OpenAi => Self::new(
kind,
"gpt-4.1-mini",
"https://api.openai.com/v1",
Some("OPENAI_API_KEY"),
true,
),
ProviderKind::Anthropic => Self::new(
kind,
"claude-3-5-sonnet-latest",
"https://api.anthropic.com/v1",
Some("ANTHROPIC_API_KEY"),
true,
),
ProviderKind::Ollama => {
Self::new(kind, "llama3.2", "http://localhost:11434/v1", None, false)
}
ProviderKind::DeepSeek => Self::new(
kind,
"deepseek-chat",
"https://api.deepseek.com/v1",
Some("DEEPSEEK_API_KEY"),
true,
),
ProviderKind::Groq => Self::new(
kind,
"llama-3.3-70b-versatile",
"https://api.groq.com/openai/v1",
Some("GROQ_API_KEY"),
true,
),
ProviderKind::Xai => Self::new(
kind,
"grok-2-latest",
"https://api.x.ai/v1",
Some("XAI_API_KEY"),
true,
),
ProviderKind::OpenRouter => Self::new(
kind,
"openai/gpt-4o-mini",
"https://openrouter.ai/api/v1",
Some("OPENROUTER_API_KEY"),
true,
),
ProviderKind::Together => Self::new(
kind,
"meta-llama/Llama-3.3-70B-Instruct-Turbo",
"https://api.together.xyz/v1",
Some("TOGETHER_API_KEY"),
true,
),
ProviderKind::Mistral => Self::new(
kind,
"mistral-small-latest",
"https://api.mistral.ai/v1",
Some("MISTRAL_API_KEY"),
true,
),
ProviderKind::Compatible => Self::new(kind, "", "", None, true),
}
}
fn new(
kind: ProviderKind,
model: impl Into<String>,
base_url: impl Into<String>,
api_key_env: Option<&str>,
requires_api_key: bool,
) -> Self {
let provider = kind.as_str().to_string();
Self {
kind,
provider,
model: model.into(),
base_url: base_url.into().trim_end_matches('/').to_string(),
api_key_env: api_key_env.map(str::to_string),
requires_api_key,
}
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = model.into();
self
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into().trim_end_matches('/').to_string();
self
}
pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
self.provider = provider.into();
self
}
pub fn with_api_key_env(mut self, env: impl Into<String>) -> Self {
self.api_key_env = Some(env.into());
self
}
}
pub(crate) enum MockBehavior {
Echo,
Constant(String),
Scripted(Vec<ModelResponse>),
ToolCall {
name: String,
arguments: Value,
},
}
#[derive(Default)]
pub(crate) struct MockInner {
pub(crate) call_count: u64,
pub(crate) scripted_index: usize,
}
pub struct MockModel {
pub(crate) behavior: MockBehavior,
pub(crate) inner: Mutex<MockInner>,
}