Spaces:
Runtime error
Runtime error
| /** | |
| * OpenCode provider plugin for OmniRoute AI Gateway. | |
| * | |
| * Generates an OpenCode-compatible provider object that points to a running | |
| * OmniRoute instance. The output follows the OpenCode config schema | |
| * (https://opencode.ai/config.json) and delegates the runtime to | |
| * `@ai-sdk/openai-compatible` so OpenCode can drive any OmniRoute-exposed | |
| * model through its standard OpenAI-compatible client. | |
| * | |
| * Two ways to consume the helper: | |
| * | |
| * 1. As code, when you build your own opencode.json programmatically: | |
| * | |
| * ```ts | |
| * import { buildOmniRouteOpenCodeConfig } from "@omniroute/opencode-provider"; | |
| * const config = buildOmniRouteOpenCodeConfig({ | |
| * baseURL: "http://localhost:20128", | |
| * apiKey: "sk_omniroute", | |
| * }); | |
| * // config -> { $schema, provider: { omniroute: { npm, name, options, models } } } | |
| * ``` | |
| * | |
| * 2. As a single-provider entry to merge into an existing opencode.json: | |
| * | |
| * ```ts | |
| * import { createOmniRouteProvider } from "@omniroute/opencode-provider"; | |
| * const provider = createOmniRouteProvider({ baseURL, apiKey }); | |
| * // provider -> the value to place under provider.omniroute in opencode.json | |
| * ``` | |
| * | |
| * Note: `baseURL` accepts both `http://host:port` and `http://host:port/v1`. | |
| * The helper normalises trailing slashes / `/v1` so you never get `/v1/v1`. | |
| */ | |
| export const OMNIROUTE_PROVIDER_KEY = "omniroute" as const; | |
| export const OMNIROUTE_PROVIDER_NPM = "@ai-sdk/openai-compatible" as const; | |
| export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json" as const; | |
| /** | |
| * Default catalog of models surfaced to OpenCode when the caller does not | |
| * supply an explicit `models` list. | |
| * | |
| * Curated set covering the most commonly deployed OmniRoute models. Synced | |
| * with the Alph4d0g/opencode-omniroute-auth OMNIROUTE_DEFAULT_MODELS constant | |
| * (https://github.com/Alph4d0g/opencode-omniroute-auth, MIT) and extended | |
| * with Claude Code passthrough models (`cc/` prefix). | |
| */ | |
| export const OMNIROUTE_DEFAULT_OPENCODE_MODELS = [ | |
| "cc/claude-opus-4-8", | |
| "cc/claude-opus-4-7", | |
| "cc/claude-sonnet-4-6", | |
| "cc/claude-haiku-4-5-20251001", | |
| "claude-opus-4-5-thinking", | |
| "claude-sonnet-4-5-thinking", | |
| "gemini-3.1-pro-high", | |
| "gemini-3-flash", | |
| ] as const; | |
| /** | |
| * Optional capability flags surfaced to OpenCode's model picker. | |
| * | |
| * OpenCode reads these per-model keys (snake_case in JSON) to render badges | |
| * and to gate features such as image attachments, reasoning mode, temperature | |
| * controls and tool-calling. Omitted flags default to OpenCode's heuristics. | |
| * | |
| * Mirrors the capability shape used by Alph4d0g/opencode-omniroute-auth | |
| * (https://github.com/Alph4d0g/opencode-omniroute-auth, MIT). | |
| */ | |
| export interface ModelCapabilities { | |
| /** Display label shown in the model picker. Falls back to the model id. */ | |
| label?: string; | |
| /** Model accepts image / file attachments. */ | |
| attachment?: boolean; | |
| /** Model exposes a "reasoning" / extended-thinking surface. */ | |
| reasoning?: boolean; | |
| /** Model honours the `temperature` parameter. */ | |
| temperature?: boolean; | |
| /** Model supports tool / function calling. */ | |
| tool_call?: boolean; | |
| } | |
| /** | |
| * Default per-model context window sizes (tokens) for the curated default catalog. | |
| * Matches the context lengths used by OmniRoute's provider registry. | |
| */ | |
| export const OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS: Record<string, number> = { | |
| "cc/claude-opus-4-8": 1_000_000, | |
| "cc/claude-opus-4-7": 1_000_000, | |
| "cc/claude-sonnet-4-6": 200_000, | |
| "cc/claude-haiku-4-5-20251001": 200_000, | |
| "claude-opus-4-5-thinking": 200_000, | |
| "claude-sonnet-4-5-thinking": 200_000, | |
| "gemini-3.1-pro-high": 1_000_000, | |
| "gemini-3-flash": 1_000_000, | |
| }; | |
| /** | |
| * Default per-model capability hints for the curated default catalog. | |
| * | |
| * Conservative defaults: every default model accepts attachments, tool calls | |
| * and temperature; `reasoning` is opt-in per model id. Callers override per | |
| * model via `OmniRouteProviderOptions.modelCapabilities`. | |
| */ | |
| export const OMNIROUTE_DEFAULT_MODEL_CAPABILITIES: Record<string, ModelCapabilities> = { | |
| "cc/claude-opus-4-8": { attachment: true, reasoning: true, temperature: true, tool_call: true }, | |
| "cc/claude-opus-4-7": { attachment: true, reasoning: true, temperature: true, tool_call: true }, | |
| "cc/claude-sonnet-4-6": { attachment: true, reasoning: true, temperature: true, tool_call: true }, | |
| "cc/claude-haiku-4-5-20251001": { attachment: true, temperature: true, tool_call: true }, | |
| "claude-opus-4-5-thinking": { | |
| attachment: true, | |
| reasoning: true, | |
| temperature: true, | |
| tool_call: true, | |
| }, | |
| "claude-sonnet-4-5-thinking": { | |
| attachment: true, | |
| reasoning: true, | |
| temperature: true, | |
| tool_call: true, | |
| }, | |
| "gemini-3.1-pro-high": { attachment: true, reasoning: true, temperature: true, tool_call: true }, | |
| "gemini-3-flash": { attachment: true, temperature: true, tool_call: true }, | |
| }; | |
| export interface OmniRouteProviderOptions { | |
| /** OmniRoute base URL, with or without trailing `/v1`. Required. */ | |
| baseURL: string; | |
| /** OmniRoute API key. Required. Use `sk_omniroute` for local instances without REQUIRE_API_KEY. */ | |
| apiKey: string; | |
| /** Override the display name shown in OpenCode. Default: `"OmniRoute"`. */ | |
| displayName?: string; | |
| /** Override the model catalog. Accepts model ids (strings) or live model entries from `fetchLiveModels`. When entries carry a `contextLength`, it is used directly — no hardcoded map needed. */ | |
| models?: readonly (string | { id: string; contextLength?: number })[]; | |
| /** Optional human-readable labels keyed by model id. Overridden by `modelCapabilities[id].label`. */ | |
| modelLabels?: Record<string, string>; | |
| /** | |
| * Optional capability overrides keyed by model id. Merged on top of | |
| * `OMNIROUTE_DEFAULT_MODEL_CAPABILITIES` for ids in the default catalog; | |
| * for custom ids the override is used verbatim. | |
| */ | |
| modelCapabilities?: Record<string, ModelCapabilities>; | |
| /** | |
| * Optional per-model context-length overrides (tokens). Takes precedence | |
| * over the static `OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS` map but is | |
| * superseded by `contextLength` on live model entries passed via `models`. | |
| */ | |
| modelContextLengths?: Record<string, string | number>; | |
| /** | |
| * Primary model for OpenCode (top-level `model` key). | |
| * Emitted as `"omniroute/<id>"`. When omitted the key is not written. | |
| */ | |
| model?: string; | |
| /** | |
| * Secondary / cheap model for OpenCode (top-level `small_model` key). | |
| * Emitted as `"omniroute/<id>"`. When omitted the key is not written. | |
| */ | |
| smallModel?: string; | |
| } | |
| /** Per-model entry written under `provider.omniroute.models[id]`. */ | |
| export interface OpenCodeModelEntry { | |
| name: string; | |
| attachment?: boolean; | |
| reasoning?: boolean; | |
| temperature?: boolean; | |
| tool_call?: boolean; | |
| /** | |
| * Context window limit. OpenCode reads this to determine usable context | |
| * length for compaction, overflow detection, and router decisions. | |
| * Maps to `limit.context` in OpenCode's provider config schema. | |
| */ | |
| limit?: { | |
| /** Maximum context length in tokens (e.g. 200000 for Claude, 1000000 for Gemini). */ | |
| context: number; | |
| /** Optional per-request max input tokens. */ | |
| input?: number; | |
| /** Optional max output tokens. */ | |
| output?: number; | |
| }; | |
| } | |
| export interface OpenCodeProviderEntry { | |
| /** Identifier of the OpenCode runtime package that will speak to OmniRoute. */ | |
| npm: typeof OMNIROUTE_PROVIDER_NPM; | |
| /** Display name in the OpenCode UI. */ | |
| name: string; | |
| /** Options forwarded to `@ai-sdk/openai-compatible`. */ | |
| options: { | |
| baseURL: string; | |
| apiKey: string; | |
| }; | |
| /** Model catalog surfaced to OpenCode. */ | |
| models: Record<string, OpenCodeModelEntry>; | |
| } | |
| export interface OpenCodeConfigDocument { | |
| $schema: typeof OPENCODE_CONFIG_SCHEMA; | |
| /** Primary model for OpenCode, e.g. `"omniroute/claude-sonnet-4-5-thinking"`. */ | |
| model?: string; | |
| /** Secondary / cheap model for OpenCode, e.g. `"omniroute/gemini-3-flash"`. */ | |
| small_model?: string; | |
| provider: { | |
| [OMNIROUTE_PROVIDER_KEY]: OpenCodeProviderEntry; | |
| }; | |
| } | |
| function requireNonEmpty(value: unknown, field: string): string { | |
| if (typeof value !== "string") { | |
| throw new TypeError(`@omniroute/opencode-provider: ${field} must be a string`); | |
| } | |
| const trimmed = value.trim(); | |
| if (!trimmed) { | |
| throw new Error(`@omniroute/opencode-provider: ${field} is required and cannot be empty`); | |
| } | |
| return trimmed; | |
| } | |
| /** | |
| * Normalise the user-supplied baseURL so the final `options.baseURL` always | |
| * ends in exactly one `/v1`. Accepts both `http://host` and `http://host/v1`. | |
| */ | |
| export function normalizeBaseURL(rawBaseURL: string): string { | |
| const trimmed = requireNonEmpty(rawBaseURL, "baseURL"); | |
| try { | |
| new URL(trimmed); | |
| } catch { | |
| throw new Error( | |
| `@omniroute/opencode-provider: baseURL is not a valid URL: ${JSON.stringify(rawBaseURL)}` | |
| ); | |
| } | |
| let base = trimmed; | |
| let end = base.length; | |
| while (end > 0 && base[end - 1] === "/") end--; | |
| base = end < base.length ? base.slice(0, end) : base; | |
| if (base.endsWith("/v1")) base = base.slice(0, -3); | |
| return base + "/v1"; | |
| } | |
| /** | |
| * Build the `provider.omniroute` entry for an OpenCode config document. | |
| * The returned object is JSON-serialisable and safe to embed verbatim. | |
| */ | |
| export function createOmniRouteProvider(options: OmniRouteProviderOptions): OpenCodeProviderEntry { | |
| const baseURL = normalizeBaseURL(options.baseURL); | |
| const apiKey = requireNonEmpty(options.apiKey, "apiKey"); | |
| const modelList = | |
| options.models && options.models.length > 0 | |
| ? [...options.models] | |
| : [...OMNIROUTE_DEFAULT_OPENCODE_MODELS]; | |
| const labels = options.modelLabels ?? {}; | |
| const overrides = options.modelCapabilities ?? {}; | |
| const models: Record<string, OpenCodeModelEntry> = {}; | |
| const seen = new Set<string>(); | |
| for (const raw of modelList) { | |
| const id = | |
| typeof raw === "object" && raw !== null && "id" in raw && typeof (raw as any).id === "string" | |
| ? (raw as { id: string }).id.trim() | |
| : typeof raw === "string" | |
| ? raw.trim() | |
| : ""; | |
| if (!id || seen.has(id)) continue; | |
| seen.add(id); | |
| const defaults = OMNIROUTE_DEFAULT_MODEL_CAPABILITIES[id] ?? {}; | |
| const override = overrides[id] ?? {}; | |
| const merged: ModelCapabilities = { ...defaults, ...override }; | |
| const explicitLabel = | |
| typeof merged.label === "string" && merged.label.trim() | |
| ? merged.label.trim() | |
| : typeof labels[id] === "string" && labels[id].trim() | |
| ? labels[id].trim() | |
| : id; | |
| const entry: OpenCodeModelEntry = { name: explicitLabel }; | |
| if (typeof merged.attachment === "boolean") entry.attachment = merged.attachment; | |
| if (typeof merged.reasoning === "boolean") entry.reasoning = merged.reasoning; | |
| if (typeof merged.temperature === "boolean") entry.temperature = merged.temperature; | |
| if (typeof merged.tool_call === "boolean") entry.tool_call = merged.tool_call; | |
| // Context window: live model entry (from API catalog) > modelContextLengths > static defaults | |
| const liveContext = | |
| typeof raw === "object" && raw !== null | |
| ? (raw as { contextLength?: number }).contextLength | |
| : undefined; | |
| const rawContextLength = | |
| liveContext ?? | |
| options.modelContextLengths?.[id] ?? | |
| OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id]; | |
| const contextLength = | |
| typeof rawContextLength === "string" ? parseInt(rawContextLength, 10) : rawContextLength; | |
| if (typeof contextLength === "number" && !isNaN(contextLength) && contextLength > 0) { | |
| entry.limit = { context: contextLength }; | |
| } | |
| models[id] = entry; | |
| } | |
| return { | |
| npm: OMNIROUTE_PROVIDER_NPM, | |
| name: options.displayName?.trim() || "OmniRoute", | |
| options: { baseURL, apiKey }, | |
| models, | |
| }; | |
| } | |
| /** | |
| * Build a full OpenCode config document (with `$schema` + `provider.omniroute`). | |
| * Useful when scaffolding a fresh `opencode.json`. | |
| * | |
| * When `options.model` / `options.smallModel` are supplied they are emitted as | |
| * top-level `model` / `small_model` keys prefixed with `"omniroute/"` so | |
| * OpenCode resolves them through the configured provider. | |
| */ | |
| export function buildOmniRouteOpenCodeConfig( | |
| options: OmniRouteProviderOptions | |
| ): OpenCodeConfigDocument { | |
| const doc: OpenCodeConfigDocument = { | |
| $schema: OPENCODE_CONFIG_SCHEMA, | |
| provider: { | |
| [OMNIROUTE_PROVIDER_KEY]: createOmniRouteProvider(options), | |
| }, | |
| }; | |
| if (options.model !== undefined) { | |
| const id = options.model.trim(); | |
| if (id) doc.model = `${OMNIROUTE_PROVIDER_KEY}/${id}`; | |
| } | |
| if (options.smallModel !== undefined) { | |
| const id = options.smallModel.trim(); | |
| if (id) doc.small_model = `${OMNIROUTE_PROVIDER_KEY}/${id}`; | |
| } | |
| return doc; | |
| } | |
| /** | |
| * Merge the OmniRoute provider entry (and optional `model` / `small_model` | |
| * keys) into an already-existing OpenCode config object. | |
| * | |
| * Performs a non-destructive merge: all top-level keys in `existing` are | |
| * preserved. The `provider` map is shallow-merged so other providers already | |
| * present are not removed. If `existing.provider.omniroute` already exists it | |
| * is overwritten by the newly built entry. | |
| * | |
| * `model` and `small_model` are only written when supplied in `options`. | |
| * | |
| * @example | |
| * ```ts | |
| * const existing = JSON.parse(readFileSync("opencode.json", "utf8")); | |
| * const updated = mergeIntoExistingConfig(existing, { | |
| * baseURL: "http://localhost:20128", | |
| * apiKey: "sk_omniroute", | |
| * model: "claude-sonnet-4-5-thinking", | |
| * }); | |
| * writeFileSync("opencode.json", JSON.stringify(updated, null, 2)); | |
| * ``` | |
| */ | |
| export function mergeIntoExistingConfig( | |
| existing: Record<string, unknown>, | |
| options: OmniRouteProviderOptions | |
| ): Record<string, unknown> { | |
| const partial = buildOmniRouteOpenCodeConfig(options); | |
| const merged: Record<string, unknown> = { ...existing }; | |
| if (partial.model !== undefined) merged.model = partial.model; | |
| if (partial.small_model !== undefined) merged.small_model = partial.small_model; | |
| const existingProvider = | |
| typeof existing.provider === "object" && existing.provider !== null | |
| ? (existing.provider as Record<string, unknown>) | |
| : {}; | |
| merged.provider = { | |
| ...existingProvider, | |
| [OMNIROUTE_PROVIDER_KEY]: partial.provider[OMNIROUTE_PROVIDER_KEY], | |
| }; | |
| return merged; | |
| } | |
| /** | |
| * The 7 read-only MCP scopes that allow inspection without any write access. | |
| * Suitable for shared / public environments. | |
| */ | |
| export const OMNIROUTE_MCP_DEFAULT_SCOPES = [ | |
| "read:health", | |
| "read:combos", | |
| "read:quota", | |
| "read:usage", | |
| "read:models", | |
| "read:cache", | |
| "read:compression", | |
| ] as const; | |
| export type OmniRouteMCPScope = (typeof OMNIROUTE_MCP_DEFAULT_SCOPES)[number] | string; | |
| export interface OmniRouteMCPOptions { | |
| /** Absolute path to the MCP server entry point (TypeScript or compiled JS). */ | |
| serverPath: string; | |
| /** OmniRoute API key forwarded to the MCP server as `OMNIROUTE_API_KEY`. */ | |
| apiKey: string; | |
| /** | |
| * Management API key used for management-scoped operations. | |
| * When supplied it is forwarded as `OMNIROUTE_MANAGEMENT_API_KEY`. | |
| */ | |
| managementApiKey?: string; | |
| /** | |
| * Comma-separated scope list passed as `OMNIROUTE_MCP_SCOPES`. | |
| * When omitted `OMNIROUTE_MCP_ENFORCE_SCOPES` is not set and all scopes are | |
| * available (development default). Pass an explicit list to restrict access. | |
| */ | |
| scopes?: OmniRouteMCPScope[]; | |
| /** | |
| * Runtime used to execute the MCP server. | |
| * | |
| * - `"tsx"` (default) — runs via `npx tsx` for TypeScript source files. | |
| * - `"node"` — runs via `node` for compiled JS outputs. | |
| */ | |
| runtime?: "tsx" | "node"; | |
| } | |
| export interface OpenCodeMCPServerEntry { | |
| command: string; | |
| args: string[]; | |
| env: Record<string, string>; | |
| } | |
| /** | |
| * Build the `mcp.servers.omniroute` entry for an OpenCode config document. | |
| * | |
| * @example | |
| * ```ts | |
| * const mcpEntry = createOmniRouteMCPEntry({ | |
| * serverPath: "/home/user/.local/share/omniroute/open-sse/mcp-server/server.ts", | |
| * apiKey: "sk_omniroute", | |
| * managementApiKey: "sk_manage_...", | |
| * scopes: ["read:health", "read:combos", "execute:completions"], | |
| * }); | |
| * // Place at config.mcp.servers.omniroute | |
| * ``` | |
| */ | |
| export function createOmniRouteMCPEntry(options: OmniRouteMCPOptions): OpenCodeMCPServerEntry { | |
| const serverPath = requireNonEmpty(options.serverPath, "serverPath"); | |
| const apiKey = requireNonEmpty(options.apiKey, "apiKey"); | |
| const runtime = options.runtime ?? "tsx"; | |
| const command = runtime === "tsx" ? "npx" : "node"; | |
| const args = runtime === "tsx" ? ["tsx", serverPath] : [serverPath]; | |
| const env: Record<string, string> = { | |
| OMNIROUTE_API_KEY: apiKey, | |
| }; | |
| if (options.managementApiKey !== undefined) { | |
| const mgmtKey = options.managementApiKey.trim(); | |
| if (mgmtKey) env.OMNIROUTE_MANAGEMENT_API_KEY = mgmtKey; | |
| } | |
| if (options.scopes !== undefined && options.scopes.length > 0) { | |
| env.OMNIROUTE_MCP_ENFORCE_SCOPES = "true"; | |
| env.OMNIROUTE_MCP_SCOPES = options.scopes.join(","); | |
| } | |
| return { command, args, env }; | |
| } | |
| async function fetchJSON<T>(url: string, apiKey: string, timeoutMs: number): Promise<T> { | |
| const controller = new AbortController(); | |
| const timer = setTimeout(() => controller.abort(), timeoutMs); | |
| try { | |
| const response = await fetch(url, { | |
| headers: { Authorization: `Bearer ${apiKey}` }, | |
| signal: controller.signal, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`received HTTP ${response.status}`); | |
| } | |
| return (await response.json()) as T; | |
| } catch (err) { | |
| const message = err instanceof Error ? err.message : String(err); | |
| throw new Error(`@omniroute/opencode-provider: request to ${url} failed: ${message}`); | |
| } finally { | |
| clearTimeout(timer); | |
| } | |
| } | |
| /** | |
| * Lightweight model descriptor returned by `fetchLiveModels`. | |
| * The shape mirrors the subset of fields that OmniRoute's `/v1/models` | |
| * endpoint reliably provides across versions, normalised from both | |
| * camelCase and snake_case variants used by different OmniRoute releases. | |
| * | |
| * Attribution: field-variant normalisation logic adapted from | |
| * https://github.com/Alph4d0g/opencode-omniroute-auth (MIT). | |
| */ | |
| export interface OmniRouteLiveModel { | |
| id: string; | |
| name: string; | |
| /** Context window length in tokens (e.g. 200000 for Claude, 1000000 for Gemini). */ | |
| contextLength?: number; | |
| } | |
| /** | |
| * Fetch the live model catalog from a running OmniRoute instance. | |
| * | |
| * Returns an array of `{ id, name }` objects from `GET /v1/models`. Handles | |
| * both the camelCase (`modelId`, `displayName`) and snake_case (`model_id`, | |
| * `display_name`) field variants across OmniRoute versions. | |
| * | |
| * Useful for dynamically populating the `models` option of | |
| * `createOmniRouteProvider` / `buildOmniRouteOpenCodeConfig` instead of | |
| * relying on `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. | |
| * | |
| * @param baseURL - OmniRoute base URL (with or without `/v1`). | |
| * @param apiKey - OmniRoute API key. | |
| * @param timeoutMs - Request timeout in milliseconds (default 5000). | |
| * | |
| * @example | |
| * ```ts | |
| * const models = await fetchLiveModels("http://localhost:20128", "sk_omniroute"); | |
| * const config = buildOmniRouteOpenCodeConfig({ | |
| * baseURL: "http://localhost:20128", | |
| * apiKey: "sk_omniroute", | |
| * models, // OmniRouteLiveModel[] — contextLength auto-extracted | |
| * modelLabels: Object.fromEntries(models.map((m) => [m.id, m.name])), | |
| * }); | |
| * ``` | |
| */ | |
| export async function fetchLiveModels( | |
| baseURL: string, | |
| apiKey: string, | |
| timeoutMs = 5_000 | |
| ): Promise<OmniRouteLiveModel[]> { | |
| const key = requireNonEmpty(apiKey, "apiKey"); | |
| const url = `${normalizeBaseURL(baseURL)}/models`; | |
| const body = await fetchJSON<unknown>(url, key, timeoutMs); | |
| const rawList: unknown[] = Array.isArray(body) | |
| ? body | |
| : body && typeof body === "object" && Array.isArray((body as { data?: unknown[] }).data) | |
| ? ((body as { data: unknown[] }).data as unknown[]) | |
| : []; | |
| const models: OmniRouteLiveModel[] = []; | |
| for (const raw of rawList) { | |
| if (typeof raw !== "object" || raw === null) continue; | |
| const r = raw as Record<string, unknown>; | |
| const id = | |
| typeof r.id === "string" | |
| ? r.id.trim() | |
| : typeof r.modelId === "string" | |
| ? r.modelId.trim() | |
| : typeof r.model_id === "string" | |
| ? r.model_id.trim() | |
| : ""; | |
| if (!id) continue; | |
| const name = | |
| typeof r.name === "string" | |
| ? r.name.trim() | |
| : typeof r.displayName === "string" | |
| ? r.displayName.trim() | |
| : typeof r.display_name === "string" | |
| ? r.display_name.trim() | |
| : id; | |
| // Extract context_length from OmniRoute's /v1/models response. | |
| // OmniRoute returns context_length in snake_case for both synced | |
| // models (with inputTokenLimit) and custom models; the catalog's | |
| // getDefaultContextFallback also injects it from registry defaults. | |
| const contextLength = | |
| typeof r.context_length === "number" && r.context_length > 0 | |
| ? r.context_length | |
| : typeof r.max_context_window_tokens === "number" && r.max_context_window_tokens > 0 | |
| ? r.max_context_window_tokens | |
| : undefined; | |
| models.push({ id, name: name || id, ...(contextLength ? { contextLength } : {}) }); | |
| } | |
| return models; | |
| } | |
| /** | |
| * Valid per-combo compression override values. | |
| * An empty string clears any existing override (inherits global setting). | |
| */ | |
| export type OmniRouteCompressionOverride = | |
| | "" | |
| | "off" | |
| | "lite" | |
| | "standard" | |
| | "aggressive" | |
| | "ultra" | |
| | "rtk" | |
| | "stacked"; | |
| const VALID_COMPRESSION_OVERRIDES = new Set<string>([ | |
| "", | |
| "off", | |
| "lite", | |
| "standard", | |
| "aggressive", | |
| "ultra", | |
| "rtk", | |
| "stacked", | |
| ]); | |
| /** Slim combo descriptor returned by `listCombos`. */ | |
| export interface OmniRouteCombo { | |
| id: string; | |
| name: string; | |
| strategy: string; | |
| active: boolean; | |
| compressionOverride: OmniRouteCompressionOverride; | |
| } | |
| /** | |
| * Fetch the active routing combo list from a running OmniRoute instance. | |
| * | |
| * Returns an array of combo descriptors from `GET /api/combos`. The | |
| * `compressionOverride` field reflects the per-combo compression strategy | |
| * (one of the 8 recognised values; empty string means "inherit global"). | |
| * | |
| * Requires a management-scoped API key (Bearer `manage` scope) when the | |
| * instance has `REQUIRE_API_KEY` enabled. | |
| * | |
| * @param baseURL - OmniRoute base URL (with or without `/v1`). | |
| * @param managementApiKey - API key with `manage` scope. | |
| * @param timeoutMs - Request timeout in milliseconds (default 5000). | |
| */ | |
| export async function listCombos( | |
| baseURL: string, | |
| managementApiKey: string, | |
| timeoutMs = 5_000 | |
| ): Promise<OmniRouteCombo[]> { | |
| const key = requireNonEmpty(managementApiKey, "managementApiKey"); | |
| const base = normalizeBaseURL(baseURL).replace(/\/v1$/, ""); | |
| const url = `${base}/api/combos`; | |
| const body = await fetchJSON<unknown>(url, key, timeoutMs); | |
| const rawList: unknown[] = Array.isArray(body) | |
| ? body | |
| : body && typeof body === "object" && Array.isArray((body as { combos?: unknown[] }).combos) | |
| ? ((body as { combos: unknown[] }).combos as unknown[]) | |
| : []; | |
| const combos: OmniRouteCombo[] = []; | |
| for (const raw of rawList) { | |
| if (typeof raw !== "object" || raw === null) continue; | |
| const r = raw as Record<string, unknown>; | |
| const id = typeof r.id === "string" ? r.id.trim() : ""; | |
| if (!id) continue; | |
| const name = typeof r.name === "string" ? r.name.trim() : id; | |
| const strategy = typeof r.strategy === "string" ? r.strategy : ""; | |
| const active = typeof r.active === "boolean" ? r.active : false; | |
| const rawOverride = typeof r.compressionOverride === "string" ? r.compressionOverride : ""; | |
| const compressionOverride = VALID_COMPRESSION_OVERRIDES.has(rawOverride) | |
| ? (rawOverride as OmniRouteCompressionOverride) | |
| : ""; | |
| combos.push({ id, name, strategy, active, compressionOverride }); | |
| } | |
| return combos; | |
| } | |
| /** | |
| * Options for `createOmniRouteComboConfig`. | |
| * Mirrors the subset of combo fields exposed by the OmniRoute `/api/combos` | |
| * PATCH / POST payload that are safe to set programmatically. | |
| */ | |
| export interface OmniRouteComboConfigOptions { | |
| /** Human-readable combo name. */ | |
| name: string; | |
| /** Routing strategy (e.g. `"priority"`, `"weighted"`, `"round-robin"`). */ | |
| strategy: string; | |
| /** | |
| * Per-combo compression override. | |
| * Empty string removes any override (inherits global setting). | |
| */ | |
| compressionOverride?: OmniRouteCompressionOverride; | |
| /** Whether this combo is active for routing. Default: `true`. */ | |
| active?: boolean; | |
| /** | |
| * Ordered list of provider IDs in this combo. | |
| * Required for create operations; optional for updates. | |
| */ | |
| providers?: string[]; | |
| } | |
| /** | |
| * Build a typed combo payload suitable for OmniRoute's management API. | |
| * | |
| * The returned object is JSON-serialisable and safe to pass as the body of a | |
| * `POST /api/combos` (create) or `PATCH /api/combos/:id` (update) request. | |
| * | |
| * @example | |
| * ```ts | |
| * const payload = createOmniRouteComboConfig({ | |
| * name: "claude-primary", | |
| * strategy: "priority", | |
| * compressionOverride: "standard", | |
| * providers: ["anthropic-claude-opus", "anthropic-claude-sonnet"], | |
| * }); | |
| * await fetch(`${baseURL}/api/combos`, { | |
| * method: "POST", | |
| * headers: { Authorization: `Bearer ${mgmtKey}`, "Content-Type": "application/json" }, | |
| * body: JSON.stringify(payload), | |
| * }); | |
| * ``` | |
| */ | |
| export function createOmniRouteComboConfig( | |
| options: OmniRouteComboConfigOptions | |
| ): Record<string, unknown> { | |
| const name = requireNonEmpty(options.name, "name"); | |
| const strategy = requireNonEmpty(options.strategy, "strategy"); | |
| const payload: Record<string, unknown> = { | |
| name, | |
| strategy, | |
| active: options.active ?? true, | |
| }; | |
| if (options.compressionOverride !== undefined) { | |
| payload.compressionOverride = options.compressionOverride; | |
| } | |
| if (options.providers !== undefined) { | |
| const providers = options.providers.filter((p) => typeof p === "string" && p.trim()); | |
| if (providers.length > 0) { | |
| payload.providers = providers; | |
| } | |
| } | |
| return payload; | |
| } | |
| /** | |
| * Override fields supported per agent / mode entry. Mirrors the subset of | |
| * OpenCode's `AgentConfig` schema that is safe to set declaratively from a | |
| * config generator. Only fields present in | |
| * https://opencode.ai/config.json#AgentConfig are exposed. | |
| */ | |
| export interface OmniRouteRoleOverrides { | |
| /** Forward to OpenCode's `temperature` field. */ | |
| temperature?: number; | |
| /** Forward to OpenCode's `top_p` field. */ | |
| top_p?: number; | |
| } | |
| /** Per-role binding used by `createOmniRouteAgentBlock`. */ | |
| export interface OmniRouteAgentRole extends OmniRouteRoleOverrides { | |
| /** OmniRoute model id, e.g. `"claude-sonnet-4-5-thinking"`. */ | |
| modelId: string; | |
| /** Optional tools allow-list; per OpenCode schema, map of tool name → enabled. */ | |
| tools?: Record<string, boolean>; | |
| /** Optional system prompt for this agent role. */ | |
| prompt?: string; | |
| } | |
| /** Options for `createOmniRouteAgentBlock`. */ | |
| export interface OmniRouteAgentBlockOptions { | |
| /** Per-role bindings. Keys become entries under OpenCode's `agent` block. */ | |
| roles: Record<string, OmniRouteAgentRole>; | |
| } | |
| /** Single entry inside the emitted OpenCode `agent` block. */ | |
| export interface OpenCodeAgentEntry extends OmniRouteRoleOverrides { | |
| /** Always emitted as `"omniroute/<modelId>"`. */ | |
| model: string; | |
| /** Per OpenCode schema, `Record<string, boolean>`. */ | |
| tools?: Record<string, boolean>; | |
| /** Optional system prompt. */ | |
| prompt?: string; | |
| } | |
| function buildAgentEntry(role: OmniRouteAgentRole): OpenCodeAgentEntry | undefined { | |
| if (!role || typeof role.modelId !== "string") return undefined; | |
| const modelId = role.modelId.trim(); | |
| if (!modelId) return undefined; | |
| const entry: OpenCodeAgentEntry = { model: `${OMNIROUTE_PROVIDER_KEY}/${modelId}` }; | |
| if (typeof role.temperature === "number") entry.temperature = role.temperature; | |
| if (typeof role.top_p === "number") entry.top_p = role.top_p; | |
| if (role.tools && typeof role.tools === "object" && !Array.isArray(role.tools)) { | |
| const tools: Record<string, boolean> = {}; | |
| for (const [name, enabled] of Object.entries(role.tools)) { | |
| if (typeof name !== "string" || !name.trim()) continue; | |
| if (typeof enabled !== "boolean") continue; | |
| tools[name] = enabled; | |
| } | |
| if (Object.keys(tools).length > 0) entry.tools = tools; | |
| } | |
| if (typeof role.prompt === "string" && role.prompt.trim()) { | |
| entry.prompt = role.prompt; | |
| } | |
| return entry; | |
| } | |
| /** | |
| * Build the OpenCode `agent` block, pre-wired so each agent role routes to a | |
| * specific OmniRoute model. Useful for `.opencode/agent/*.md` defaults and | |
| * scaffolded `opencode.json` files. | |
| * | |
| * Emitted fields are limited to those declared in OpenCode's `AgentConfig` | |
| * schema (`model`, `temperature`, `top_p`, `tools`, `prompt`). The `tools` | |
| * field is a `Record<string, boolean>` per the schema, not a string array. | |
| * | |
| * Roles with empty / missing `modelId` are skipped. | |
| * | |
| * @example | |
| * ```ts | |
| * const agentBlock = createOmniRouteAgentBlock({ | |
| * roles: { | |
| * build: { modelId: "claude-sonnet-4-5-thinking", temperature: 0.2 }, | |
| * plan: { modelId: "claude-opus-4-5-thinking", top_p: 0.95 }, | |
| * review: { modelId: "gemini-3-flash", tools: { edit: false, bash: false } }, | |
| * }, | |
| * }); | |
| * // -> { build: { model: "omniroute/claude-sonnet-4-5-thinking", temperature: 0.2 }, ... } | |
| * ``` | |
| */ | |
| export function createOmniRouteAgentBlock( | |
| options: OmniRouteAgentBlockOptions | |
| ): Record<string, OpenCodeAgentEntry> { | |
| const out: Record<string, OpenCodeAgentEntry> = {}; | |
| const roles = options.roles ?? {}; | |
| for (const [roleName, role] of Object.entries(roles)) { | |
| const entry = buildAgentEntry(role); | |
| if (entry) out[roleName] = entry; | |
| } | |
| return out; | |
| } | |
| /** | |
| * Per-mode binding used by `createOmniRouteModesBlock`. | |
| * | |
| * @deprecated OpenCode's top-level `mode` block is deprecated in favour of | |
| * `agent`. Prefer `OmniRouteAgentRole` + `createOmniRouteAgentBlock`. This | |
| * type and the corresponding helper are kept for back-compat with configs | |
| * still using `mode:`. | |
| */ | |
| export interface OmniRouteMode extends OmniRouteAgentRole {} | |
| /** | |
| * Options for `createOmniRouteModesBlock`. | |
| * | |
| * @deprecated See `OmniRouteMode`. | |
| */ | |
| export interface OmniRouteModesBlockOptions { | |
| /** Per-mode bindings. Keys become entries under OpenCode's deprecated top-level `mode` block. */ | |
| modes: Record<string, OmniRouteMode>; | |
| } | |
| /** | |
| * Single entry inside the emitted OpenCode `mode` block. | |
| * | |
| * @deprecated See `OmniRouteMode`. | |
| */ | |
| export interface OpenCodeModeEntry extends OpenCodeAgentEntry {} | |
| /** | |
| * Build the OpenCode top-level `mode` block, pre-wired so each mode routes to | |
| * a specific OmniRoute model. Emits the same shape as the `agent` block since | |
| * OpenCode's schema treats them identically (both reference `AgentConfig`). | |
| * | |
| * Modes with empty / missing `modelId` are skipped. | |
| * | |
| * @deprecated OpenCode's top-level `mode` block is deprecated in favour of | |
| * `agent`. Prefer `createOmniRouteAgentBlock`. This helper is kept for | |
| * back-compat with configs still using `mode:`. | |
| * | |
| * @example | |
| * ```ts | |
| * const modesBlock = createOmniRouteModesBlock({ | |
| * modes: { | |
| * build: { modelId: "claude-sonnet-4-5-thinking", tools: { edit: true, bash: true } }, | |
| * plan: { modelId: "claude-opus-4-5-thinking", prompt: "Plan first, code later." }, | |
| * review: { modelId: "gemini-3-flash" }, | |
| * }, | |
| * }); | |
| * ``` | |
| */ | |
| export function createOmniRouteModesBlock( | |
| options: OmniRouteModesBlockOptions | |
| ): Record<string, OpenCodeModeEntry> { | |
| const out: Record<string, OpenCodeModeEntry> = {}; | |
| const modes = options.modes ?? {}; | |
| for (const [modeName, mode] of Object.entries(modes)) { | |
| const entry = buildAgentEntry(mode); | |
| if (entry) out[modeName] = entry; | |
| } | |
| return out; | |
| } | |
| export default createOmniRouteProvider; | |