@@ -108,6 +108,25 @@ vitest.mock("../fetchers/modelCache", () => ({
108108 excludedTools : [ "existing_excluded" ] ,
109109 includedTools : [ "existing_included" ] ,
110110 } ,
111+ "google/gemini-2.5-pro" : {
112+ maxTokens : 65536 ,
113+ contextWindow : 1048576 ,
114+ supportsImages : true ,
115+ supportsPromptCache : true ,
116+ inputPrice : 1.25 ,
117+ outputPrice : 10 ,
118+ description : "Gemini 2.5 Pro" ,
119+ thinking : true ,
120+ } ,
121+ "google/gemini-2.5-flash" : {
122+ maxTokens : 65536 ,
123+ contextWindow : 1048576 ,
124+ supportsImages : true ,
125+ supportsPromptCache : true ,
126+ inputPrice : 0.15 ,
127+ outputPrice : 0.6 ,
128+ description : "Gemini 2.5 Flash" ,
129+ } ,
111130 } )
112131 } ) ,
113132 getModelsFromCache : vitest . fn ( ) . mockReturnValue ( null ) ,
@@ -275,7 +294,6 @@ describe("OpenRouterHandler", () => {
275294 // Verify streamText was called with correct parameters
276295 expect ( mockStreamText ) . toHaveBeenCalledWith (
277296 expect . objectContaining ( {
278- system : systemPrompt ,
279297 messages : expect . any ( Array ) ,
280298 maxOutputTokens : 8192 ,
281299 temperature : 0 ,
@@ -516,7 +534,7 @@ describe("OpenRouterHandler", () => {
516534 expect ( chunks [ 3 ] ) . toEqual ( { type : "tool_call_end" , id : "call_1" } )
517535 } )
518536
519- it ( "handles complete tool call events" , async ( ) => {
537+ it ( "ignores tool- call events (handled by tool-input-start/delta/end) " , async ( ) => {
520538 const handler = new OpenRouterHandler ( mockOptions )
521539
522540 const mockFullStream = ( async function * ( ) {
@@ -541,12 +559,10 @@ describe("OpenRouterHandler", () => {
541559 chunks . push ( chunk )
542560 }
543561
544- expect ( chunks [ 0 ] ) . toEqual ( {
545- type : "tool_call" ,
546- id : "call_1" ,
547- name : "read_file" ,
548- arguments : '{"path":"test.ts"}' ,
549- } )
562+ // tool-call is intentionally ignored by processAiSdkStreamPart,
563+ // only usage chunk should be present
564+ expect ( chunks ) . toHaveLength ( 1 )
565+ expect ( chunks [ 0 ] ) . toMatchObject ( { type : "usage" } )
550566 } )
551567
552568 it ( "handles API errors gracefully" , async ( ) => {
@@ -715,11 +731,13 @@ describe("OpenRouterHandler", () => {
715731 // consume
716732 }
717733
718- // Verify that createOpenRouter was NOT called with extraBody
719- expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith ( {
720- apiKey : "test-key" ,
721- baseURL : "https://openrouter.ai/api/v1" ,
722- } )
734+ // Verify that createOpenRouter was called with correct base config
735+ expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith (
736+ expect . objectContaining ( {
737+ apiKey : "test-key" ,
738+ baseURL : "https://openrouter.ai/api/v1" ,
739+ } ) ,
740+ )
723741
724742 // Verify that providerOptions is undefined when no provider routing
725743 expect ( mockStreamText ) . toHaveBeenCalledWith (
@@ -852,10 +870,12 @@ describe("OpenRouterHandler", () => {
852870 // consume
853871 }
854872
855- expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith ( {
856- apiKey : "custom-key" ,
857- baseURL : "https://custom.openrouter.ai/api/v1" ,
858- } )
873+ expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith (
874+ expect . objectContaining ( {
875+ apiKey : "custom-key" ,
876+ baseURL : "https://custom.openrouter.ai/api/v1" ,
877+ } ) ,
878+ )
859879 } )
860880
861881 it ( "uses default base URL when not specified" , async ( ) => {
@@ -877,10 +897,12 @@ describe("OpenRouterHandler", () => {
877897 // consume
878898 }
879899
880- expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith ( {
881- apiKey : "test-key" ,
882- baseURL : "https://openrouter.ai/api/v1" ,
883- } )
900+ expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith (
901+ expect . objectContaining ( {
902+ apiKey : "test-key" ,
903+ baseURL : "https://openrouter.ai/api/v1" ,
904+ } ) ,
905+ )
884906 } )
885907 } )
886908
@@ -964,4 +986,176 @@ describe("OpenRouterHandler", () => {
964986 expect ( reasoningDetails ) . toBeUndefined ( )
965987 } )
966988 } )
989+
990+ describe ( "model-specific handling" , ( ) => {
991+ const mockStreamResult = ( ) => {
992+ const mockFullStream = ( async function * ( ) {
993+ yield { type : "text-delta" , text : "response" , id : "1" }
994+ } ) ( )
995+ mockStreamText . mockReturnValue ( {
996+ fullStream : mockFullStream ,
997+ usage : Promise . resolve ( { inputTokens : 10 , outputTokens : 20 , totalTokens : 30 } ) ,
998+ totalUsage : Promise . resolve ( { inputTokens : 10 , outputTokens : 20 , totalTokens : 30 } ) ,
999+ } )
1000+ }
1001+
1002+ const consumeGenerator = async (
1003+ handler : any ,
1004+ system = "test" ,
1005+ msgs : any [ ] = [ { role : "user" , content : "test" } ] ,
1006+ ) => {
1007+ const generator = handler . createMessage ( system , msgs )
1008+ for await ( const _ of generator ) {
1009+ // consume
1010+ }
1011+ }
1012+
1013+ it ( "passes topP for DeepSeek R1 models" , async ( ) => {
1014+ const handler = new OpenRouterHandler ( {
1015+ openRouterApiKey : "test-key" ,
1016+ openRouterModelId : "deepseek/deepseek-r1" ,
1017+ } )
1018+ mockStreamResult ( )
1019+ await consumeGenerator ( handler )
1020+
1021+ expect ( mockStreamText ) . toHaveBeenCalledWith (
1022+ expect . objectContaining ( {
1023+ topP : 0.95 ,
1024+ } ) ,
1025+ )
1026+ } )
1027+
1028+ it ( "does not pass topP for non-R1 models" , async ( ) => {
1029+ const handler = new OpenRouterHandler ( {
1030+ openRouterApiKey : "test-key" ,
1031+ openRouterModelId : "openai/gpt-4o" ,
1032+ } )
1033+ mockStreamResult ( )
1034+ await consumeGenerator ( handler )
1035+
1036+ expect ( mockStreamText ) . toHaveBeenCalledWith (
1037+ expect . objectContaining ( {
1038+ topP : undefined ,
1039+ } ) ,
1040+ )
1041+ } )
1042+
1043+ it ( "uses R1 format for DeepSeek R1 models (extraBody.messages)" , async ( ) => {
1044+ const handler = new OpenRouterHandler ( {
1045+ openRouterApiKey : "test-key" ,
1046+ openRouterModelId : "deepseek/deepseek-r1" ,
1047+ } )
1048+ mockStreamResult ( )
1049+ await consumeGenerator ( handler , "system prompt" )
1050+
1051+ // R1 models should pass OpenAI messages via extraBody (including system as user message)
1052+ expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith (
1053+ expect . objectContaining ( {
1054+ extraBody : expect . objectContaining ( {
1055+ messages : expect . any ( Array ) ,
1056+ } ) ,
1057+ } ) ,
1058+ )
1059+
1060+ // System prompt should NOT be passed to streamText (it is in extraBody.messages)
1061+ const streamTextCall = mockStreamText . mock . calls [ 0 ] [ 0 ]
1062+ expect ( streamTextCall . system ) . toBeUndefined ( )
1063+ } )
1064+
1065+ it ( "applies Anthropic beta headers for Anthropic models" , async ( ) => {
1066+ const handler = new OpenRouterHandler ( {
1067+ openRouterApiKey : "test-key" ,
1068+ openRouterModelId : "anthropic/claude-sonnet-4" ,
1069+ } )
1070+ mockStreamResult ( )
1071+ await consumeGenerator ( handler )
1072+
1073+ expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith (
1074+ expect . objectContaining ( {
1075+ headers : { "x-anthropic-beta" : "fine-grained-tool-streaming-2025-05-14" } ,
1076+ } ) ,
1077+ )
1078+ } )
1079+
1080+ it ( "does not apply Anthropic beta headers for non-Anthropic models" , async ( ) => {
1081+ const handler = new OpenRouterHandler ( {
1082+ openRouterApiKey : "test-key" ,
1083+ openRouterModelId : "openai/gpt-4o" ,
1084+ } )
1085+ mockStreamResult ( )
1086+ await consumeGenerator ( handler )
1087+
1088+ const call = mockCreateOpenRouter . mock . calls [ 0 ] [ 0 ]
1089+ expect ( call . headers ) . toBeUndefined ( )
1090+ } )
1091+
1092+ it ( "applies prompt caching for Anthropic models in caching set" , async ( ) => {
1093+ const handler = new OpenRouterHandler ( {
1094+ openRouterApiKey : "test-key" ,
1095+ openRouterModelId : "anthropic/claude-sonnet-4" ,
1096+ } )
1097+ mockStreamResult ( )
1098+ await consumeGenerator ( handler )
1099+
1100+ // Should have extraBody.messages with cache_control applied
1101+ expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith (
1102+ expect . objectContaining ( {
1103+ extraBody : expect . objectContaining ( {
1104+ messages : expect . arrayContaining ( [ expect . objectContaining ( { role : "system" } ) ] ) ,
1105+ } ) ,
1106+ } ) ,
1107+ )
1108+ } )
1109+
1110+ it ( "disables reasoning for Gemini 2.5 Pro when not explicitly configured" , async ( ) => {
1111+ const handler = new OpenRouterHandler ( {
1112+ openRouterApiKey : "test-key" ,
1113+ openRouterModelId : "google/gemini-2.5-pro" ,
1114+ } )
1115+ mockStreamResult ( )
1116+ await consumeGenerator ( handler )
1117+
1118+ expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith (
1119+ expect . objectContaining ( {
1120+ extraBody : expect . objectContaining ( {
1121+ reasoning : { exclude : true } ,
1122+ } ) ,
1123+ } ) ,
1124+ )
1125+ } )
1126+
1127+ it ( "applies Gemini sanitization and encrypted block injection" , async ( ) => {
1128+ const handler = new OpenRouterHandler ( {
1129+ openRouterApiKey : "test-key" ,
1130+ openRouterModelId : "google/gemini-2.5-flash" ,
1131+ } )
1132+ mockStreamResult ( )
1133+ await consumeGenerator ( handler )
1134+
1135+ // Gemini models should have extraBody.messages set (via buildOpenAiMessages)
1136+ expect ( mockCreateOpenRouter ) . toHaveBeenCalledWith (
1137+ expect . objectContaining ( {
1138+ extraBody : expect . objectContaining ( {
1139+ messages : expect . any ( Array ) ,
1140+ } ) ,
1141+ } ) ,
1142+ )
1143+ } )
1144+
1145+ it ( "passes topP to completePrompt for R1 models" , async ( ) => {
1146+ const handler = new OpenRouterHandler ( {
1147+ openRouterApiKey : "test-key" ,
1148+ openRouterModelId : "deepseek/deepseek-r1" ,
1149+ } )
1150+ mockGenerateText . mockResolvedValue ( { text : "completion" } )
1151+
1152+ await handler . completePrompt ( "test prompt" )
1153+
1154+ expect ( mockGenerateText ) . toHaveBeenCalledWith (
1155+ expect . objectContaining ( {
1156+ topP : 0.95 ,
1157+ } ) ,
1158+ )
1159+ } )
1160+ } )
9671161} )
0 commit comments