rig_core/providers/openai/responses_api/
wire.rs1use crate::catalog::ModelFacts;
9use crate::completion::{self, ProviderCapabilities};
10use crate::error::EncodeError;
11use crate::json_utils::Lenient;
12use crate::observe::ObservedError;
13use crate::operation::Completion;
14use crate::providers::openai::wire::OpenAIConfig;
15pub(crate) use crate::providers::openai::wire::ResponsesContract;
16use crate::wire::{
17 AdapterVerdict, Capabilities, Descriptor, Encoded, Framing, Mode, ObservationSink, Wire,
18};
19use serde::{Deserialize, Serialize};
20use serde_json::{Map, Value};
21
22use super::streaming::ResponsesDecoder;
23use super::{ResponsesToolDefinition, SystemInstructionsPlacement};
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct Responses {
28 pub provider: OpenAIConfig,
30 pub model: String,
32 pub tools: Vec<ResponsesToolDefinition>,
34 pub strict_tools: bool,
36 pub system_instructions: SystemInstructionsPlacement,
39 #[serde(skip)]
41 pub facts: ModelFacts,
42}
43
44impl Responses {
45 pub fn new(provider: OpenAIConfig, model: impl Into<String>) -> Self {
48 Self {
49 system_instructions: provider.system_instructions_placement(),
50 strict_tools: provider.dialect.quirks.responses.strict_tools_by_default,
51 provider,
52 model: model.into(),
53 tools: Vec::new(),
54 facts: ModelFacts::default(),
55 }
56 }
57
58 pub fn with_facts(mut self, facts: ModelFacts) -> Self {
61 self.facts = facts;
62 self
63 }
64
65 pub fn with_strict_tools(mut self) -> Self {
67 self.strict_tools = true;
68 self
69 }
70
71 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
73 self.tools.push(tool.into());
74 self
75 }
76
77 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
79 where
80 I: IntoIterator<Item = Tool>,
81 Tool: Into<ResponsesToolDefinition>,
82 {
83 self.tools.extend(tools.into_iter().map(Into::into));
84 self
85 }
86
87 pub fn with_system_instructions_placement(
90 mut self,
91 placement: SystemInstructionsPlacement,
92 ) -> Self {
93 self.system_instructions = placement;
94 self
95 }
96
97 pub fn with_system_instructions_as_messages(self) -> Self {
100 self.with_system_instructions_placement(SystemInstructionsPlacement::InputSystemMessages)
101 }
102}
103
104impl Wire for Responses {
105 type Op = Completion;
106 type Payload = crate::wire::Encoded;
107 type Frame = crate::wire::WireFrame;
108 type Event = super::streaming::ResponsesEvent;
109 type Decoder<'id> = ResponsesDecoder;
110 type Reassembler = super::streaming::document::Response;
111
112 fn describe(&self) -> Descriptor<'_> {
114 Descriptor::new(self.provider.dialect.name)
115 .model(self.model.as_str())
116 .facts(&self.facts)
117 .capabilities(Capabilities::completion(
118 ProviderCapabilities::default().with_native_output_tool_composition(
119 self.provider.dialect.quirks.responses.contract != ResponsesContract::Xai,
120 ),
121 ))
122 .replay(self)
123 }
124
125 fn encode(
126 &self,
127 request: completion::CompletionRequest,
128 mode: Mode,
129 ) -> Result<Encoded, EncodeError> {
130 let quirks = &self.provider.dialect.quirks.responses;
131 let codex = quirks.contract == ResponsesContract::Codex;
136 let streaming = matches!(mode, Mode::Streaming) || codex;
137 let builder = self.provider.completion_headers(
138 &request,
139 http::Request::post(self.provider.uri(quirks.path, None)),
140 );
141 let mode = if streaming {
142 Mode::Streaming
143 } else {
144 Mode::Unary
145 };
146 let body = self.responses_request(&request, super::Delivery::Http(mode))?;
147 crate::providers::internal::trace_json(
148 crate::providers::internal::LogTarget::Completions,
149 "Responses completion request",
150 &body,
151 );
152 let request = builder
153 .header(http::header::CONTENT_TYPE, "application/json")
154 .body(body.into_body())?;
155
156 let framing = if streaming {
157 Framing::Sse
158 } else {
159 Framing::Whole
160 };
161 let encoded = Encoded::new(request, framing)
162 .with_request_id_header(self.provider.dialect.request_id_header)
163 .with_route(Some(self.provider.dialect.quirks.responses.path))
164 .with_projection(project_payload);
165 Ok(if codex {
166 encoded.with_relaxed_content_type()
167 } else {
168 encoded
169 })
170 }
171
172 fn decoder<'id>(&self) -> Self::Decoder<'id> {
173 ResponsesDecoder::new()
174 }
175}
176
177impl crate::completion::ReplayTarget for Responses {
178 fn map_options(
180 &self,
181 request: &crate::completion::CompletionRequest,
182 fields: crate::completion::options::OptionFields<'_>,
183 ) -> crate::completion::options::OptionMap {
184 crate::providers::openai::options::responses_options(self, request, fields)
185 }
186
187 fn facts(&self) -> Option<&ModelFacts> {
188 Some(&self.facts)
189 }
190
191 fn api(&self) -> crate::message::Api {
192 crate::message::Api::from_static("openai.responses")
193 }
194
195 fn provider(&self) -> &str {
196 self.provider.dialect.name
197 }
198
199 fn model(&self) -> &str {
200 &self.model
201 }
202
203 fn declares_tools(&self, request: &completion::CompletionRequest) -> bool {
205 !self.tools.is_empty() || crate::completion::history::declares_tools(request)
206 }
207
208 fn call_id_slot(&self) -> Option<&'static str> {
210 Some("/call_id")
211 }
212
213 fn identity(&self, item: &Value) -> Map<String, Value> {
217 let keys: &[&str] = match item.str("type") {
218 Some("message") => &["type", "id", "phase"],
219 Some("reasoning") => &["type", "id", "encrypted_content"],
220 Some("function_call" | "custom_tool_call") => &["type", "id"],
221 _ => &[],
222 };
223 keys.iter()
224 .filter_map(|key| Some(((*key).to_owned(), item.get(*key)?.clone())))
225 .collect()
226 }
227
228 fn needs_next(&self, item: &Value) -> bool {
230 item.str("type") == Some("reasoning")
231 }
232
233 fn accepts(&self, model: &str) -> crate::completion::Accepts {
237 let images = reads_images(
238 &self.facts,
239 self.provider.dialect.quirks.responses.contract,
240 model,
241 );
242 let model = model.to_ascii_lowercase();
243 crate::completion::Accepts {
244 user_images: images,
245 assistant_images: false,
246 tool_result_images: images,
247 tools: !(model.starts_with("o1-mini") || model.starts_with("o1-preview")),
248 foreign_reasoning: false,
250 }
251 }
252
253 fn encodes(&self, _model: &str, media: crate::completion::Media<'_>) -> bool {
258 use crate::completion::{Media, Place};
259 use crate::message::{DocumentData, DocumentSourceKind as Source};
260 let quirks = &self.provider.dialect.quirks;
261 let file_ids = quirks.accepts_file_ids;
262 match media {
263 Media::Image(_, Place::Assistant) | Media::Audio(_) | Media::Video(_) => false,
264 Media::Image(image, _) => {
265 super::image_part(image).is_some()
266 && (!matches!(image.data, Source::FileId(_))
267 || file_ids && quirks.responses.contract != ResponsesContract::Xai)
268 }
269 Media::Document(document) => {
270 super::document_part(document).is_some()
271 && (file_ids || !matches!(document.data, DocumentData::File(Source::FileId(_))))
272 }
273 }
274 }
275
276 fn continues_stored(&self, request: &completion::CompletionRequest) -> bool {
280 ["previous_response_id", "conversation"].iter().any(|key| {
281 crate::completion::options::param(self, request, key)
282 .is_some_and(|value| !value.is_null())
283 })
284 }
285
286 fn normalize_tool_call_id(
289 &self,
290 id: &str,
291 _model: &str,
292 _source: Option<&crate::message::Origin>,
293 ) -> String {
294 use crate::providers::internal::wire_ids::{legal_call_id, short_hash};
295 let legal = legal_call_id(id, 64);
296 match legal.trim_end_matches('_') {
297 "" => short_hash(id),
299 trimmed => trimmed.to_owned(),
300 }
301 }
302}
303
304fn reads_images(facts: &ModelFacts, contract: ResponsesContract, model: &str) -> bool {
308 let model = model.rsplit('/').next().unwrap_or_default();
309 match contract {
310 ResponsesContract::Xai => facts.reads_images_or(
311 crate::providers::xai::DIALECT.name,
312 model,
313 crate::providers::xai::reads_images,
314 ),
315 ResponsesContract::OpenAi | ResponsesContract::Codex => facts.reads_images_or(
316 crate::providers::openai::wire::OPENAI.name,
317 model,
318 crate::providers::openai::reads_images,
319 ),
320 }
321}
322
323pub(crate) fn project_payload(payload: &[u8], sink: &mut ObservationSink<'_>) {
330 let Ok(payload) = serde_json::from_slice::<Value>(payload) else {
331 return;
332 };
333 if payload.str("type") != Some("error") {
335 project_verdict(&payload, sink);
336 }
337 if let Some(error) = ObservedError::of(&payload) {
338 error.emit(sink);
339 }
340}
341
342fn project_verdict(payload: &Value, sink: &mut ObservationSink<'_>) {
345 let object = payload
346 .get("response")
347 .filter(|response| response.is_object())
348 .unwrap_or(payload);
349 let verdict = AdapterVerdict {
352 finish_reason: object
353 .str("status")
354 .filter(|status| *status != "in_progress" && *status != "queued")
355 .map(|value| sink.scrub(value)),
356 block_reason: None,
357 detail: object
358 .at("/incomplete_details/reason")
359 .and_then(Value::as_str)
360 .map(|value| sink.scrub(value)),
361 model: object.str("model").map(|value| sink.scrub(value)),
362 };
363 let response_id = object.str("id").map(|value| sink.scrub(value));
364 sink.provider(verdict, response_id);
365}
366
367#[cfg(test)]
368mod tests;