1use serde::Deserialize;
2use serde::Serialize;
3use std::borrow::Cow;
4use std::collections::BTreeMap;
5
6use crate::ContentBlock;
7use crate::Message;
8use crate::ProviderError;
9use crate::model::ToolChoice;
10use crate::tool::ToolSpec;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct ReasoningOptions {
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub effort: Option<ReasoningEffort>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub summary: Option<ReasoningSummary>,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24#[non_exhaustive]
25pub enum ReasoningEffort {
26 Low,
27 Medium,
28 High,
29 #[serde(rename = "xhigh")]
30 XHigh,
31 Max,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum ReasoningSummary {
38 Auto,
39 Concise,
40 Detailed,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
45#[serde(rename_all = "snake_case")]
46pub enum ToolSearchMode {
47 #[default]
48 Disabled,
49 Hosted,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
54#[serde(rename_all = "lowercase")]
55pub enum ResponsesVerbosity {
56 Low,
57 #[default]
58 Medium,
59 High,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
64#[serde(rename_all = "lowercase")]
65pub enum ResponsesRequestCompression {
66 #[default]
67 None,
68 Zstd,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
73#[serde(rename_all = "snake_case")]
74pub enum ResponsesStateMode {
75 ReplayOnly,
77 #[default]
83 Hybrid,
84 Stateful,
86}
87
88impl ResponsesStateMode {
89 pub fn uses_provider_state(self) -> bool {
90 matches!(self, Self::Hybrid | Self::Stateful)
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
96#[serde(rename_all = "snake_case")]
97pub enum ResponsesTransport {
98 #[default]
100 HttpSse,
101 WebSocket,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
107#[serde(rename_all = "snake_case")]
108pub enum ResponsesTextFormatType {
109 #[default]
110 JsonSchema,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
115pub struct ResponsesTextFormat {
116 #[serde(default)]
117 pub r#type: ResponsesTextFormatType,
118 #[serde(default)]
119 pub strict: bool,
120 pub schema: serde_json::Value,
121 pub name: String,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
126pub struct ResponsesTextControls {
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub verbosity: Option<ResponsesVerbosity>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub format: Option<ResponsesTextFormat>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct ResponsesRequestOptions {
136 #[serde(default)]
137 pub parallel_tool_calls: Option<bool>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub previous_response_id: Option<String>,
140 #[serde(default)]
141 pub state_mode: ResponsesStateMode,
142 #[serde(default)]
143 pub transport: ResponsesTransport,
144 #[serde(default)]
145 pub store: Option<bool>,
146 #[serde(default)]
147 pub stream: Option<bool>,
148 #[serde(default, skip_serializing_if = "Vec::is_empty")]
149 pub include: Vec<String>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub service_tier: Option<String>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub prompt_cache_key: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub text: Option<ResponsesTextControls>,
156 #[serde(default)]
157 pub compression: ResponsesRequestCompression,
158}
159
160impl Default for ResponsesRequestOptions {
161 fn default() -> Self {
162 Self {
163 parallel_tool_calls: None,
164 previous_response_id: None,
165 state_mode: ResponsesStateMode::Hybrid,
166 transport: ResponsesTransport::HttpSse,
167 store: None,
168 stream: Some(true),
169 include: Vec::new(),
170 service_tier: None,
171 prompt_cache_key: None,
172 text: None,
173 compression: ResponsesRequestCompression::None,
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
180pub struct AnthropicRequestOptions {
181 #[serde(default)]
182 pub disable_parallel_tool_use: Option<bool>,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
187pub struct GeminiRequestOptions {
188 #[serde(default)]
189 pub thoughts: Option<bool>,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
194pub struct SessionRequestOptions {
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub sticky_turn_state: Option<String>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub turn_metadata: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub subagent: Option<String>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub prefer_connection_reuse: Option<bool>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub session_affinity: Option<String>,
205 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
206 pub extra_headers: BTreeMap<String, String>,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
211pub struct ProviderRequestOptions {
212 #[serde(default)]
213 pub tool_search_mode: ToolSearchMode,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub reasoning: Option<ReasoningOptions>,
216 #[serde(default)]
217 pub responses: ResponsesRequestOptions,
218 #[serde(default)]
219 pub anthropic: AnthropicRequestOptions,
220 #[serde(default)]
221 pub gemini: GeminiRequestOptions,
222 #[serde(default)]
223 pub session: SessionRequestOptions,
224}
225
226#[derive(Debug, Clone)]
228pub struct Request<'a> {
229 pub model: Cow<'a, str>,
230 pub system: Option<Cow<'a, str>>,
231 pub messages: Cow<'a, [Message]>,
232 pub tools: Cow<'a, [ToolSpec]>,
233 pub tool_choice: Option<ToolChoice>,
234 pub temperature: Option<f32>,
235 pub max_output_tokens: Option<u32>,
236 pub metadata: Cow<'a, BTreeMap<String, String>>,
237 pub provider_request_options: ProviderRequestOptions,
238}
239
240impl Request<'_> {
241 pub fn into_owned(self) -> Request<'static> {
242 Request {
243 model: Cow::Owned(self.model.into_owned()),
244 system: self.system.map(|system| Cow::Owned(system.into_owned())),
245 messages: Cow::Owned(self.messages.into_owned()),
246 tools: Cow::Owned(self.tools.into_owned()),
247 tool_choice: self.tool_choice,
248 temperature: self.temperature,
249 max_output_tokens: self.max_output_tokens,
250 metadata: Cow::Owned(self.metadata.into_owned()),
251 provider_request_options: self.provider_request_options,
252 }
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(tag = "type", rename_all = "snake_case")]
259pub enum CompactionInputItem {
260 UserTurn {
261 content: String,
262 },
263 AssistantTurn {
264 content: String,
265 },
266 ToolExchange {
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 request: Option<String>,
269 result: String,
270 is_error: bool,
271 },
272 CanonicalContext {
273 content: String,
274 },
275 MemoryRecall {
276 content: String,
277 },
278 DelegationResult {
279 agent_id: String,
280 agent_name: String,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 role: Option<String>,
283 status: String,
284 content: String,
285 },
286 CompactionSummary {
287 content: String,
288 },
289}
290
291#[derive(Debug, Clone)]
293pub struct CompactionRequest<'a> {
294 pub model: Cow<'a, str>,
295 pub instructions: Cow<'a, str>,
296 pub input: Cow<'a, [CompactionInputItem]>,
297 pub metadata: Cow<'a, BTreeMap<String, String>>,
298 pub provider_request_options: ProviderRequestOptions,
299}
300
301impl CompactionRequest<'_> {
302 pub fn into_model_request(self) -> Result<Request<'static>, ProviderError> {
304 let input_json =
305 serde_json::to_string(self.input.as_ref()).map_err(ProviderError::Serialize)?;
306
307 Ok(Request {
308 model: Cow::Owned(self.model.into_owned()),
309 system: Some(Cow::Owned(self.instructions.into_owned())),
310 messages: Cow::Owned(vec![Message::user(ContentBlock::text(format!(
311 "Compaction input JSON:\n{input_json}"
312 )))]),
313 tools: Cow::Owned(Vec::new()),
314 tool_choice: None,
315 temperature: None,
316 max_output_tokens: None,
317 metadata: Cow::Owned(self.metadata.into_owned()),
318 provider_request_options: self.provider_request_options,
319 })
320 }
321}
322
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
325pub struct RawMemory {
326 pub id: String,
327 pub metadata: RawMemoryMetadata,
328 pub items: Vec<serde_json::Value>,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332pub struct RawMemoryMetadata {
333 pub source_path: String,
334}
335
336#[derive(Debug, Clone)]
338pub struct MemorySummarizeRequest<'a> {
339 pub model: Cow<'a, str>,
340 pub raw_memories: Cow<'a, [RawMemory]>,
341 pub reasoning: Option<ReasoningOptions>,
342 pub metadata: Cow<'a, BTreeMap<String, String>>,
343 pub provider_request_options: ProviderRequestOptions,
344}
345
346impl MemorySummarizeRequest<'_> {
347 pub fn into_model_request(self) -> Result<Request<'static>, ProviderError> {
349 let raw_memories_json =
350 serde_json::to_string(self.raw_memories.as_ref()).map_err(ProviderError::Serialize)?;
351
352 Ok(Request {
353 model: Cow::Owned(self.model.into_owned()),
354 system: Some(Cow::Borrowed(MEMORY_SUMMARIZE_SYSTEM_PROMPT)),
355 messages: Cow::Owned(vec![Message::user(ContentBlock::text(format!(
356 "Memory summarize input JSON:\n{raw_memories_json}"
357 )))]),
358 tools: Cow::Owned(Vec::new()),
359 tool_choice: None,
360 temperature: None,
361 max_output_tokens: None,
362 metadata: Cow::Owned(self.metadata.into_owned()),
363 provider_request_options: ProviderRequestOptions {
364 reasoning: self.reasoning,
365 ..self.provider_request_options
366 },
367 })
368 }
369}
370
371const MEMORY_SUMMARIZE_SYSTEM_PROMPT: &str = concat!(
372 "You summarize trace memories for Codex.\n",
373 "Return valid JSON only.\n",
374 "The output must be a JSON array with one object per input trace, in the same order.\n",
375 "Each object must have exactly these string fields: `raw_memory` and `memory_summary`.\n",
376 "`raw_memory` should be a concrete, detailed summary of the trace contents.\n",
377 "`memory_summary` should be a shorter durable takeaway focused on reusable context.\n",
378 "Use empty strings when information is unavailable.\n",
379 "Do not include markdown fences or extra commentary.\n",
380);
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use serde_json::Value;
386
387 #[test]
388 fn reasoning_effort_uses_the_five_public_spellings() {
389 for (effort, expected) in [
390 (ReasoningEffort::Low, "low"),
391 (ReasoningEffort::Medium, "medium"),
392 (ReasoningEffort::High, "high"),
393 (ReasoningEffort::XHigh, "xhigh"),
394 (ReasoningEffort::Max, "max"),
395 ] {
396 let serialized = serde_json::to_string(&effort).expect("effort should serialize");
397 assert_eq!(serialized, format!("\"{expected}\""));
398 assert_eq!(
399 serde_json::from_str::<ReasoningEffort>(&serialized)
400 .expect("effort should deserialize"),
401 effort
402 );
403 }
404 }
405
406 #[test]
407 fn compaction_request_into_model_request_serializes_input_as_prompt_text() {
408 let request = CompactionRequest {
409 model: Cow::Borrowed("gpt-5"),
410 instructions: Cow::Borrowed("Summarize the transcript."),
411 input: Cow::Owned(vec![
412 CompactionInputItem::UserTurn {
413 content: "hello".to_string(),
414 },
415 CompactionInputItem::AssistantTurn {
416 content: "world".to_string(),
417 },
418 ]),
419 metadata: Cow::Owned(BTreeMap::from([("scope".to_string(), "test".to_string())])),
420 provider_request_options: ProviderRequestOptions {
421 session: SessionRequestOptions {
422 sticky_turn_state: Some("sticky".to_string()),
423 turn_metadata: None,
424 subagent: Some("compact".to_string()),
425 prefer_connection_reuse: Some(true),
426 session_affinity: None,
427 extra_headers: BTreeMap::new(),
428 },
429 ..ProviderRequestOptions::default()
430 },
431 };
432
433 let model_request = request
434 .into_model_request()
435 .expect("compaction request should convert");
436
437 assert_eq!(model_request.model.as_ref(), "gpt-5");
438 assert_eq!(
439 model_request.system.as_deref(),
440 Some("Summarize the transcript.")
441 );
442 assert_eq!(model_request.metadata["scope"], "test");
443 assert_eq!(
444 model_request
445 .provider_request_options
446 .session
447 .sticky_turn_state
448 .as_deref(),
449 Some("sticky")
450 );
451 assert_eq!(
452 model_request
453 .provider_request_options
454 .session
455 .subagent
456 .as_deref(),
457 Some("compact")
458 );
459 assert_eq!(model_request.messages.len(), 1);
460
461 let prompt = model_request.messages[0].text();
462 assert!(prompt.starts_with("Compaction input JSON:\n"));
463 let payload = prompt
464 .strip_prefix("Compaction input JSON:\n")
465 .expect("prompt should contain the compaction prefix");
466 let input: Vec<Value> = serde_json::from_str(payload).expect("prompt should be json");
467 assert_eq!(input[0]["type"], "user_turn");
468 assert_eq!(input[0]["content"], "hello");
469 assert_eq!(input[1]["type"], "assistant_turn");
470 assert_eq!(input[1]["content"], "world");
471 }
472
473 #[test]
474 fn memory_summarize_request_into_model_request_serializes_input_as_prompt_text() {
475 let request = MemorySummarizeRequest {
476 model: Cow::Borrowed("gpt-5"),
477 raw_memories: Cow::Owned(vec![RawMemory {
478 id: "memory-1".to_string(),
479 metadata: RawMemoryMetadata {
480 source_path: "/tmp/trace.jsonl".to_string(),
481 },
482 items: vec![serde_json::json!({"type":"message","role":"user"})],
483 }]),
484 reasoning: Some(ReasoningOptions {
485 effort: Some(ReasoningEffort::Medium),
486 summary: None,
487 }),
488 metadata: Cow::Owned(BTreeMap::from([("scope".to_string(), "test".to_string())])),
489 provider_request_options: ProviderRequestOptions {
490 session: SessionRequestOptions {
491 sticky_turn_state: None,
492 turn_metadata: Some("{\"turn_id\":\"t1\"}".to_string()),
493 subagent: None,
494 prefer_connection_reuse: Some(true),
495 session_affinity: Some("thread-1".to_string()),
496 extra_headers: BTreeMap::new(),
497 },
498 ..ProviderRequestOptions::default()
499 },
500 };
501
502 let model_request = request
503 .into_model_request()
504 .expect("memory summarize request should convert");
505
506 assert_eq!(model_request.model.as_ref(), "gpt-5");
507 assert_eq!(
508 model_request.system.as_deref(),
509 Some(MEMORY_SUMMARIZE_SYSTEM_PROMPT)
510 );
511 assert_eq!(model_request.metadata["scope"], "test");
512 assert_eq!(
513 model_request
514 .provider_request_options
515 .session
516 .turn_metadata
517 .as_deref(),
518 Some("{\"turn_id\":\"t1\"}")
519 );
520 assert_eq!(
521 model_request
522 .provider_request_options
523 .reasoning
524 .as_ref()
525 .expect("reasoning options")
526 .effort,
527 Some(ReasoningEffort::Medium)
528 );
529 assert_eq!(model_request.messages.len(), 1);
530
531 let prompt = model_request.messages[0].text();
532 assert!(prompt.starts_with("Memory summarize input JSON:\n"));
533 let payload = prompt
534 .strip_prefix("Memory summarize input JSON:\n")
535 .expect("prompt should contain the memory summarize prefix");
536 let input: Vec<RawMemory> = serde_json::from_str(payload).expect("prompt should be json");
537 assert_eq!(input[0].id, "memory-1");
538 assert_eq!(input[0].metadata.source_path, "/tmp/trace.jsonl");
539 assert_eq!(input[0].items[0]["role"], "user");
540 }
541}