Skip to content
Merged
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

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

## [1.2.3] - Unreleased

### Added

- **`modelNameDisplay` option** — New `OmniRouteConfig` option that controls how model names appear in the model picker. Set to `"id"` to show provider-qualified model IDs (e.g. `gh/gpt-5.5`) instead of human-readable names, which disambiguates entries when multiple providers serve the same base model. (`src/plugin.ts`, `src/types.ts`) (@Rahulsharma0810)

### Fixed

- **Exclude cached tokens from chat usage** — `prompt_tokens` and `total_tokens` in `/chat/completions` responses now exclude `prompt_tokens_details.cached_tokens`, matching OpenCode's separate cached-input accounting. Applies to both JSON and streaming responses. (`src/plugin.ts`) (@makcimbx)
- **Use `@ai-sdk/openai` for responses mode** — When `apiMode` is `responses`, the plugin now selects `@ai-sdk/openai` as the provider package instead of `@ai-sdk/openai-compatible`, and reconciles explicit model `api.npm` values when the provider package changes. (`src/plugin.ts`) (@makcimbx)
- **Strip Claude title reasoning effort** — The fetch interceptor now removes `reasoning_effort`/`reasoningEffort` from identified Claude title-generation requests, avoiding Anthropic OAuth temperature/thinking validation errors while preserving reasoning options for normal Claude chat requests. (`src/plugin.ts`) (@thomasmaerz)

## [1.2.2] - 2026-05-22

