Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project are documented in this file.

## [Unreleased]

### Added

- **Auto-Router Capability Enrichment** — OmniRoute's zero-config `auto` model is no longer left without capability metadata. If a model with id `auto` (or `<provider>/auto`) is present in the fetched model list without an explicit `contextWindow`/`maxTokens`, the plugin now computes its capabilities as the lowest common denominator across all other known models (min context window/max tokens, boolean-AND for vision/tools/streaming/temperature/attachment, boolean-OR for reasoning). This removes the need to hardcode or manually maintain a context window override for `auto` as the OmniRoute catalog grows. (`src/omniroute-combos.ts`: `enrichAutoModel`, `isAutoModel`; wired into `src/models.ts`) (resolves #43)
- **5 New Test Cases** (`test/auto-model.test.mjs`) covering `isAutoModel` id matching and `enrichAutoModel` computation, no-op, and override-preservation behavior.

## [1.2.2] - 2026-05-22

### Added
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- ✅ **Model Metadata Normalization** - Reads all OmniRoute field variants (camelCase, snake_case, capabilities object) with proper precedence
- ✅ **Provider Alias Deduplication** - Automatically deduplicates alias/canonical model entries (e.g., `cx/gpt-5.5` → `codex/gpt-5.5`)
- ✅ **Combo Model Capability Enrichment** - Automatically calculates lowest common capabilities for OmniRoute combo models
- ✅ **Auto-Router Capability Enrichment** - Automatically computes context window/capabilities for OmniRoute's `auto` zero-config router from the rest of your catalog, no hardcoding needed
- ✅ **models.dev Enrichment** - Enriches model metadata from models.dev API with provider alias resolution
- ✅ **Subscription Provider Fallback** - Falls back to public providers for subscription-based models
- ✅ **Model Variant Support** - Automatically strips reasoning effort suffixes (e.g., `gpt-5.5-xhigh` → `gpt-5.5`) for lookup
Expand Down Expand Up @@ -206,6 +207,23 @@ Calculated capabilities:

Note: Some underlying models may not be found in `models.dev` (e.g., custom models). In such cases, they are excluded from capability calculation, and a warning is logged.

### Auto-Router Model Capability Enrichment

OmniRoute's zero-config `auto` router (added to `models` as `auto`) is not listed in `/api/combos`
like user-defined combos, so it can't be resolved to underlying models the same way. Instead, if a
model with id `auto` (or `<provider>/auto`) appears in your fetched model list without a
`contextWindow`/`maxTokens` already set, this plugin automatically computes its capabilities as the
lowest common denominator across every other known model in your catalog:

- **Context Window / Max Tokens**: minimum across all other fetched models
- **Vision / Tools / Streaming / Temperature / Attachment**: `true` only if ALL other models support it
- **Reasoning**: `true` if ANY other model supports it

This means you never need to hardcode or manually update `auto`'s context window as OmniRoute's
provider/model catalog grows or shrinks — it's recalculated automatically on every model fetch. If
OmniRoute itself ever starts returning explicit capabilities for `auto` (or you set them via
`modelMetadata` overrides), those take precedence and this calculation is skipped.

### API Mode

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: The new Auto-Router sectiongot a little too eager and stamped ### API Mode twice. Two identical headings now sit back-to-back like a copy-paste that forgot to press delete. Anyone following the TOC will think they've fallen into a documentation time loop.

🩹 The Fix: Remove the duplicate ### API Mode heading this PR added (keep the original one that introduces the API Modes content).

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


### API Mode
Expand Down
7 changes: 5 additions & 2 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
resolveModelAlias,
} from './models-dev.js';
import type { ModelsDevIndex, ModelsDevModel } from './models-dev.js';
import { enrichComboModels, clearComboCache, splitModelId } from './omniroute-combos.js';
import { enrichComboModels, enrichAutoModel, clearComboCache, splitModelId } from './omniroute-combos.js';
import { warn, debug } from './logger.js';

