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 catalog_priced(&self) -> bool {
181 self.provider.dialect.quirks.catalog_priced
182 }
183
184 fn map_options(
186 &self,
187 request: &crate::completion::CompletionRequest,
188 fields: crate::completion::options::OptionFields<'_>,
189 ) -> crate::completion::options::OptionMap {
190 crate::providers::openai::options::responses_options(self, request, fields)
191 }
192
193 fn facts(&self) -> Option<&ModelFacts> {
194 Some(&self.facts)
195 }
196
197 fn api(&self) -> crate::message::Api {
198 crate::message::Api::from_static("openai.responses")
199 }
200
201 fn provider(&self) -> &str {
202 self.provider.dialect.name
203 }
204
205 fn model(&self) -> &str {
206 &self.model
207 }
208
209 fn declares_tools(&self, request: &completion::CompletionRequest) -> bool {
211 !self.tools.is_empty() || crate::completion::history::declares_tools(request)
212 }
213
214 fn call_id_slot(&self) -> Option<&'static str> {
216 Some("/call_id")
217 }
218
219 fn blob_slots(&self) -> &'static [&'static str] {
220 &["/encrypted_content"]
221 }
222
223 fn identity(&self, item: &Value) -> Map<String, Value> {
227 let keys: &[&str] = match item.str("type") {
228 Some("message") => &["type", "id", "phase"],
229 Some("reasoning") => &["type", "id", "encrypted_content"],
230 Some("function_call" | "custom_tool_call") => &["type", "id"],
231 _ => &[],
232 };
233 keys.iter()
234 .filter_map(|key| Some(((*key).to_owned(), item.get(*key)?.clone())))
235 .collect()
236 }
237
238 fn needs_next(&self, item: &Value) -> bool {
240 item.str("type") == Some("reasoning")
241 }
242
243 fn accepts(&self, model: &str) -> crate::completion::Accepts {
247 let images = reads_images(
248 &self.facts,
249 self.provider.dialect.quirks.responses.contract,
250 model,
251 );
252 let model = model.to_ascii_lowercase();
253 crate::completion::Accepts {
254 user_images: images,
255 assistant_images: false,
256 tool_result_images: images,
257 tools: !(model.starts_with("o1-mini") || model.starts_with("o1-preview")),
258 foreign_reasoning: false,
260 }
261 }
262
263 fn encodes(&self, _model: &str, media: crate::completion::Media<'_>) -> bool {
268 use crate::completion::{Media, Place};
269 use crate::message::{DocumentData, DocumentSourceKind as Source};
270 let quirks = &self.provider.dialect.quirks;
271 let file_ids = quirks.accepts_file_ids;
272 match media {
273 Media::Image(_, Place::Assistant) | Media::Audio(_) | Media::Video(_) => false,
274 Media::Image(image, _) => {
275 super::image_part(image).is_some()
276 && (!matches!(image.data, Source::FileId(_))
277 || file_ids && quirks.responses.contract != ResponsesContract::Xai)
278 }
279 Media::Document(document) => {
280 super::document_part(document).is_some()
281 && (file_ids || !matches!(document.data, DocumentData::File(Source::FileId(_))))
282 }
283 }
284 }
285
286 fn continues_stored(&self, request: &completion::CompletionRequest) -> bool {
291 ["previous_response_id", "conversation", "input"]
292 .iter()
293 .any(|key| {
294 crate::completion::options::param(self, request, key)
295 .is_some_and(|value| !value.is_null())
296 })
297 }
298
299 fn normalize_tool_call_id(
302 &self,
303 id: &str,
304 _model: &str,
305 _source: Option<&crate::message::Origin>,
306 ) -> String {
307 use crate::providers::internal::wire_ids::{legal_call_id, short_hash};
308 let legal = legal_call_id(id, 64);
309 match legal.trim_end_matches('_') {
310 "" => short_hash(id),
312 trimmed => trimmed.to_owned(),
313 }
314 }
315}
316
317fn reads_images(facts: &ModelFacts, contract: ResponsesContract, model: &str) -> bool {
321 let model = model.rsplit('/').next().unwrap_or_default();
322 match contract {
323 ResponsesContract::Xai => facts.reads_images_or(
324 crate::providers::xai::DIALECT.name,
325 model,
326 crate::providers::xai::reads_images,
327 ),
328 ResponsesContract::OpenAi | ResponsesContract::Codex => facts.reads_images_or(
329 crate::providers::openai::wire::OPENAI.name,
330 model,
331 crate::providers::openai::reads_images,
332 ),
333 }
334}
335
336pub(crate) fn project_payload(payload: &[u8], sink: &mut ObservationSink<'_>) {
343 let Ok(payload) = serde_json::from_slice::<Value>(payload) else {
344 return;
345 };
346 if payload.str("type") != Some("error") {
348 project_verdict(&payload, sink);
349 }
350 if let Some(error) = ObservedError::of(&payload) {
351 error.emit(sink);
352 }
353}
354
355fn project_verdict(payload: &Value, sink: &mut ObservationSink<'_>) {
358 let object = payload
359 .get("response")
360 .filter(|response| response.is_object())
361 .unwrap_or(payload);
362 let verdict = AdapterVerdict {
365 finish_reason: object
366 .str("status")
367 .filter(|status| *status != "in_progress" && *status != "queued")
368 .map(|value| sink.scrub(value)),
369 block_reason: None,
370 detail: object
371 .at("/incomplete_details/reason")
372 .and_then(Value::as_str)
373 .map(|value| sink.scrub(value)),
374 model: object.str("model").map(|value| sink.scrub(value)),
375 };
376 let response_id = object.str("id").map(|value| sink.scrub(value));
377 sink.provider(verdict, response_id);
378}
379
380#[cfg(test)]
381mod tests;