1use std::sync::Arc;
2
3use agent_sdk_core::{
4 AgentError, ProviderAdapter, ProviderCapabilities, ProviderRequest, ProviderResponse,
5 ProviderStreamChunk,
6};
7use serde_json::{Value, json};
8
9use crate::{
10 ProviderApiKey,
11 error::unsupported_response,
12 http::{CurlJsonHttpTransport, JsonHttpRequest, JsonHttpTransport},
13 openai_compatible::{
14 OpenAiCompatibleResponsesAdapter, OpenAiResponsesConfig, OpenAiResponsesRequest,
15 OpenAiResponsesResponse, OpenAiResponsesTransport, OpenAiToolArgumentSink,
16 },
17};
18
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct OpenAiLiveResponsesConfig {
22 pub provider_ref: String,
24 pub model: String,
26 pub endpoint_url: String,
28 pub supports_streaming: bool,
30 pub max_input_tokens: Option<u32>,
32}
33
34impl OpenAiLiveResponsesConfig {
35 pub fn new(model: impl Into<String>) -> Self {
37 Self {
38 provider_ref: "provider.openai.responses".to_string(),
39 model: model.into(),
40 endpoint_url: "https://api.openai.com/v1/responses".to_string(),
41 supports_streaming: false,
42 max_input_tokens: None,
43 }
44 }
45
46 pub fn provider_ref(mut self, provider_ref: impl Into<String>) -> Self {
48 self.provider_ref = provider_ref.into();
49 self
50 }
51
52 pub fn endpoint_url(mut self, endpoint_url: impl Into<String>) -> Self {
54 self.endpoint_url = endpoint_url.into();
55 self
56 }
57
58 pub fn max_input_tokens(mut self, max_input_tokens: u32) -> Self {
60 self.max_input_tokens = Some(max_input_tokens);
61 self
62 }
63}
64
65#[derive(Clone)]
66pub struct OpenAiResponsesAdapter {
71 inner: OpenAiCompatibleResponsesAdapter,
72}
73
74impl OpenAiResponsesAdapter {
75 pub fn from_env(model: impl Into<String>) -> Result<Self, AgentError> {
77 Self::new(
78 OpenAiLiveResponsesConfig::new(model),
79 ProviderApiKey::from_env("OPENAI_API_KEY")?,
80 )
81 }
82
83 pub fn new(
85 config: OpenAiLiveResponsesConfig,
86 api_key: ProviderApiKey,
87 ) -> Result<Self, AgentError> {
88 Self::with_transport(config, api_key, Arc::new(CurlJsonHttpTransport::new()))
89 }
90
91 pub fn with_transport(
94 config: OpenAiLiveResponsesConfig,
95 api_key: ProviderApiKey,
96 http: Arc<dyn JsonHttpTransport>,
97 ) -> Result<Self, AgentError> {
98 let compatible_config =
99 OpenAiResponsesConfig::new(config.provider_ref.clone(), config.model.clone())
100 .endpoint_ref(config.endpoint_url.clone())
101 .supports_streaming(config.supports_streaming);
102 let compatible_config = match config.max_input_tokens {
103 Some(max_input_tokens) => compatible_config.max_input_tokens(max_input_tokens),
104 None => compatible_config,
105 };
106 let transport = Arc::new(OpenAiLiveResponsesTransport {
107 endpoint_url: config.endpoint_url,
108 api_key,
109 http,
110 });
111 Ok(Self {
112 inner: OpenAiCompatibleResponsesAdapter::new(compatible_config, transport),
113 })
114 }
115
116 pub fn with_argument_sink(mut self, sink: Arc<dyn OpenAiToolArgumentSink>) -> Self {
118 self.inner = self.inner.with_argument_sink(sink);
119 self
120 }
121}
122
123impl ProviderAdapter for OpenAiResponsesAdapter {
124 fn capabilities(&self) -> ProviderCapabilities {
125 self.inner.capabilities()
126 }
127
128 fn project_request(
129 &self,
130 projection: &agent_sdk_core::ContextProjection,
131 policy: &agent_sdk_core::ProviderProjectionPolicy,
132 ) -> Result<ProviderRequest, AgentError> {
133 self.inner.project_request(projection, policy)
134 }
135
136 fn complete(&self, request: &ProviderRequest) -> Result<ProviderResponse, AgentError> {
137 self.inner.complete(request)
138 }
139
140 fn stream(&self, request: &ProviderRequest) -> Result<Vec<ProviderStreamChunk>, AgentError> {
141 self.inner.stream(request)
142 }
143
144 fn extract_usage(&self, response: &ProviderResponse) -> agent_sdk_core::ProviderUsage {
145 self.inner.extract_usage(response)
146 }
147}
148
149struct OpenAiLiveResponsesTransport {
150 endpoint_url: String,
151 api_key: ProviderApiKey,
152 http: Arc<dyn JsonHttpTransport>,
153}
154
155impl OpenAiResponsesTransport for OpenAiLiveResponsesTransport {
156 fn complete(
157 &self,
158 request: OpenAiResponsesRequest,
159 ) -> Result<OpenAiResponsesResponse, AgentError> {
160 let body = openai_responses_body(request);
161 let http_request = JsonHttpRequest::new(self.endpoint_url.clone(), body)
162 .header(
163 "Authorization",
164 format!("Bearer {}", self.api_key.expose_secret()),
165 )
166 .header("Content-Type", "application/json");
167 let response = self.http.post_json(http_request)?;
168 serde_json::from_value(response.body)
169 .map_err(|error| unsupported_response("OpenAI Responses", error.to_string()))
170 }
171}
172
173fn openai_responses_body(request: OpenAiResponsesRequest) -> Value {
174 let mut instructions = Vec::new();
175 let mut input = Vec::new();
176 for message in request.input {
177 match message.role.as_str() {
178 "system" | "developer" => instructions.push(message.content),
179 "assistant" => input.push(json!({
180 "role": "assistant",
181 "content": message.content,
182 })),
183 "tool" => input.push(json!({
184 "role": "user",
185 "content": format!("Tool result:\n{}", message.content),
186 })),
187 _ => input.push(json!({
188 "role": "user",
189 "content": message.content,
190 })),
191 }
192 }
193
194 let mut body = json!({
195 "model": request.model,
196 "input": input,
197 });
198 if !instructions.is_empty() {
199 body["instructions"] = Value::String(instructions.join("\n\n"));
200 }
201 if let Some(text) = request.text.and_then(openai_text_format) {
202 body["text"] = text;
203 }
204 body
205}
206
207fn openai_text_format(text: crate::OpenAiTextFormatHint) -> Option<Value> {
208 let schema = text.schema?;
209 Some(json!({
210 "format": {
211 "type": "json_schema",
212 "name": text.name,
213 "schema": schema,
214 "strict": true,
215 }
216 }))
217}