@@ -216,6 +216,9 @@ impl TranslatingLlmClient {
216216 strip_anthropic_incompatible_fields ( & mut body) ;
217217 strip_unsigned_thinking_blocks ( & mut body) ;
218218 }
219+ if matches ! ( backend, Backend :: OpenAiResponses ( _) ) {
220+ strip_openai_responses_replay_status ( & mut body) ;
221+ }
219222 merge_extra_body ( & mut body, backend. extra_body ( ) ) ;
220223 if matches ! ( backend, Backend :: Anthropic ( _) ) {
221224 enable_anthropic_prompt_caching ( & mut body) ;
@@ -688,6 +691,26 @@ fn strip_anthropic_incompatible_fields(body: &mut Value) {
688691 }
689692}
690693
694+ // Drops response-only status metadata that coding agents replay as Responses input.
695+ // OpenAI rejects `status` on replayed assistant messages and reasoning items even
696+ // though those fields are present on the corresponding response objects.
697+ fn strip_openai_responses_replay_status ( body : & mut Value ) {
698+ let Some ( Value :: Array ( input) ) = body. get_mut ( "input" ) else {
699+ return ;
700+ } ;
701+ for item in input {
702+ let Value :: Object ( item) = item else {
703+ continue ;
704+ } ;
705+ if matches ! (
706+ item. get( "type" ) . and_then( Value :: as_str) ,
707+ Some ( "message" | "reasoning" )
708+ ) {
709+ item. remove ( "status" ) ;
710+ }
711+ }
712+ }
713+
691714// Removes replayed `thinking` blocks that carry no signature.
692715//
693716// Anthropic requires signed thinking blocks on replay. A router can serve earlier
@@ -869,6 +892,14 @@ mod tests {
869892 ) ]
870893 }
871894
895+ fn responses_map ( base_url : & str ) -> Vec < ModelConfig > {
896+ vec ! [ ModelConfig :: new(
897+ "gpt" ,
898+ Backend :: OpenAiResponses ( config( base_url) ) ,
899+ None ,
900+ ) ]
901+ }
902+
872903 fn chat_map_with_retries ( base_url : & str , max_retries : u32 ) -> Vec < ModelConfig > {
873904 vec ! [ ModelConfig :: new(
874905 "gpt" ,
@@ -1103,6 +1134,70 @@ mod tests {
11031134 Ok ( ( ) )
11041135 }
11051136
1137+ #[ tokio:: test]
1138+ async fn responses_backend_strips_pi_replay_status_before_sending ( )
1139+ -> std:: result:: Result < ( ) , Box < dyn Error + Sync + Send + ' static > > {
1140+ let server = MockServer :: start ( ) . await ;
1141+ Mock :: given ( method ( "POST" ) )
1142+ . and ( path ( "/v1/responses" ) )
1143+ . respond_with ( ResponseTemplate :: new ( 200 ) . set_body_json ( json ! ( {
1144+ "id" : "resp_1" ,
1145+ "object" : "response" ,
1146+ "model" : "gpt" ,
1147+ "status" : "completed" ,
1148+ "output" : [ {
1149+ "id" : "msg_1" ,
1150+ "type" : "message" ,
1151+ "role" : "assistant" ,
1152+ "status" : "completed" ,
1153+ "content" : [ { "type" : "output_text" , "text" : "ok" } ]
1154+ } ] ,
1155+ "usage" : { }
1156+ } ) ) )
1157+ . mount ( & server)
1158+ . await ;
1159+ let client = TranslatingLlmClient :: new ( & responses_map ( & format ! ( "{}/v1" , server. uri( ) ) ) ) ?;
1160+ let body = json ! ( {
1161+ "model" : "ctm-auto" ,
1162+ "input" : [
1163+ { "type" : "message" , "role" : "user" , "content" : "start" } ,
1164+ {
1165+ "type" : "reasoning" ,
1166+ "id" : "rs_1" ,
1167+ "status" : "completed" ,
1168+ "summary" : [ ]
1169+ } ,
1170+ {
1171+ "type" : "message" ,
1172+ "id" : "msg_pi_0" ,
1173+ "role" : "assistant" ,
1174+ "status" : "completed" ,
1175+ "content" : [ { "type" : "output_text" , "text" : "working" } ]
1176+ } ,
1177+ { "type" : "message" , "role" : "user" , "content" : "continue" }
1178+ ]
1179+ } ) ;
1180+
1181+ client
1182+ . call_rewrite_model_raw (
1183+ body,
1184+ None ,
1185+ Some ( & ModelId :: from ( "gpt" ) ) ,
1186+ WireFormat :: OpenAiResponses ,
1187+ )
1188+ . await ?;
1189+
1190+ let requests = server
1191+ . received_requests ( )
1192+ . await
1193+ . ok_or ( "request recording should be enabled" ) ?;
1194+ let forwarded: Value = serde_json:: from_slice ( & requests[ 0 ] . body ) ?;
1195+ assert_eq ! ( forwarded[ "model" ] , "gpt" ) ;
1196+ assert_eq ! ( forwarded[ "input" ] [ 1 ] . get( "status" ) , None ) ;
1197+ assert_eq ! ( forwarded[ "input" ] [ 2 ] . get( "status" ) , None ) ;
1198+ Ok ( ( ) )
1199+ }
1200+
11061201 #[ tokio:: test]
11071202 async fn invalid_json_is_a_response_translation_error ( )
11081203 -> std:: result:: Result < ( ) , Box < dyn Error + Sync + Send + ' static > > {
0 commit comments