Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:28:52.096Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

opencode.rs

907 lines · 30.5 KB · rust
1use anyhow::{Result, anyhow};
2use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
3use http_client::{
4    AsyncBody, CustomHeaders, HttpClient, Method, Request as HttpRequest, RequestBuilderExt,
5};
6use language_model_core::ReasoningEffort;
7use serde::{Deserialize, Serialize};
8use strum::EnumIter;
9
10pub const OPENCODE_API_URL: &str = "https://opencode.ai/zen";
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
14#[serde(rename_all = "snake_case")]
15pub enum ApiProtocol {
16    #[default]
17    Anthropic,
18    OpenAiResponses,
19    OpenAiChat,
20    Google,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
25#[serde(rename_all = "snake_case")]
26pub enum OpenCodeSubscription {
27    Zen,
28    Go,
29    Free,
30}
31
32impl OpenCodeSubscription {
33    pub fn display_name(&self) -> &'static str {
34        match self {
35            Self::Zen => "Zen",
36            Self::Go => "Go",
37            Self::Free => "Free",
38        }
39    }
40
41    pub fn id_prefix(&self) -> &'static str {
42        match self {
43            Self::Zen => "zen",
44            Self::Go => "go",
45            Self::Free => "free",
46        }
47    }
48
49    pub fn api_path_suffix(&self) -> &'static str {
50        match self {
51            Self::Zen | Self::Free => "",
52            Self::Go => "/go",
53        }
54    }
55}
56
57#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
58#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
59pub enum Model {
60    // -- Anthropic protocol models --
61    #[serde(rename = "claude-opus-4-8")]
62    ClaudeOpus4_8,
63    #[serde(rename = "claude-opus-4-7")]
64    ClaudeOpus4_7,
65    #[serde(rename = "claude-opus-4-6")]
66    ClaudeOpus4_6,
67    #[serde(rename = "claude-opus-4-5")]
68    ClaudeOpus4_5,
69    #[serde(rename = "claude-opus-4-1")]
70    ClaudeOpus4_1,
71    #[default]
72    #[serde(rename = "claude-sonnet-4-6")]
73    ClaudeSonnet4_6,
74    #[serde(rename = "claude-sonnet-4-5")]
75    ClaudeSonnet4_5,
76    #[serde(rename = "claude-sonnet-4")]
77    ClaudeSonnet4,
78    #[serde(rename = "claude-haiku-4-5")]
79    ClaudeHaiku4_5,
80    #[serde(rename = "claude-sonnet-5")]
81    ClaudeSonnet5,
82    #[serde(rename = "claude-fable-5")]
83    ClaudeFable5,
84
85    // -- OpenAI Responses API models --
86    #[serde(rename = "gpt-5.6-sol")]
87    Gpt5_6Sol,
88    #[serde(rename = "gpt-5.6-terra")]
89    Gpt5_6Terra,
90    #[serde(rename = "gpt-5.6-luna")]
91    Gpt5_6Luna,
92    #[serde(rename = "gpt-5.5")]
93    Gpt5_5,
94    #[serde(rename = "gpt-5.5-pro")]
95    Gpt5_5Pro,
96    #[serde(rename = "gpt-5.4")]
97    Gpt5_4,
98    #[serde(rename = "gpt-5.4-pro")]
99    Gpt5_4Pro,
100    #[serde(rename = "gpt-5.4-mini")]
101    Gpt5_4Mini,
102    #[serde(rename = "gpt-5.4-nano")]
103    Gpt5_4Nano,
104    #[serde(rename = "gpt-5.3-codex")]
105    Gpt5_3Codex,
106    #[serde(rename = "gpt-5.3-codex-spark")]
107    Gpt5_3Spark,
108    #[serde(rename = "gpt-5.2")]
109    Gpt5_2,
110    #[serde(rename = "gpt-5.2-codex")]
111    Gpt5_2Codex,
112    #[serde(rename = "gpt-5.1")]
113    Gpt5_1,
114    #[serde(rename = "gpt-5.1-codex")]
115    Gpt5_1Codex,
116    #[serde(rename = "gpt-5.1-codex-max")]
117    Gpt5_1CodexMax,
118    #[serde(rename = "gpt-5.1-codex-mini")]
119    Gpt5_1CodexMini,
120    #[serde(rename = "gpt-5")]
121    Gpt5,
122    #[serde(rename = "gpt-5-codex")]
123    Gpt5Codex,
124    #[serde(rename = "gpt-5-nano")]
125    Gpt5Nano,
126
127    // -- Google protocol models --
128    #[serde(rename = "gemini-3.1-pro")]
129    Gemini3_1Pro,
130    #[serde(rename = "gemini-3-flash")]
131    Gemini3Flash,
132    #[serde(rename = "gemini-3.5-flash")]
133    Gemini3_5Flash,
134
135    // -- OpenAI Chat Completions protocol models --
136    #[serde(rename = "deepseek-v4-pro")]
137    DeepSeekV4Pro,
138    #[serde(rename = "deepseek-v4-flash")]
139    DeepSeekV4Flash,
140    #[serde(rename = "minimax-m2.5")]
141    MiniMaxM2_5,
142    #[serde(rename = "glm-5")]
143    Glm5,
144    #[serde(rename = "glm-5.1")]
145    Glm5_1,
146    #[serde(rename = "glm-5.2")]
147    Glm5_2,
148    #[serde(rename = "grok-build-0.1")]
149    GrokBuild0_1,
150    #[serde(rename = "grok-4.5")]
151    Grok4_5,
152    #[serde(rename = "kimi-k2.5")]
153    KimiK2_5,
154    #[serde(rename = "kimi-k2.6")]
155    KimiK2_6,
156    #[serde(rename = "kimi-k2.7-code")]
157    KimiK2_7Code,
158    #[serde(rename = "minimax-m2.7")]
159    MiniMaxM2_7,
160    #[serde(rename = "minimax-m3")]
161    MiniMaxM3,
162    #[serde(rename = "mimo-v2.5-pro")]
163    MimoV2_5Pro,
164    #[serde(rename = "mimo-v2.5")]
165    MimoV2_5,
166    #[serde(rename = "big-pickle")]
167    BigPickle,
168    #[serde(rename = "nemotron-3-ultra-free")]
169    Nemotron3UltraFree,
170    #[serde(rename = "qwen3.5-plus")]
171    Qwen3_5Plus,
172    #[serde(rename = "qwen3.6-plus")]
173    Qwen3_6Plus,
174    #[serde(rename = "qwen3.7-plus")]
175    Qwen3_7Plus,
176    #[serde(rename = "qwen3.7-max")]
177    Qwen3_7Max,
178
179    // -- Custom model --
180    #[serde(rename = "custom")]
181    Custom {
182        name: String,
183        display_name: Option<String>,
184        max_tokens: u64,
185        max_output_tokens: Option<u64>,
186        protocol: ApiProtocol,
187        reasoning_effort_levels: Option<Vec<ReasoningEffort>>,
188        custom_model_api_url: Option<String>,
189        interleaved_reasoning: bool,
190    },
191}
192
193impl Model {
194    pub fn default_fast() -> Self {
195        Self::ClaudeHaiku4_5
196    }
197
198    pub fn default_go() -> Self {
199        Self::KimiK2_6
200    }
201
202    pub fn default_go_fast() -> Self {
203        Self::MiniMaxM2_7
204    }
205
206    pub fn default_free() -> Self {
207        Self::BigPickle
208    }
209
210    pub fn default_free_fast() -> Self {
211        Self::Nemotron3UltraFree
212    }
213
214    pub fn available_subscriptions(&self) -> &'static [OpenCodeSubscription] {
215        match self {
216            // Models available in both Zen and Go
217            Self::Glm5_1
218            | Self::Glm5_2
219            | Self::KimiK2_6
220            | Self::KimiK2_7Code
221            | Self::MiniMaxM2_7
222            | Self::MiniMaxM3
223            | Self::DeepSeekV4Pro
224            | Self::DeepSeekV4Flash
225            | Self::Qwen3_6Plus => &[OpenCodeSubscription::Zen, OpenCodeSubscription::Go],
226
227            // Go-only models
228            Self::MimoV2_5Pro | Self::MimoV2_5 | Self::Qwen3_7Plus | Self::Qwen3_7Max => {
229                &[OpenCodeSubscription::Go]
230            }
231
232            // Deprecated on Go (per models.dev); still offered on Zen
233            Self::Glm5 | Self::KimiK2_5 | Self::MiniMaxM2_5 | Self::Qwen3_5Plus => {
234                &[OpenCodeSubscription::Zen]
235            }
236
237            // Free models
238            Self::Nemotron3UltraFree | Self::BigPickle => &[OpenCodeSubscription::Free],
239
240            // Custom models get their subscription from settings, not from here
241            Self::Custom { .. } => &[],
242
243            // All other built-in models are Zen-only
244            _ => &[OpenCodeSubscription::Zen],
245        }
246    }
247
248    pub fn id(&self) -> &str {
249        match self {
250            Self::ClaudeOpus4_8 => "claude-opus-4-8",
251            Self::ClaudeOpus4_7 => "claude-opus-4-7",
252            Self::ClaudeOpus4_6 => "claude-opus-4-6",
253            Self::ClaudeOpus4_5 => "claude-opus-4-5",
254            Self::ClaudeOpus4_1 => "claude-opus-4-1",
255            Self::ClaudeSonnet4_6 => "claude-sonnet-4-6",
256            Self::ClaudeSonnet4_5 => "claude-sonnet-4-5",
257            Self::ClaudeSonnet4 => "claude-sonnet-4",
258            Self::ClaudeHaiku4_5 => "claude-haiku-4-5",
259            Self::ClaudeSonnet5 => "claude-sonnet-5",
260            Self::ClaudeFable5 => "claude-fable-5",
261
262            Self::Gpt5_6Sol => "gpt-5.6-sol",
263            Self::Gpt5_6Terra => "gpt-5.6-terra",
264            Self::Gpt5_6Luna => "gpt-5.6-luna",
265            Self::Gpt5_5 => "gpt-5.5",
266            Self::Gpt5_5Pro => "gpt-5.5-pro",
267            Self::Gpt5_4 => "gpt-5.4",
268            Self::Gpt5_4Pro => "gpt-5.4-pro",
269            Self::Gpt5_4Mini => "gpt-5.4-mini",
270            Self::Gpt5_4Nano => "gpt-5.4-nano",
271            Self::Gpt5_3Codex => "gpt-5.3-codex",
272            Self::Gpt5_3Spark => "gpt-5.3-codex-spark",
273            Self::Gpt5_2 => "gpt-5.2",
274            Self::Gpt5_2Codex => "gpt-5.2-codex",
275            Self::Gpt5_1 => "gpt-5.1",
276            Self::Gpt5_1Codex => "gpt-5.1-codex",
277            Self::Gpt5_1CodexMax => "gpt-5.1-codex-max",
278            Self::Gpt5_1CodexMini => "gpt-5.1-codex-mini",
279            Self::Gpt5 => "gpt-5",
280            Self::Gpt5Codex => "gpt-5-codex",
281            Self::Gpt5Nano => "gpt-5-nano",
282
283            Self::Gemini3_1Pro => "gemini-3.1-pro",
284            Self::Gemini3Flash => "gemini-3-flash",
285            Self::Gemini3_5Flash => "gemini-3.5-flash",
286
287            Self::DeepSeekV4Pro => "deepseek-v4-pro",
288            Self::DeepSeekV4Flash => "deepseek-v4-flash",
289            Self::MiniMaxM2_5 => "minimax-m2.5",
290            Self::Glm5 => "glm-5",
291            Self::Glm5_1 => "glm-5.1",
292            Self::Glm5_2 => "glm-5.2",
293            Self::GrokBuild0_1 => "grok-build-0.1",
294            Self::Grok4_5 => "grok-4.5",
295            Self::KimiK2_5 => "kimi-k2.5",
296            Self::KimiK2_6 => "kimi-k2.6",
297            Self::KimiK2_7Code => "kimi-k2.7-code",
298            Self::MiniMaxM2_7 => "minimax-m2.7",
299            Self::MiniMaxM3 => "minimax-m3",
300            Self::MimoV2_5Pro => "mimo-v2.5-pro",
301            Self::MimoV2_5 => "mimo-v2.5",
302            Self::Qwen3_5Plus => "qwen3.5-plus",
303            Self::Qwen3_6Plus => "qwen3.6-plus",
304            Self::Qwen3_7Plus => "qwen3.7-plus",
305            Self::Qwen3_7Max => "qwen3.7-max",
306            Self::BigPickle => "big-pickle",
307            Self::Nemotron3UltraFree => "nemotron-3-ultra-free",
308
309            Self::Custom { name, .. } => name,
310        }
311    }
312
313    pub fn display_name(&self) -> &str {
314        match self {
315            Self::ClaudeOpus4_8 => "Claude Opus 4.8",
316            Self::ClaudeOpus4_7 => "Claude Opus 4.7",
317            Self::ClaudeOpus4_6 => "Claude Opus 4.6",
318            Self::ClaudeOpus4_5 => "Claude Opus 4.5",
319            Self::ClaudeOpus4_1 => "Claude Opus 4.1",
320            Self::ClaudeSonnet4_6 => "Claude Sonnet 4.6",
321            Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
322            Self::ClaudeSonnet4 => "Claude Sonnet 4",
323            Self::ClaudeHaiku4_5 => "Claude Haiku 4.5",
324            Self::ClaudeSonnet5 => "Claude Sonnet 5",
325            Self::ClaudeFable5 => "Claude Fable 5",
326
327            Self::Gpt5_6Sol => "GPT 5.6 Sol",
328            Self::Gpt5_6Terra => "GPT 5.6 Terra",
329            Self::Gpt5_6Luna => "GPT 5.6 Luna",
330            Self::Gpt5_5 => "GPT 5.5",
331            Self::Gpt5_5Pro => "GPT 5.5 Pro",
332            Self::Gpt5_4 => "GPT 5.4",
333            Self::Gpt5_4Pro => "GPT 5.4 Pro",
334            Self::Gpt5_4Mini => "GPT 5.4 Mini",
335            Self::Gpt5_4Nano => "GPT 5.4 Nano",
336            Self::Gpt5_3Codex => "GPT 5.3 Codex",
337            Self::Gpt5_3Spark => "GPT 5.3 Codex Spark",
338            Self::Gpt5_2 => "GPT 5.2",
339            Self::Gpt5_2Codex => "GPT 5.2 Codex",
340            Self::Gpt5_1 => "GPT 5.1",
341            Self::Gpt5_1Codex => "GPT 5.1 Codex",
342            Self::Gpt5_1CodexMax => "GPT 5.1 Codex Max",
343            Self::Gpt5_1CodexMini => "GPT 5.1 Codex Mini",
344            Self::Gpt5 => "GPT 5",
345            Self::Gpt5Codex => "GPT 5 Codex",
346            Self::Gpt5Nano => "GPT 5 Nano",
347
348            Self::Gemini3_1Pro => "Gemini 3.1 Pro",
349            Self::Gemini3Flash => "Gemini 3 Flash",
350            Self::Gemini3_5Flash => "Gemini 3.5 Flash",
351
352            Self::DeepSeekV4Pro => "DeepSeek V4 Pro",
353            Self::DeepSeekV4Flash => "DeepSeek V4 Flash",
354            Self::MiniMaxM2_5 => "MiniMax M2.5",
355            Self::Glm5 => "GLM 5",
356            Self::Glm5_1 => "GLM 5.1",
357            Self::Glm5_2 => "GLM 5.2",
358            Self::GrokBuild0_1 => "Grok Build 0.1",
359            Self::Grok4_5 => "Grok 4.5",
360            Self::KimiK2_5 => "Kimi K2.5",
361            Self::KimiK2_6 => "Kimi K2.6",
362            Self::KimiK2_7Code => "Kimi K2.7 Code",
363            Self::MiniMaxM2_7 => "MiniMax M2.7",
364            Self::MiniMaxM3 => "MiniMax M3",
365            Self::MimoV2_5Pro => "MiMo V2.5 Pro",
366            Self::MimoV2_5 => "MiMo V2.5",
367            Self::Qwen3_5Plus => "Qwen3.5 Plus",
368            Self::Qwen3_6Plus => "Qwen3.6 Plus",
369            Self::Qwen3_7Plus => "Qwen3.7 Plus",
370            Self::Qwen3_7Max => "Qwen3.7 Max",
371            Self::BigPickle => "Big Pickle",
372            Self::Nemotron3UltraFree => "Nemotron 3 Ultra Free",
373
374            Self::Custom {
375                name, display_name, ..
376            } => display_name.as_deref().unwrap_or(name),
377        }
378    }
379
380    pub fn protocol(&self, subscription: OpenCodeSubscription) -> ApiProtocol {
381        match self {
382            // Models offered by OpenCode have the same configuration across subscriptions
383            //  with one outlier: non-free MiniMax models
384            Self::MiniMaxM3 | Self::MiniMaxM2_7 | Self::MiniMaxM2_5 => {
385                if subscription == OpenCodeSubscription::Zen {
386                    ApiProtocol::OpenAiChat
387                } else {
388                    ApiProtocol::Anthropic
389                }
390            }
391
392            Self::ClaudeFable5
393            | Self::ClaudeOpus4_8
394            | Self::ClaudeOpus4_7
395            | Self::ClaudeOpus4_6
396            | Self::ClaudeOpus4_5
397            | Self::ClaudeOpus4_1
398            | Self::ClaudeSonnet5
399            | Self::ClaudeSonnet4_6
400            | Self::ClaudeSonnet4_5
401            | Self::ClaudeSonnet4
402            | Self::ClaudeHaiku4_5 => ApiProtocol::Anthropic,
403
404            Self::Gpt5_6Sol
405            | Self::Gpt5_6Terra
406            | Self::Gpt5_6Luna
407            | Self::Gpt5_5
408            | Self::Gpt5_5Pro
409            | Self::Gpt5_4
410            | Self::Gpt5_4Pro
411            | Self::Gpt5_4Mini
412            | Self::Gpt5_4Nano
413            | Self::Gpt5_3Codex
414            | Self::Gpt5_3Spark
415            | Self::Gpt5_2
416            | Self::Gpt5_2Codex
417            | Self::Gpt5_1
418            | Self::Gpt5_1Codex
419            | Self::Gpt5_1CodexMax
420            | Self::Gpt5_1CodexMini
421            | Self::Gpt5
422            | Self::Gpt5Codex
423            | Self::Gpt5Nano => ApiProtocol::OpenAiResponses,
424
425            Self::Gemini3_1Pro | Self::Gemini3Flash | Self::Gemini3_5Flash => ApiProtocol::Google,
426
427            Self::Qwen3_7Max | Self::Qwen3_7Plus | Self::Qwen3_6Plus | Self::Qwen3_5Plus => {
428                ApiProtocol::Anthropic
429            }
430
431            Self::Glm5
432            | Self::Glm5_1
433            | Self::Glm5_2
434            | Self::GrokBuild0_1
435            | Self::Grok4_5
436            | Self::KimiK2_5
437            | Self::KimiK2_6
438            | Self::KimiK2_7Code
439            | Self::MimoV2_5Pro
440            | Self::MimoV2_5
441            | Self::DeepSeekV4Pro
442            | Self::DeepSeekV4Flash
443            | Self::BigPickle
444            | Self::Nemotron3UltraFree => ApiProtocol::OpenAiChat,
445
446            Self::Custom { protocol, .. } => *protocol,
447        }
448    }
449
450    pub fn interleaved_reasoning(&self) -> bool {
451        match self {
452            Self::DeepSeekV4Pro
453            | Self::DeepSeekV4Flash
454            | Self::KimiK2_5
455            | Self::KimiK2_6
456            | Self::KimiK2_7Code
457            | Self::MimoV2_5
458            | Self::MimoV2_5Pro
459            | Self::Glm5
460            | Self::Glm5_1
461            | Self::Glm5_2
462            | Self::MiniMaxM2_5
463            | Self::MiniMaxM2_7
464            | Self::MiniMaxM3
465            | Self::Nemotron3UltraFree
466            | Self::BigPickle => true,
467
468            Self::Custom {
469                interleaved_reasoning,
470                ..
471            } => *interleaved_reasoning,
472
473            _ => false,
474        }
475    }
476
477    pub fn max_token_count(&self, subscription: OpenCodeSubscription) -> u64 {
478        match self {
479            // Anthropic models
480            Self::ClaudeOpus4_8 | Self::ClaudeOpus4_7 => 1_000_000,
481            Self::ClaudeOpus4_6 | Self::ClaudeSonnet4_6 => 1_000_000,
482            Self::ClaudeSonnet4_5 => 1_000_000,
483            Self::ClaudeOpus4_5 | Self::ClaudeHaiku4_5 => 200_000,
484            Self::ClaudeOpus4_1 => 200_000,
485            Self::ClaudeSonnet4 => 1_000_000,
486            Self::ClaudeSonnet5 => 1_000_000,
487            Self::ClaudeFable5 => 1_000_000,
488
489            // OpenAI models
490            Self::Gpt5_6Sol | Self::Gpt5_6Terra | Self::Gpt5_6Luna => 1_050_000,
491            Self::Gpt5_5 | Self::Gpt5_5Pro => 1_050_000,
492            Self::Gpt5_4 | Self::Gpt5_4Pro => 1_050_000,
493            Self::Gpt5_4Mini | Self::Gpt5_4Nano => 400_000,
494            Self::Gpt5_3Codex => 400_000,
495            Self::Gpt5_3Spark => 128_000,
496            Self::Gpt5_2 | Self::Gpt5_2Codex => 400_000,
497            Self::Gpt5_1 | Self::Gpt5_1Codex | Self::Gpt5_1CodexMax | Self::Gpt5_1CodexMini => {
498                400_000
499            }
500            Self::Gpt5 | Self::Gpt5Codex | Self::Gpt5Nano => 400_000,
501
502            // Google models
503            Self::Gemini3_1Pro => 1_048_576,
504            Self::Gemini3Flash => 1_048_576,
505            Self::Gemini3_5Flash => 1_048_576,
506
507            // OpenAI-compatible models
508            Self::MiniMaxM2_7 => 204_800,
509            Self::MiniMaxM3 => {
510                if subscription == OpenCodeSubscription::Go {
511                    1_000_000
512                } else {
513                    512_000
514                }
515            }
516            Self::MiniMaxM2_5 => 204_800,
517            Self::Glm5 | Self::Glm5_1 => {
518                if subscription == OpenCodeSubscription::Go {
519                    202_752
520                } else {
521                    204_800
522                }
523            }
524            Self::Glm5_2 => 1_000_000,
525            Self::KimiK2_6 | Self::KimiK2_5 | Self::KimiK2_7Code => 262_144,
526            Self::GrokBuild0_1 => 256_000,
527            Self::Grok4_5 => 500_000,
528            Self::MimoV2_5Pro => 1_048_576,
529            Self::MimoV2_5 => 1_000_000,
530            Self::Qwen3_5Plus => 262_144,
531            Self::Qwen3_6Plus => {
532                if subscription == OpenCodeSubscription::Go {
533                    1_000_000
534                } else {
535                    262_144
536                }
537            }
538            Self::Qwen3_7Max | Self::Qwen3_7Plus => 1_000_000,
539            Self::BigPickle => 200_000,
540            Self::Nemotron3UltraFree => 1_000_000,
541            Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => 1_000_000,
542
543            Self::Custom { max_tokens, .. } => *max_tokens,
544        }
545    }
546
547    pub fn max_output_tokens(&self, subscription: OpenCodeSubscription) -> Option<u64> {
548        match self {
549            // Anthropic models
550            Self::ClaudeOpus4_8 | Self::ClaudeOpus4_7 | Self::ClaudeOpus4_6 => Some(128_000),
551            Self::ClaudeOpus4_5
552            | Self::ClaudeSonnet4_6
553            | Self::ClaudeSonnet4_5
554            | Self::ClaudeHaiku4_5
555            | Self::ClaudeSonnet4 => Some(64_000),
556            Self::ClaudeOpus4_1 => Some(32_000),
557            Self::ClaudeSonnet5 => Some(128_000),
558            Self::ClaudeFable5 => Some(128_000),
559
560            // OpenAI models
561            Self::Gpt5_6Sol
562            | Self::Gpt5_6Terra
563            | Self::Gpt5_6Luna
564            | Self::Gpt5_5
565            | Self::Gpt5_5Pro
566            | Self::Gpt5_4
567            | Self::Gpt5_4Pro
568            | Self::Gpt5_4Mini
569            | Self::Gpt5_4Nano
570            | Self::Gpt5_3Codex
571            | Self::Gpt5_3Spark
572            | Self::Gpt5_2
573            | Self::Gpt5_2Codex
574            | Self::Gpt5_1
575            | Self::Gpt5_1Codex
576            | Self::Gpt5_1CodexMax
577            | Self::Gpt5_1CodexMini
578            | Self::Gpt5
579            | Self::Gpt5Codex
580            | Self::Gpt5Nano => Some(128_000),
581
582            // Google models
583            Self::Gemini3_1Pro | Self::Gemini3Flash | Self::Gemini3_5Flash => Some(65_536),
584
585            // OpenAI-compatible models
586            Self::MiniMaxM2_7 => Some(131_072),
587            Self::MiniMaxM3 => {
588                if subscription == OpenCodeSubscription::Go {
589                    Some(131_072)
590                } else {
591                    Some(128_000)
592                }
593            }
594            Self::MiniMaxM2_5 => {
595                if subscription == OpenCodeSubscription::Go {
596                    Some(65_536)
597                } else {
598                    Some(131_072)
599                }
600            }
601            Self::Glm5 | Self::Glm5_1 => {
602                if subscription == OpenCodeSubscription::Go {
603                    Some(32_768)
604                } else {
605                    Some(131_072)
606                }
607            }
608            Self::Glm5_2 => Some(131_072),
609            Self::BigPickle => Some(32_000),
610            Self::KimiK2_6 | Self::KimiK2_5 => Some(65_536),
611            Self::KimiK2_7Code => Some(262_144),
612            Self::GrokBuild0_1 => Some(256_000),
613            Self::Grok4_5 => Some(500_000),
614            Self::Qwen3_7Max | Self::Qwen3_7Plus | Self::Qwen3_6Plus | Self::Qwen3_5Plus => {
615                Some(65_536)
616            }
617            Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(384_000),
618            Self::Nemotron3UltraFree => Some(128_000),
619            Self::MimoV2_5Pro | Self::MimoV2_5 => Some(128_000),
620
621            Self::Custom {
622                max_output_tokens, ..
623            } => *max_output_tokens,
624        }
625    }
626
627    pub fn supports_tools(&self) -> bool {
628        true
629    }
630
631    pub fn supports_images(&self) -> bool {
632        match self {
633            // Anthropic models support images
634            Self::ClaudeOpus4_8
635            | Self::ClaudeOpus4_7
636            | Self::ClaudeOpus4_6
637            | Self::ClaudeOpus4_5
638            | Self::ClaudeOpus4_1
639            | Self::ClaudeSonnet4_6
640            | Self::ClaudeSonnet4_5
641            | Self::ClaudeSonnet4
642            | Self::ClaudeHaiku4_5
643            | Self::ClaudeSonnet5
644            | Self::ClaudeFable5 => true,
645
646            // OpenAI models support images
647            Self::Gpt5_6Sol
648            | Self::Gpt5_6Terra
649            | Self::Gpt5_6Luna
650            | Self::Gpt5_5
651            | Self::Gpt5_5Pro
652            | Self::Gpt5_4
653            | Self::Gpt5_4Pro
654            | Self::Gpt5_4Mini
655            | Self::Gpt5_4Nano
656            | Self::Gpt5_3Codex
657            | Self::Gpt5_2
658            | Self::Gpt5_2Codex
659            | Self::Gpt5_1
660            | Self::Gpt5_1Codex
661            | Self::Gpt5_1CodexMax
662            | Self::Gpt5_1CodexMini
663            | Self::Gpt5
664            | Self::Gpt5Codex
665            | Self::Gpt5Nano => true,
666
667            // OpenAI models without image support
668            Self::Gpt5_3Spark => false,
669
670            // Google models support images
671            Self::Gemini3_1Pro | Self::Gemini3Flash | Self::Gemini3_5Flash => true,
672
673            // OpenAI-compatible models with image support
674            Self::KimiK2_6
675            | Self::KimiK2_7Code
676            | Self::KimiK2_5
677            | Self::GrokBuild0_1
678            | Self::Grok4_5
679            | Self::MimoV2_5
680            | Self::Qwen3_5Plus
681            | Self::Qwen3_6Plus
682            | Self::Qwen3_7Plus
683            | Self::MiniMaxM3 => true,
684
685            // OpenAI-compatible models without image support
686            Self::MiniMaxM2_5
687            | Self::Glm5
688            | Self::Glm5_1
689            | Self::Glm5_2
690            | Self::MiniMaxM2_7
691            | Self::MimoV2_5Pro
692            | Self::DeepSeekV4Pro
693            | Self::DeepSeekV4Flash
694            | Self::Qwen3_7Max
695            | Self::BigPickle
696            | Self::Nemotron3UltraFree => false,
697
698            Self::Custom { protocol, .. } => matches!(
699                protocol,
700                ApiProtocol::Anthropic
701                    | ApiProtocol::OpenAiResponses
702                    | ApiProtocol::OpenAiChat
703                    | ApiProtocol::Google
704            ),
705        }
706    }
707
708    pub fn supported_reasoning_effort_levels(&self) -> Option<Vec<ReasoningEffort>> {
709        match self {
710            // Anthropic models
711            Self::ClaudeFable5
712            | Self::ClaudeOpus4_8
713            | Self::ClaudeOpus4_7
714            | Self::ClaudeSonnet5 => Some(vec![
715                ReasoningEffort::Low,
716                ReasoningEffort::Medium,
717                ReasoningEffort::High,
718                ReasoningEffort::XHigh,
719                ReasoningEffort::Max,
720            ]),
721
722            Self::ClaudeOpus4_6 | Self::ClaudeSonnet4_6 => Some(vec![
723                ReasoningEffort::Low,
724                ReasoningEffort::Medium,
725                ReasoningEffort::High,
726                ReasoningEffort::Max,
727            ]),
728
729            Self::ClaudeOpus4_5 => Some(vec![
730                ReasoningEffort::Low,
731                ReasoningEffort::Medium,
732                ReasoningEffort::High,
733            ]),
734
735            // OpenAI models
736            Self::Gpt5_5
737            | Self::Gpt5_4
738            | Self::Gpt5_4Mini
739            | Self::Gpt5_4Nano
740            | Self::Gpt5_3Codex
741            | Self::Gpt5_2 => Some(vec![
742                ReasoningEffort::None,
743                ReasoningEffort::Low,
744                ReasoningEffort::Medium,
745                ReasoningEffort::High,
746                ReasoningEffort::XHigh,
747            ]),
748
749            Self::Gpt5_5Pro | Self::Gpt5_4Pro => Some(vec![
750                ReasoningEffort::Medium,
751                ReasoningEffort::High,
752                ReasoningEffort::XHigh,
753            ]),
754
755            Self::Gpt5_2Codex | Self::Gpt5_3Spark | Self::Gpt5_1CodexMax => Some(vec![
756                ReasoningEffort::Low,
757                ReasoningEffort::Medium,
758                ReasoningEffort::High,
759                ReasoningEffort::XHigh,
760            ]),
761
762            Self::Gpt5_1 => Some(vec![
763                ReasoningEffort::None,
764                ReasoningEffort::Low,
765                ReasoningEffort::Medium,
766                ReasoningEffort::High,
767            ]),
768
769            Self::Gpt5Codex | Self::Gpt5_1Codex | Self::Gpt5_1CodexMini => Some(vec![
770                ReasoningEffort::Low,
771                ReasoningEffort::Medium,
772                ReasoningEffort::High,
773            ]),
774
775            Self::Gpt5 | Self::Gpt5Nano => Some(vec![
776                ReasoningEffort::Minimal,
777                ReasoningEffort::Low,
778                ReasoningEffort::Medium,
779                ReasoningEffort::High,
780            ]),
781
782            Self::Gpt5_6Sol | Self::Gpt5_6Terra | Self::Gpt5_6Luna => Some(vec![
783                ReasoningEffort::None,
784                ReasoningEffort::Low,
785                ReasoningEffort::Medium,
786                ReasoningEffort::High,
787                ReasoningEffort::XHigh,
788                ReasoningEffort::Max,
789            ]),
790
791            // Google models
792            Self::Gemini3Flash | Self::Gemini3_5Flash => Some(vec![
793                ReasoningEffort::Minimal,
794                ReasoningEffort::Low,
795                ReasoningEffort::Medium,
796                ReasoningEffort::High,
797            ]),
798
799            Self::Gemini3_1Pro => Some(vec![
800                ReasoningEffort::Low,
801                ReasoningEffort::Medium,
802                ReasoningEffort::High,
803            ]),
804
805            // DeepSeek models
806            Self::DeepSeekV4Pro | Self::DeepSeekV4Flash => Some(vec![
807                // OpenCode also supports Low&Medium but as per DeepSeek those are mapped to High
808                ReasoningEffort::High,
809                ReasoningEffort::Max,
810            ]),
811
812            // MiniMax models
813            Self::MiniMaxM3 => Some(vec![ReasoningEffort::None]),
814
815            // NVIDIA models
816            Self::Nemotron3UltraFree => Some(vec![
817                ReasoningEffort::Low,
818                ReasoningEffort::Medium,
819                ReasoningEffort::High,
820            ]),
821
822            // Xiaomi MiMo models
823            Self::MimoV2_5Pro | Self::MimoV2_5 => Some(vec![
824                ReasoningEffort::Low,
825                ReasoningEffort::Medium,
826                ReasoningEffort::High,
827            ]),
828
829            // Z AI models
830            Self::Glm5_2 => Some(vec![ReasoningEffort::High, ReasoningEffort::Max]),
831
832            // SpaceXAI models
833            Self::Grok4_5 => Some(vec![
834                ReasoningEffort::Low,
835                ReasoningEffort::Medium,
836                ReasoningEffort::High,
837            ]),
838
839            Self::Custom {
840                reasoning_effort_levels,
841                ..
842            } => reasoning_effort_levels.clone(),
843
844            _ => None,
845        }
846    }
847}
848
849/// Stream generate content for Google models via OpenCode.
850///
851/// Unlike `google_ai::stream_generate_content()`, this uses:
852/// - `/v1/models/{model}` path (not `/v1beta/models/{model}`)
853/// - `Authorization: Bearer` header (not `key=` query param)
854pub async fn stream_generate_content(
855    client: &dyn HttpClient,
856    api_url: &str,
857    api_key: &str,
858    request: google_ai::GenerateContentRequest,
859    extra_headers: &CustomHeaders,
860) -> Result<BoxStream<'static, Result<google_ai::GenerateContentResponse>>> {
861    let api_key = api_key.trim();
862
863    let model_id = &request.model.model_id;
864
865    let uri = format!("{api_url}/v1/models/{model_id}:streamGenerateContent?alt=sse");
866
867    let request = HttpRequest::builder()
868        .method(Method::POST)
869        .uri(uri)
870        .header("Content-Type", "application/json")
871        .header("Authorization", format!("Bearer {api_key}"))
872        .extra_headers(extra_headers)
873        .body(AsyncBody::from(serde_json::to_string(&request)?))?;
874    let mut response = client.send(request).await?;
875    if response.status().is_success() {
876        let reader = BufReader::new(response.into_body());
877        Ok(reader
878            .lines()
879            .filter_map(|line| async move {
880                match line {
881                    Ok(line) => {
882                        if let Some(line) = line.strip_prefix("data: ") {
883                            match serde_json::from_str(line) {
884                                Ok(response) => Some(Ok(response)),
885                                Err(error) => {
886                                    Some(Err(anyhow!("Error parsing JSON: {error:?}\n{line:?}")))
887                                }
888                            }
889                        } else {
890                            None
891                        }
892                    }
893                    Err(error) => Some(Err(anyhow!(error))),
894                }
895            })
896            .boxed())
897    } else {
898        let mut text = String::new();
899        response.body_mut().read_to_string(&mut text).await?;
900        Err(anyhow!(
901            "error during streamGenerateContent via OpenCode, status code: {:?}, body: {}",
902            response.status(),
903            text
904        ))
905    }
906}
907
Served at tenant.openagents/omega Member data and write actions are omitted.