use crate::config::ReasoningEffort;
#[derive(Debug)]
pub struct HttpPreset {
pub endpoint: Option<&'static str>,
pub api_key_env: Option<&'static str>,
pub protocol: Option<&'static str>,
pub key_url: Option<&'static str>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
}
#[derive(Debug)]
pub struct CodexPreset {
pub reasoning_effort: Option<ReasoningEffort>,
pub max_concurrent: usize,
}
#[derive(Debug)]
pub enum PresetBackend {
Http(HttpPreset),
Codex(CodexPreset),
}
#[derive(Debug)]
pub struct LlmPreset {
pub key: &'static str,
pub display_name: &'static str,
pub description: &'static str,
pub backend: PresetBackend,
pub default_model: Option<&'static str>,
pub timeout_secs: Option<u64>,
}
pub static PRESETS: &[&LlmPreset] = &[
&LOCAL,
&OPENROUTER,
&ZAI,
&MINIMAX,
&KIMI,
&OPENAI,
&CODEX,
&CUSTOM,
];
pub static LOCAL: LlmPreset = LlmPreset {
key: "local",
display_name: "Local model",
description: "LM Studio, Ollama or llama.cpp on this machine. No key, no cost.",
backend: PresetBackend::Http(HttpPreset {
endpoint: Some("http://localhost:1234/v1"),
key_url: None,
api_key_env: None,
protocol: None,
max_tokens: None,
temperature: Some(0.2),
}),
default_model: Some("qwen3-30b-a3b"),
timeout_secs: None,
};
pub static OPENROUTER: LlmPreset = LlmPreset {
key: "openrouter",
display_name: "OpenRouter",
description: "One key for many providers. Good default for cloud analysis.",
backend: PresetBackend::Http(HttpPreset {
endpoint: Some("https://openrouter.ai/api/v1"),
key_url: Some("https://openrouter.ai/keys"),
api_key_env: Some("OPENROUTER_API_KEY"),
protocol: None,
max_tokens: None,
temperature: Some(0.2),
}),
default_model: Some("deepseek/deepseek-v4-pro-0813"),
timeout_secs: Some(1800),
};
pub static OPENAI: LlmPreset = LlmPreset {
key: "openai",
display_name: "OpenAI API",
description: "Directly against the OpenAI API.",
backend: PresetBackend::Http(HttpPreset {
endpoint: Some("https://api.openai.com/v1"),
key_url: Some("https://platform.openai.com/api-keys"),
api_key_env: Some("OPENAI_API_KEY"),
protocol: None,
max_tokens: None,
temperature: None,
}),
default_model: Some("gpt-5.6-sol"),
timeout_secs: Some(1800),
};
pub static CODEX: LlmPreset = LlmPreset {
key: "codex",
display_name: "ChatGPT / Codex subscription",
description: "Codex CLI with ChatGPT-managed authentication; no API billing.",
backend: PresetBackend::Codex(CodexPreset {
reasoning_effort: Some(ReasoningEffort::High),
max_concurrent: 1,
}),
default_model: Some("gpt-5.6-sol"),
timeout_secs: Some(1800),
};
pub static ZAI: LlmPreset = LlmPreset {
key: "zai",
display_name: "z.ai GLM Coding Plan",
description: "GLM models on a coding-plan subscription. OpenAI-compatible.",
backend: PresetBackend::Http(HttpPreset {
endpoint: Some("https://api.z.ai/api/coding/paas/v4"),
key_url: Some("https://z.ai/manage-apikey/apikey-list"),
api_key_env: Some("ZAI_API_KEY"),
protocol: None,
max_tokens: None,
temperature: Some(0.2),
}),
default_model: Some("glm-5.3"),
timeout_secs: Some(1800),
};
pub static MINIMAX: LlmPreset = LlmPreset {
key: "minimax",
display_name: "MiniMax Token Plan",
description: "MiniMax M-series on a token-plan subscription. Anthropic protocol.",
backend: PresetBackend::Http(HttpPreset {
endpoint: Some("https://api.minimax.io/anthropic/v1"),
key_url: Some("https://platform.minimax.io/user-center/payment/token-plan"),
api_key_env: Some("MINIMAX_API_KEY"),
protocol: Some("anthropic"),
max_tokens: None,
temperature: Some(0.2),
}),
default_model: Some("MiniMax-M3"),
timeout_secs: Some(1800),
};
pub static KIMI: LlmPreset = LlmPreset {
key: "kimi",
display_name: "Kimi for Coding",
description: "Moonshot's k3 on a coding-plan subscription. Anthropic protocol.",
backend: PresetBackend::Http(HttpPreset {
endpoint: Some("https://api.kimi.com/coding/v1"),
key_url: Some("https://www.kimi.com/code"),
api_key_env: Some("KIMI_API_KEY"),
protocol: Some("anthropic"),
max_tokens: Some(200_000),
temperature: None,
}),
default_model: Some("k3"),
timeout_secs: Some(1800),
};
pub static CUSTOM: LlmPreset = LlmPreset {
key: "custom",
display_name: "Custom endpoint",
description: "Any other OpenAI-compatible endpoint.",
backend: PresetBackend::Http(HttpPreset {
endpoint: None,
key_url: None,
api_key_env: Some("LLM_API_KEY"),
protocol: None,
max_tokens: None,
temperature: Some(0.2),
}),
default_model: None,
timeout_secs: None,
};
impl LlmPreset {
#[cfg(test)]
pub fn backend_kind(&self) -> crate::config::BackendKind {
match self.backend {
PresetBackend::Http(_) => crate::config::BackendKind::Http,
PresetBackend::Codex(_) => crate::config::BackendKind::Codex,
}
}
pub fn http(&self) -> Option<&HttpPreset> {
match &self.backend {
PresetBackend::Http(http) => Some(http),
PresetBackend::Codex(_) => None,
}
}
#[cfg(test)]
pub fn codex(&self) -> Option<&CodexPreset> {
match &self.backend {
PresetBackend::Codex(codex) => Some(codex),
PresetBackend::Http(_) => None,
}
}
pub fn endpoint(&self) -> Option<&'static str> {
self.http().and_then(|http| http.endpoint)
}
pub fn api_key_env(&self) -> Option<&'static str> {
self.http().and_then(|http| http.api_key_env)
}
pub fn key_url(&self) -> Option<&'static str> {
self.http().and_then(|http| http.key_url)
}
#[cfg(test)]
pub fn protocol_name(&self) -> Option<&'static str> {
self.http().and_then(|http| http.protocol)
}
#[cfg(test)]
pub fn max_tokens(&self) -> Option<u32> {
self.http().and_then(|http| http.max_tokens)
}
#[cfg(test)]
pub fn temperature(&self) -> Option<f32> {
self.http().and_then(|http| http.temperature)
}
pub fn quirks(&self) -> crate::llm::quirks::Quirks {
match self.http() {
Some(http) => crate::llm::quirks::Quirks {
temperature: http.temperature,
max_tokens: http.max_tokens,
max_tokens_from_registry: false,
},
None => crate::llm::quirks::Quirks {
temperature: None,
max_tokens: None,
max_tokens_from_registry: false,
},
}
}
pub fn protocol(&self) -> open_agent::ApiProtocol {
let http = self
.http()
.unwrap_or_else(|| panic!("preset `{}` has no HTTP wire protocol", self.key));
crate::config::parse_protocol(http.protocol)
.unwrap_or_else(|| panic!("preset `{}` names an unknown protocol", self.key))
}
}
pub fn preset(key: &str) -> Option<&'static LlmPreset> {
PRESETS.iter().copied().find(|p| p.key == key)
}
pub fn preset_keys() -> Vec<&'static str> {
PRESETS.iter().map(|p| p.key).collect()
}