mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 06:12:10 +03:00
feat(opencode-plugin): provider-tag becomes a prefix + traffic-light compression intensity emoji (#2604)
Integrated into release/v3.8.2
This commit is contained in:
@@ -119,21 +119,21 @@ npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-prepro
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | What it does | Hook |
|
||||
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
|
||||
| Dynamic `/v1/models` | Pulls live catalog (455+ entries on prod) on each refresh, TTL-cached | `provider.models` |
|
||||
| Variants pass-through | `-low`/`-medium`/`-high`/`-thinking` ship as first-class IDs from OmniRoute (no client synthesis) | `provider.models` |
|
||||
| Combo LCD aggregation | Combos appear with intersected capabilities + min context/output across members | `provider.models` + `config` |
|
||||
| `combo/<slug>` namespace + `Combo: ` prefix | Combos surface under `combo/claude-primary` (not the upstream UUID) and the picker shows `Combo: claude-primary` so they stand apart from raw provider/model pairs | both hooks |
|
||||
| Nice names + cost | `/api/pricing/models` display names AND `/api/pricing` per-million-token cost overlaid onto the live catalog | both hooks |
|
||||
| Compression pipeline tags | Combo names get tagged with their compression pipeline (e.g. `Combo: claude-primary [rtk:standard → caveman:full]`) when `features.compressionMetadata: true` | both hooks |
|
||||
| Provider-tag suffix | Append upstream provider label to enriched names (e.g. `Claude Opus 4.7 · Claude` vs `Claude Opus 4.7 · Kiro`) so same-id models routed via different upstream connections are visibly distinct in the picker (default-on, opt-out via `features.providerTag: false`) | both hooks |
|
||||
| Usable-only filter | Filter to providers with at least one healthy connection in `/api/providers` (opt-in via `features.usableOnly`) | both hooks |
|
||||
| Disk-cache fallback | Last-known-good catalog persisted to disk; hydrates on a cold start when `/v1/models` is unreachable (default-on, opt-out via `features.diskCache: false`) | `config` |
|
||||
| Bearer injection + suffix-spoof guard | Adds `Authorization` on baseURL-matched requests only | `auth.loader.fetch` |
|
||||
| Gemini schema sanitization | Strips `$schema`/`$ref`/`additionalProperties` for `gemini-*`/`google-vertex-gemini/*` | `auth.loader.fetch` wrap |
|
||||
| Multi-instance | Each plugin entry binds to its own `providerId`; closures isolated | factory |
|
||||
| Config-hook shim | OC ≤1.15.5 fallback: writes static catalog into `config.provider[id]` (config hook is the only one that fires in `serve` mode on these versions) | `config` |
|
||||
| Feature | What it does | Hook |
|
||||
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- |
|
||||
| Dynamic `/v1/models` | Pulls live catalog (455+ entries on prod) on each refresh, TTL-cached | `provider.models` |
|
||||
| Variants pass-through | `-low`/`-medium`/`-high`/`-thinking` ship as first-class IDs from OmniRoute (no client synthesis) | `provider.models` |
|
||||
| Combo LCD aggregation | Combos appear with intersected capabilities + min context/output across members | `provider.models` + `config` |
|
||||
| `combo/<slug>` namespace + `Combo: ` prefix | Combos surface under `combo/claude-primary` (not the upstream UUID) and the picker shows `Combo: claude-primary` so they stand apart from raw provider/model pairs | both hooks |
|
||||
| Nice names + cost | `/api/pricing/models` display names AND `/api/pricing` per-million-token cost overlaid onto the live catalog | both hooks |
|
||||
| Compression pipeline tags | Combo names get tagged with their compression pipeline (e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`) when `features.compressionMetadata: true`. Intensity tokens render as a traffic-light emoji: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra | both hooks |
|
||||
| Provider-tag prefix | Prepend short upstream-provider label to enriched names (e.g. `Claude - Claude Opus 4.7` vs `Kiro - Claude Opus 4.7`, `GHM - GPT 5`) so same-id models routed via different upstream connections group visibly in the picker (default-on, opt-out via `features.providerTag: false`) | both hooks |
|
||||
| Usable-only filter | Filter to providers with at least one healthy connection in `/api/providers` (opt-in via `features.usableOnly`) | both hooks |
|
||||
| Disk-cache fallback | Last-known-good catalog persisted to disk; hydrates on a cold start when `/v1/models` is unreachable (default-on, opt-out via `features.diskCache: false`) | `config` |
|
||||
| Bearer injection + suffix-spoof guard | Adds `Authorization` on baseURL-matched requests only | `auth.loader.fetch` |
|
||||
| Gemini schema sanitization | Strips `$schema`/`$ref`/`additionalProperties` for `gemini-*`/`google-vertex-gemini/*` | `auth.loader.fetch` wrap |
|
||||
| Multi-instance | Each plugin entry binds to its own `providerId`; closures isolated | factory |
|
||||
| Config-hook shim | OC ≤1.15.5 fallback: writes static catalog into `config.provider[id]` (config hook is the only one that fires in `serve` mode on these versions) | `config` |
|
||||
|
||||
## Plugin options
|
||||
|
||||
@@ -149,18 +149,18 @@ npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-prepro
|
||||
|
||||
Every field is optional. Defaults mirror v0.1.0 behaviour so existing `opencode.json` files do not need to change.
|
||||
|
||||
| Feature | Type | Default | What it does |
|
||||
| --------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `combos` | `boolean` | `true` | Discover `/api/combos` and surface them as pseudo-models with LCD capabilities. Combos are keyed under the `combo/<slug>` namespace and labelled `Combo: <name>` in the model picker so they're distinguishable from raw provider/model pairs. |
|
||||
| `enrichment` | `boolean` | `true` | Pull display names from `/api/pricing/models` AND per-million-token pricing (`input`, `output`, `cached` → `cacheRead`, `cache_creation` → `cacheWrite`) from `/api/pricing`, then overlay both onto the live catalog (so the UI shows `Claude 4.7 Opus` with `cost.input: 5`, `cost.output: 25` instead of raw IDs and zeroed cost). |
|
||||
| `compressionMetadata` | `boolean` | `false` | Pull `/api/context/combos` so combo names get tagged with their compression pipeline, e.g. `Combo: claude-primary [rtk:standard → caveman:full]` |
|
||||
| `providerTag` | `boolean` | `true` | Append the upstream provider's human label (from `/api/pricing/models[<alias>].name`) to the enriched display name with a middle-dot separator, so `cc/claude-opus-4-7 → Claude Opus 4.7 · Claude` differs visibly from `kr/claude-opus-4-7 → Claude Opus 4.7 · Kiro` in the OC TUI model picker. Idempotent. Combos are intentionally skipped (the `Combo: ` prefix already conveys multi-upstream). No-op when `providerDisplayName` is missing from the enrichment entry. |
|
||||
| `usableOnly` | `boolean` | `false` | Read `/api/providers` and filter the catalog to providers that have at least one connection with `isActive: true` AND `testStatus: 'active'`. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like `agentrouter/*` survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
|
||||
| `diskCache` | `boolean` | `true` | Persist the last successful `/v1/models` + `/api/combos` + enrichment + connections + compression snapshot to `${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json`. On a subsequent cold start where `/v1/models` throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
|
||||
| `geminiSanitization` | `boolean` | `true` | Strip `$schema`/`$ref`/`additionalProperties` from tool params when the model id matches `gemini` |
|
||||
| `mcpAutoEmit` | `boolean` | `false` | Auto-write an `mcp.<providerId>` remote entry into the OC config pointing at `<baseURL>/api/mcp/stream` with the resolved Bearer token |
|
||||
| `mcpToken` | `string` | _unset_ | Optional separate Bearer for the auto-emitted MCP entry. Falls back to the provider's `apiKey` (from `auth.json`) when unset |
|
||||
| `fetchInterceptor` | `boolean` | `true` | Inject `Authorization: Bearer` + default `Content-Type` on every outbound request targeting `baseURL` (suffix-spoof guarded) |
|
||||
| Feature | Type | Default | What it does |
|
||||
| --------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `combos` | `boolean` | `true` | Discover `/api/combos` and surface them as pseudo-models with LCD capabilities. Combos are keyed under the `combo/<slug>` namespace and labelled `Combo: <name>` in the model picker so they're distinguishable from raw provider/model pairs. |
|
||||
| `enrichment` | `boolean` | `true` | Pull display names from `/api/pricing/models` AND per-million-token pricing (`input`, `output`, `cached` → `cacheRead`, `cache_creation` → `cacheWrite`) from `/api/pricing`, then overlay both onto the live catalog (so the UI shows `Claude 4.7 Opus` with `cost.input: 5`, `cost.output: 25` instead of raw IDs and zeroed cost). |
|
||||
| `compressionMetadata` | `boolean` | `false` | Pull `/api/context/combos` so combo names get tagged with their compression pipeline, e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`. Intensity tokens render as traffic-light emoji (🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra) so the picker advertises "how compressed" each combo is at a glance. |
|
||||
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models` → `GHM`, `Gemini-cli` → `GEMINI-CLI`). Idempotent. Combos intentionally skipped (the `Combo: ` prefix already conveys multi-upstream). |
|
||||
| `usableOnly` | `boolean` | `false` | Read `/api/providers` and filter the catalog to providers that have at least one connection with `isActive: true` AND `testStatus: 'active'`. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like `agentrouter/*` survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
|
||||
| `diskCache` | `boolean` | `true` | Persist the last successful `/v1/models` + `/api/combos` + enrichment + connections + compression snapshot to `${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json`. On a subsequent cold start where `/v1/models` throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
|
||||
| `geminiSanitization` | `boolean` | `true` | Strip `$schema`/`$ref`/`additionalProperties` from tool params when the model id matches `gemini` |
|
||||
| `mcpAutoEmit` | `boolean` | `false` | Auto-write an `mcp.<providerId>` remote entry into the OC config pointing at `<baseURL>/api/mcp/stream` with the resolved Bearer token |
|
||||
| `mcpToken` | `string` | _unset_ | Optional separate Bearer for the auto-emitted MCP entry. Falls back to the provider's `apiKey` (from `auth.json`) when unset |
|
||||
| `fetchInterceptor` | `boolean` | `true` | Inject `Authorization: Bearer` + default `Content-Type` on every outbound request targeting `baseURL` (suffix-spoof guarded) |
|
||||
|
||||
#### Example — enrichment + compression tags + MCP auto-emit
|
||||
|
||||
@@ -224,8 +224,8 @@ If you want a narrower-scoped Bearer for MCP (different from the chat/inference
|
||||
|
||||
- `usableOnly: true` drops models whose canonical provider has no healthy connection in your OmniRoute instance — your `/models` picker stays focused on what you can actually call.
|
||||
- `diskCache: true` (default) writes a snapshot to `${OPENCODE_DATA_DIR}/plugins/omniroute-<providerId>.json` on every healthy refresh. On a cold start where `/v1/models` is unreachable (laptop offline, IP whitelist drop), the snapshot hydrates the static block so OC still shows the catalog instead of a stub.
|
||||
- `compressionMetadata: true` annotates combo display names with their pipeline (e.g. `Combo: claude-primary [rtk:standard → caveman:full]`) so the picker advertises which compression each combo applies.
|
||||
- `providerTag: true` (default) appends the upstream provider label after the model's friendly name, so the picker shows `Claude Opus 4.7 · Claude` for `cc/claude-opus-4-7` and `Claude Opus 4.7 · Kiro` for `kr/claude-opus-4-7`. Critical when the same model id is sold through multiple upstream connections with different cost/auth/rate-limit profiles. Set to `false` to keep the pre-v3.8.3 unsuffixed format.
|
||||
- `compressionMetadata: true` annotates combo display names with their pipeline using traffic-light emoji for intensity (e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`) so the picker advertises which compression each combo applies and how heavy it is at a glance. Palette: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra. Unknown intensities fall through to raw text (`[rtk:custom-thing]`) so the plugin never hides a value OmniRoute knows but the plugin doesn't.
|
||||
- `providerTag: true` (default) prepends a short upstream-provider label so the picker shows `Claude - Claude Opus 4.7` for `cc/claude-opus-4-7`, `Kiro - Claude Opus 4.7` for `kr/claude-opus-4-7`, and `GHM - GPT 5` for `ghm/gpt-5` (slot.name `GitHub Models` > 8 chars → abbreviated). Critical when the same model id is sold through multiple upstream connections with different cost/auth/rate-limit profiles. Set to `false` to keep the pre-v3.8.3 unsuffixed format.
|
||||
|
||||
## Comparison vs `@omniroute/opencode-provider`
|
||||
|
||||
|
||||
@@ -1068,38 +1068,82 @@ export const defaultOmniRouteEnrichmentFetcher: OmniRouteEnrichmentFetcher = asy
|
||||
};
|
||||
|
||||
/**
|
||||
* Separator used by `applyProviderTag` between the enriched model name
|
||||
* and the upstream provider label. Middle-dot (U+00B7) chosen for visual
|
||||
* compactness in narrow TUI columns; trivially distinct from any model
|
||||
* id character so it survives a roundtrip through `model.name`.
|
||||
* Separator used by `applyProviderTag` between the upstream provider
|
||||
* label (prefix) and the enriched model name. ASCII hyphen with
|
||||
* surrounding spaces — terminal-safe everywhere, never collides with
|
||||
* a model id (those use slashes / dots / underscores).
|
||||
*
|
||||
* Layout: `<short-label> - <model name>` (label leads so column scans
|
||||
* group by provider — e.g. `Claude - Claude Opus 4.7`,
|
||||
* `Kiro - Claude Opus 4.7`).
|
||||
*/
|
||||
export const PROVIDER_TAG_SEPARATOR = " · ";
|
||||
export const PROVIDER_TAG_SEPARATOR = " - ";
|
||||
|
||||
/** Threshold beyond which `providerDisplayName` is abbreviated to UPPER(alias). */
|
||||
const PROVIDER_LABEL_MAX_CHARS = 8;
|
||||
|
||||
/**
|
||||
* Append the upstream provider label to `model.name` so the OC TUI picker
|
||||
* can differentiate the same model id sold through different upstream
|
||||
* connections (e.g. `cc/claude-opus-4-7` via Anthropic vs `kr/claude-opus-4-7`
|
||||
* via Kiro). Mutates the model in place and is idempotent — running twice
|
||||
* never double-suffixes. No-op when:
|
||||
* Pick the short label for an upstream provider that goes into the
|
||||
* `<label> · <model>` prefix.
|
||||
*
|
||||
* Rule (matches user spec — no hardcoded registry, fully data-driven):
|
||||
*
|
||||
* 1. Trim `enrichment.providerDisplayName` (= `/api/pricing/models[<alias>].name`).
|
||||
* 2. If the trimmed label is non-empty AND ≤ {@link PROVIDER_LABEL_MAX_CHARS},
|
||||
* use it verbatim (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`).
|
||||
* 3. Otherwise fall back to `UPPER(enrichment.providerAlias)` so long
|
||||
* slot.names (`GitHub Models`, `Gemini-cli`) compress to `GHM`,
|
||||
* `GEMINI-CLI`.
|
||||
* 4. If neither field is usable, return `undefined` (caller should
|
||||
* skip the prefix decoration).
|
||||
*/
|
||||
export function shortProviderLabel(
|
||||
enrichment: OmniRouteEnrichmentEntry | undefined
|
||||
): string | undefined {
|
||||
if (!enrichment) return undefined;
|
||||
const raw =
|
||||
typeof enrichment.providerDisplayName === "string" ? enrichment.providerDisplayName.trim() : "";
|
||||
if (raw.length > 0 && raw.length <= PROVIDER_LABEL_MAX_CHARS) return raw;
|
||||
const alias = typeof enrichment.providerAlias === "string" ? enrichment.providerAlias.trim() : "";
|
||||
if (alias.length > 0) return alias.toUpperCase();
|
||||
// Tolerate "label too long + no alias" by falling back to the long
|
||||
// label itself — better than dropping the prefix entirely. Rare case.
|
||||
return raw.length > 0 ? raw : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend the upstream provider label to `model.name` so the OC TUI
|
||||
* picker can differentiate the same model id sold through different
|
||||
* upstream connections (e.g. `cc/claude-opus-4-7` via Anthropic
|
||||
* vs `kr/claude-opus-4-7` via Kiro). Result shape:
|
||||
*
|
||||
* `<label>${PROVIDER_TAG_SEPARATOR}<enriched name>`
|
||||
* → `Claude - Claude Opus 4.7`
|
||||
* → `Kiro - Claude Opus 4.7`
|
||||
* → `GHM - GPT 5` (slot.name "GitHub Models" > 8 chars → UPPER(alias))
|
||||
*
|
||||
* Mutates the model in place and is idempotent — running twice never
|
||||
* double-prefixes. No-op when:
|
||||
*
|
||||
* - `enrichment` is undefined,
|
||||
* - `enrichment.providerDisplayName` is missing/empty,
|
||||
* - the current `model.name` already ends with the suffix.
|
||||
* - {@link shortProviderLabel} returns `undefined`
|
||||
* (no `providerDisplayName` AND no `providerAlias`),
|
||||
* - the current `model.name` already starts with the prefix.
|
||||
*
|
||||
* Combos are intentionally skipped by callers (they're multi-upstream by
|
||||
* definition; the `Combo: ` prefix conveys that). Raw models call this
|
||||
* after `applyEnrichment` so the tag layers on top of the friendly name.
|
||||
* Combos are intentionally skipped by callers (they're multi-upstream
|
||||
* by definition; the `Combo: ` prefix conveys that). Raw models call
|
||||
* this after `applyEnrichment` so the tag layers on top of the
|
||||
* friendly name.
|
||||
*/
|
||||
export function applyProviderTag(
|
||||
model: ModelV2,
|
||||
enrichment: OmniRouteEnrichmentEntry | undefined
|
||||
): ModelV2 {
|
||||
if (!enrichment) return model;
|
||||
const tag = enrichment.providerDisplayName;
|
||||
if (typeof tag !== "string" || tag.trim().length === 0) return model;
|
||||
const suffix = `${PROVIDER_TAG_SEPARATOR}${tag}`;
|
||||
if (model.name.endsWith(suffix)) return model;
|
||||
model.name = `${model.name}${suffix}`;
|
||||
const label = shortProviderLabel(enrichment);
|
||||
if (!label) return model;
|
||||
const prefix = `${label}${PROVIDER_TAG_SEPARATOR}`;
|
||||
if (model.name.startsWith(prefix)) return model;
|
||||
model.name = `${prefix}${model.name}`;
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -1230,14 +1274,52 @@ export const defaultOmniRouteCompressionMetaFetcher: OmniRouteCompressionMetaFet
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a compression pipeline as a short human-readable string for combo
|
||||
* `name` decoration. Example: `[rtk:standard → caveman:full]`.
|
||||
* Map of well-known compression-intensity tokens to a single emoji
|
||||
* conveying "how much" compression is applied. Traffic-light palette:
|
||||
*
|
||||
* 🟢 minimal / lite — almost no loss
|
||||
* 🟡 standard — balanced
|
||||
* 🟠 aggressive / full — heavy
|
||||
* 🔴 ultra — extreme
|
||||
*
|
||||
* Lookup is case-insensitive. Unknown intensities fall through to the
|
||||
* raw text form (`engine:<intensity>`) so we never hide a value that
|
||||
* OmniRoute knows but the plugin doesn't.
|
||||
*
|
||||
* Exported for callers (and tests) that want to assemble their own
|
||||
* pipeline strings.
|
||||
*/
|
||||
export const COMPRESSION_INTENSITY_EMOJI: Record<string, string> = {
|
||||
minimal: "🟢",
|
||||
lite: "🟢",
|
||||
standard: "🟡",
|
||||
aggressive: "🟠",
|
||||
full: "🟠",
|
||||
ultra: "🔴",
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a compression pipeline as a short human-readable string for
|
||||
* combo `name` decoration. Intensity tokens render as a traffic-light
|
||||
* emoji so a column scan reveals "how compressed" the combo is at a
|
||||
* glance:
|
||||
*
|
||||
* `[rtk🟡 → caveman🟠]` (rtk:standard → caveman:full)
|
||||
* `[rtk🔴]` (rtk:ultra, single-step)
|
||||
* `[caveman]` (engine without intensity, no emoji)
|
||||
* `[rtk:custom-thing]` (unknown intensity, raw-text fallback)
|
||||
*/
|
||||
export function formatCompressionPipeline(pipeline: OmniRouteCompressionStep[]): string {
|
||||
if (!pipeline || pipeline.length === 0) return "";
|
||||
return (
|
||||
"[" +
|
||||
pipeline.map((s) => (s.intensity ? `${s.engine}:${s.intensity}` : s.engine)).join(" → ") +
|
||||
pipeline
|
||||
.map((s) => {
|
||||
if (!s.intensity) return s.engine;
|
||||
const emoji = COMPRESSION_INTENSITY_EMOJI[s.intensity.toLowerCase()];
|
||||
return emoji ? `${s.engine}${emoji}` : `${s.engine}:${s.intensity}`;
|
||||
})
|
||||
.join(" → ") +
|
||||
"]"
|
||||
);
|
||||
}
|
||||
@@ -1789,10 +1871,10 @@ export function createOmniRouteProviderHook(
|
||||
});
|
||||
const enrichEntry = rawEnrichment.get(entry.id);
|
||||
applyEnrichment(model, enrichEntry);
|
||||
// Append upstream provider label (e.g. `Claude Opus 4.7 · Claude`)
|
||||
// so the picker can differentiate same-id models routed through
|
||||
// different upstream connections. Idempotent + gated by
|
||||
// `features.providerTag` (default-on). Combos skip this on purpose.
|
||||
// Prepend upstream provider label (e.g. `Claude - Claude Opus 4.7`)
|
||||
// so the picker groups same-model rows by upstream connection.
|
||||
// Idempotent + gated by `features.providerTag` (default-on).
|
||||
// Combos skip this on purpose.
|
||||
if (wantProviderTag) applyProviderTag(model, enrichEntry);
|
||||
models[entry.id] = model;
|
||||
}
|
||||
@@ -2400,8 +2482,8 @@ export function buildStaticProviderEntry(
|
||||
? usableProviderAliasSet(connections, enrichment)
|
||||
: undefined;
|
||||
// Provider-tag suffix — default-on, opt-out via `features.providerTag: false`.
|
||||
// Appends e.g. ` · Claude` to enriched raw-model names so the picker can
|
||||
// tell `cc/claude-opus-4-7` (Anthropic) apart from `kr/claude-opus-4-7`
|
||||
// Prepends e.g. `Claude - ` to enriched raw-model names so the picker
|
||||
// can tell `cc/claude-opus-4-7` (Anthropic) apart from `kr/claude-opus-4-7`
|
||||
// (Kiro). Combos skip this by design.
|
||||
const wantProviderTag = opts.features?.providerTag !== false;
|
||||
|
||||
@@ -2435,15 +2517,15 @@ export function buildStaticProviderEntry(
|
||||
const enrichmentEntry = enrichment?.get(raw.id);
|
||||
const enrichmentName = enrichmentEntry?.name;
|
||||
let displayName = enrichmentName && enrichmentName.length > 0 ? enrichmentName : raw.id;
|
||||
// Provider-tag suffix — append upstream provider label so the picker
|
||||
// can differentiate same-id models routed through different upstream
|
||||
// connections (mirrors `applyProviderTag` used in the dynamic hook).
|
||||
// Idempotent: skip when the name already ends with the suffix.
|
||||
// Provider-tag PREFIX — `<label> - <name>` so the picker groups by
|
||||
// upstream provider when scanning a column of model names. Mirrors
|
||||
// `applyProviderTag` used in the dynamic hook. Idempotent: skip
|
||||
// when the name already starts with the prefix.
|
||||
if (wantProviderTag) {
|
||||
const tag = enrichmentEntry?.providerDisplayName;
|
||||
if (typeof tag === "string" && tag.trim().length > 0) {
|
||||
const suffix = `${PROVIDER_TAG_SEPARATOR}${tag}`;
|
||||
if (!displayName.endsWith(suffix)) displayName = `${displayName}${suffix}`;
|
||||
const label = shortProviderLabel(enrichmentEntry);
|
||||
if (label) {
|
||||
const prefix = `${label}${PROVIDER_TAG_SEPARATOR}`;
|
||||
if (!displayName.startsWith(prefix)) displayName = `${prefix}${displayName}`;
|
||||
}
|
||||
}
|
||||
const entry: OmniRouteStaticModelEntry = { name: displayName };
|
||||
|
||||
@@ -1081,7 +1081,7 @@ test("config: cached rawEnrichment from earlier provider hook is reused (no refe
|
||||
// (Anthropic) from `kr/claude-opus-4-7` (Kiro) etc.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("config: providerTag (default-on) appends ' · <provider>' to enriched raw-model names", async () => {
|
||||
test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-model names", async () => {
|
||||
const readAuthJson = stubReadAuthJson({
|
||||
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
|
||||
});
|
||||
@@ -1121,8 +1121,8 @@ test("config: providerTag (default-on) appends ' · <provider>' to enriched raw-
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
|
||||
.omniroute;
|
||||
assert.ok(entry);
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6 · Claude");
|
||||
assert.equal(entry.models["gemini-3-flash"].name, "Gemini 3 Flash · Gemini-cli");
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
|
||||
assert.equal(entry.models["gemini-3-flash"].name, "GEMINI-CLI - Gemini 3 Flash");
|
||||
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
|
||||
assert.equal(entry.models["combo/claude-tier"].name, "Combo: Claude Tier");
|
||||
});
|
||||
@@ -1135,10 +1135,7 @@ test("config: providerTag=false suppresses the suffix", async () => {
|
||||
const combosFetcher = stubCombosFetcher([]);
|
||||
const enrichmentFetcher = stubEnrichmentFetcher(
|
||||
new Map<string, OmniRouteEnrichmentEntry>([
|
||||
[
|
||||
"claude-sonnet-4-6",
|
||||
{ name: "Claude Sonnet 4.6", providerDisplayName: "Claude" },
|
||||
],
|
||||
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6", providerDisplayName: "Claude" }],
|
||||
])
|
||||
);
|
||||
const logger = captureWarn();
|
||||
@@ -1159,14 +1156,15 @@ test("config: providerTag=false suppresses the suffix", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("config: providerTag skipped when providerDisplayName missing (falls back to enriched name only)", async () => {
|
||||
test("config: providerTag falls back to UPPER(alias) when providerDisplayName missing", async () => {
|
||||
const readAuthJson = stubReadAuthJson({
|
||||
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
|
||||
});
|
||||
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
|
||||
const combosFetcher = stubCombosFetcher([]);
|
||||
// Enrichment has the friendly name but NO providerDisplayName — e.g.
|
||||
// a slot OmniRoute hasn't curated a human label for yet.
|
||||
// a slot OmniRoute hasn't curated a human label for yet. We still
|
||||
// have the alias though, so the prefix uses UPPER(alias) = "CC".
|
||||
const enrichmentFetcher = stubEnrichmentFetcher(
|
||||
new Map<string, OmniRouteEnrichmentEntry>([
|
||||
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6", providerAlias: "cc" }],
|
||||
@@ -1181,6 +1179,32 @@ test("config: providerTag skipped when providerDisplayName missing (falls back t
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
|
||||
.omniroute;
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
test("config: providerTag skipped entirely when neither providerDisplayName nor providerAlias set", async () => {
|
||||
const readAuthJson = stubReadAuthJson({
|
||||
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
|
||||
});
|
||||
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
|
||||
const combosFetcher = stubCombosFetcher([]);
|
||||
// No metadata at all — defensive case, e.g. legacy enrichment payload.
|
||||
const enrichmentFetcher = stubEnrichmentFetcher(
|
||||
new Map<string, OmniRouteEnrichmentEntry>([
|
||||
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6" }],
|
||||
])
|
||||
);
|
||||
const logger = captureWarn();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{ readAuthJson, fetcher, combosFetcher, enrichmentFetcher, logger }
|
||||
);
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
|
||||
.omniroute;
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
|
||||
@@ -1194,10 +1218,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
|
||||
const combosFetcher = stubCombosFetcher([]);
|
||||
const enrichmentFetcher = stubEnrichmentFetcher(
|
||||
new Map<string, OmniRouteEnrichmentEntry>([
|
||||
[
|
||||
"claude-sonnet-4-6",
|
||||
{ name: "Claude Sonnet 4.6", providerDisplayName: "Claude" },
|
||||
],
|
||||
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6", providerDisplayName: "Claude" }],
|
||||
])
|
||||
);
|
||||
const logger = captureWarn();
|
||||
@@ -1212,12 +1233,12 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
|
||||
await hook(inputA);
|
||||
const entryA = (inputA as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
|
||||
.omniroute;
|
||||
assert.equal(entryA.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6 · Claude");
|
||||
assert.equal(entryA.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
|
||||
|
||||
// Second invocation (cache hit) — name must still be single-suffixed.
|
||||
const inputB = makeInput();
|
||||
await hook(inputB);
|
||||
const entryB = (inputB as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
|
||||
.omniroute;
|
||||
assert.equal(entryB.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6 · Claude");
|
||||
assert.equal(entryB.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
@@ -176,8 +176,8 @@ test("applyEnrichment: partial pricing preserves untouched fields", () => {
|
||||
// applyProviderTag (Option E)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("applyProviderTag: PROVIDER_TAG_SEPARATOR is middle-dot with surrounding spaces", () => {
|
||||
assert.equal(PROVIDER_TAG_SEPARATOR, " \u00b7 ");
|
||||
test("applyProviderTag: PROVIDER_TAG_SEPARATOR is hyphen with surrounding spaces", () => {
|
||||
assert.equal(PROVIDER_TAG_SEPARATOR, " - ");
|
||||
});
|
||||
|
||||
test("applyProviderTag: undefined enrichment → no-op", () => {
|
||||
@@ -187,46 +187,63 @@ test("applyProviderTag: undefined enrichment → no-op", () => {
|
||||
assert.equal(m.name, "Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
test("applyProviderTag: providerDisplayName present → suffix appended", () => {
|
||||
test("applyProviderTag: providerDisplayName present (short) → label prefix prepended", () => {
|
||||
const m = baseModel();
|
||||
m.name = "Claude Sonnet 4.6";
|
||||
applyProviderTag(m as never, { providerDisplayName: "Claude" });
|
||||
assert.equal(m.name, "Claude Sonnet 4.6 \u00b7 Claude");
|
||||
assert.equal(m.name, "Claude - Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
test("applyProviderTag: missing providerDisplayName → no-op", () => {
|
||||
test("applyProviderTag: providerDisplayName too long → falls back to UPPER(alias) prefix", () => {
|
||||
const m = baseModel();
|
||||
m.name = "GPT 5";
|
||||
applyProviderTag(m as never, {
|
||||
providerDisplayName: "GitHub Models",
|
||||
providerAlias: "ghm",
|
||||
});
|
||||
assert.equal(m.name, "GHM - GPT 5");
|
||||
});
|
||||
|
||||
test("applyProviderTag: long displayName + no alias → uses long label rather than dropping prefix", () => {
|
||||
const m = baseModel();
|
||||
m.name = "GPT 5";
|
||||
applyProviderTag(m as never, { providerDisplayName: "GitHub Models" });
|
||||
assert.equal(m.name, "GitHub Models - GPT 5");
|
||||
});
|
||||
|
||||
test("applyProviderTag: only providerAlias known → UPPER(alias) prefix", () => {
|
||||
const m = baseModel();
|
||||
m.name = "Claude Sonnet 4.6";
|
||||
applyProviderTag(m as never, { providerAlias: "cc" });
|
||||
assert.equal(m.name, "Claude Sonnet 4.6");
|
||||
assert.equal(m.name, "CC - Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
test("applyProviderTag: empty/whitespace providerDisplayName → no-op", () => {
|
||||
test("applyProviderTag: empty/whitespace providerDisplayName + no alias → no-op", () => {
|
||||
const m = baseModel();
|
||||
m.name = "Claude Sonnet 4.6";
|
||||
applyProviderTag(m as never, { providerDisplayName: " " });
|
||||
assert.equal(m.name, "Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
test("applyProviderTag: idempotent — second call doesn't double-suffix", () => {
|
||||
test("applyProviderTag: idempotent — second call doesn't double-prefix", () => {
|
||||
const m = baseModel();
|
||||
m.name = "Claude Sonnet 4.6";
|
||||
applyProviderTag(m as never, { providerDisplayName: "Claude" });
|
||||
applyProviderTag(m as never, { providerDisplayName: "Claude" });
|
||||
applyProviderTag(m as never, { providerDisplayName: "Claude" });
|
||||
assert.equal(m.name, "Claude Sonnet 4.6 \u00b7 Claude");
|
||||
assert.equal(m.name, "Claude - Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
test("applyProviderTag: distinct providers for same model id → two separate suffixes", () => {
|
||||
test("applyProviderTag: distinct providers for same model id → two separate prefixes", () => {
|
||||
const a = baseModel();
|
||||
a.name = "Claude Opus 4.7";
|
||||
applyProviderTag(a as never, { providerDisplayName: "Claude" });
|
||||
assert.equal(a.name, "Claude Opus 4.7 \u00b7 Claude");
|
||||
assert.equal(a.name, "Claude - Claude Opus 4.7");
|
||||
|
||||
const b = baseModel();
|
||||
b.name = "Claude Opus 4.7";
|
||||
applyProviderTag(b as never, { providerDisplayName: "Kiro" });
|
||||
assert.equal(b.name, "Claude Opus 4.7 \u00b7 Kiro");
|
||||
assert.equal(b.name, "Kiro - Claude Opus 4.7");
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -237,27 +254,60 @@ test("formatCompressionPipeline: empty pipeline → empty string", () => {
|
||||
assert.equal(formatCompressionPipeline([]), "");
|
||||
});
|
||||
|
||||
test("formatCompressionPipeline: single step with intensity", () => {
|
||||
test("formatCompressionPipeline: single step with intensity → emoji", () => {
|
||||
assert.equal(
|
||||
formatCompressionPipeline([{ engine: "caveman", intensity: "full" }]),
|
||||
"[caveman:full]"
|
||||
"[caveman\u{1F7E0}]"
|
||||
);
|
||||
});
|
||||
|
||||
test("formatCompressionPipeline: multi-step pipeline", () => {
|
||||
test("formatCompressionPipeline: multi-step pipeline → emoji per step", () => {
|
||||
assert.equal(
|
||||
formatCompressionPipeline([
|
||||
{ engine: "rtk", intensity: "standard" },
|
||||
{ engine: "caveman", intensity: "full" },
|
||||
]),
|
||||
"[rtk:standard → caveman:full]"
|
||||
"[rtk\u{1F7E1} → caveman\u{1F7E0}]"
|
||||
);
|
||||
});
|
||||
|
||||
test("formatCompressionPipeline: step without intensity", () => {
|
||||
test("formatCompressionPipeline: step without intensity → engine bare", () => {
|
||||
assert.equal(formatCompressionPipeline([{ engine: "rtk" }]), "[rtk]");
|
||||
});
|
||||
|
||||
test("formatCompressionPipeline: ultra → red", () => {
|
||||
assert.equal(
|
||||
formatCompressionPipeline([{ engine: "caveman", intensity: "ultra" }]),
|
||||
"[caveman\u{1F534}]"
|
||||
);
|
||||
});
|
||||
|
||||
test("formatCompressionPipeline: lite/minimal → green", () => {
|
||||
assert.equal(formatCompressionPipeline([{ engine: "rtk", intensity: "lite" }]), "[rtk\u{1F7E2}]");
|
||||
assert.equal(
|
||||
formatCompressionPipeline([{ engine: "rtk", intensity: "minimal" }]),
|
||||
"[rtk\u{1F7E2}]"
|
||||
);
|
||||
});
|
||||
|
||||
test("formatCompressionPipeline: intensity case-insensitive", () => {
|
||||
assert.equal(
|
||||
formatCompressionPipeline([{ engine: "caveman", intensity: "ULTRA" }]),
|
||||
"[caveman\u{1F534}]"
|
||||
);
|
||||
assert.equal(
|
||||
formatCompressionPipeline([{ engine: "caveman", intensity: "Standard" }]),
|
||||
"[caveman\u{1F7E1}]"
|
||||
);
|
||||
});
|
||||
|
||||
test("formatCompressionPipeline: unknown intensity falls back to raw text", () => {
|
||||
assert.equal(
|
||||
formatCompressionPipeline([{ engine: "rtk", intensity: "custom-thing" }]),
|
||||
"[rtk:custom-thing]"
|
||||
);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Provider hook — enrichment applied via injected fetcher
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -384,7 +434,11 @@ test("provider hook: compression metadata fetcher called when opted in", async (
|
||||
assert.equal(called, 1, "compression metadata fetcher called");
|
||||
const combo = out["combo/claude-primary"];
|
||||
assert.ok(combo, "combo entry present");
|
||||
assert.match(combo.name, /\[rtk:standard → caveman:full\]/, "combo name decorated with pipeline");
|
||||
assert.match(
|
||||
combo.name,
|
||||
/\[rtk\u{1F7E1} → caveman\u{1F7E0}\]/u,
|
||||
"combo name decorated with emoji pipeline (rtk:standard=🟡, caveman:full=🟠)"
|
||||
);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user