@@ -2368,6 +2368,161 @@ describe("buildFixtureResponse format detection", () => {
23682368 expect ( fixtureContent . fixtures [ 0 ] . response . embedding ) . toEqual ( [ 0.1 , 0.2 , 0.3 ] ) ;
23692369 } ) ;
23702370
2371+ it ( "decodes base64-encoded embeddings when encoding_format is base64" , async ( ) => {
2372+ // Float32Array([0.5, 1.0, -0.25]) encoded as base64
2373+ const base64Embedding = "AAAAPwAAgD8AAIC+" ;
2374+ const { url : upstreamUrl } = await createRawUpstreamWithStatus ( {
2375+ object : "list" ,
2376+ data : [ { object : "embedding" , index : 0 , embedding : base64Embedding } ] ,
2377+ model : "text-embedding-3-small" ,
2378+ usage : { prompt_tokens : 5 , total_tokens : 5 } ,
2379+ } ) ;
2380+
2381+ tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "llmock-record-" ) ) ;
2382+ recorder = await createServer ( [ ] , {
2383+ port : 0 ,
2384+ record : { providers : { openai : upstreamUrl } , fixturePath : tmpDir } ,
2385+ } ) ;
2386+
2387+ const resp = await post ( `${ recorder . url } /v1/embeddings` , {
2388+ model : "text-embedding-3-small" ,
2389+ input : "base64 embedding test" ,
2390+ encoding_format : "base64" ,
2391+ } ) ;
2392+
2393+ expect ( resp . status ) . toBe ( 200 ) ;
2394+
2395+ const files = fs . readdirSync ( tmpDir ) ;
2396+ const fixtureFiles = files . filter ( ( f ) => f . endsWith ( ".json" ) ) ;
2397+ expect ( fixtureFiles ) . toHaveLength ( 1 ) ;
2398+
2399+ const fixtureContent = JSON . parse (
2400+ fs . readFileSync ( path . join ( tmpDir , fixtureFiles [ 0 ] ) , "utf-8" ) ,
2401+ ) as {
2402+ fixtures : Array < {
2403+ response : { embedding ?: number [ ] } ;
2404+ } > ;
2405+ } ;
2406+ // Should decode base64 → Float32Array → number[]
2407+ expect ( fixtureContent . fixtures [ 0 ] . response . embedding ) . toEqual ( [ 0.5 , 1 , - 0.25 ] ) ;
2408+ } ) ;
2409+
2410+ it ( "does not decode base64 embedding when encoding_format is not set" , async ( ) => {
2411+ // Same base64 string but no encoding_format in request — should NOT decode
2412+ const base64Embedding = "AAAAPwAAgD8AAIC+" ;
2413+ const { url : upstreamUrl } = await createRawUpstreamWithStatus ( {
2414+ object : "list" ,
2415+ data : [ { object : "embedding" , index : 0 , embedding : base64Embedding } ] ,
2416+ model : "text-embedding-3-small" ,
2417+ usage : { prompt_tokens : 5 , total_tokens : 5 } ,
2418+ } ) ;
2419+
2420+ tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "llmock-record-" ) ) ;
2421+ recorder = await createServer ( [ ] , {
2422+ port : 0 ,
2423+ record : { providers : { openai : upstreamUrl } , fixturePath : tmpDir } ,
2424+ } ) ;
2425+
2426+ const resp = await post ( `${ recorder . url } /v1/embeddings` , {
2427+ model : "text-embedding-3-small" ,
2428+ input : "base64 no format test" ,
2429+ } ) ;
2430+
2431+ expect ( resp . status ) . toBe ( 200 ) ;
2432+
2433+ const files = fs . readdirSync ( tmpDir ) ;
2434+ const fixtureFiles = files . filter ( ( f ) => f . endsWith ( ".json" ) ) ;
2435+ expect ( fixtureFiles ) . toHaveLength ( 1 ) ;
2436+
2437+ const fixtureContent = JSON . parse (
2438+ fs . readFileSync ( path . join ( tmpDir , fixtureFiles [ 0 ] ) , "utf-8" ) ,
2439+ ) as {
2440+ fixtures : Array < {
2441+ response : { error ?: { type : string } } ;
2442+ } > ;
2443+ } ;
2444+ // Without encoding_format, base64 string embedding is not an array →
2445+ // falls through to proxy_error
2446+ expect ( fixtureContent . fixtures [ 0 ] . response . error ?. type ) . toBe ( "proxy_error" ) ;
2447+ } ) ;
2448+
2449+ it ( "still detects array embeddings when encoding_format is base64" , async ( ) => {
2450+ // Some upstream responses return array format even when base64 was requested
2451+ const { url : upstreamUrl } = await createRawUpstreamWithStatus ( {
2452+ object : "list" ,
2453+ data : [ { object : "embedding" , index : 0 , embedding : [ 0.5 , 1.0 , - 0.25 ] } ] ,
2454+ model : "text-embedding-3-small" ,
2455+ usage : { prompt_tokens : 5 , total_tokens : 5 } ,
2456+ } ) ;
2457+
2458+ tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "llmock-record-" ) ) ;
2459+ recorder = await createServer ( [ ] , {
2460+ port : 0 ,
2461+ record : { providers : { openai : upstreamUrl } , fixturePath : tmpDir } ,
2462+ } ) ;
2463+
2464+ const resp = await post ( `${ recorder . url } /v1/embeddings` , {
2465+ model : "text-embedding-3-small" ,
2466+ input : "array with base64 format test" ,
2467+ encoding_format : "base64" ,
2468+ } ) ;
2469+
2470+ expect ( resp . status ) . toBe ( 200 ) ;
2471+
2472+ const files = fs . readdirSync ( tmpDir ) ;
2473+ const fixtureFiles = files . filter ( ( f ) => f . endsWith ( ".json" ) ) ;
2474+ expect ( fixtureFiles ) . toHaveLength ( 1 ) ;
2475+
2476+ const fixtureContent = JSON . parse (
2477+ fs . readFileSync ( path . join ( tmpDir , fixtureFiles [ 0 ] ) , "utf-8" ) ,
2478+ ) as {
2479+ fixtures : Array < {
2480+ response : { embedding ?: number [ ] } ;
2481+ } > ;
2482+ } ;
2483+ // Array.isArray check comes first, so array embeddings work regardless of encoding_format
2484+ expect ( fixtureContent . fixtures [ 0 ] . response . embedding ) . toEqual ( [ 0.5 , 1 , - 0.25 ] ) ;
2485+ } ) ;
2486+
2487+ it ( "handles truncated base64 embedding gracefully (odd byte count)" , async ( ) => {
2488+ // 2 bytes decodes to 0 float32 elements — produces empty embedding, not a crash
2489+ const shortBase64 = Buffer . from ( [ 0x00 , 0x01 ] ) . toString ( "base64" ) ;
2490+ const { url : upstreamUrl } = await createRawUpstreamWithStatus ( {
2491+ object : "list" ,
2492+ data : [ { object : "embedding" , index : 0 , embedding : shortBase64 } ] ,
2493+ model : "text-embedding-3-small" ,
2494+ usage : { prompt_tokens : 5 , total_tokens : 5 } ,
2495+ } ) ;
2496+
2497+ tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "llmock-record-" ) ) ;
2498+ recorder = await createServer ( [ ] , {
2499+ port : 0 ,
2500+ record : { providers : { openai : upstreamUrl } , fixturePath : tmpDir } ,
2501+ } ) ;
2502+
2503+ const resp = await post ( `${ recorder . url } /v1/embeddings` , {
2504+ model : "text-embedding-3-small" ,
2505+ input : "truncated base64 test" ,
2506+ encoding_format : "base64" ,
2507+ } ) ;
2508+
2509+ expect ( resp . status ) . toBe ( 200 ) ;
2510+
2511+ const files = fs . readdirSync ( tmpDir ) ;
2512+ const fixtureFiles = files . filter ( ( f ) => f . endsWith ( ".json" ) ) ;
2513+ expect ( fixtureFiles ) . toHaveLength ( 1 ) ;
2514+
2515+ const fixtureContent = JSON . parse (
2516+ fs . readFileSync ( path . join ( tmpDir , fixtureFiles [ 0 ] ) , "utf-8" ) ,
2517+ ) as {
2518+ fixtures : Array < {
2519+ response : { embedding ?: number [ ] } ;
2520+ } > ;
2521+ } ;
2522+ // Truncated base64 decodes to empty array rather than crashing
2523+ expect ( fixtureContent . fixtures [ 0 ] . response . embedding ) . toEqual ( [ ] ) ;
2524+ } ) ;
2525+
23712526 it ( "preserves error code field from upstream error response" , async ( ) => {
23722527 const { url : upstreamUrl } = await createRawUpstreamWithStatus (
23732528 {
0 commit comments