vtcode_core/llm/providers/
opencode_go.rs1use crate::config::TimeoutsConfig;
2use crate::config::constants::{env_vars, models, urls};
3use crate::config::core::{AnthropicConfig, ModelConfig, PromptCachingConfig};
4use crate::config::models::model_catalog_entry;
5use crate::llm::client::LLMClient;
6use crate::llm::provider::{LLMError, LLMProvider, LLMRequest, LLMResponse, LLMStream};
7use async_trait::async_trait;
8use reqwest::Client as HttpClient;
9
10use super::AnthropicProvider;
11use super::common::{override_base_url, resolve_model};
12use super::opencode_shared::OpenCodeCompatibleProvider;
13
14const PROVIDER_NAME: &str = "OpenCode Go";
15const PROVIDER_KEY: &str = "opencode-go";
16const API_KEY_ENV: &str = "OPENCODE_GO_API_KEY";
17
18enum GoProtocol {
19 MessagesApi,
20 OpenAICompatible,
21}
22
23pub struct OpenCodeGoProvider {
24 api_key: String,
25 http_client: HttpClient,
26 base_url: String,
27 model: String,
28}
29
30impl OpenCodeGoProvider {
31 fn normalize_model(model: &str) -> &str {
32 model
33 .trim()
34 .strip_prefix("opencode-go/")
35 .unwrap_or(model.trim())
36 }
37
38 pub fn new(api_key: String) -> Self {
39 Self::with_model_internal(
40 api_key,
41 models::opencode_go::DEFAULT_MODEL.to_string(),
42 None,
43 None,
44 None,
45 )
46 }
47
48 pub fn with_model(api_key: String, model: String) -> Self {
49 Self::with_model_internal(api_key, model, None, None, None)
50 }
51
52 pub fn new_with_client(
53 api_key: String,
54 model: String,
55 http_client: reqwest::Client,
56 base_url: String,
57 _timeouts: TimeoutsConfig,
58 ) -> Self {
59 Self {
60 api_key,
61 http_client,
62 base_url,
63 model: Self::normalize_model(&model).to_string(),
64 }
65 }
66
67 pub fn from_config(
68 api_key: Option<String>,
69 model: Option<String>,
70 base_url: Option<String>,
71 _prompt_cache: Option<PromptCachingConfig>,
72 timeouts: Option<TimeoutsConfig>,
73 _anthropic: Option<AnthropicConfig>,
74 _model_behavior: Option<ModelConfig>,
75 ) -> Self {
76 let api_key_value = api_key.unwrap_or_default();
77 let model_value = resolve_model(model, models::opencode_go::DEFAULT_MODEL);
78
79 Self::with_model_internal(
80 api_key_value,
81 model_value,
82 base_url,
83 timeouts,
84 _model_behavior,
85 )
86 }
87
88 fn with_model_internal(
89 api_key: String,
90 model: String,
91 base_url: Option<String>,
92 timeouts: Option<TimeoutsConfig>,
93 _model_behavior: Option<ModelConfig>,
94 ) -> Self {
95 use crate::llm::http_client::HttpClientFactory;
96
97 let timeouts = timeouts.unwrap_or_default();
98
99 Self {
100 api_key,
101 http_client: HttpClientFactory::for_llm(&timeouts),
102 base_url: override_base_url(
103 urls::OPENCODE_GO_API_BASE,
104 base_url,
105 Some(env_vars::OPENCODE_GO_BASE_URL),
106 ),
107 model: Self::normalize_model(&model).to_string(),
108 }
109 }
110
111 fn requested_model<'a>(&'a self, model: &'a str) -> &'a str {
112 if model.trim().is_empty() {
113 self.model.as_str()
114 } else {
115 Self::normalize_model(model)
116 }
117 }
118
119 fn catalog_entry(&self, model: &str) -> Option<vtcode_config::models::ModelCatalogEntry> {
120 model_catalog_entry(PROVIDER_KEY, self.requested_model(model))
121 }
122
123 fn protocol_for_model(model: &str) -> GoProtocol {
124 if models::opencode_go::MESSAGES_API_MODELS.contains(&model) {
125 GoProtocol::MessagesApi
126 } else {
127 GoProtocol::OpenAICompatible
128 }
129 }
130
131 fn delegate_for_model(&self, model: &str) -> Box<dyn LLMProvider> {
132 let requested = self.requested_model(model).to_string();
133 match Self::protocol_for_model(requested.as_str()) {
134 GoProtocol::MessagesApi => Box::new(AnthropicProvider::new_with_client(
135 self.api_key.clone(),
136 requested,
137 self.http_client.clone(),
138 self.base_url.clone(),
139 TimeoutsConfig::default(),
140 )),
141 GoProtocol::OpenAICompatible => Box::new(OpenCodeCompatibleProvider::new(
142 PROVIDER_NAME,
143 PROVIDER_KEY,
144 API_KEY_ENV,
145 self.api_key.clone(),
146 self.http_client.clone(),
147 self.base_url.clone(),
148 requested,
149 models::opencode_go::SUPPORTED_MODELS,
150 )),
151 }
152 }
153}
154
155#[async_trait]
156impl LLMProvider for OpenCodeGoProvider {
157 fn name(&self) -> &str {
158 PROVIDER_KEY
159 }
160
161 fn supports_streaming(&self) -> bool {
162 true
163 }
164
165 fn supports_reasoning(&self, model: &str) -> bool {
166 self.catalog_entry(model)
167 .map(|entry| entry.reasoning)
168 .unwrap_or(false)
169 }
170
171 fn supports_tools(&self, model: &str) -> bool {
172 self.catalog_entry(model)
173 .map(|entry| entry.tool_call)
174 .unwrap_or(true)
175 }
176
177 fn supports_structured_output(&self, model: &str) -> bool {
178 self.catalog_entry(model)
179 .map(|entry| entry.structured_output)
180 .unwrap_or(false)
181 }
182
183 fn supports_context_caching(&self, model: &str) -> bool {
184 self.catalog_entry(model)
185 .map(|entry| entry.caching)
186 .unwrap_or(false)
187 }
188
189 fn supports_vision(&self, model: &str) -> bool {
190 self.catalog_entry(model)
191 .map(|entry| entry.vision)
192 .unwrap_or(false)
193 }
194
195 fn effective_context_size(&self, model: &str) -> usize {
196 self.catalog_entry(model)
197 .map(|entry| entry.context_window)
198 .filter(|value| *value > 0)
199 .unwrap_or(128_000)
200 }
201
202 async fn generate(&self, mut request: LLMRequest) -> Result<LLMResponse, LLMError> {
203 if request.model.trim().is_empty() {
204 request.model = self.model.clone();
205 } else {
206 request.model = self.requested_model(&request.model).to_string();
207 }
208 self.validate_request(&request)?;
209 self.delegate_for_model(&request.model)
210 .generate(request)
211 .await
212 }
213
214 async fn stream(&self, mut request: LLMRequest) -> Result<LLMStream, LLMError> {
215 if request.model.trim().is_empty() {
216 request.model = self.model.clone();
217 } else {
218 request.model = self.requested_model(&request.model).to_string();
219 }
220 self.validate_request(&request)?;
221 self.delegate_for_model(&request.model)
222 .stream(request)
223 .await
224 }
225
226 fn supported_models(&self) -> Vec<String> {
227 models::opencode_go::SUPPORTED_MODELS
228 .iter()
229 .map(|model| model.to_string())
230 .collect()
231 }
232
233 fn validate_request(&self, request: &LLMRequest) -> Result<(), LLMError> {
234 let mut normalized = request.clone();
235 if !normalized.model.trim().is_empty() {
236 normalized.model = self.requested_model(&normalized.model).to_string();
237 }
238
239 let supported_models = models::opencode_go::SUPPORTED_MODELS
240 .iter()
241 .map(|model| model.to_string())
242 .collect::<Vec<_>>();
243
244 super::common::validate_request_common(
245 &normalized,
246 PROVIDER_NAME,
247 PROVIDER_KEY,
248 Some(&supported_models),
249 )
250 }
251}
252
253#[async_trait]
254impl LLMClient for OpenCodeGoProvider {
255 async fn generate(&mut self, prompt: &str) -> Result<LLMResponse, LLMError> {
256 let request = LLMRequest {
257 messages: vec![crate::llm::provider::Message::user(prompt.to_string())],
258 model: self.model.clone(),
259 ..Default::default()
260 };
261 LLMProvider::generate(self, request).await
262 }
263
264 fn model_id(&self) -> &str {
265 &self.model
266 }
267}