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