11import type { LanguageModelV3Prompt } from '@ai-sdk/provider' ;
22
3- import { describe , expect , it , vi } from 'vitest' ;
3+ import { beforeEach , describe , expect , it , vi } from 'vitest' ;
44import { OpenRouterChatLanguageModel } from '../../chat/openrouter-chat-language-model.js' ;
55
66const createTestSettings = ( ) => ( {
@@ -9,37 +9,52 @@ const createTestSettings = () => ({
99 userAgent : 'test-user-agent/0.0.0' ,
1010} ) ;
1111
12+ // Track OpenRouter constructor calls
13+ const openRouterConstructorCalls : unknown [ ] [ ] = [ ] ;
14+
1215// Mock the OpenRouter SDK
1316vi . mock ( '@openrouter/sdk' , ( ) => {
1417 return {
15- OpenRouter : vi . fn ( ) . mockImplementation ( ( ) => ( {
16- beta : {
17- responses : {
18- send : vi . fn ( ) . mockResolvedValue ( {
19- id : 'resp-test' ,
20- model : 'test-model' ,
21- status : 'completed' ,
22- createdAt : 1704067200 ,
23- output : [
24- {
25- type : 'message' ,
26- content : [ { type : 'output_text' , text : 'Hello' } ] ,
18+ OpenRouter : vi . fn ( ) . mockImplementation ( ( ...args : unknown [ ] ) => {
19+ openRouterConstructorCalls . push ( args ) ;
20+ return {
21+ beta : {
22+ responses : {
23+ send : vi . fn ( ) . mockResolvedValue ( {
24+ id : 'resp-test' ,
25+ model : 'test-model' ,
26+ status : 'completed' ,
27+ createdAt : 1704067200 ,
28+ output : [
29+ {
30+ type : 'message' ,
31+ content : [ { type : 'output_text' , text : 'Hello' } ] ,
32+ } ,
33+ ] ,
34+ outputText : 'Hello' ,
35+ usage : {
36+ inputTokens : 10 ,
37+ outputTokens : 5 ,
38+ totalTokens : 15 ,
2739 } ,
28- ] ,
29- outputText : 'Hello' ,
30- usage : {
31- inputTokens : 10 ,
32- outputTokens : 5 ,
33- totalTokens : 15 ,
34- } ,
35- } ) ,
40+ } ) ,
41+ } ,
3642 } ,
37- } ,
38- } ) ) ,
43+ } ;
44+ } ) ,
3945 SDK_METADATA : { userAgent : 'test-sdk/1.0.0' } ,
4046 } ;
4147} ) ;
4248
49+ // Mock HTTPClient
50+ vi . mock ( '@openrouter/sdk/lib/http' , ( ) => {
51+ return {
52+ HTTPClient : vi . fn ( ) . mockImplementation ( ( options : unknown ) => ( {
53+ _options : options ,
54+ } ) ) ,
55+ } ;
56+ } ) ;
57+
4358const createTestPrompt = ( ) : LanguageModelV3Prompt => [
4459 {
4560 role : 'user' ,
@@ -48,6 +63,10 @@ const createTestPrompt = (): LanguageModelV3Prompt => [
4863] ;
4964
5065describe ( 'OpenRouterChatLanguageModel' , ( ) => {
66+ beforeEach ( ( ) => {
67+ // Clear constructor call tracking before each test
68+ openRouterConstructorCalls . length = 0 ;
69+ } ) ;
5170 describe ( 'constructor' , ( ) => {
5271 it ( 'should set specificationVersion to v3' , ( ) => {
5372 const model = new OpenRouterChatLanguageModel (
@@ -399,4 +418,127 @@ describe('OpenRouterChatLanguageModel', () => {
399418 expect ( body . route ) . toEqual ( 'fallback' ) ;
400419 } ) ;
401420 } ) ;
421+
422+ describe ( 'extraBody forwarding' , ( ) => {
423+ it ( 'should spread extraBody into doGenerate request params' , async ( ) => {
424+ const model = new OpenRouterChatLanguageModel ( 'openai/gpt-4o' , {
425+ ...createTestSettings ( ) ,
426+ extraBody : {
427+ customField : 'customValue' ,
428+ anotherField : 123 ,
429+ } ,
430+ } ) ;
431+
432+ const result = await model . doGenerate ( {
433+ prompt : createTestPrompt ( ) ,
434+ } ) ;
435+
436+ // Verify request body includes extraBody fields
437+ const body = result . request ?. body as Record < string , unknown > ;
438+ expect ( body ) . toBeDefined ( ) ;
439+ expect ( body ) . toHaveProperty ( 'customField' , 'customValue' ) ;
440+ expect ( body ) . toHaveProperty ( 'anotherField' , 123 ) ;
441+ } ) ;
442+
443+ it ( 'should spread extraBody into doStream request params' , async ( ) => {
444+ const model = new OpenRouterChatLanguageModel ( 'openai/gpt-4o' , {
445+ ...createTestSettings ( ) ,
446+ extraBody : {
447+ customField : 'streamValue' ,
448+ } ,
449+ } ) ;
450+
451+ const result = await model . doStream ( {
452+ prompt : createTestPrompt ( ) ,
453+ } ) ;
454+
455+ // Verify request body includes extraBody fields
456+ const body = result . request ?. body as Record < string , unknown > ;
457+ expect ( body ) . toBeDefined ( ) ;
458+ expect ( body ) . toHaveProperty ( 'customField' , 'streamValue' ) ;
459+ } ) ;
460+
461+ it ( 'should allow explicit params to override extraBody' , async ( ) => {
462+ const model = new OpenRouterChatLanguageModel ( 'openai/gpt-4o' , {
463+ ...createTestSettings ( ) ,
464+ extraBody : {
465+ model : 'should-be-overridden' ,
466+ temperature : 0.5 ,
467+ } ,
468+ } ) ;
469+
470+ const result = await model . doGenerate ( {
471+ prompt : createTestPrompt ( ) ,
472+ temperature : 0.9 ,
473+ } ) ;
474+
475+ // Verify explicit params override extraBody
476+ const body = result . request ?. body as Record < string , unknown > ;
477+ expect ( body ) . toBeDefined ( ) ;
478+ // Model should be from the constructor, not extraBody
479+ expect ( body . model ) . toBe ( 'openai/gpt-4o' ) ;
480+ // Temperature from call options should override extraBody
481+ expect ( body . temperature ) . toBe ( 0.9 ) ;
482+ } ) ;
483+ } ) ;
484+
485+ describe ( 'custom fetch forwarding' , ( ) => {
486+ it ( 'should pass custom fetch to OpenRouter client via HTTPClient in doGenerate' , async ( ) => {
487+ const customFetch = vi . fn ( ) ;
488+ const model = new OpenRouterChatLanguageModel ( 'openai/gpt-4o' , {
489+ ...createTestSettings ( ) ,
490+ fetch : customFetch ,
491+ } ) ;
492+
493+ await model . doGenerate ( {
494+ prompt : createTestPrompt ( ) ,
495+ } ) ;
496+
497+ // Verify OpenRouter constructor was called with httpClient
498+ expect ( openRouterConstructorCalls . length ) . toBeGreaterThan ( 0 ) ;
499+ const lastCall = openRouterConstructorCalls [
500+ openRouterConstructorCalls . length - 1
501+ ] as [ { httpClient ?: unknown } ] ;
502+ expect ( lastCall [ 0 ] ) . toHaveProperty ( 'httpClient' ) ;
503+ expect ( lastCall [ 0 ] . httpClient ) . toBeDefined ( ) ;
504+ } ) ;
505+
506+ it ( 'should pass custom fetch to OpenRouter client via HTTPClient in doStream' , async ( ) => {
507+ const customFetch = vi . fn ( ) ;
508+ const model = new OpenRouterChatLanguageModel ( 'openai/gpt-4o' , {
509+ ...createTestSettings ( ) ,
510+ fetch : customFetch ,
511+ } ) ;
512+
513+ await model . doStream ( {
514+ prompt : createTestPrompt ( ) ,
515+ } ) ;
516+
517+ // Verify OpenRouter constructor was called with httpClient
518+ expect ( openRouterConstructorCalls . length ) . toBeGreaterThan ( 0 ) ;
519+ const lastCall = openRouterConstructorCalls [
520+ openRouterConstructorCalls . length - 1
521+ ] as [ { httpClient ?: unknown } ] ;
522+ expect ( lastCall [ 0 ] ) . toHaveProperty ( 'httpClient' ) ;
523+ expect ( lastCall [ 0 ] . httpClient ) . toBeDefined ( ) ;
524+ } ) ;
525+
526+ it ( 'should not pass httpClient when no custom fetch is provided' , async ( ) => {
527+ const model = new OpenRouterChatLanguageModel ( 'openai/gpt-4o' , {
528+ ...createTestSettings ( ) ,
529+ // No custom fetch
530+ } ) ;
531+
532+ await model . doGenerate ( {
533+ prompt : createTestPrompt ( ) ,
534+ } ) ;
535+
536+ // Verify OpenRouter constructor was called without httpClient (or with undefined)
537+ expect ( openRouterConstructorCalls . length ) . toBeGreaterThan ( 0 ) ;
538+ const lastCall = openRouterConstructorCalls [
539+ openRouterConstructorCalls . length - 1
540+ ] as [ { httpClient ?: unknown } ] ;
541+ expect ( lastCall [ 0 ] . httpClient ) . toBeUndefined ( ) ;
542+ } ) ;
543+ } ) ;
402544} ) ;
0 commit comments