Skip to main content

vtcode_llm/providers/
opencode_go.rs

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        // Pinned so the stream-timeout fallback cannot silently regress.
143        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}