@@ -403,6 +403,97 @@ test('preserves usage from final OpenAI stream chunk with empty choices', async
403403 expect ( usageEvent ?. usage ?. output_tokens ) . toBe ( 45 )
404404} )
405405
406+ test ( 'uses max_tokens instead of max_completion_tokens for local providers' , async ( ) => {
407+ process . env . OPENAI_BASE_URL = 'http://localhost:11434/v1'
408+
409+ globalThis . fetch = ( async ( _input , init ) => {
410+ const body = JSON . parse ( String ( init ?. body ) )
411+ expect ( body . max_tokens ) . toBe ( 64 )
412+ expect ( body . max_completion_tokens ) . toBeUndefined ( )
413+ expect ( body . stream_options ) . toBeUndefined ( )
414+
415+ return new Response (
416+ JSON . stringify ( {
417+ id : 'chatcmpl-1' ,
418+ model : 'llama3.1:8b' ,
419+ choices : [
420+ {
421+ message : {
422+ role : 'assistant' ,
423+ content : 'hello' ,
424+ } ,
425+ finish_reason : 'stop' ,
426+ } ,
427+ ] ,
428+ usage : {
429+ prompt_tokens : 5 ,
430+ completion_tokens : 1 ,
431+ total_tokens : 6 ,
432+ } ,
433+ } ) ,
434+ {
435+ headers : {
436+ 'Content-Type' : 'application/json' ,
437+ } ,
438+ } ,
439+ )
440+ } ) as FetchType
441+
442+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
443+
444+ await client . beta . messages . create ( {
445+ model : 'llama3.1:8b' ,
446+ messages : [ { role : 'user' , content : 'hello' } ] ,
447+ max_tokens : 64 ,
448+ stream : false ,
449+ } )
450+ } )
451+
452+ test ( 'keeps max_completion_tokens for non-local non-github providers' , async ( ) => {
453+ process . env . OPENAI_BASE_URL = 'https://api.openai.com/v1'
454+
455+ globalThis . fetch = ( async ( _input , init ) => {
456+ const body = JSON . parse ( String ( init ?. body ) )
457+ expect ( body . max_completion_tokens ) . toBe ( 64 )
458+ expect ( body . max_tokens ) . toBeUndefined ( )
459+
460+ return new Response (
461+ JSON . stringify ( {
462+ id : 'chatcmpl-1' ,
463+ model : 'gpt-4o' ,
464+ choices : [
465+ {
466+ message : {
467+ role : 'assistant' ,
468+ content : 'hello' ,
469+ } ,
470+ finish_reason : 'stop' ,
471+ } ,
472+ ] ,
473+ usage : {
474+ prompt_tokens : 5 ,
475+ completion_tokens : 1 ,
476+ total_tokens : 6 ,
477+ } ,
478+ } ) ,
479+ {
480+ headers : {
481+ 'Content-Type' : 'application/json' ,
482+ } ,
483+ } ,
484+ )
485+ } ) as FetchType
486+
487+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
488+
489+ await client . beta . messages . create ( {
490+ model : 'gpt-4o' ,
491+ messages : [ { role : 'user' , content : 'hello' } ] ,
492+ max_tokens : 64 ,
493+ stream : false ,
494+ } )
495+ } )
496+
406497test ( 'preserves Gemini tool call extra_content in follow-up requests' , async ( ) => {
407498 let requestBody : Record < string , unknown > | undefined
408499
@@ -689,9 +780,117 @@ test('preserves image tool results as placeholders in follow-up requests', async
689780
690781 const toolMessage = ( requestBody ?. messages as Array < Record < string , unknown > > ) . find (
691782 message => message . role === 'tool' ,
692- ) as { content ?: string } | undefined
783+ ) as {
784+ content ?: Array < {
785+ type : string
786+ text ?: string
787+ image_url ?: { url : string }
788+ } > | string
789+ } | undefined
790+
791+ expect ( Array . isArray ( toolMessage ?. content ) ) . toBe ( true )
792+ const parts = toolMessage ?. content as Array < {
793+ type : string
794+ text ?: string
795+ image_url ?: { url : string }
796+ } >
797+ const imagePart = parts . find ( part => part . type === 'image_url' )
798+ expect ( imagePart ?. image_url ?. url ) . toBe ( 'data:image/png;base64,ZmFrZQ==' )
799+ } )
693800
694- expect ( toolMessage ?. content ) . toContain ( '[image:image/png]' )
801+ test ( 'preserves mixed text and image tool results as multipart content' , async ( ) => {
802+ let requestBody : Record < string , unknown > | undefined
803+
804+ globalThis . fetch = ( async ( _input , init ) => {
805+ requestBody = JSON . parse ( String ( init ?. body ) )
806+
807+ return new Response (
808+ JSON . stringify ( {
809+ id : 'chatcmpl-1' ,
810+ model : 'gpt-4o' ,
811+ choices : [
812+ {
813+ message : {
814+ role : 'assistant' ,
815+ content : 'done' ,
816+ } ,
817+ finish_reason : 'stop' ,
818+ } ,
819+ ] ,
820+ usage : {
821+ prompt_tokens : 12 ,
822+ completion_tokens : 4 ,
823+ total_tokens : 16 ,
824+ } ,
825+ } ) ,
826+ {
827+ headers : {
828+ 'Content-Type' : 'application/json' ,
829+ } ,
830+ } ,
831+ )
832+ } ) as FetchType
833+
834+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
835+
836+ await client . beta . messages . create ( {
837+ model : 'gpt-4o' ,
838+ system : 'test system' ,
839+ messages : [
840+ { role : 'user' , content : 'Read this screenshot' } ,
841+ {
842+ role : 'assistant' ,
843+ content : [
844+ {
845+ type : 'tool_use' ,
846+ id : 'call_image_2' ,
847+ name : 'Read' ,
848+ input : { file_path : 'C:\\temp\\screenshot.png' } ,
849+ } ,
850+ ] ,
851+ } ,
852+ {
853+ role : 'user' ,
854+ content : [
855+ {
856+ type : 'tool_result' ,
857+ tool_use_id : 'call_image_2' ,
858+ content : [
859+ { type : 'text' , text : 'Screenshot captured' } ,
860+ {
861+ type : 'image' ,
862+ source : {
863+ type : 'base64' ,
864+ media_type : 'image/png' ,
865+ data : 'ZmFrZQ==' ,
866+ } ,
867+ } ,
868+ ] ,
869+ } ,
870+ ] ,
871+ } ,
872+ ] ,
873+ max_tokens : 64 ,
874+ stream : false ,
875+ } )
876+
877+ const toolMessage = ( requestBody ?. messages as Array < Record < string , unknown > > ) . find (
878+ message => message . role === 'tool' ,
879+ ) as {
880+ content ?: Array < {
881+ type : string
882+ text ?: string
883+ image_url ?: { url : string }
884+ } >
885+ } | undefined
886+
887+ expect ( Array . isArray ( toolMessage ?. content ) ) . toBe ( true )
888+ const parts = toolMessage ?. content ?? [ ]
889+ expect ( parts [ 0 ] ) . toEqual ( { type : 'text' , text : 'Screenshot captured' } )
890+ expect ( parts [ 1 ] ) . toEqual ( {
891+ type : 'image_url' ,
892+ image_url : { url : 'data:image/png;base64,ZmFrZQ==' } ,
893+ } )
695894} )
696895
697896test ( 'uses GEMINI_ACCESS_TOKEN for Gemini OpenAI-compatible requests' , async ( ) => {
0 commit comments