From 0d0468cf0753f97b2d6c5762fbecfcc1fb686f58 Mon Sep 17 00:00:00 2001 From: Kilo Code Cloud Date: Fri, 25 Sep 2026 22:54:43 +0000 Subject: [PATCH 1/2] feat(models): advertise tools for OpenRouter virtual routers --- .../providers/openrouter/index.test.ts | 66 +++++++++++++++++++ .../ai-gateway/providers/openrouter/index.ts | 18 ++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts index 657bd59933..5b32a59393 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts @@ -311,6 +311,72 @@ describe('reasoning variants', () => { }); }); +describe('virtual router tool support', () => { + afterEach(() => { + global.fetch = originalFetch; + }); + + function buildVirtualModel(overrides: Partial): OpenRouterModel { + return buildModel({ + architecture: { + input_modalities: ['text'], + output_modalities: ['text'], + tokenizer: 'Router', + }, + pricing: { prompt: '-1', completion: '-1' }, + supported_parameters: [], + ...overrides, + }); + } + + async function supportedParametersById(models: OpenRouterModel[]) { + global.fetch = jest.fn(() => + Promise.resolve(createMockResponse({ jsonData: { data: models } })) + ) as unknown as typeof fetch; + const catalog = await getEnhancedOpenRouterModels(); + return new Map(catalog.data.map(model => [model.id, model.supported_parameters])); + } + + it('adds tools to virtual routers that omit it', async () => { + const params = await supportedParametersById([ + buildVirtualModel({ id: 'typesafe/jev-router', name: 'TypeSafe: Jev Router' }), + buildVirtualModel({ + id: 'openrouter/pareto-code', + name: 'Pareto Code Router', + supported_parameters: undefined, + }), + ]); + + expect(params.get('typesafe/jev-router')).toEqual(['tools']); + expect(params.get('openrouter/pareto-code')).toEqual(['tools']); + }); + + it('keeps upstream parameters of virtual routers that already list tools', async () => { + const upstream = ['max_tokens', 'tools', 'reasoning']; + const params = await supportedParametersById([ + buildVirtualModel({ + id: 'openrouter/auto', + name: 'Auto Router', + supported_parameters: upstream, + }), + ]); + + expect(params.get('openrouter/auto')).toEqual(upstream); + }); + + it('does not add tools to virtual models that are not routers or to regular models', async () => { + const params = await supportedParametersById([ + buildVirtualModel({ id: 'openrouter/fusion', name: 'OpenRouter: Fusion' }), + buildVirtualModel({ id: 'openrouter/bodybuilder', name: 'Body Builder (beta)' }), + buildModel({ id: 'vendor/router-model', name: 'Router Model', supported_parameters: [] }), + ]); + + expect(params.get('openrouter/fusion')).toEqual([]); + expect(params.get('openrouter/bodybuilder')).toEqual([]); + expect(params.get('vendor/router-model')).toEqual([]); + }); +}); + describe('mayTrainOnYourPrompts', () => { afterEach(() => { global.fetch = originalFetch; diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts index da3956b777..19223a4afb 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts @@ -109,6 +109,22 @@ export function formatName(model: OpenRouterModel, preferredIndex: number) { return name; } +// OpenRouter lists virtual models (tokenizer "Router") such as typesafe/jev-router with +// empty supported_parameters, which makes Kilo clients drop them. Routers that forward to +// regular chat models are likely to accept tools; other virtual models such as +// openrouter/fusion or openrouter/bodybuilder are not, so only models named "Router" qualify. +function isLikelyToolCapableVirtualRouter(model: OpenRouterModel): boolean { + return model.architecture.tokenizer === 'Router' && /\brouter\b/i.test(model.name); +} + +function addLikelyVirtualRouterToolSupport(model: OpenRouterModel): OpenRouterModel { + const supportedParameters = model.supported_parameters ?? []; + if (supportedParameters.includes('tools') || !isLikelyToolCapableVirtualRouter(model)) { + return model; + } + return { ...model, supported_parameters: [...supportedParameters, 'tools'] }; +} + export function shouldSuppressOpenRouterModel(model: KiloExclusiveModel): boolean { return model.status !== 'disabled' || model.pricing === null; } @@ -146,7 +162,7 @@ async function enhancedModelList(models: OpenRouterModel[]) { const pricing = getModelDisplayPricing(rawPricing); const terminalBench = terminalBenchFor(summaries, model.id); return { - ...model, + ...addLikelyVirtualRouterToolSupport(model), ...(pricing && { pricing }), ...(terminalBench && { terminalBench }), }; From d88cf97c192d96f5dce34b033846c3ff8054b0b8 Mon Sep 17 00:00:00 2001 From: Kilo Code Cloud Date: Fri, 25 Sep 2026 23:13:18 +0000 Subject: [PATCH 2/2] refactor(models): hardcode virtual routers that get tools --- .../providers/openrouter/index.test.ts | 22 +++++++------------ .../ai-gateway/providers/openrouter/index.ts | 16 +++++--------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts index 5b32a59393..371c4202a3 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts @@ -337,7 +337,7 @@ describe('virtual router tool support', () => { return new Map(catalog.data.map(model => [model.id, model.supported_parameters])); } - it('adds tools to virtual routers that omit it', async () => { + it('adds tools to the allowlisted virtual routers', async () => { const params = await supportedParametersById([ buildVirtualModel({ id: 'typesafe/jev-router', name: 'TypeSafe: Jev Router' }), buildVirtualModel({ @@ -351,29 +351,23 @@ describe('virtual router tool support', () => { expect(params.get('openrouter/pareto-code')).toEqual(['tools']); }); - it('keeps upstream parameters of virtual routers that already list tools', async () => { - const upstream = ['max_tokens', 'tools', 'reasoning']; + it('does not duplicate tools when upstream already lists it', async () => { + const upstream = ['max_tokens', 'tools']; const params = await supportedParametersById([ - buildVirtualModel({ - id: 'openrouter/auto', - name: 'Auto Router', - supported_parameters: upstream, - }), + buildVirtualModel({ id: 'typesafe/jev-router', supported_parameters: upstream }), ]); - expect(params.get('openrouter/auto')).toEqual(upstream); + expect(params.get('typesafe/jev-router')).toEqual(upstream); }); - it('does not add tools to virtual models that are not routers or to regular models', async () => { + it('does not add tools to other virtual models', async () => { const params = await supportedParametersById([ buildVirtualModel({ id: 'openrouter/fusion', name: 'OpenRouter: Fusion' }), - buildVirtualModel({ id: 'openrouter/bodybuilder', name: 'Body Builder (beta)' }), - buildModel({ id: 'vendor/router-model', name: 'Router Model', supported_parameters: [] }), + buildVirtualModel({ id: 'vendor/other-router', name: 'Other Router' }), ]); expect(params.get('openrouter/fusion')).toEqual([]); - expect(params.get('openrouter/bodybuilder')).toEqual([]); - expect(params.get('vendor/router-model')).toEqual([]); + expect(params.get('vendor/other-router')).toEqual([]); }); }); diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts index 19223a4afb..9c0a830db9 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts @@ -109,17 +109,13 @@ export function formatName(model: OpenRouterModel, preferredIndex: number) { return name; } -// OpenRouter lists virtual models (tokenizer "Router") such as typesafe/jev-router with -// empty supported_parameters, which makes Kilo clients drop them. Routers that forward to -// regular chat models are likely to accept tools; other virtual models such as -// openrouter/fusion or openrouter/bodybuilder are not, so only models named "Router" qualify. -function isLikelyToolCapableVirtualRouter(model: OpenRouterModel): boolean { - return model.architecture.tokenizer === 'Router' && /\brouter\b/i.test(model.name); -} +// OpenRouter lists these virtual routers with empty supported_parameters, which makes Kilo +// clients drop them, even though they route to regular chat models that accept tools. +const TOOL_CAPABLE_VIRTUAL_ROUTER_IDS = new Set(['typesafe/jev-router', 'openrouter/pareto-code']); -function addLikelyVirtualRouterToolSupport(model: OpenRouterModel): OpenRouterModel { +function addVirtualRouterToolSupport(model: OpenRouterModel): OpenRouterModel { const supportedParameters = model.supported_parameters ?? []; - if (supportedParameters.includes('tools') || !isLikelyToolCapableVirtualRouter(model)) { + if (supportedParameters.includes('tools') || !TOOL_CAPABLE_VIRTUAL_ROUTER_IDS.has(model.id)) { return model; } return { ...model, supported_parameters: [...supportedParameters, 'tools'] }; @@ -162,7 +158,7 @@ async function enhancedModelList(models: OpenRouterModel[]) { const pricing = getModelDisplayPricing(rawPricing); const terminalBench = terminalBenchFor(summaries, model.id); return { - ...addLikelyVirtualRouterToolSupport(model), + ...addVirtualRouterToolSupport(model), ...(pricing && { pricing }), ...(terminalBench && { terminalBench }), };