@@ -4,9 +4,9 @@ import { parseOpenAiBatchOutput, runOpenAiEmbeddingBatches } from "./embedding-b
44
55const jsonlEncoder = new TextEncoder ( ) ;
66
7- function jsonResponse ( body : unknown ) : Response {
7+ function jsonResponse ( body : unknown , status = 200 ) : Response {
88 return new Response ( JSON . stringify ( body ) , {
9- status : 200 ,
9+ status,
1010 headers : { "Content-Type" : "application/json" } ,
1111 } ) ;
1212}
@@ -129,4 +129,118 @@ describe("OpenAI embedding batch output", () => {
129129 [ "2" , [ 3 ] ] ,
130130 ] ) ;
131131 } ) ;
132+
133+ it ( "adapts OpenAI-compatible upload groups after payload-size rejection" , async ( ) => {
134+ const requests : Parameters < typeof runOpenAiEmbeddingBatches > [ 0 ] [ "requests" ] = Array . from (
135+ { length : 4 } ,
136+ ( _ , index ) => ( {
137+ custom_id : String ( index ) ,
138+ method : "POST" as const ,
139+ url : "/v1/embeddings" ,
140+ body : {
141+ model : "text-embedding-3-small" ,
142+ input : `payload-${ index } ` ,
143+ } ,
144+ } ) ,
145+ ) ;
146+ const uploadedGroups : string [ ] [ ] = [ ] ;
147+ const requestsByFileId = new Map < string , Array < { custom_id ?: string } > > ( ) ;
148+ const outputByFileId = new Map < string , string > ( ) ;
149+ const debug = vi . fn ( ) ;
150+ let fileIndex = 0 ;
151+ let batchIndex = 0 ;
152+ const fetchImpl = vi . fn ( async ( input : RequestInfo | URL , init ?: RequestInit ) => {
153+ const url = fetchInputUrl ( input ) ;
154+ if ( url . endsWith ( "/files" ) && init ?. method === "POST" ) {
155+ const form = init . body as FormData ;
156+ const file = form . get ( "file" ) ;
157+ if ( ! ( file instanceof Blob ) ) {
158+ throw new Error ( "missing batch upload file" ) ;
159+ }
160+ const uploadedRequests = ( await file . text ( ) )
161+ . split ( "\n" )
162+ . map ( ( line ) => JSON . parse ( line ) as { custom_id ?: string } ) ;
163+ const customIds = uploadedRequests . map ( ( request ) => request . custom_id ?? "" ) ;
164+ uploadedGroups . push ( customIds ) ;
165+ if ( uploadedRequests . length > 2 ) {
166+ return jsonResponse (
167+ {
168+ error : {
169+ message : "Request body too large. Maximum allowed: 10 MB" ,
170+ type : "payload_too_large" ,
171+ code : "PAYLOAD_TOO_LARGE" ,
172+ } ,
173+ } ,
174+ 413 ,
175+ ) ;
176+ }
177+ const fileId = `file-${ fileIndex } ` ;
178+ fileIndex += 1 ;
179+ requestsByFileId . set ( fileId , uploadedRequests ) ;
180+ return jsonResponse ( { id : fileId } ) ;
181+ }
182+ if ( url . endsWith ( "/batches" ) && init ?. method === "POST" ) {
183+ const body = parseStringBody ( init ) as { input_file_id ?: string } ;
184+ const batchId = `batch-${ batchIndex } ` ;
185+ const outputFileId = `output-${ batchIndex } ` ;
186+ batchIndex += 1 ;
187+ const uploadedRequests = requestsByFileId . get ( body . input_file_id ?? "" ) ?? [ ] ;
188+ outputByFileId . set (
189+ outputFileId ,
190+ uploadedRequests
191+ . map ( ( request ) =>
192+ JSON . stringify ( {
193+ custom_id : request . custom_id ,
194+ response : {
195+ status_code : 200 ,
196+ body : { data : [ { embedding : [ Number ( request . custom_id ) + 1 ] } ] } ,
197+ } ,
198+ } ) ,
199+ )
200+ . join ( "\n" ) ,
201+ ) ;
202+ return jsonResponse ( { id : batchId , status : "completed" , output_file_id : outputFileId } ) ;
203+ }
204+ const contentMatch = url . match ( / \/ f i l e s \/ ( [ ^ / ] + ) \/ c o n t e n t $ / ) ;
205+ if ( contentMatch ) {
206+ return new Response ( outputByFileId . get ( contentMatch [ 1 ] ?? "" ) ?? "" , { status : 200 } ) ;
207+ }
208+ return new Response ( "unexpected request" , { status : 500 } ) ;
209+ } ) ;
210+
211+ const byCustomId = await runOpenAiEmbeddingBatches ( {
212+ openAi : {
213+ baseUrl : "https://openai-compatible.example/v1" ,
214+ headers : { Authorization : "Bearer test" } ,
215+ model : "text-embedding-3-small" ,
216+ fetchImpl,
217+ } ,
218+ agentId : "main" ,
219+ requests,
220+ wait : true ,
221+ concurrency : 1 ,
222+ pollIntervalMs : 1000 ,
223+ timeoutMs : 60_000 ,
224+ debug,
225+ } ) ;
226+
227+ expect ( uploadedGroups ) . toEqual ( [
228+ [ "0" , "1" , "2" , "3" ] ,
229+ [ "0" , "1" ] ,
230+ [ "2" , "3" ] ,
231+ ] ) ;
232+ expect ( debug ) . toHaveBeenCalledWith (
233+ "memory embeddings: openai batch upload too large; splitting group" ,
234+ expect . objectContaining ( {
235+ requests : 4 ,
236+ parts : [ 2 , 2 ] ,
237+ } ) ,
238+ ) ;
239+ expect ( [ ...byCustomId . entries ( ) ] ) . toEqual ( [
240+ [ "0" , [ 1 ] ] ,
241+ [ "1" , [ 2 ] ] ,
242+ [ "2" , [ 3 ] ] ,
243+ [ "3" , [ 4 ] ] ,
244+ ] ) ;
245+ } ) ;
132246} ) ;
0 commit comments