@@ -130,13 +130,7 @@ describe('ProviderClient', () => {
130130 it ( 'does not include anthropic-beta header (caching is GA)' , async ( ) => {
131131 mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
132132
133- await client . forward (
134- 'anthropic' ,
135- 'sk-ant-test' ,
136- 'claude-sonnet-4-20250514' ,
137- body ,
138- false ,
139- ) ;
133+ await client . forward ( 'anthropic' , 'sk-ant-test' , 'claude-sonnet-4-20250514' , body , false ) ;
140134
141135 const headers = mockFetch . mock . calls [ 0 ] [ 1 ] . headers ;
142136 expect ( headers [ 'anthropic-beta' ] ) . toBeUndefined ( ) ;
@@ -187,13 +181,7 @@ describe('ProviderClient', () => {
187181 it ( 'uses query param auth and Gemini path' , async ( ) => {
188182 mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
189183
190- const result = await client . forward (
191- 'google' ,
192- 'AIza-test' ,
193- 'gemini-2.0-flash' ,
194- body ,
195- false ,
196- ) ;
184+ const result = await client . forward ( 'google' , 'AIza-test' , 'gemini-2.0-flash' , body , false ) ;
197185
198186 const url = mockFetch . mock . calls [ 0 ] [ 0 ] as string ;
199187 expect ( url ) . toContain ( 'generativelanguage.googleapis.com' ) ;
@@ -259,13 +247,7 @@ describe('ProviderClient', () => {
259247 { role : 'user' , content : 'Hi' } ,
260248 ] ,
261249 } ;
262- await client . forward (
263- 'openrouter' ,
264- 'sk-or' ,
265- 'openai/gpt-4o' ,
266- bodyWithSystem ,
267- false ,
268- ) ;
250+ await client . forward ( 'openrouter' , 'sk-or' , 'openai/gpt-4o' , bodyWithSystem , false ) ;
269251
270252 const sentBody = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
271253 expect ( typeof sentBody . messages [ 0 ] . content ) . toBe ( 'string' ) ;
@@ -276,15 +258,9 @@ describe('ProviderClient', () => {
276258 it ( 'merges extraHeaders into outgoing request' , async ( ) => {
277259 mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
278260
279- await client . forward (
280- 'xai' ,
281- 'sk-xai' ,
282- 'grok-2' ,
283- body ,
284- false ,
285- undefined ,
286- { 'x-grok-conv-id' : 'session-123' } ,
287- ) ;
261+ await client . forward ( 'xai' , 'sk-xai' , 'grok-2' , body , false , undefined , {
262+ 'x-grok-conv-id' : 'session-123' ,
263+ } ) ;
288264
289265 const fetchOptions = mockFetch . mock . calls [ 0 ] [ 1 ] ;
290266 expect ( fetchOptions . headers [ 'x-grok-conv-id' ] ) . toBe ( 'session-123' ) ;
@@ -293,15 +269,9 @@ describe('ProviderClient', () => {
293269 it ( 'does not override base headers' , async ( ) => {
294270 mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
295271
296- await client . forward (
297- 'openai' ,
298- 'sk-test' ,
299- 'gpt-4o' ,
300- body ,
301- false ,
302- undefined ,
303- { 'X-Custom' : 'value' } ,
304- ) ;
272+ await client . forward ( 'openai' , 'sk-test' , 'gpt-4o' , body , false , undefined , {
273+ 'X-Custom' : 'value' ,
274+ } ) ;
305275
306276 const fetchOptions = mockFetch . mock . calls [ 0 ] [ 1 ] ;
307277 expect ( fetchOptions . headers [ 'Authorization' ] ) . toBe ( 'Bearer sk-test' ) ;
@@ -313,13 +283,7 @@ describe('ProviderClient', () => {
313283 it ( 'resolves gemini to google endpoint' , async ( ) => {
314284 mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
315285
316- const result = await client . forward (
317- 'gemini' ,
318- 'AIza-test' ,
319- 'gemini-2.0-flash' ,
320- body ,
321- false ,
322- ) ;
286+ const result = await client . forward ( 'gemini' , 'AIza-test' , 'gemini-2.0-flash' , body , false ) ;
323287
324288 const url = mockFetch . mock . calls [ 0 ] [ 0 ] as string ;
325289 expect ( url ) . toContain ( 'generativelanguage.googleapis.com' ) ;
@@ -394,10 +358,12 @@ describe('ProviderClient', () => {
394358 describe ( 'convertGoogleResponse' , ( ) => {
395359 it ( 'delegates to fromGoogleResponse' , ( ) => {
396360 const googleBody = {
397- candidates : [ {
398- content : { parts : [ { text : 'Hello' } ] } ,
399- finishReason : 'STOP' ,
400- } ] ,
361+ candidates : [
362+ {
363+ content : { parts : [ { text : 'Hello' } ] } ,
364+ finishReason : 'STOP' ,
365+ } ,
366+ ] ,
401367 } ;
402368 const result = client . convertGoogleResponse ( googleBody , 'gemini-2.0-flash' ) ;
403369
@@ -411,9 +377,11 @@ describe('ProviderClient', () => {
411377 describe ( 'convertGoogleStreamChunk' , ( ) => {
412378 it ( 'delegates to transformGoogleStreamChunk' , ( ) => {
413379 const chunk = JSON . stringify ( {
414- candidates : [ {
415- content : { parts : [ { text : 'Hi' } ] } ,
416- } ] ,
380+ candidates : [
381+ {
382+ content : { parts : [ { text : 'Hi' } ] } ,
383+ } ,
384+ ] ,
417385 } ) ;
418386 const result = client . convertGoogleStreamChunk ( chunk , 'gemini-2.0-flash' ) ;
419387
@@ -445,7 +413,8 @@ describe('ProviderClient', () => {
445413
446414 describe ( 'convertAnthropicStreamChunk' , ( ) => {
447415 it ( 'delegates to transformAnthropicStreamChunk' , ( ) => {
448- const chunk = 'event: content_block_delta\n{"type":"content_block_delta","delta":{"type":"text_delta","text":"Hi"}}' ;
416+ const chunk =
417+ 'event: content_block_delta\n{"type":"content_block_delta","delta":{"type":"text_delta","text":"Hi"}}' ;
449418 const result = client . convertAnthropicStreamChunk ( chunk , 'claude-sonnet-4-20250514' ) ;
450419
451420 expect ( result ) . toContain ( 'data: ' ) ;
@@ -471,12 +440,8 @@ describe('ProviderClient', () => {
471440
472441 await client . forward ( 'google' , 'AIzaSyABCDEF12345' , 'gemini-2.0-flash' , body , false ) ;
473442
474- expect ( debugSpy ) . toHaveBeenCalledWith (
475- expect . stringContaining ( 'key=***' ) ,
476- ) ;
477- expect ( debugSpy ) . toHaveBeenCalledWith (
478- expect . not . stringContaining ( 'AIzaSyABCDEF12345' ) ,
479- ) ;
443+ expect ( debugSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( 'key=***' ) ) ;
444+ expect ( debugSpy ) . toHaveBeenCalledWith ( expect . not . stringContaining ( 'AIzaSyABCDEF12345' ) ) ;
480445
481446 debugSpy . mockRestore ( ) ;
482447 } ) ;
@@ -507,19 +472,91 @@ describe('ProviderClient', () => {
507472 } ) ;
508473 } ) ;
509474
475+ describe ( 'Body sanitization for non-OpenAI providers' , ( ) => {
476+ const bodyWithOpenAiFields = {
477+ messages : [ { role : 'user' , content : 'Hello' } ] ,
478+ temperature : 0.7 ,
479+ store : false ,
480+ max_completion_tokens : 8192 ,
481+ metadata : { user : 'test' } ,
482+ service_tier : 'default' ,
483+ stream_options : { include_usage : true } ,
484+ } ;
485+
486+ it ( 'strips OpenAI-only fields for Mistral' , async ( ) => {
487+ mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
488+ await client . forward ( 'mistral' , 'sk-mi' , 'mistral-small' , bodyWithOpenAiFields , false ) ;
489+
490+ const sentBody = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
491+ expect ( sentBody . store ) . toBeUndefined ( ) ;
492+ expect ( sentBody . metadata ) . toBeUndefined ( ) ;
493+ expect ( sentBody . service_tier ) . toBeUndefined ( ) ;
494+ expect ( sentBody . stream_options ) . toBeUndefined ( ) ;
495+ expect ( sentBody . messages ) . toEqual ( bodyWithOpenAiFields . messages ) ;
496+ expect ( sentBody . temperature ) . toBe ( 0.7 ) ;
497+ } ) ;
498+
499+ it ( 'converts max_completion_tokens to max_tokens for non-OpenAI providers' , async ( ) => {
500+ mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
501+ await client . forward ( 'mistral' , 'sk-mi' , 'mistral-small' , bodyWithOpenAiFields , false ) ;
502+
503+ const sentBody = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
504+ expect ( sentBody . max_completion_tokens ) . toBeUndefined ( ) ;
505+ expect ( sentBody . max_tokens ) . toBe ( 8192 ) ;
506+ } ) ;
507+
508+ it ( 'does not overwrite existing max_tokens when converting' , async ( ) => {
509+ mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
510+ const bodyWithBoth = { ...bodyWithOpenAiFields , max_tokens : 4096 } ;
511+ await client . forward ( 'deepseek' , 'sk-ds' , 'deepseek-chat' , bodyWithBoth , false ) ;
512+
513+ const sentBody = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
514+ expect ( sentBody . max_tokens ) . toBe ( 4096 ) ;
515+ expect ( sentBody . max_completion_tokens ) . toBeUndefined ( ) ;
516+ } ) ;
517+
518+ it ( 'strips OpenAI-only fields for DeepSeek' , async ( ) => {
519+ mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
520+ await client . forward ( 'deepseek' , 'sk-ds' , 'deepseek-chat' , bodyWithOpenAiFields , false ) ;
521+
522+ const sentBody = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
523+ expect ( sentBody . store ) . toBeUndefined ( ) ;
524+ expect ( sentBody . service_tier ) . toBeUndefined ( ) ;
525+ } ) ;
526+
527+ it ( 'preserves all fields for OpenAI' , async ( ) => {
528+ mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
529+ await client . forward ( 'openai' , 'sk-test' , 'gpt-4o' , bodyWithOpenAiFields , false ) ;
530+
531+ const sentBody = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
532+ expect ( sentBody . store ) . toBe ( false ) ;
533+ expect ( sentBody . max_completion_tokens ) . toBe ( 8192 ) ;
534+ expect ( sentBody . metadata ) . toEqual ( { user : 'test' } ) ;
535+ } ) ;
536+
537+ it ( 'preserves all fields for OpenRouter' , async ( ) => {
538+ mockFetch . mockResolvedValue ( new Response ( '{}' , { status : 200 } ) ) ;
539+ await client . forward ( 'openrouter' , 'sk-or' , 'openai/gpt-4o' , bodyWithOpenAiFields , false ) ;
540+
541+ const sentBody = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
542+ expect ( sentBody . store ) . toBe ( false ) ;
543+ expect ( sentBody . max_completion_tokens ) . toBe ( 8192 ) ;
544+ } ) ;
545+ } ) ;
546+
510547 describe ( 'Error handling' , ( ) => {
511548 it ( 'throws for unknown provider' , async ( ) => {
512- await expect (
513- client . forward ( 'unknown-provider' , 'key' , 'model' , body , false ) ,
514- ) . rejects . toThrow ( 'No endpoint configured for provider' ) ;
549+ await expect ( client . forward ( 'unknown-provider' , 'key' , 'model' , body , false ) ) . rejects . toThrow (
550+ 'No endpoint configured for provider' ,
551+ ) ;
515552 } ) ;
516553
517554 it ( 'propagates fetch errors' , async ( ) => {
518555 mockFetch . mockRejectedValue ( new Error ( 'Network error' ) ) ;
519556
520- await expect (
521- client . forward ( 'openai' , 'sk-test' , 'gpt-4o' , body , false ) ,
522- ) . rejects . toThrow ( 'Network error' ) ;
557+ await expect ( client . forward ( 'openai' , 'sk-test' , 'gpt-4o' , body , false ) ) . rejects . toThrow (
558+ 'Network error' ,
559+ ) ;
523560 } ) ;
524561 } ) ;
525562} ) ;
0 commit comments