1use super::Message;
2use std::borrow::Cow;
3use std::sync::Arc;
4
5#[derive(Debug, Clone, PartialEq)]
6pub struct ResponsesContinuationState<M = Message> {
7 pub response_id: String,
8 pub messages: Arc<Vec<M>>,
11}
12
13pub struct PreparedResponsesRequest<'a, M: Clone = Message> {
14 pub messages: Cow<'a, [M]>,
15 pub previous_response_id: Option<String>,
16 pub clear_stale_chain: bool,
17}
18
19pub fn responses_continuation_key(provider: &str, model: &str) -> Option<(String, String)> {
20 let provider = provider.trim().to_ascii_lowercase();
21 let model = model.trim();
22 if provider.is_empty() || model.is_empty() {
23 return None;
24 }
25
26 Some((provider, model.to_string()))
27}
28
29pub fn supports_responses_chaining(provider_name: &str, provider_supports_responses_compaction: bool) -> bool {
30 provider_supports_responses_compaction
31 || provider_name.eq_ignore_ascii_case("openai")
32 || provider_name.eq_ignore_ascii_case("openresponses")
33 || provider_name.eq_ignore_ascii_case("gemini")
34}
35
36pub fn records_responses_continuation_state(
37 provider_name: &str,
38 _provider_supports_responses_compaction: bool,
39) -> bool {
40 provider_name.eq_ignore_ascii_case("openresponses") || provider_name.eq_ignore_ascii_case("gemini")
41}
42
43pub fn uses_incremental_responses_history(_provider_name: &str, _provider_supports_responses_compaction: bool) -> bool {
48 false
49}
50
51pub fn prepare_responses_continuation_request<'a, M>(
52 provider_name: &str,
53 provider_supports_responses_compaction: bool,
54 messages: &'a [M],
55 continuation: Option<&ResponsesContinuationState<M>>,
56) -> PreparedResponsesRequest<'a, M>
57where
58 M: Clone + PartialEq,
59{
60 if provider_name.eq_ignore_ascii_case("openai") {
61 return prepare_openai_responses_request(messages, continuation);
62 }
63
64 if !supports_responses_chaining(provider_name, provider_supports_responses_compaction) {
65 return PreparedResponsesRequest {
66 messages: Cow::Borrowed(messages),
67 previous_response_id: None,
68 clear_stale_chain: false,
69 };
70 }
71
72 if provider_name.eq_ignore_ascii_case("openresponses") || provider_name.eq_ignore_ascii_case("gemini") {
73 return PreparedResponsesRequest {
74 messages: Cow::Borrowed(messages),
75 previous_response_id: continuation.map(|chain| chain.response_id.clone()),
76 clear_stale_chain: false,
77 };
78 }
79
80 prepare_openai_responses_request(messages, continuation)
81}
82
83pub fn prepare_openai_responses_request<'a, M>(
84 messages: &'a [M],
85 _continuation: Option<&ResponsesContinuationState<M>>,
86) -> PreparedResponsesRequest<'a, M>
87where
88 M: Clone,
89{
90 PreparedResponsesRequest {
91 messages: Cow::Borrowed(messages),
92 previous_response_id: None,
93 clear_stale_chain: false,
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::{
100 ResponsesContinuationState, prepare_openai_responses_request, prepare_responses_continuation_request,
101 records_responses_continuation_state, responses_continuation_key,
102 };
103 use crate::provider::Message;
104 use std::borrow::Cow;
105
106 #[test]
107 fn continuation_key_requires_non_empty_provider_and_model() {
108 assert_eq!(responses_continuation_key("", "gpt-5"), None);
109 assert_eq!(responses_continuation_key("openai", ""), None);
110 assert_eq!(responses_continuation_key("OpenAI", "gpt-5"), Some(("openai".to_string(), "gpt-5".to_string())));
111 }
112
113 #[test]
114 fn openai_does_not_record_normal_responses_continuation_state() {
115 assert!(!records_responses_continuation_state("openai", true));
116 assert!(!records_responses_continuation_state("OpenAI", false));
117 }
118
119 #[test]
120 fn provider_specific_responses_chaining_records_continuation_state() {
121 assert!(!records_responses_continuation_state("mycorp", true));
122 assert!(records_responses_continuation_state("gemini", false));
123 assert!(records_responses_continuation_state("openresponses", false));
124 assert!(!records_responses_continuation_state("anthropic", false));
125 }
126
127 #[test]
128 fn prepare_openai_request_keeps_full_history_without_previous_response_id() {
129 let messages = vec![
130 Message::user("hello".to_string()),
131 Message::user("continue".to_string()),
132 ];
133 let prepared = prepare_openai_responses_request(
134 &messages,
135 Some(&ResponsesContinuationState {
136 response_id: "resp_123".to_string(),
137 messages: vec![Message::user("hello".to_string())].into(),
138 }),
139 );
140
141 assert_eq!(prepared.previous_response_id, None);
142 assert!(matches!(prepared.messages, Cow::Borrowed(_)));
143 assert_eq!(prepared.messages.as_ref(), messages.as_slice());
144 assert!(!prepared.clear_stale_chain);
145 }
146
147 #[test]
148 fn prepare_openai_request_ignores_stale_chain_without_retry_recovery() {
149 let messages = vec![Message::user("continue".to_string())];
150 let prepared = prepare_openai_responses_request(
151 &messages,
152 Some(&ResponsesContinuationState {
153 response_id: "resp_123".to_string(),
154 messages: vec![Message::user("hello".to_string())].into(),
155 }),
156 );
157
158 assert_eq!(prepared.previous_response_id, None);
159 assert!(matches!(prepared.messages, Cow::Borrowed(_)));
160 assert_eq!(prepared.messages.as_ref(), messages.as_slice());
161 assert!(!prepared.clear_stale_chain);
162 }
163
164 #[test]
165 fn prepare_responses_continuation_request_keeps_openai_stateless() {
166 let messages = vec![
167 Message::user("hello".to_string()),
168 Message::user("continue".to_string()),
169 ];
170 let prepared = prepare_responses_continuation_request(
171 "openai",
172 false,
173 &messages,
174 Some(&ResponsesContinuationState {
175 response_id: "resp_123".to_string(),
176 messages: vec![Message::user("hello".to_string())].into(),
177 }),
178 );
179
180 assert_eq!(prepared.previous_response_id, None);
181 assert!(matches!(prepared.messages, Cow::Borrowed(_)));
182 assert_eq!(prepared.messages.as_ref(), messages.as_slice());
183 assert!(!prepared.clear_stale_chain);
184 }
185
186 #[test]
187 fn prepare_responses_continuation_request_keeps_compatible_provider_stateless() {
188 let messages = vec![
189 Message::user("hello".to_string()),
190 Message::user("continue".to_string()),
191 ];
192 let prepared = prepare_responses_continuation_request(
193 "mycorp",
194 true,
195 &messages,
196 Some(&ResponsesContinuationState {
197 response_id: "resp_123".to_string(),
198 messages: vec![Message::user("hello".to_string())].into(),
199 }),
200 );
201
202 assert_eq!(prepared.previous_response_id, None);
203 assert!(matches!(prepared.messages, Cow::Borrowed(_)));
204 assert_eq!(prepared.messages.as_ref(), messages.as_slice());
205 assert!(!prepared.clear_stale_chain);
206 }
207
208 #[test]
209 fn prepare_responses_continuation_request_keeps_full_history_for_gemini() {
210 let messages = vec![
211 Message::user("hello".to_string()),
212 Message::user("continue".to_string()),
213 ];
214 let prepared = prepare_responses_continuation_request(
215 "gemini",
216 false,
217 &messages,
218 Some(&ResponsesContinuationState {
219 response_id: "resp_123".to_string(),
220 messages: vec![Message::user("hello".to_string())].into(),
221 }),
222 );
223
224 assert_eq!(prepared.previous_response_id.as_deref(), Some("resp_123"));
225 assert!(matches!(prepared.messages, Cow::Borrowed(_)));
226 assert_eq!(prepared.messages.as_ref(), messages.as_slice());
227 assert!(!prepared.clear_stale_chain);
228 }
229
230 #[test]
231 fn prepare_responses_continuation_request_ignores_chain_for_unsupported_provider() {
232 let messages = vec![Message::user("hello".to_string())];
233 let prepared = prepare_responses_continuation_request(
234 "local",
235 false,
236 &messages,
237 Some(&ResponsesContinuationState {
238 response_id: "resp_123".to_string(),
239 messages: messages.clone().into(),
240 }),
241 );
242
243 assert_eq!(prepared.previous_response_id, None);
244 assert!(matches!(prepared.messages, Cow::Borrowed(_)));
245 assert_eq!(prepared.messages.as_ref(), messages.as_slice());
246 assert!(!prepared.clear_stale_chain);
247 }
248
249 #[test]
250 fn prepare_responses_continuation_request_ignores_openai_stale_chain_without_retry_recovery() {
251 let messages = vec![Message::user("continue".to_string())];
252 let prepared = prepare_responses_continuation_request(
253 "openai",
254 false,
255 &messages,
256 Some(&ResponsesContinuationState {
257 response_id: "resp_123".to_string(),
258 messages: vec![Message::user("hello".to_string())].into(),
259 }),
260 );
261
262 assert_eq!(prepared.previous_response_id, None);
263 assert!(matches!(prepared.messages, Cow::Borrowed(_)));
264 assert_eq!(prepared.messages.as_ref(), messages.as_slice());
265 assert!(!prepared.clear_stale_chain);
266 }
267}