@@ -10,7 +10,7 @@ const PNG_2 = Buffer.concat([PNG, Buffer.from('second image')]);
1010const dataUrl = ( buffer ) => `data:image/png;base64,${ buffer . toString ( 'base64' ) } ` ;
1111
1212function startMock ( ) {
13- const state = { pows : [ ] , uploads : [ ] , polls : 0 , completions : [ ] , fileStatus : ( ) => 'SUCCESS' , files : 0 , chats : 0 } ;
13+ const state = { pows : [ ] , uploads : [ ] , polls : 0 , completions : [ ] , fileStatus : ( ) => 'SUCCESS' , uploadReply : null , pollReply : null , files : 0 , chats : 0 } ;
1414 const server = http . createServer ( ( req , res ) => {
1515 const chunks = [ ] ;
1616 req . on ( 'data' , chunk => chunks . push ( chunk ) ) ;
@@ -25,11 +25,13 @@ function startMock() {
2525 if ( req . url === '/api/v0/file/upload_file' ) {
2626 const id = `file-${ ++ state . files } ` ;
2727 state . uploads . push ( { id, contentType : req . headers [ 'content-type' ] , pow : req . headers [ 'x-ds-pow-response' ] , body : raw } ) ;
28+ if ( state . uploadReply ) return json ( state . uploadReply . status , state . uploadReply . body ) ;
2829 return json ( 200 , { code : 0 , data : { biz_code : 0 , biz_data : { id, status : 'PENDING' , file_name : 'image.png' } } } ) ;
2930 }
3031 if ( req . url . startsWith ( '/api/v0/file/fetch_files' ) ) {
3132 state . polls ++ ;
3233 const id = new URL ( req . url , 'http://x' ) . searchParams . get ( 'file_ids' ) ;
34+ if ( state . pollReply ) return json ( state . pollReply . status , state . pollReply . body ) ;
3335 return json ( 200 , { code : 0 , data : { biz_code : 0 , biz_data : { files : [ { id, status : state . fileStatus ( id , state . polls ) } ] } } } ) ;
3436 }
3537 if ( req . url === '/api/v0/chat/completion' ) {
@@ -78,7 +80,7 @@ test.beforeEach(() => {
7880 } ) ;
7981 internals . sessions . clear ( ) ;
8082 internals . uploadedFileCache . clear ( ) ;
81- Object . assign ( mock . state , { pows : [ ] , uploads : [ ] , polls : 0 , completions : [ ] , files : 0 , fileStatus : ( ) => 'SUCCESS' } ) ;
83+ Object . assign ( mock . state , { pows : [ ] , uploads : [ ] , polls : 0 , completions : [ ] , files : 0 , uploadReply : null , pollReply : null , fileStatus : ( ) => 'SUCCESS' } ) ;
8284} ) ;
8385
8486async function post ( path , body ) {
@@ -108,6 +110,57 @@ test('OpenAI image_url data URLs are uploaded and referenced by ref_file_ids', a
108110 assert . doesNotMatch ( completion . prompt , / b a s e 6 4 / ) ;
109111} ) ;
110112
113+ test ( 'rate-limit messages in HTTP 200 business errors and HTTP 400 upload errors are classified' , async t => {
114+ const body = { model : 'deepseek-vision' , user : 'vision-upload-rate-limit' , messages : [
115+ { role : 'user' , content : [ { type : 'image_url' , image_url : { url : dataUrl ( PNG ) } } ] } ,
116+ ] } ;
117+
118+ const resetCase = ( ) => {
119+ internals . accounts [ 0 ] . cooldownUntil = 0 ;
120+ internals . accounts [ 0 ] . failures = 0 ;
121+ internals . sessions . clear ( ) ;
122+ internals . uploadedFileCache . clear ( ) ;
123+ Object . assign ( mock . state , { pows : [ ] , uploads : [ ] , polls : 0 , completions : [ ] , files : 0 , uploadReply : null , pollReply : null } ) ;
124+ } ;
125+
126+ await t . test ( 'HTTP 200 nonzero biz_code on upload' , async ( ) => {
127+ resetCase ( ) ;
128+ mock . state . uploadReply = {
129+ status : 200 ,
130+ body : { code : 0 , data : { biz_code : 1234 , biz_msg : 'Too many messages in a short period' } } ,
131+ } ;
132+ const res = await post ( '/v1/chat/completions' , body ) ;
133+ assert . equal ( res . status , 429 , res . text ) ;
134+ assert . equal ( res . json . error . type , 'rate_limit_error' ) ;
135+ assert . equal ( mock . state . completions . length , 0 ) ;
136+ } ) ;
137+
138+ await t . test ( 'HTTP 200 nonzero biz_code on file poll' , async ( ) => {
139+ resetCase ( ) ;
140+ mock . state . pollReply = {
141+ status : 200 ,
142+ body : { code : 0 , data : { biz_code : 1234 , biz_msg : 'Too many messages in a short period' } } ,
143+ } ;
144+ const res = await post ( '/v1/chat/completions' , body ) ;
145+ assert . equal ( res . status , 429 , res . text ) ;
146+ assert . equal ( res . json . error . type , 'rate_limit_error' ) ;
147+ assert . equal ( mock . state . polls , 1 ) ;
148+ assert . equal ( mock . state . completions . length , 0 ) ;
149+ } ) ;
150+
151+ await t . test ( 'HTTP 400 upload error body' , async ( ) => {
152+ resetCase ( ) ;
153+ mock . state . uploadReply = {
154+ status : 400 ,
155+ body : { error : { message : 'Too many messages in a short period' } } ,
156+ } ;
157+ const res = await post ( '/v1/chat/completions' , body ) ;
158+ assert . equal ( res . status , 429 , res . text ) ;
159+ assert . equal ( res . json . error . type , 'rate_limit_error' ) ;
160+ assert . equal ( mock . state . completions . length , 0 ) ;
161+ } ) ;
162+ } ) ;
163+
111164test ( 'Anthropic images: follow-up turns upload only new images' , async ( ) => {
112165 const image = data => ( { type : 'image' , source : { type : 'base64' , media_type : 'image/png' , data : data . toString ( 'base64' ) } } ) ;
113166 const turn1 = [ { role : 'user' , content : [ image ( PNG ) , { type : 'text' , text : 'Describe it' } ] } ] ;
0 commit comments