vtcode_llm/providers/openresponses/
provider.rs1use 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 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 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;