Skip to main content

codex_api/
common.rs

1use crate::error::ApiError;
2use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
3use codex_protocol::config_types::Verbosity as VerbosityConfig;
4use codex_protocol::models::ResponseItem;
5use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
6use codex_protocol::protocol::ModelVerification;
7use codex_protocol::protocol::RateLimitSnapshot;
8use codex_protocol::protocol::TokenUsage;
9use codex_protocol::protocol::TurnModerationMetadataEvent;
10use codex_protocol::protocol::W3cTraceContext;
11use futures::Stream;
12use serde::Deserialize;
13use serde::Serialize;
14use serde_json::Value;
15use std::collections::HashMap;
16use std::pin::Pin;
17use std::task::Context;
18use std::task::Poll;
19use tokio::sync::mpsc;
20
21pub const WS_REQUEST_HEADER_TRACEPARENT_CLIENT_METADATA_KEY: &str = "ws_request_header_traceparent";
22pub const WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY: &str = "ws_request_header_tracestate";
23
24/// Canonical input payload for the compaction endpoint.
25#[derive(Debug, Clone, Serialize)]
26pub struct CompactionInput<'a> {
27    pub model: &'a str,
28    pub input: &'a [ResponseItem],
29    #[serde(skip_serializing_if = "str::is_empty")]
30    pub instructions: &'a str,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub tools: Option<Vec<Value>>,
33    pub parallel_tool_calls: bool,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub reasoning: Option<Reasoning>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub service_tier: Option<&'a str>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub prompt_cache_key: Option<&'a str>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub text: Option<TextControls>,
42}
43
44/// Canonical input payload for the memory summarize endpoint.
45#[derive(Debug, Clone, Serialize)]
46pub struct MemorySummarizeInput {
47    pub model: String,
48    #[serde(rename = "traces")]
49    pub raw_memories: Vec<RawMemory>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub reasoning: Option<Reasoning>,
52}
53
54#[derive(Debug, Clone, Serialize)]
55pub struct RawMemory {
56    pub id: String,
57    pub metadata: RawMemoryMetadata,
58    pub items: Vec<Value>,
59}
60
61#[derive(Debug, Clone, Serialize)]
62pub struct RawMemoryMetadata {
63    pub source_path: String,
64}
65
66#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
67pub struct MemorySummarizeOutput {
68    #[serde(rename = "trace_summary", alias = "raw_memory")]
69    pub raw_memory: String,
70    pub memory_summary: String,
71}
72
73#[derive(Debug)]
74pub enum ResponseEvent {
75    Created,
76    SafetyBuffering(SafetyBuffering),
77    OutputItemDone(ResponseItem),
78    OutputItemAdded(ResponseItem),
79    /// Emitted when the server includes `OpenAI-Model` on the stream response.
80    /// This can differ from the requested model when backend safety routing applies.
81    ServerModel(String),
82    /// Emitted when the server recommends additional account verification.
83    ModelVerifications(Vec<ModelVerification>),
84    /// Emitted when the server includes moderation metadata for first-party turn presentation.
85    TurnModerationMetadata(TurnModerationMetadataEvent),
86    /// Emitted when `X-Reasoning-Included: true` is present on the response,
87    /// meaning the server already accounted for past reasoning tokens and the
88    /// client should not re-estimate them.
89    ServerReasoningIncluded(bool),
90    Completed {
91        response_id: String,
92        token_usage: Option<TokenUsage>,
93        /// Did the model affirmatively end its turn? Some providers do not set this,
94        /// so we rely on fallback logic when this is `None`.
95        end_turn: Option<bool>,
96    },
97    OutputTextDelta(String),
98    ToolCallInputDelta {
99        item_id: String,
100        call_id: Option<String>,
101        delta: String,
102    },
103    ReasoningSummaryDelta {
104        delta: String,
105        summary_index: i64,
106    },
107    ReasoningSummaryDone {
108        item_id: String,
109        text: String,
110        summary_index: i64,
111    },
112    ReasoningContentDelta {
113        delta: String,
114        content_index: i64,
115    },
116    ReasoningSummaryPartAdded {
117        summary_index: i64,
118    },
119    RateLimits(RateLimitSnapshot),
120    ModelsEtag(String),
121}
122
123#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
124pub struct SafetyBuffering {
125    pub use_cases: Vec<String>,
126    pub reasons: Vec<String>,
127    #[serde(skip)]
128    pub show_buffering_ui: bool,
129    #[serde(rename = "retry_model")]
130    pub faster_model: Option<String>,
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Eq)]
134pub(crate) struct SafetyBufferingTreatment {
135    pub faster_model: Option<String>,
136}
137
138#[derive(Debug, Serialize, Clone, PartialEq)]
139#[serde(rename_all = "snake_case")]
140pub enum ReasoningContext {
141    Auto,
142    CurrentTurn,
143    AllTurns,
144}
145
146#[derive(Debug, Serialize, Clone, PartialEq)]
147pub struct Reasoning {
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub effort: Option<ReasoningEffortConfig>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub summary: Option<ReasoningSummaryConfig>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub context: Option<ReasoningContext>,
154}
155
156#[derive(Debug, Serialize, Clone, PartialEq)]
157#[serde(rename_all = "snake_case")]
158pub enum ReasoningSummaryDelivery {
159    SequentialCutoff,
160}
161
162#[derive(Debug, Serialize, Clone, PartialEq)]
163pub struct StreamOptions {
164    pub reasoning_summary_delivery: ReasoningSummaryDelivery,
165}
166
167#[derive(Debug, Serialize, Default, Clone, PartialEq)]
168#[serde(rename_all = "snake_case")]
169pub enum TextFormatType {
170    #[default]
171    JsonSchema,
172}
173
174#[derive(Debug, Serialize, Default, Clone, PartialEq)]
175pub struct TextFormat {
176    /// Format type used by the OpenAI text controls.
177    pub r#type: TextFormatType,
178    /// When true, the server is expected to strictly validate responses.
179    pub strict: bool,
180    /// JSON schema for the desired output.
181    pub schema: Value,
182    /// Friendly name for the format, used in telemetry/debugging.
183    pub name: String,
184}
185
186/// Controls the `text` field for the Responses API, combining verbosity and
187/// optional JSON schema output formatting.
188#[derive(Debug, Serialize, Default, Clone, PartialEq)]
189pub struct TextControls {
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub verbosity: Option<OpenAiVerbosity>,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub format: Option<TextFormat>,
194}
195
196#[derive(Debug, Serialize, Default, Clone, PartialEq)]
197#[serde(rename_all = "lowercase")]
198pub enum OpenAiVerbosity {
199    Low,
200    #[default]
201    Medium,
202    High,
203}
204
205impl From<VerbosityConfig> for OpenAiVerbosity {
206    fn from(v: VerbosityConfig) -> Self {
207        match v {
208            VerbosityConfig::Low => OpenAiVerbosity::Low,
209            VerbosityConfig::Medium => OpenAiVerbosity::Medium,
210            VerbosityConfig::High => OpenAiVerbosity::High,
211        }
212    }
213}
214
215#[derive(Debug, Serialize, Clone, PartialEq)]
216pub struct ResponsesApiRequest {
217    pub model: String,
218    #[serde(skip_serializing_if = "String::is_empty")]
219    pub instructions: String,
220    pub input: Vec<ResponseItem>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub tools: Option<Vec<serde_json::Value>>,
223    pub tool_choice: String,
224    pub parallel_tool_calls: bool,
225    pub reasoning: Option<Reasoning>,
226    pub store: bool,
227    pub stream: bool,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub stream_options: Option<StreamOptions>,
230    pub include: Vec<String>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub service_tier: Option<String>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub prompt_cache_key: Option<String>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub text: Option<TextControls>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub client_metadata: Option<HashMap<String, String>>,
239}
240
241impl<'a> From<&'a ResponsesApiRequest> for ResponseCreateWsRequest<'a> {
242    fn from(request: &'a ResponsesApiRequest) -> Self {
243        Self {
244            model: &request.model,
245            instructions: &request.instructions,
246            previous_response_id: None,
247            input: &request.input,
248            tools: request.tools.as_deref(),
249            tool_choice: &request.tool_choice,
250            parallel_tool_calls: request.parallel_tool_calls,
251            reasoning: request.reasoning.as_ref(),
252            store: request.store,
253            stream: request.stream,
254            stream_options: request.stream_options.as_ref(),
255            include: &request.include,
256            service_tier: request.service_tier.as_deref(),
257            prompt_cache_key: request.prompt_cache_key.as_deref(),
258            text: request.text.as_ref(),
259            generate: None,
260            client_metadata: request.client_metadata.clone(),
261        }
262    }
263}
264
265#[derive(Debug, Serialize)]
266pub struct ResponseCreateWsRequest<'a> {
267    pub model: &'a str,
268    #[serde(skip_serializing_if = "str::is_empty")]
269    pub instructions: &'a str,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub previous_response_id: Option<String>,
272    pub input: &'a [ResponseItem],
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub tools: Option<&'a [Value]>,
275    pub tool_choice: &'a str,
276    pub parallel_tool_calls: bool,
277    pub reasoning: Option<&'a Reasoning>,
278    pub store: bool,
279    pub stream: bool,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub stream_options: Option<&'a StreamOptions>,
282    pub include: &'a [String],
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub service_tier: Option<&'a str>,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub prompt_cache_key: Option<&'a str>,
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub text: Option<&'a TextControls>,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub generate: Option<bool>,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub client_metadata: Option<HashMap<String, String>>,
293}
294
295pub fn response_create_client_metadata(
296    client_metadata: Option<HashMap<String, String>>,
297    trace: Option<&W3cTraceContext>,
298) -> Option<HashMap<String, String>> {
299    let mut client_metadata = client_metadata.unwrap_or_default();
300
301    if let Some(traceparent) = trace.and_then(|trace| trace.traceparent.as_deref()) {
302        client_metadata.insert(
303            WS_REQUEST_HEADER_TRACEPARENT_CLIENT_METADATA_KEY.to_string(),
304            traceparent.to_string(),
305        );
306    }
307    if let Some(tracestate) = trace.and_then(|trace| trace.tracestate.as_deref()) {
308        client_metadata.insert(
309            WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY.to_string(),
310            tracestate.to_string(),
311        );
312    }
313
314    (!client_metadata.is_empty()).then_some(client_metadata)
315}
316
317#[derive(Debug, Serialize)]
318#[serde(tag = "type")]
319#[allow(clippy::large_enum_variant)]
320pub enum ResponsesWsRequest<'a> {
321    #[serde(rename = "response.create")]
322    ResponseCreate(ResponseCreateWsRequest<'a>),
323}
324
325pub fn create_text_param_for_request(
326    verbosity: Option<VerbosityConfig>,
327    output_schema: &Option<Value>,
328    output_schema_strict: bool,
329) -> Option<TextControls> {
330    if verbosity.is_none() && output_schema.is_none() {
331        return None;
332    }
333
334    Some(TextControls {
335        verbosity: verbosity.map(std::convert::Into::into),
336        format: output_schema.as_ref().map(|schema| TextFormat {
337            r#type: TextFormatType::JsonSchema,
338            strict: output_schema_strict,
339            schema: schema.clone(),
340            name: "codex_output_schema".to_string(),
341        }),
342    })
343}
344
345pub struct ResponseStream {
346    pub rx_event: mpsc::Receiver<Result<ResponseEvent, ApiError>>,
347    /// Server-assigned `x-request-id` response header, when present.
348    pub upstream_request_id: Option<String>,
349}
350
351impl Stream for ResponseStream {
352    type Item = Result<ResponseEvent, ApiError>;
353
354    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
355        self.rx_event.poll_recv(cx)
356    }
357}