/**
Expand Down Expand Up @@ -443,7 +443,10 @@ async function enrichModelMetadata(
// Enrich combo models with lowest common capabilities
const withComboCapabilities = await enrichComboModels(withModelsDev, config, modelsDevIndex);

return withComboCapabilities;
// Enrich the "auto" zero-config router model (if present) with capabilities
// computed from every other known model, so it never needs manual overrides
// as OmniRoute's catalog grows.
return enrichAutoModel(withComboCapabilities);
}

/**
Expand Down
67 changes: 67 additions & 0 deletions src/omniroute-combos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,73 @@ export function isComboModel(model: OmniRouteModel): boolean {
return false;
}

/**
* Identify the OmniRoute "auto" zero-config router model.
* OmniRoute exposes this as a model id of literally `auto` (optionally
* prefixed, e.g. `omniroute/auto`). Unlike user-defined combos, it is not
* listed in `/api/combos`, so it needs its own capability calculation.
*/
export function isAutoModel(model: OmniRouteModel): boolean {
const { modelKey } = splitModelId(model.id);
return modelKey.toLowerCase() === 'auto';
}

/**
* Enrich the "auto" model (if present in the fetched model list) with
* capabilities computed as the lowest common denominator across every
* other known model. This mirrors how user-defined combo capabilities are
* calculated, and ensures the "auto" router's advertised context window /
* capabilities automatically track the OmniRoute catalog as it grows or
* shrinks, without requiring any hardcoded overrides.
*
* This should be called after models.dev + combo enrichment, so that the
* "other models" pool already has resolved capabilities where possible.
*/
export function enrichAutoModel(models: OmniRouteModel[]): OmniRouteModel[] {
const autoIndex = models.findIndex(isAutoModel);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: findIndex(isAutoModel) enriches only the first auto model, then the others filter excludes every other auto model via !isAutoModel(model). So if the catalog ever contains two auto routers (e.g. auto and omniroute/auto, or two providers' auto), the first wins and the rest are quietly left as empty shells — excluded from the pool and never enriched themselves. It's a coin-flip which one gets capabilities.

🩹 The Fix: Pick a single canonical auto model (prefer bare auto over provider/auto, or merge all matched auto models) instead of enriching only findIndex's first hit and discarding the rest.

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if (autoIndex === -1) return models;

const autoModel = models[autoIndex];

// If OmniRoute or models.dev already provided full capability data, don't override it.
if (autoModel.contextWindow !== undefined && autoModel.maxTokens !== undefined) {
return models;
}

const others = models.filter((model, index) => index !== autoIndex && !isAutoModel(model));
if (others.length === 0) {
return models;
}

const withContext = others.filter((m): m is OmniRouteModel & { contextWindow: number } => m.contextWindow !== undefined);
const withMaxTokens = others.filter((m): m is OmniRouteModel & { maxTokens: number } => m.maxTokens !== undefined);

const computedContextWindow =
autoModel.contextWindow ?? (withContext.length > 0 ? Math.min(...withContext.map((m) => m.contextWindow)) : undefined);
const computedMaxTokens =
autoModel.maxTokens ?? (withMaxTokens.length > 0 ? Math.min(...withMaxTokens.map((m) => m.maxTokens)) : undefined);

debug(
`Calculated capabilities for auto-router model "${sanitizeForLog(autoModel.id)}" from ${others.length} known models: context=${computedContextWindow ?? 'N/A'}, maxTokens=${computedMaxTokens ?? 'N/A'}`,
);

const updated: OmniRouteModel = {
...autoModel,
...(computedContextWindow !== undefined ? { contextWindow: computedContextWindow } : {}),
...(computedMaxTokens !== undefined ? { maxTokens: computedMaxTokens } : {}),
supportsVision: autoModel.supportsVision ?? others.every((m) => m.supportsVision === true),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: The "lowest common denominator" AND logic treats undefined as false. Every other model in your catalog that merely lacks a resolved capability — e.g. a custom provider model not found in models.dev, which leaves supportsVision/supportsTools/etc. undefined — poisons the AND into false. So adding any single model with unknown capabilities silently flips vision/tools/streaming off for the auto router. Meanwhile the combo path this claims to mirror (calculateLowestCommonCapabilities in models-dev.ts:444) explicitly guards with hasTemperatureMetadata/hasReasoningMetadata so missing metadata doesn't drag results down. This isn't even consistent with its own inspiration.

🩹 The Fix: Ignore models whose capability is unknown when computing the AND, e.g. const visionKnown = others.filter(m => m.supportsVision !== undefined); supportsVision: autoModel.supportsVision ?? (visionKnown.length ? visionKnown.every(m => m.supportsVision === true) : undefined). Apply the same pattern to the other five booleans (reasoning should stay OR, also ignoring unknown). This matches the combo behavior's "don't punish missing metadata" intent.

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

supportsTools: autoModel.supportsTools ?? others.every((m) => m.supportsTools === true),
supportsStreaming: autoModel.supportsStreaming ?? others.every((m) => m.supportsStreaming === true),
supportsTemperature: autoModel.supportsTemperature ?? others.every((m) => m.supportsTemperature === true),
supportsReasoning: autoModel.supportsReasoning ?? others.some((m) => m.supportsReasoning === true),
supportsAttachment: autoModel.supportsAttachment ?? others.every((m) => m.supportsAttachment === true),
};

const result = [...models];
result[autoIndex] = updated;
return result;
}

/**
* Enrich models with combo-specific capabilities
* This should be called after models.dev enrichment
Expand Down
81 changes: 81 additions & 0 deletions test/auto-model.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';

import { enrichAutoModel, isAutoModel } from '../dist/src/omniroute-combos.js';

test('isAutoModel matches bare "auto" and prefixed ids', () => {
assert.equal(isAutoModel({ id: 'auto' }), true);
assert.equal(isAutoModel({ id: 'omniroute/auto' }), true);
assert.equal(isAutoModel({ id: 'auto-pilot' }), false);
assert.equal(isAutoModel({ id: 'gpt-4o' }), false);
});

test('enrichAutoModel computes lowest-common capabilities from other models', () => {
const models = [
{ id: 'auto', name: 'Auto' },
{
id: 'openai/gpt-4o',
name: 'GPT-4o',
contextWindow: 128000,
maxTokens: 16384,
supportsVision: true,
supportsTools: true,
supportsStreaming: true,
supportsTemperature: true,
supportsReasoning: false,
supportsAttachment: true,
},
{
id: 'anthropic/claude-3-5-sonnet',
name: 'Claude 3.5 Sonnet',
contextWindow: 200000,
maxTokens: 8192,
supportsVision: true,
supportsTools: true,
supportsStreaming: true,
supportsTemperature: true,
supportsReasoning: true,
supportsAttachment: true,
},
];

const result = enrichAutoModel(models);
const auto = result.find((m) => m.id === 'auto');

assert.equal(auto.contextWindow, 128000, 'context window should be the minimum across models');
assert.equal(auto.maxTokens, 8192, 'max tokens should be the minimum across models');
assert.equal(auto.supportsVision, true);
assert.equal(auto.supportsTools, true);
assert.equal(auto.supportsReasoning, true, 'reasoning should be true if any model supports it');
});

test('enrichAutoModel is a no-op when no auto model is present', () => {
const models = [
{ id: 'openai/gpt-4o', name: 'GPT-4o', contextWindow: 128000 },
];
const result = enrichAutoModel(models);
assert.deepEqual(result, models);
});

test('enrichAutoModel does not override explicit capabilities already provided', () => {
const models = [
{ id: 'auto', name: 'Auto', contextWindow: 999999, maxTokens: 999 },
{ id: 'openai/gpt-4o', name: 'GPT-4o', contextWindow: 128000, maxTokens: 16384 },
];
const result = enrichAutoModel(models);
const auto = result.find((m) => m.id === 'auto');
assert.equal(auto.contextWindow, 999999);
assert.equal(auto.maxTokens, 999);
});

test('enrichAutoModel recalculates when only one capability field is missing', () => {
const models = [
{ id: 'auto', name: 'Auto', maxTokens: 4096 },
{ id: 'openai/gpt-4o', name: 'GPT-4o', contextWindow: 128000, maxTokens: 16384 },
{ id: 'anthropic/claude-3-5-sonnet', name: 'Claude', contextWindow: 64000, maxTokens: 8192 },
];
const result = enrichAutoModel(models);
const auto = result.find((m) => m.id === 'auto');
assert.equal(auto.contextWindow, 64000, 'should fill in missing context window from minimum');
assert.equal(auto.maxTokens, 4096, 'should keep pre-existing maxTokens untouched');
});
Loading