@@ -48,6 +48,19 @@ function chatgptAccount(id, token, createdAt, extra = {}) {
4848 } ;
4949}
5050
51+ function claudeAccount ( id , token , createdAt , extra = { } ) {
52+ return {
53+ id,
54+ type : "claude" ,
55+ name : id ,
56+ accessToken : token ,
57+ models : [ { id : "claude-fable-5" , name : "Claude Fable 5" , enabled : true } ] ,
58+ enabled : true ,
59+ createdAt,
60+ ...extra ,
61+ } ;
62+ }
63+
5164function captureLogger ( ) {
5265 const entries = [ ] ;
5366 const add = ( level ) => ( message , meta ) => entries . push ( { level, message, meta } ) ;
@@ -1076,6 +1089,165 @@ describe("same-provider OAuth account fallback", () => {
10761089 assert . match ( chunks . join ( "" ) , / s e c o n d / ) ;
10771090 } ) ;
10781091
1092+ it ( "does not time out a Claude stream that starts with native thinking" , async ( ) => {
1093+ const store = createStore ( tmpConfig ( ) ) ;
1094+ store . seed ( { providers : [ claudeAccount ( "prov_a" , "token-a" , 100 ) ] } ) ;
1095+ const encoder = new TextEncoder ( ) ;
1096+ let timedOut = false ;
1097+ const router = createRouter ( {
1098+ store,
1099+ timeoutMs : 20 ,
1100+ logger : captureLogger ( ) ,
1101+ fetchImpl : async ( _url , options ) =>
1102+ new Response (
1103+ new ReadableStream ( {
1104+ start ( controller ) {
1105+ controller . enqueue (
1106+ encoder . encode (
1107+ [
1108+ `event: content_block_start` ,
1109+ `data: ${ JSON . stringify ( {
1110+ type : "content_block_start" ,
1111+ index : 0 ,
1112+ content_block : { type : "thinking" , thinking : "" } ,
1113+ } ) } `,
1114+ "" ,
1115+ `event: content_block_delta` ,
1116+ `data: ${ JSON . stringify ( {
1117+ type : "content_block_delta" ,
1118+ index : 0 ,
1119+ delta : { type : "thinking_delta" , thinking : "Working through it" } ,
1120+ } ) } `,
1121+ "" ,
1122+ "" ,
1123+ ] . join ( "\n" )
1124+ )
1125+ ) ;
1126+ options . signal . addEventListener (
1127+ "abort" ,
1128+ ( ) => {
1129+ timedOut = true ;
1130+ controller . error ( options . signal . reason || new Error ( "timed out" ) ) ;
1131+ } ,
1132+ { once : true }
1133+ ) ;
1134+ setTimeout ( ( ) => {
1135+ controller . enqueue (
1136+ encoder . encode (
1137+ [
1138+ `event: content_block_stop` ,
1139+ `data: ${ JSON . stringify ( { type : "content_block_stop" , index : 0 } ) } ` ,
1140+ "" ,
1141+ `event: content_block_start` ,
1142+ `data: ${ JSON . stringify ( {
1143+ type : "content_block_start" ,
1144+ index : 1 ,
1145+ content_block : { type : "text" , text : "" } ,
1146+ } ) } `,
1147+ "" ,
1148+ `event: content_block_delta` ,
1149+ `data: ${ JSON . stringify ( {
1150+ type : "content_block_delta" ,
1151+ index : 1 ,
1152+ delta : { type : "text_delta" , text : "answer" } ,
1153+ } ) } `,
1154+ "" ,
1155+ "" ,
1156+ ] . join ( "\n" )
1157+ )
1158+ ) ;
1159+ controller . close ( ) ;
1160+ } , 50 ) ;
1161+ } ,
1162+ } ) ,
1163+ { status : 200 , headers : { "Content-Type" : "text/event-stream" } }
1164+ ) ,
1165+ } ) ;
1166+
1167+ const body = {
1168+ model : "claude/claude-fable-5" ,
1169+ messages : [ { role : "user" , content : "hello" } ] ,
1170+ stream : true ,
1171+ } ;
1172+ body [ Symbol . for ( "rerouted.anthropic.metadata" ) ] = { } ;
1173+ const result = await router . chatCompletions ( { body } ) ;
1174+ const chunks = [ ] ;
1175+ await result . streamPipe ( { write : ( chunk ) => chunks . push ( String ( chunk ) ) } ) ;
1176+
1177+ assert . equal ( result . ok , true , JSON . stringify ( result . error ) ) ;
1178+ assert . equal ( timedOut , false ) ;
1179+ assert . match ( chunks . join ( "" ) , / W o r k i n g t h r o u g h i t / ) ;
1180+ assert . match ( chunks . join ( "" ) , / a n s w e r / ) ;
1181+ } ) ;
1182+
1183+ it ( "does not time out a Responses stream that starts with reasoning" , async ( ) => {
1184+ const store = createStore ( tmpConfig ( ) ) ;
1185+ store . seed ( { providers : [ chatgptAccount ( "prov_a" , "token-a" , 100 ) ] } ) ;
1186+ const encoder = new TextEncoder ( ) ;
1187+ let timedOut = false ;
1188+ const router = createRouter ( {
1189+ store,
1190+ timeoutMs : 20 ,
1191+ logger : captureLogger ( ) ,
1192+ fetchImpl : async ( _url , options ) =>
1193+ new Response (
1194+ new ReadableStream ( {
1195+ start ( controller ) {
1196+ controller . enqueue (
1197+ encoder . encode (
1198+ `event: response.reasoning_summary_text.delta\ndata: ${ JSON . stringify ( {
1199+ type : "response.reasoning_summary_text.delta" ,
1200+ delta : "Working through it" ,
1201+ } ) } \n\n`
1202+ )
1203+ ) ;
1204+ options . signal . addEventListener (
1205+ "abort" ,
1206+ ( ) => {
1207+ timedOut = true ;
1208+ controller . error ( options . signal . reason || new Error ( "timed out" ) ) ;
1209+ } ,
1210+ { once : true }
1211+ ) ;
1212+ setTimeout ( ( ) => {
1213+ controller . enqueue (
1214+ encoder . encode (
1215+ `event: response.output_text.delta\ndata: ${ JSON . stringify ( {
1216+ type : "response.output_text.delta" ,
1217+ delta : "answer" ,
1218+ } ) } \n\n`
1219+ )
1220+ ) ;
1221+ controller . enqueue (
1222+ encoder . encode (
1223+ `event: response.completed\ndata: ${ JSON . stringify ( {
1224+ type : "response.completed" ,
1225+ } ) } \n\n`
1226+ )
1227+ ) ;
1228+ controller . close ( ) ;
1229+ } , 50 ) ;
1230+ } ,
1231+ } ) ,
1232+ { status : 200 , headers : { "Content-Type" : "text/event-stream" } }
1233+ ) ,
1234+ } ) ;
1235+
1236+ const result = await router . chatCompletions ( {
1237+ body : {
1238+ model : "chatgpt/gpt-5.4" ,
1239+ messages : [ { role : "user" , content : "hello" } ] ,
1240+ stream : true ,
1241+ } ,
1242+ } ) ;
1243+ const chunks = [ ] ;
1244+ await result . streamPipe ( { write : ( chunk ) => chunks . push ( String ( chunk ) ) } ) ;
1245+
1246+ assert . equal ( result . ok , true , JSON . stringify ( result . error ) ) ;
1247+ assert . equal ( timedOut , false ) ;
1248+ assert . match ( chunks . join ( "" ) , / a n s w e r / ) ;
1249+ } ) ;
1250+
10791251 it ( "propagates a gateway client disconnect through an active stream" , async ( ) => {
10801252 const store = createStore ( tmpConfig ( ) ) ;
10811253 store . seed ( { providers : [ ] } ) ;
0 commit comments