### Added
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Use `/connect omniroute` to store your API key in `~/.local/share/opencode/auth.
| `provider.omniroute.options.refreshOnList` | boolean | No | Whether to refresh models when provider options load (default: true) |
| `provider.omniroute.options.modelsDev` | object | No | Enrich model metadata from models.dev on refresh (default: enabled) |
| `provider.omniroute.options.modelMetadata` | object \| array | No | Override/add metadata for custom/virtual models (works well in `opencode.js`) |
| `provider.omniroute.options.modelNameDisplay` | `'name' \| 'id'` | No | Use model `name` (default) or `id` in the model picker; `id` disambiguates duplicate display names |

### Model Metadata Enrichment (models.dev)

Expand Down Expand Up @@ -273,6 +274,8 @@ interface OmniRouteConfig {
refreshOnList?: boolean;
modelsDev?: OmniRouteModelsDevConfig;
modelMetadata?: OmniRouteModelMetadataConfig;
/** Controls how model names appear in the picker: `"name"` (default) or `"id"`. */
modelNameDisplay?: 'name' | 'id';
}

type OmniRouteApiMode = 'chat' | 'responses';
Expand Down
100 changes: 74 additions & 26 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,10 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => {
setRawUserModelMetadata(providerOptions, rawUserModelMetadata);

const shouldRefreshModels = shouldRefreshProviderModels(existingProvider);
const modelNameDisplay = getModelNameDisplay(existingProvider?.options);
const providerModels = shouldRefreshModels
? toProviderModels(effectiveModels, baseUrl, providerNpm)
: reconcileExplicitModelsNpm(existingProvider?.models, providerNpm);
? toProviderModels(effectiveModels, baseUrl, providerNpm, modelNameDisplay)
: reconcileExplicitModels(existingProvider?.models, providerNpm, modelNameDisplay);
setModelsGeneratedByPlugin(providerOptions, shouldRefreshModels);

providers[OMNIROUTE_PROVIDER_ID] = {
Expand All @@ -123,11 +124,11 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => {
provider: {
id: OMNIROUTE_PROVIDER_ID,
models: async (provider, ctx) => {
const baseUrl = getBaseUrl(provider.options);
const providerNpm = resolveProviderNpm(
isRecord(provider) ? provider.npm : undefined,
isRecord(provider) ? getApiMode(provider.options) : 'chat',
);
const baseUrl = getBaseUrl(provider.options);
const providerNpm = resolveProviderNpm(
isRecord(provider) ? provider.npm : undefined,
isRecord(provider) ? getApiMode(provider.options) : 'chat',
);

// Auth available — fetch /v1/models (fetchModels falls back to defaults on error)
if (ctx.auth?.type === 'api' && ctx.auth.key) {
Expand All @@ -137,7 +138,12 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => {
models,
getRawUserModelMetadata(provider.options),
);
return toProviderModels(effectiveModels, baseUrl, providerNpm);
return toProviderModels(
effectiveModels,
baseUrl,
providerNpm,
runtimeConfig.modelNameDisplay,
);
}

// No auth yet (user hasn't /connect'd): return built-in defaults.
Expand All @@ -146,7 +152,12 @@ export const OmniRouteAuthPlugin: Plugin = async (_input) => {
OMNIROUTE_DEFAULT_MODELS,
getRawUserModelMetadata(provider.options),
);
return toProviderModels(effectiveModels, baseUrl, providerNpm);
return toProviderModels(
effectiveModels,
baseUrl,
providerNpm,
getModelNameDisplay(provider.options),
);
},
},
auth: createAuthHook(),
Expand Down Expand Up @@ -196,7 +207,12 @@ async function loadProviderOptions(
const providerNpm = resolveProviderNpm(provider.npm, config.apiMode);
replaceProviderModels(
provider,
toProviderModels(effectiveModels, config.baseUrl, providerNpm),
toProviderModels(
effectiveModels,
config.baseUrl,
providerNpm,
config.modelNameDisplay,
),
);
if (isRecord(provider.models)) {
debug(`Provider models hydrated: ${Object.keys(provider.models).length}`);
Expand All @@ -218,6 +234,7 @@ function createRuntimeConfig(
const refreshOnList = getBoolean(options, 'refreshOnList');
const modelsDev = getModelsDevConfig(options);
const modelMetadata = getModelMetadataConfig(options);
const modelNameDisplay = getModelNameDisplay(options);

return {
baseUrl,
Expand All @@ -227,6 +244,7 @@ function createRuntimeConfig(
refreshOnList,
modelsDev,
modelMetadata,
modelNameDisplay,
};
}

Expand Down Expand Up @@ -361,6 +379,19 @@ function getBoolean(
return undefined;
}

function getModelNameDisplay(
options: Record<string, unknown> | undefined,
): 'name' | 'id' | undefined {
const value = options?.modelNameDisplay;
if (value === 'name' || value === 'id') {
return value;
}
if (value !== undefined) {
warn(`Unsupported modelNameDisplay option: ${sanitizeForLog(String(value))}. Using name.`);
}
return undefined;
}

function getModelsDevConfig(options: Record<string, unknown> | undefined): OmniRouteModelsDevConfig | undefined {
const raw = options?.modelsDev;
if (!isRecord(raw)) return undefined;
Expand Down Expand Up @@ -502,30 +533,43 @@ function isGeneratedOmniRouteProviderModel(value: unknown): boolean {
return typeof value.api.npm === 'string' && isOmniRouteProviderNpm(value.api.npm);
}

function reconcileExplicitModelsNpm(
function reconcileExplicitModels(
models: Record<string, unknown> | undefined,
providerNpm: string,
modelNameDisplay?: 'name' | 'id',
): Record<string, unknown> | undefined {
if (!isRecord(models)) return models;
let changed = false;
const next: Record<string, unknown> = {};
for (const [id, model] of Object.entries(models)) {
if (!isRecord(model) || !isRecord(model.api)) {
if (!isRecord(model)) {
next[id] = model;
continue;
}
if (model.api.npm === providerNpm) {
next[id] = model;
continue;

let updatedModel = model;
if (isRecord(model.api) && model.api.npm !== providerNpm) {
updatedModel = {
...updatedModel,
api: {
...model.api,
npm: providerNpm,
},
};
}
changed = true;
next[id] = {
...model,
api: {
...model.api,
npm: providerNpm,
},
};

const expectedName = modelNameDisplay === 'id' ? id : (model.name ?? id);
if (typeof model.name === 'string' ? model.name !== expectedName : modelNameDisplay === 'id') {
updatedModel = {
...updatedModel,
name: expectedName,
};
}

if (updatedModel !== model) {
changed = true;
}
next[id] = updatedModel;
}
return changed ? next : models;
}
Expand Down Expand Up @@ -836,10 +880,11 @@ function toProviderModels(
models: OmniRouteModel[],
baseUrl: string,
providerNpm: string,
modelNameDisplay?: 'name' | 'id',
): Record<string, OmniRouteProviderModel> {
const entries: Array<[string, OmniRouteProviderModel]> = models.map((model) => [

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Style: this replaceProviderModels call is ~108 chars, exceeding the 100-char limit. Please format across multiple lines.

model.id,
toProviderModel(model, baseUrl, providerNpm),
toProviderModel(model, baseUrl, providerNpm, modelNameDisplay),
]);
return Object.fromEntries(entries);
}
Expand All @@ -848,18 +893,21 @@ function toProviderModel(
model: OmniRouteModel,
baseUrl: string,
providerNpm: string,
modelNameDisplay?: 'name' | 'id',
): OmniRouteProviderModel {
const supportsVision = model.supportsVision === true;
// Default to true: if API doesn't explicitly say no tools, assume capability exists
// This aligns with OpenAI-compatible behavior where most models support tools
const supportsTools = model.supportsTools !== false;
const supportsTemperature = model.supportsTemperature !== false;
const supportsReasoning = model.supportsReasoning === true;
const supportsAttachment = model.supportsAttachment !== undefined ? model.supportsAttachment : supportsVision;
const supportsAttachment = model.supportsAttachment !== undefined
? model.supportsAttachment
: supportsVision;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Style: this signature line is ~107 chars, exceeding the 100-char limit. Please break across multiple lines.

return {
id: model.id,
name: model.name || model.id,
name: modelNameDisplay === 'id' ? model.id : (model.name || model.id),
providerID: OMNIROUTE_PROVIDER_ID,
family: getModelFamily(model.id),
release_date: '',
Expand Down
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ export interface OmniRouteConfig {
modelsDev?: OmniRouteModelsDevConfig;
/** Optional metadata overrides/additions for custom/virtual models */

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Style: new literal type uses double quotes ("name" | "id"), inconsistent with existing OmniRouteApiMode which uses single quotes. AGENTS.md requires single quotes for strings. Please change to 'name' | 'id'.

modelMetadata?: OmniRouteModelMetadataConfig;
/**
* Controls how model names are displayed in the model picker.
*
* - `"name"` (default): use the `name` field returned by `/v1/models`
* (e.g. `"GPT-5.5"`). When OmniRoute's `MODELS_CATALOG_PREFIX_MODE` is
* set to `dual` (the default), different providers that serve the same
* base model will all share the same display name, making them
* indistinguishable in the picker.
* - `"id"`: use the model `id` instead (e.g. `"gh/gpt-5.5"`,
* `"cx/gpt-5.5"`). Always unique per entry — useful when multiple
* providers serve the same model or when OmniRoute name disambiguation
* is not enabled.
*/
modelNameDisplay?: 'name' | 'id';
}

export interface OmniRouteProviderModelModalities {
Expand Down
Loading