Skip to main content

vtcode_llm/providers/openresponses/
provider.rs

1//! OpenResponses provider setup and transport routing.
2
3use crate::error_display;
4use crate::provider::LLMError;
5use crate::providers::error_handling::format_network_error;
6use anyhow::Result;
7use reqwest::Client as HttpClient;
8use serde_json::Value;
9use vtcode_config::TimeoutsConfig;
10use vtcode_config::constants::{env_vars, models, urls};
11use vtcode_config::core::{AnthropicConfig, ModelConfig, PromptCachingConfig};
12
13use crate::providers::common::{override_base_url, resolve_model};
14
15mod compaction;
16mod legacy;
17mod native;
18mod provider_impl;
19
20#[cfg(test)]
21use crate::provider::{LLMProvider, LLMRequest, LLMStreamEvent, Message, ResponsesCompactionOptions, ToolCall};
22#[cfg(test)]
23use serde_json::json;
24
25pub struct OpenResponsesProvider {
26    http_client: HttpClient,
27    base_url: String,
28    model: String,
29    api_key: String,
30    model_behavior: Option<ModelConfig>,
31}
32
33impl OpenResponsesProvider {
34    pub fn new(api_key: String) -> Self {
35        Self::with_model(api_key, models::openresponses::DEFAULT_MODEL.to_string())
36    }
37
38    pub fn with_model(api_key: String, model: String) -> Self {
39        Self::with_model_internal(model, None, api_key, TimeoutsConfig::default(), None)
40    }
41
42    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            http_client,
51            base_url,
52            model,
53            api_key,
54            model_behavior: None,
55        }
56    }
57
58    pub fn from_config(
59        api_key: Option<String>,
60        model: Option<String>,
61        base_url: Option<String>,
62        _prompt_cache: Option<PromptCachingConfig>,
63        timeouts: Option<TimeoutsConfig>,
64        _anthropic: Option<AnthropicConfig>,
65        model_behavior: Option<ModelConfig>,
66    ) -> Self {
67        let api_key_value = api_key.unwrap_or_default();
68        let resolved_model = resolve_model(model, models::openresponses::DEFAULT_MODEL);
69        Self::with_model_internal(resolved_model, base_url, api_key_value, timeouts.unwrap_or_default(), model_behavior)
70    }
71
72    fn with_model_internal(
73        model: String,
74        base_url: Option<String>,
75        api_key: String,
76        timeouts: TimeoutsConfig,
77        model_behavior: Option<ModelConfig>,
78    ) -> Self {
79        use crate::http_client::HttpClientFactory;
80
81        Self {
82            http_client: HttpClientFactory::for_llm(&timeouts),
83            base_url: override_base_url(urls::OPENRESPONSES_API_BASE, base_url, Some(env_vars::OPENRESPONSES_BASE_URL)),
84            model,
85            api_key,
86            model_behavior,
87        }
88    }
89
90    fn responses_url(&self) -> String {
91        format!("{}/responses", self.base_url.trim_end_matches('/'))
92    }
93
94    /// Leave missing-endpoint responses to the caller's legacy adapter.
95    async fn send_native_request(&self, payload: &Value) -> Result<reqwest::Response, LLMError> {
96        let response = self
97            .http_client
98            .post(self.responses_url())
99            .bearer_auth(&self.api_key)
100            .json(payload)
101            .send()
102            .await
103            .map_err(|e| format_network_error("OpenResponses", &e))?;
104
105        if !response.status().is_success() && response.status() != reqwest::StatusCode::NOT_FOUND {
106            let status = response.status();
107            let body = crate::providers::common::read_provider_error_body(response).await;
108            let formatted_error = error_display::format_llm_error("OpenResponses", &format!("HTTP {status}: {body}"));
109            return Err(LLMError::Provider { message: formatted_error, metadata: None });
110        }
111
112        Ok(response)
113    }
114
115    fn responses_compact_url(&self) -> String {
116        format!("{}/responses/compact", self.base_url.trim_end_matches('/'))
117    }
118
119    /// Client for driving another endpoint's OpenAI-compatible
120    /// `/responses/compact` surface (e.g. Vercel AI Gateway OpenAI routes, xAI
121    /// Grok models) with this provider's transport and response parsing.
122    /// Capability gating stays with the caller: only construct this for routes
123    /// whose upstream documents the endpoint.
124    pub(crate) fn compact_endpoint_client(configured_model: &str, base_url: &str, api_key: &str, model: &str) -> Self {
125        let resolved = if model.trim().is_empty() {
126            configured_model.to_string()
127        } else {
128            model.to_string()
129        };
130        Self::from_config(Some(api_key.to_string()), Some(resolved), Some(base_url.to_string()), None, None, None, None)
131    }
132
133    fn supports_compaction_endpoint(&self) -> bool {
134        self.base_url.contains("api.openai.com") || self.base_url.contains("api.openresponses.com")
135    }
136}
137
138#[cfg(test)]
139mod tests;