@@ -11,6 +11,24 @@ import {
1111 expectUnifiedModelCatalogProviderRegistration ,
1212} from "openclaw/plugin-sdk/provider-test-contracts" ;
1313import { describe , expect , it , vi } from "vitest" ;
14+
15+ const { getOpenRouterModelCapabilitiesMock, loadOpenRouterModelCapabilitiesMock } = vi . hoisted (
16+ ( ) => ( {
17+ getOpenRouterModelCapabilitiesMock : vi . fn ( ) ,
18+ loadOpenRouterModelCapabilitiesMock : vi . fn ( async ( ) => { } ) ,
19+ } ) ,
20+ ) ;
21+
22+ vi . mock ( "openclaw/plugin-sdk/provider-stream-family" , async ( importOriginal ) => {
23+ const actual =
24+ await importOriginal < typeof import ( "openclaw/plugin-sdk/provider-stream-family" ) > ( ) ;
25+ return {
26+ ...actual ,
27+ getOpenRouterModelCapabilities : getOpenRouterModelCapabilitiesMock ,
28+ loadOpenRouterModelCapabilities : loadOpenRouterModelCapabilitiesMock ,
29+ } ;
30+ } ) ;
31+
1432import openrouterPlugin from "./index.js" ;
1533import {
1634 buildOpenrouterProvider ,
@@ -204,6 +222,59 @@ describe("openrouter provider hooks", () => {
204222 expect ( buildOpenrouterProvider ( ) . models ?. map ( ( model ) => model . id ) ) . not . toContain ( "auto" ) ;
205223 } ) ;
206224
225+ it ( "normalizes OpenRouter API ids before capability loading and lookup" , async ( ) => {
226+ getOpenRouterModelCapabilitiesMock . mockReset ( ) ;
227+ loadOpenRouterModelCapabilitiesMock . mockClear ( ) ;
228+ getOpenRouterModelCapabilitiesMock . mockReturnValue ( {
229+ name : "Claude Sonnet 4.6" ,
230+ reasoning : true ,
231+ input : [ "text" , "image" ] ,
232+ supportsTools : true ,
233+ cost : { input : 1 , output : 2 , cacheRead : 0 , cacheWrite : 0 } ,
234+ contextWindow : 200_000 ,
235+ maxTokens : 64_000 ,
236+ } ) ;
237+ const provider = await registerSingleProviderPlugin ( openrouterPlugin ) ;
238+ const modelId = "openrouter/anthropic/claude-sonnet-4.6" ;
239+ const context = {
240+ provider : "openrouter" ,
241+ modelId,
242+ modelRegistry : { find : vi . fn ( ( ) => null ) } ,
243+ } as never ;
244+
245+ await provider . prepareDynamicModel ?.( context ) ;
246+ const model = provider . resolveDynamicModel ?.( context ) ;
247+
248+ expect ( loadOpenRouterModelCapabilitiesMock ) . toHaveBeenCalledWith ( "anthropic/claude-sonnet-4.6" ) ;
249+ expect ( getOpenRouterModelCapabilitiesMock ) . toHaveBeenCalledWith ( "anthropic/claude-sonnet-4.6" ) ;
250+ expect ( model ) . toMatchObject ( {
251+ id : modelId ,
252+ name : "Claude Sonnet 4.6" ,
253+ reasoning : true ,
254+ input : [ "text" , "image" ] ,
255+ compat : { supportsTools : true } ,
256+ contextWindow : 200_000 ,
257+ maxTokens : 64_000 ,
258+ } ) ;
259+ } ) ;
260+
261+ it ( "keeps native OpenRouter namespace ids for capability lookup" , async ( ) => {
262+ getOpenRouterModelCapabilitiesMock . mockReset ( ) ;
263+ loadOpenRouterModelCapabilitiesMock . mockClear ( ) ;
264+ const provider = await registerSingleProviderPlugin ( openrouterPlugin ) ;
265+ const context = {
266+ provider : "openrouter" ,
267+ modelId : "openrouter/auto" ,
268+ modelRegistry : { find : vi . fn ( ( ) => null ) } ,
269+ } as never ;
270+
271+ await provider . prepareDynamicModel ?.( context ) ;
272+ provider . resolveDynamicModel ?.( context ) ;
273+
274+ expect ( loadOpenRouterModelCapabilitiesMock ) . toHaveBeenCalledWith ( "openrouter/auto" ) ;
275+ expect ( getOpenRouterModelCapabilitiesMock ) . toHaveBeenCalledWith ( "openrouter/auto" ) ;
276+ } ) ;
277+
207278 it ( "does not include retired stealth models in the bundled catalog" , ( ) => {
208279 const modelIds = buildOpenrouterProvider ( ) . models ?. map ( ( model ) => model . id ) ?? [ ] ;
209280 expect ( modelIds ) . not . toContain ( "openrouter/hunter-alpha" ) ;
0 commit comments