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