use serde_json::Value;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Provider {
Anthropic,
OpenAi,
OpenRouter,
Gemini,
Custom,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ProviderPromo {
pub badge: &'static str,
pub note: &'static str,
pub signup_url: &'static str,
}
impl Provider {
pub const ALL: [Provider; 5] = [
Provider::OpenRouter,
Provider::Gemini,
Provider::Anthropic,
Provider::OpenAi,
Provider::Custom,
];
pub fn display_name(self) -> &'static str {
match self {
Provider::Anthropic => "Anthropic",
Provider::OpenAi => "OpenAI",
Provider::OpenRouter => "OpenRouter",
Provider::Gemini => "Google Gemini",
Provider::Custom => "Custom (OpenAI-compatible)",
}
}
pub fn short_name(self) -> &'static str {
match self {
Provider::Anthropic => "Anthropic",
Provider::OpenAi => "OpenAI",
Provider::OpenRouter => "OpenRouter",
Provider::Gemini => "Gemini",
Provider::Custom => "Custom",
}
}
pub fn default_model(self) -> &'static str {
match self {
Provider::Anthropic => "claude-haiku-4-5",
Provider::OpenAi => "gpt-4o-mini",
Provider::OpenRouter => "meta-llama/llama-3.3-70b-instruct:free",
Provider::Gemini => "gemini-3.1-flash-lite",
Provider::Custom => "",
}
}
pub fn needs_base_url(self) -> bool {
matches!(self, Provider::Custom)
}
pub fn default_base_url(self) -> &'static str {
match self {
Provider::Custom => "http://localhost:11434/v1",
_ => "",
}
}
pub fn promo(self) -> Option<ProviderPromo> {
match self {
Provider::OpenRouter => Some(ProviderPromo {
badge: "free",
note: "Easiest to start \u{2014} free models, no credit card. One key, many models.",
signup_url: "https://openrouter.ai/keys",
}),
Provider::Gemini => Some(ProviderPromo {
badge: "free",
note: "Easiest to start \u{2014} generous free tier, no credit card.",
signup_url: "https://aistudio.google.com/apikey",
}),
_ => None,
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct LlmConfig {
pub provider: Provider,
pub api_key: String,
pub model: String,
pub base_url: String,
pub max_tokens: u32,
}
impl LlmConfig {
pub fn new(provider: Provider) -> Self {
LlmConfig {
provider,
api_key: String::new(),
model: provider.default_model().to_string(),
base_url: provider.default_base_url().to_string(),
max_tokens: 1024,
}
}
pub(crate) fn effective_model(&self) -> String {
let m = self.model.trim();
if m.is_empty() {
self.provider.default_model().to_string()
} else {
m.to_string()
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct ToolDecl {
pub name: String,
pub description: String,
pub input_schema: Value,
}
#[derive(Clone, PartialEq, Debug)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub args: Value,
pub thought_signature: Option<String>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct ToolResult {
pub id: String,
pub name: String,
pub content: String,
pub is_error: bool,
}
#[derive(Clone, PartialEq, Debug, Default)]
pub struct ChatResponse {
pub text: Option<String>,
pub tool_calls: Vec<ToolCall>,
}
#[derive(Clone, PartialEq, Debug)]
pub enum Turn {
User(String),
Assistant(String),
AssistantTools {
text: Option<String>,
calls: Vec<ToolCall>,
},
ToolResults(Vec<ToolResult>),
}
impl Turn {
pub fn user(s: impl Into<String>) -> Self {
Turn::User(s.into())
}
pub fn assistant(s: impl Into<String>) -> Self {
Turn::Assistant(s.into())
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
#[test]
fn openrouter_first_gemini_second() {
assert_eq!(Provider::ALL[0], Provider::OpenRouter);
assert_eq!(Provider::ALL[1], Provider::Gemini);
}
#[test]
fn openrouter_default_model_is_free() {
assert!(Provider::OpenRouter.default_model().ends_with(":free"));
assert_eq!(Provider::Gemini.default_model(), "gemini-3.1-flash-lite");
}
#[test]
fn promo_only_for_free_browser_providers() {
assert!(Provider::OpenRouter.promo().is_some());
assert!(Provider::Gemini.promo().is_some());
assert!(Provider::Anthropic.promo().is_none());
assert!(Provider::OpenAi.promo().is_none());
assert!(Provider::Custom.promo().is_none());
}
}