Skip to repository content907 lines · 30.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:27:33.107Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
opencode.rs
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