Skip to main content

vtcode_core/llm/providers/
opencode_go.rs

1use 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}