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