1use crate::config::ReasoningEffort;
32
33#[derive(Debug)]
35pub struct HttpPreset {
36 pub endpoint: Option<&'static str>,
37 pub api_key_env: Option<&'static str>,
38 pub protocol: Option<&'static str>,
39 pub key_url: Option<&'static str>,
40 pub max_tokens: Option<u32>,
41 pub temperature: Option<f32>,
42}
43
44#[derive(Debug)]
46pub struct CodexPreset {
47 pub reasoning_effort: Option<ReasoningEffort>,
48 pub max_concurrent: usize,
49}
50
51#[derive(Debug)]
53pub enum PresetBackend {
54 Http(HttpPreset),
55 Codex(CodexPreset),
56}
57
58#[derive(Debug)]
60pub struct LlmPreset {
61 pub key: &'static str,
63 pub display_name: &'static str,
65 pub description: &'static str,
67 pub backend: PresetBackend,
70 pub default_model: Option<&'static str>,
72 pub timeout_secs: Option<u64>,
74}
75
76pub static PRESETS: &[&LlmPreset] = &[
81 &LOCAL,
82 &OPENROUTER,
83 &ZAI,
84 &MINIMAX,
85 &KIMI,
86 &OPENAI,
87 &CODEX,
88 &CUSTOM,
89];
90
91pub static LOCAL: LlmPreset = LlmPreset {
93 key: "local",
94 display_name: "Local model",
95 description: "LM Studio, Ollama or llama.cpp on this machine. No key, no cost.",
96 backend: PresetBackend::Http(HttpPreset {
97 endpoint: Some("http://localhost:1234/v1"),
98 key_url: None,
99 api_key_env: None,
100 protocol: None,
101 max_tokens: None,
102 temperature: Some(0.2),
103 }),
104 default_model: Some("qwen3-30b-a3b"),
105 timeout_secs: None,
106};
107
108pub static OPENROUTER: LlmPreset = LlmPreset {
110 key: "openrouter",
111 display_name: "OpenRouter",
112 description: "One key for many providers. Good default for cloud analysis.",
113 backend: PresetBackend::Http(HttpPreset {
114 endpoint: Some("https://openrouter.ai/api/v1"),
115 key_url: Some("https://openrouter.ai/keys"),
116 api_key_env: Some("OPENROUTER_API_KEY"),
117 protocol: None,
118 max_tokens: None,
119 temperature: Some(0.2),
120 }),
121 default_model: Some("deepseek/deepseek-v4-pro-0813"),
122 timeout_secs: Some(1800),
123};
124
125pub static OPENAI: LlmPreset = LlmPreset {
127 key: "openai",
128 display_name: "OpenAI API",
129 description: "Directly against the OpenAI API.",
130 backend: PresetBackend::Http(HttpPreset {
131 endpoint: Some("https://api.openai.com/v1"),
132 key_url: Some("https://platform.openai.com/api-keys"),
133 api_key_env: Some("OPENAI_API_KEY"),
134 protocol: None,
135 max_tokens: None,
136 temperature: None,
138 }),
139 default_model: Some("gpt-5.6-sol"),
140 timeout_secs: Some(1800),
141};
142
143pub static CODEX: LlmPreset = LlmPreset {
145 key: "codex",
146 display_name: "ChatGPT / Codex subscription",
147 description: "Codex CLI with ChatGPT-managed authentication; no API billing.",
148 backend: PresetBackend::Codex(CodexPreset {
149 reasoning_effort: Some(ReasoningEffort::High),
150 max_concurrent: 1,
151 }),
152 default_model: Some("gpt-5.6-sol"),
153 timeout_secs: Some(1800),
154};
155
156pub static ZAI: LlmPreset = LlmPreset {
158 key: "zai",
159 display_name: "z.ai GLM Coding Plan",
160 description: "GLM models on a coding-plan subscription. OpenAI-compatible.",
161 backend: PresetBackend::Http(HttpPreset {
162 endpoint: Some("https://api.z.ai/api/coding/paas/v4"),
163 key_url: Some("https://z.ai/manage-apikey/apikey-list"),
164 api_key_env: Some("ZAI_API_KEY"),
165 protocol: None,
166 max_tokens: None,
167 temperature: Some(0.2),
168 }),
169 default_model: Some("glm-5.3"),
170 timeout_secs: Some(1800),
171};
172
173pub static MINIMAX: LlmPreset = LlmPreset {
181 key: "minimax",
182 display_name: "MiniMax Token Plan",
183 description: "MiniMax M-series on a token-plan subscription. Anthropic protocol.",
184 backend: PresetBackend::Http(HttpPreset {
185 endpoint: Some("https://api.minimax.io/anthropic/v1"),
186 key_url: Some("https://platform.minimax.io/user-center/payment/token-plan"),
187 api_key_env: Some("MINIMAX_API_KEY"),
188 protocol: Some("anthropic"),
189 max_tokens: None,
190 temperature: Some(0.2),
191 }),
192 default_model: Some("MiniMax-M3"),
193 timeout_secs: Some(1800),
194};
195
196pub static KIMI: LlmPreset = LlmPreset {
198 key: "kimi",
199 display_name: "Kimi for Coding",
200 description: "Moonshot's k3 on a coding-plan subscription. Anthropic protocol.",
201 backend: PresetBackend::Http(HttpPreset {
202 endpoint: Some("https://api.kimi.com/coding/v1"),
203 key_url: Some("https://www.kimi.com/code"),
204 api_key_env: Some("KIMI_API_KEY"),
205 protocol: Some("anthropic"),
206 max_tokens: Some(200_000),
212 temperature: None,
215 }),
216 default_model: Some("k3"),
217 timeout_secs: Some(1800),
218};
219
220pub static CUSTOM: LlmPreset = LlmPreset {
222 key: "custom",
223 display_name: "Custom endpoint",
224 description: "Any other OpenAI-compatible endpoint.",
225 backend: PresetBackend::Http(HttpPreset {
226 endpoint: None,
227 key_url: None,
228 api_key_env: Some("LLM_API_KEY"),
229 protocol: None,
230 max_tokens: None,
231 temperature: Some(0.2),
232 }),
233 default_model: None,
234 timeout_secs: None,
235};
236
237impl LlmPreset {
238 #[cfg(test)]
239 pub fn backend_kind(&self) -> crate::config::BackendKind {
240 match self.backend {
241 PresetBackend::Http(_) => crate::config::BackendKind::Http,
242 PresetBackend::Codex(_) => crate::config::BackendKind::Codex,
243 }
244 }
245
246 pub fn http(&self) -> Option<&HttpPreset> {
247 match &self.backend {
248 PresetBackend::Http(http) => Some(http),
249 PresetBackend::Codex(_) => None,
250 }
251 }
252
253 #[cfg(test)]
254 pub fn codex(&self) -> Option<&CodexPreset> {
255 match &self.backend {
256 PresetBackend::Codex(codex) => Some(codex),
257 PresetBackend::Http(_) => None,
258 }
259 }
260
261 pub fn endpoint(&self) -> Option<&'static str> {
262 self.http().and_then(|http| http.endpoint)
263 }
264
265 pub fn api_key_env(&self) -> Option<&'static str> {
266 self.http().and_then(|http| http.api_key_env)
267 }
268
269 pub fn key_url(&self) -> Option<&'static str> {
270 self.http().and_then(|http| http.key_url)
271 }
272
273 #[cfg(test)]
274 pub fn protocol_name(&self) -> Option<&'static str> {
275 self.http().and_then(|http| http.protocol)
276 }
277
278 #[cfg(test)]
279 pub fn max_tokens(&self) -> Option<u32> {
280 self.http().and_then(|http| http.max_tokens)
281 }
282
283 #[cfg(test)]
284 pub fn temperature(&self) -> Option<f32> {
285 self.http().and_then(|http| http.temperature)
286 }
287
288 pub fn quirks(&self) -> crate::llm::quirks::Quirks {
294 match self.http() {
295 Some(http) => crate::llm::quirks::Quirks {
296 temperature: http.temperature,
297 max_tokens: http.max_tokens,
298 max_tokens_from_registry: false,
299 },
300 None => crate::llm::quirks::Quirks {
301 temperature: None,
302 max_tokens: None,
303 max_tokens_from_registry: false,
304 },
305 }
306 }
307
308 pub fn protocol(&self) -> open_agent::ApiProtocol {
316 let http = self
317 .http()
318 .unwrap_or_else(|| panic!("preset `{}` has no HTTP wire protocol", self.key));
319 crate::config::parse_protocol(http.protocol)
320 .unwrap_or_else(|| panic!("preset `{}` names an unknown protocol", self.key))
321 }
322}
323
324pub fn preset(key: &str) -> Option<&'static LlmPreset> {
326 PRESETS.iter().copied().find(|p| p.key == key)
327}
328
329pub fn preset_keys() -> Vec<&'static str> {
331 PRESETS.iter().map(|p| p.key).collect()
332}