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