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