@@ -3,7 +3,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
33
44import { LiteLLMHandler } from "../lite-llm"
55import { ApiHandlerOptions } from "../../../shared/api"
6- import { litellmDefaultModelId , litellmDefaultModelInfo } from "@roo-code/types"
6+ import { litellmDefaultModelId , litellmDefaultModelInfo , TOOL_PROTOCOL } from "@roo-code/types"
77
88// Mock vscode first to avoid import errors
99vi . mock ( "vscode" , ( ) => ( { } ) )
@@ -40,6 +40,12 @@ vi.mock("../fetchers/modelCache", () => ({
4040 "claude-3-opus" : { ...litellmDefaultModelInfo , maxTokens : 8192 } ,
4141 "llama-3" : { ...litellmDefaultModelInfo , maxTokens : 8192 } ,
4242 "gpt-4-turbo" : { ...litellmDefaultModelInfo , maxTokens : 8192 } ,
43+ // Gemini models for thought signature injection tests
44+ "gemini-3-pro" : { ...litellmDefaultModelInfo , maxTokens : 8192 , supportsNativeTools : true } ,
45+ "gemini-3-flash" : { ...litellmDefaultModelInfo , maxTokens : 8192 , supportsNativeTools : true } ,
46+ "gemini-2.5-pro" : { ...litellmDefaultModelInfo , maxTokens : 8192 , supportsNativeTools : true } ,
47+ "google/gemini-3-pro" : { ...litellmDefaultModelInfo , maxTokens : 8192 , supportsNativeTools : true } ,
48+ "vertex_ai/gemini-3-pro" : { ...litellmDefaultModelInfo , maxTokens : 8192 , supportsNativeTools : true } ,
4349 } )
4450 } ) ,
4551 getModelsFromCache : vi . fn ( ) . mockReturnValue ( undefined ) ,
@@ -388,4 +394,315 @@ describe("LiteLLMHandler", () => {
388394 expect ( createCall . max_completion_tokens ) . toBeUndefined ( )
389395 } )
390396 } )
397+
398+ describe ( "Gemini thought signature injection" , ( ) => {
399+ describe ( "isGeminiModel detection" , ( ) => {
400+ it ( "should detect Gemini 3 models" , ( ) => {
401+ const handler = new LiteLLMHandler ( mockOptions )
402+ const isGeminiModel = ( handler as any ) . isGeminiModel . bind ( handler )
403+
404+ expect ( isGeminiModel ( "gemini-3-pro" ) ) . toBe ( true )
405+ expect ( isGeminiModel ( "gemini-3-flash" ) ) . toBe ( true )
406+ expect ( isGeminiModel ( "gemini-3-pro-preview" ) ) . toBe ( true )
407+ } )
408+
409+ it ( "should detect Gemini 2.5 models" , ( ) => {
410+ const handler = new LiteLLMHandler ( mockOptions )
411+ const isGeminiModel = ( handler as any ) . isGeminiModel . bind ( handler )
412+
413+ expect ( isGeminiModel ( "gemini-2.5-pro" ) ) . toBe ( true )
414+ expect ( isGeminiModel ( "gemini-2.5-flash" ) ) . toBe ( true )
415+ } )
416+
417+ it ( "should detect provider-prefixed Gemini models" , ( ) => {
418+ const handler = new LiteLLMHandler ( mockOptions )
419+ const isGeminiModel = ( handler as any ) . isGeminiModel . bind ( handler )
420+
421+ expect ( isGeminiModel ( "google/gemini-3-pro" ) ) . toBe ( true )
422+ expect ( isGeminiModel ( "vertex_ai/gemini-3-pro" ) ) . toBe ( true )
423+ expect ( isGeminiModel ( "vertex/gemini-2.5-pro" ) ) . toBe ( true )
424+ } )
425+
426+ it ( "should not detect non-Gemini models" , ( ) => {
427+ const handler = new LiteLLMHandler ( mockOptions )
428+ const isGeminiModel = ( handler as any ) . isGeminiModel . bind ( handler )
429+
430+ expect ( isGeminiModel ( "gpt-4" ) ) . toBe ( false )
431+ expect ( isGeminiModel ( "claude-3-opus" ) ) . toBe ( false )
432+ expect ( isGeminiModel ( "gemini-1.5-pro" ) ) . toBe ( false )
433+ expect ( isGeminiModel ( "gemini-2.0-flash" ) ) . toBe ( false )
434+ } )
435+ } )
436+
437+ describe ( "injectThoughtSignatureForGemini" , ( ) => {
438+ // Base64 encoded "skip_thought_signature_validator"
439+ const dummySignature = Buffer . from ( "skip_thought_signature_validator" ) . toString ( "base64" )
440+
441+ it ( "should inject provider_specific_fields.thought_signature for assistant messages with tool_calls" , ( ) => {
442+ const handler = new LiteLLMHandler ( mockOptions )
443+ const injectThoughtSignature = ( handler as any ) . injectThoughtSignatureForGemini . bind ( handler )
444+
445+ const messages = [
446+ { role : "user" , content : "Hello" } ,
447+ {
448+ role : "assistant" ,
449+ content : "" ,
450+ tool_calls : [
451+ { id : "call_123" , type : "function" , function : { name : "test_tool" , arguments : "{}" } } ,
452+ ] ,
453+ } ,
454+ { role : "tool" , tool_call_id : "call_123" , content : "result" } ,
455+ ]
456+
457+ const result = injectThoughtSignature ( messages )
458+
459+ // The first tool call should have provider_specific_fields.thought_signature injected
460+ expect ( result [ 1 ] . tool_calls [ 0 ] . provider_specific_fields ) . toBeDefined ( )
461+ expect ( result [ 1 ] . tool_calls [ 0 ] . provider_specific_fields . thought_signature ) . toBe ( dummySignature )
462+ } )
463+
464+ it ( "should not inject if assistant message has no tool_calls" , ( ) => {
465+ const handler = new LiteLLMHandler ( mockOptions )
466+ const injectThoughtSignature = ( handler as any ) . injectThoughtSignatureForGemini . bind ( handler )
467+
468+ const messages = [
469+ { role : "user" , content : "Hello" } ,
470+ { role : "assistant" , content : "Hi there!" } ,
471+ ]
472+
473+ const result = injectThoughtSignature ( messages )
474+
475+ // No changes should be made
476+ expect ( result [ 1 ] . tool_calls ) . toBeUndefined ( )
477+ } )
478+
479+ it ( "should always overwrite existing thought_signature" , ( ) => {
480+ const handler = new LiteLLMHandler ( mockOptions )
481+ const injectThoughtSignature = ( handler as any ) . injectThoughtSignatureForGemini . bind ( handler )
482+
483+ const existingSignature = "existing_signature_base64"
484+
485+ const messages = [
486+ { role : "user" , content : "Hello" } ,
487+ {
488+ role : "assistant" ,
489+ content : "" ,
490+ tool_calls : [
491+ {
492+ id : "call_123" ,
493+ type : "function" ,
494+ function : { name : "test_tool" , arguments : "{}" } ,
495+ provider_specific_fields : { thought_signature : existingSignature } ,
496+ } ,
497+ ] ,
498+ } ,
499+ ]
500+
501+ const result = injectThoughtSignature ( messages )
502+
503+ // Should overwrite with dummy signature (always inject to ensure compatibility)
504+ expect ( result [ 1 ] . tool_calls [ 0 ] . provider_specific_fields . thought_signature ) . toBe ( dummySignature )
505+ } )
506+
507+ it ( "should inject signature into ALL tool calls for parallel calls" , ( ) => {
508+ const handler = new LiteLLMHandler ( mockOptions )
509+ const injectThoughtSignature = ( handler as any ) . injectThoughtSignatureForGemini . bind ( handler )
510+
511+ const messages = [
512+ { role : "user" , content : "Hello" } ,
513+ {
514+ role : "assistant" ,
515+ content : "" ,
516+ tool_calls : [
517+ { id : "call_first" , type : "function" , function : { name : "tool1" , arguments : "{}" } } ,
518+ { id : "call_second" , type : "function" , function : { name : "tool2" , arguments : "{}" } } ,
519+ { id : "call_third" , type : "function" , function : { name : "tool3" , arguments : "{}" } } ,
520+ ] ,
521+ } ,
522+ ]
523+
524+ const result = injectThoughtSignature ( messages )
525+
526+ // ALL tool calls should have the signature
527+ expect ( result [ 1 ] . tool_calls [ 0 ] . provider_specific_fields . thought_signature ) . toBe ( dummySignature )
528+ expect ( result [ 1 ] . tool_calls [ 1 ] . provider_specific_fields . thought_signature ) . toBe ( dummySignature )
529+ expect ( result [ 1 ] . tool_calls [ 2 ] . provider_specific_fields . thought_signature ) . toBe ( dummySignature )
530+ } )
531+
532+ it ( "should preserve existing provider_specific_fields when adding thought_signature" , ( ) => {
533+ const handler = new LiteLLMHandler ( mockOptions )
534+ const injectThoughtSignature = ( handler as any ) . injectThoughtSignatureForGemini . bind ( handler )
535+
536+ const messages = [
537+ { role : "user" , content : "Hello" } ,
538+ {
539+ role : "assistant" ,
540+ content : "" ,
541+ tool_calls : [
542+ {
543+ id : "call_123" ,
544+ type : "function" ,
545+ function : { name : "test_tool" , arguments : "{}" } ,
546+ provider_specific_fields : { other_field : "value" } ,
547+ } ,
548+ ] ,
549+ } ,
550+ ]
551+
552+ const result = injectThoughtSignature ( messages )
553+
554+ // Should have both existing field and new thought_signature
555+ expect ( result [ 1 ] . tool_calls [ 0 ] . provider_specific_fields . other_field ) . toBe ( "value" )
556+ expect ( result [ 1 ] . tool_calls [ 0 ] . provider_specific_fields . thought_signature ) . toBe ( dummySignature )
557+ } )
558+ } )
559+
560+ describe ( "createMessage integration with Gemini models" , ( ) => {
561+ // Base64 encoded "skip_thought_signature_validator"
562+ const dummySignature = Buffer . from ( "skip_thought_signature_validator" ) . toString ( "base64" )
563+
564+ it ( "should inject thought signatures for Gemini 3 models with native tools" , async ( ) => {
565+ const optionsWithGemini : ApiHandlerOptions = {
566+ ...mockOptions ,
567+ litellmModelId : "gemini-3-pro" ,
568+ }
569+ handler = new LiteLLMHandler ( optionsWithGemini )
570+
571+ // Mock fetchModel to return a Gemini model with native tool support
572+ vi . spyOn ( handler as any , "fetchModel" ) . mockResolvedValue ( {
573+ id : "gemini-3-pro" ,
574+ info : { ...litellmDefaultModelInfo , maxTokens : 8192 , supportsNativeTools : true } ,
575+ } )
576+
577+ const systemPrompt = "You are a helpful assistant"
578+ // Simulate conversation history with a tool call from a previous model (Claude)
579+ const messages : Anthropic . Messages . MessageParam [ ] = [
580+ { role : "user" , content : "Hello" } ,
581+ {
582+ role : "assistant" ,
583+ content : [
584+ { type : "text" , text : "I'll help you with that." } ,
585+ { type : "tool_use" , id : "toolu_123" , name : "read_file" , input : { path : "test.txt" } } ,
586+ ] ,
587+ } ,
588+ {
589+ role : "user" ,
590+ content : [ { type : "tool_result" , tool_use_id : "toolu_123" , content : "file contents" } ] ,
591+ } ,
592+ { role : "user" , content : "Thanks!" } ,
593+ ]
594+
595+ // Mock the stream response
596+ const mockStream = {
597+ async * [ Symbol . asyncIterator ] ( ) {
598+ yield {
599+ choices : [ { delta : { content : "You're welcome!" } } ] ,
600+ usage : {
601+ prompt_tokens : 100 ,
602+ completion_tokens : 20 ,
603+ } ,
604+ }
605+ } ,
606+ }
607+
608+ mockCreate . mockReturnValue ( {
609+ withResponse : vi . fn ( ) . mockResolvedValue ( { data : mockStream } ) ,
610+ } )
611+
612+ // Provide tools and native protocol to trigger the injection
613+ const metadata = {
614+ tools : [
615+ {
616+ type : "function" ,
617+ function : { name : "read_file" , description : "Read a file" , parameters : { } } ,
618+ } ,
619+ ] ,
620+ toolProtocol : TOOL_PROTOCOL . NATIVE ,
621+ }
622+
623+ const generator = handler . createMessage ( systemPrompt , messages , metadata as any )
624+ for await ( const _chunk of generator ) {
625+ // Consume the generator
626+ }
627+
628+ // Verify that the assistant message with tool_calls has thought_signature injected
629+ const createCall = mockCreate . mock . calls [ 0 ] [ 0 ]
630+ const assistantMessage = createCall . messages . find (
631+ ( msg : any ) => msg . role === "assistant" && msg . tool_calls && msg . tool_calls . length > 0 ,
632+ )
633+
634+ expect ( assistantMessage ) . toBeDefined ( )
635+ // First tool call should have the thought signature
636+ expect ( assistantMessage . tool_calls [ 0 ] . provider_specific_fields ) . toBeDefined ( )
637+ expect ( assistantMessage . tool_calls [ 0 ] . provider_specific_fields . thought_signature ) . toBe ( dummySignature )
638+ } )
639+
640+ it ( "should not inject thought signatures for non-Gemini models" , async ( ) => {
641+ const optionsWithGPT4 : ApiHandlerOptions = {
642+ ...mockOptions ,
643+ litellmModelId : "gpt-4" ,
644+ }
645+ handler = new LiteLLMHandler ( optionsWithGPT4 )
646+
647+ vi . spyOn ( handler as any , "fetchModel" ) . mockResolvedValue ( {
648+ id : "gpt-4" ,
649+ info : { ...litellmDefaultModelInfo , maxTokens : 8192 , supportsNativeTools : true } ,
650+ } )
651+
652+ const systemPrompt = "You are a helpful assistant"
653+ const messages : Anthropic . Messages . MessageParam [ ] = [
654+ { role : "user" , content : "Hello" } ,
655+ {
656+ role : "assistant" ,
657+ content : [
658+ { type : "text" , text : "I'll help you with that." } ,
659+ { type : "tool_use" , id : "toolu_123" , name : "read_file" , input : { path : "test.txt" } } ,
660+ ] ,
661+ } ,
662+ {
663+ role : "user" ,
664+ content : [ { type : "tool_result" , tool_use_id : "toolu_123" , content : "file contents" } ] ,
665+ } ,
666+ ]
667+
668+ const mockStream = {
669+ async * [ Symbol . asyncIterator ] ( ) {
670+ yield {
671+ choices : [ { delta : { content : "Response" } } ] ,
672+ usage : { prompt_tokens : 100 , completion_tokens : 20 } ,
673+ }
674+ } ,
675+ }
676+
677+ mockCreate . mockReturnValue ( {
678+ withResponse : vi . fn ( ) . mockResolvedValue ( { data : mockStream } ) ,
679+ } )
680+
681+ const metadata = {
682+ tools : [
683+ {
684+ type : "function" ,
685+ function : { name : "read_file" , description : "Read a file" , parameters : { } } ,
686+ } ,
687+ ] ,
688+ toolProtocol : TOOL_PROTOCOL . NATIVE ,
689+ }
690+
691+ const generator = handler . createMessage ( systemPrompt , messages , metadata as any )
692+ for await ( const _chunk of generator ) {
693+ // Consume
694+ }
695+
696+ // Verify that thought_signature was NOT injected for non-Gemini model
697+ const createCall = mockCreate . mock . calls [ 0 ] [ 0 ]
698+ const assistantMessage = createCall . messages . find (
699+ ( msg : any ) => msg . role === "assistant" && msg . tool_calls && msg . tool_calls . length > 0 ,
700+ )
701+
702+ expect ( assistantMessage ) . toBeDefined ( )
703+ // Tool calls should not have provider_specific_fields added
704+ expect ( assistantMessage . tool_calls [ 0 ] . provider_specific_fields ) . toBeUndefined ( )
705+ } )
706+ } )
707+ } )
391708} )
0 commit comments