diff --git a/docs/frameworks/MCP-SERVER.md b/docs/frameworks/MCP-SERVER.md index 3e5790fb91..4c1bb66a97 100644 --- a/docs/frameworks/MCP-SERVER.md +++ b/docs/frameworks/MCP-SERVER.md @@ -10,7 +10,7 @@ lastUpdated: 2026-05-30 > > Source of truth: `open-sse/mcp-server/schemas/tools.ts` (30 tools) + `open-sse/mcp-server/tools/memoryTools.ts` (3 tools) + `open-sse/mcp-server/tools/skillTools.ts` (4 tools) + `open-sse/mcp-server/tools/notionTools.ts` (6 tools). Tool registration and scope wiring lives in `open-sse/mcp-server/server.ts`. -![MCP tool inventory (43 tools by category)](../diagrams/exported/mcp-tools-43.svg) +![MCP tool inventory (43 tools by category)](../diagrams/exported/mcp-tools-37.svg) > Source: [diagrams/mcp-tools-43.mmd](../diagrams/mcp-tools-43.mmd) (regenerate via `npm run docs:render-diagrams`). diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 90ce824e21..bef0b02233 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -207,6 +207,16 @@ const GPT_5_5_CODEX_CAPABILITIES = { contextLength: GPT_5_5_CONTEXT_LENGTH, } as const; +const GPT_5_4_CODEX_CAPABILITIES = { + targetFormat: "openai-responses", + toolCalling: true, + supportsReasoning: true, + supportsVision: true, + supportsXHighEffort: true, + contextLength: 200000, + maxOutputTokens: 128000, +} as const; + const CHAT_OPENAI_COMPAT_MODELS: Record = { deepinfra: buildModels([ "anthropic/claude-4-opus", @@ -866,9 +876,27 @@ const _REGISTRY_EAGER: Record = { { id: "gpt-5.4", name: "GPT 5.4", - targetFormat: "openai-responses", - supportsReasoning: true, - supportsXHighEffort: true, + ...GPT_5_4_CODEX_CAPABILITIES, + }, + { + id: "gpt-5.4-xhigh", + name: "GPT 5.4 (xHigh)", + ...GPT_5_4_CODEX_CAPABILITIES, + }, + { + id: "gpt-5.4-high", + name: "GPT 5.4 (High)", + ...GPT_5_4_CODEX_CAPABILITIES, + }, + { + id: "gpt-5.4-medium", + name: "GPT 5.4 (Medium)", + ...GPT_5_4_CODEX_CAPABILITIES, + }, + { + id: "gpt-5.4-low", + name: "GPT 5.4 (Low)", + ...GPT_5_4_CODEX_CAPABILITIES, }, { id: "gpt-5.4-mini", name: "GPT 5.4 Mini", targetFormat: "openai-responses" }, { id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" }, @@ -1007,7 +1035,7 @@ const _REGISTRY_EAGER: Record = { { id: "gpt-5-mini", name: "GPT-5 Mini", targetFormat: "openai-responses" }, { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", targetFormat: "openai-responses" }, { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", targetFormat: "openai-responses" }, - { id: "gpt-5.4", name: "GPT-5.4", targetFormat: "openai-responses" }, + { id: "gpt-5.4", name: "GPT-5.4", targetFormat: "openai-responses", supportsXHighEffort: true }, { id: "gpt-5.5", name: "GPT-5.5", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "claude-haiku-4.5", diff --git a/open-sse/mcp-server/schemas/tools.ts b/open-sse/mcp-server/schemas/tools.ts index f4f0e71252..1d7065e70e 100644 --- a/open-sse/mcp-server/schemas/tools.ts +++ b/open-sse/mcp-server/schemas/tools.ts @@ -370,6 +370,7 @@ export const listModelsCatalogOutput = z.object({ provider: z.string(), capabilities: z.array(z.string()), status: z.enum(["available", "degraded", "unavailable"]), + thinkingEffort: z.string().optional(), pricing: z .object({ inputPerMillion: z.number().nullable(), diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index ba2ee2b976..0418d0c544 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -91,7 +91,10 @@ import { type McpAccessibilityConfig, } from "../services/compression/engines/mcpAccessibility/constants.ts"; import { getDbInstance } from "../../src/lib/db/core.ts"; +import { getProviderConnections } from "../../src/lib/db/providers.ts"; +import { getCodexRequestDefaults } from "../../src/lib/providers/requestDefaults.ts"; import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts"; +import { AI_PROVIDERS, NOAUTH_PROVIDERS } from "../../src/shared/constants/providers.ts"; import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts"; // ============ Configuration ============ @@ -147,6 +150,28 @@ type TextToolResult = { isError?: boolean; }; +type McpCatalogStatus = "available" | "degraded" | "unavailable"; + +type McpCatalogResponse = { + models: Array<{ + id: string; + provider: string; + capabilities: string[]; + status: McpCatalogStatus; + thinkingEffort?: string; + pricing?: unknown; + }>; + source: string; + warning?: string; +}; + +type ProviderConnectionLike = { + id?: string; + provider?: string; + isActive?: boolean; + providerSpecificData?: unknown; +}; + function toRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; } @@ -214,6 +239,188 @@ async function omniRouteFetch(path: string, options: RequestInit = {}): Promise< return response.json(); } +function buildProviderAliasMap(): Record { + const aliasMap: Record = {}; + + for (const provider of Object.values(AI_PROVIDERS)) { + if (!provider?.id) continue; + aliasMap[provider.id] = provider.id; + if (typeof provider.alias === "string" && provider.alias.length > 0) { + aliasMap[provider.alias] = provider.id; + } + } + + for (const provider of Object.values(NOAUTH_PROVIDERS)) { + if (!provider?.id) continue; + aliasMap[provider.id] = provider.id; + if ("alias" in provider && typeof provider.alias === "string" && provider.alias.length > 0) { + aliasMap[provider.alias] = provider.id; + } + } + + return aliasMap; +} + +function normalizeCapability(value: string): string { + switch (value) { + case "embeddings": + return "embedding"; + case "images": + return "image"; + case "videos": + return "video"; + case "moderations": + return "moderation"; + case "chat-completions": + return "chat"; + default: + return value; + } +} + +function getCatalogModelCapabilities(model: JsonRecord): string[] { + if (Array.isArray(model.capabilities) && model.capabilities.length > 0) { + return toStringArray(model.capabilities, ["chat"]).map(normalizeCapability); + } + + if (Array.isArray(model.supportedEndpoints) && model.supportedEndpoints.length > 0) { + return toStringArray(model.supportedEndpoints, ["chat"]).map(normalizeCapability); + } + + const type = toString(model.type); + if (type) return [normalizeCapability(type)]; + + return ["chat"]; +} + +function normalizeCatalogStatus(model: JsonRecord, source: string, warning?: string): McpCatalogStatus { + const explicitStatus = toString(model.status); + if (explicitStatus === "available" || explicitStatus === "degraded" || explicitStatus === "unavailable") { + return explicitStatus; + } + + if (warning || source === "local_catalog") return "degraded"; + return "available"; +} + +function getConnectionThinkingEffort(connection: ProviderConnectionLike): string | undefined { + const provider = typeof connection.provider === "string" ? connection.provider : null; + const providerSpecificData = toRecord(connection.providerSpecificData); + + if (provider === "codex") { + return getCodexRequestDefaults(providerSpecificData).reasoningEffort || "medium"; + } + + const rawThinkingEffort = toString(providerSpecificData.thinkingEffort); + return rawThinkingEffort || undefined; +} + +function normalizeProviderModelRecord( + rawModel: unknown, + fallbackProvider: string, + source: string, + warning?: string, + thinkingEffort?: string +) { + const model = toRecord(rawModel); + const id = toString(model.id, ""); + + return { + id, + provider: toString(model.owned_by, toString(model.provider, fallbackProvider)), + capabilities: getCatalogModelCapabilities(model), + status: normalizeCatalogStatus(model, source, warning), + ...(thinkingEffort ? { thinkingEffort } : {}), + pricing: model.pricing, + }; +} + +export async function getMcpModelsCatalog( + args: { provider?: string; capability?: string }, + deps: { + fetchJson?: (path: string) => Promise; + listProviderConnections?: () => Promise; + } = {} +): Promise { + const fetchJson = deps.fetchJson ?? ((path: string) => omniRouteFetch(path)); + const listProviderConnections = deps.listProviderConnections ?? getProviderConnections; + const aliasMap = buildProviderAliasMap(); + const normalizeProviderId = (value: string) => aliasMap[value] || value; + const requestedProvider = args.provider ? normalizeProviderId(args.provider) : null; + const requestedCapability = args.capability ? normalizeCapability(args.capability) : null; + + let connections = await listProviderConnections(); + connections = Array.isArray(connections) ? connections : []; + + const activeConnections = connections.filter((connection) => { + const provider = typeof connection?.provider === "string" ? normalizeProviderId(connection.provider) : null; + if (!provider || !connection?.id || connection.isActive === false) return false; + if (requestedProvider && provider !== requestedProvider) return false; + return true; + }); + + const requestSpecs = activeConnections.map((connection) => ({ + provider: normalizeProviderId(String(connection.provider)), + path: `/api/providers/${encodeURIComponent(String(connection.id))}/models?excludeHidden=true`, + thinkingEffort: getConnectionThinkingEffort(connection), + })); + + if (requestedProvider && requestSpecs.length === 0) { + const isNoAuthProvider = Object.values(NOAUTH_PROVIDERS).some((provider) => provider.id === requestedProvider); + if (isNoAuthProvider) { + requestSpecs.push({ + provider: requestedProvider, + path: `/api/v1/providers/${encodeURIComponent(requestedProvider)}/models`, + thinkingEffort: undefined, + }); + } else { + return { + models: [], + source: "provider_connections", + warning: `No active connections found for provider '${requestedProvider}'.`, + }; + } + } + + const collectedModels = new Map(); + const warnings = new Set(); + const sources = new Set(); + + for (const spec of requestSpecs) { + const raw = toRecord(await fetchJson(spec.path)); + const source = toString(raw.source, spec.path.startsWith("/api/providers/") ? "api" : "v1_catalog"); + const warning = raw.warning ? String(raw.warning) : undefined; + if (warning) warnings.add(warning); + sources.add(source); + + const rawModels = Array.isArray(raw.models) + ? raw.models + : Array.isArray(raw.data) + ? raw.data + : []; + + for (const rawModel of rawModels) { + const normalized = normalizeProviderModelRecord(rawModel, spec.provider, source, warning); + if (spec.thinkingEffort && !normalized.thinkingEffort) { + normalized.thinkingEffort = spec.thinkingEffort; + } + if (!normalized.id) continue; + if (requestedCapability && !normalized.capabilities.includes(requestedCapability)) continue; + + const key = `${normalized.provider}:${normalized.id}`; + if (!collectedModels.has(key)) { + collectedModels.set(key, normalized); + } + } + } + + return { + models: [...collectedModels.values()], + source: sources.size === 1 ? [...sources][0] : "aggregated_provider_models", + ...(warnings.size > 0 ? { warning: [...warnings].join(" | ") } : {}), + }; +} + function withScopeEnforcement( toolName: string, handler: (args: unknown, extra?: McpToolExtraLike) => Promise, @@ -522,50 +729,7 @@ async function handleCostReport(args: { period?: string }) { async function handleListModelsCatalog(args: { provider?: string; capability?: string }) { const start = Date.now(); try { - let path = "/v1/models"; - let isProviderSpecific = false; - let source = "local_catalog"; - let warning: string | undefined; - - if (args.provider && !args.capability) { - // Use direct provider fetch to get real-time API status - path = `/api/providers/${encodeURIComponent(args.provider)}/models?excludeHidden=true`; - isProviderSpecific = true; - } else { - const params = new URLSearchParams(); - if (args.provider) params.set("provider", args.provider); - if (args.capability) params.set("capability", args.capability); - if (params.toString()) path += `?${params.toString()}`; - } - - const raw = toRecord(await omniRouteFetch(path)); - - // If we used the direct provider endpoint - let rawModels: unknown[] = []; - if (isProviderSpecific) { - rawModels = Array.isArray(raw.models) ? raw.models : []; - source = typeof raw.source === "string" ? raw.source : "api"; - if (raw.warning) warning = String(raw.warning); - } else { - rawModels = Array.isArray(raw.data) ? raw.data : []; - source = "local_catalog"; - // OmniRoute's global /v1/models is always a cached/local catalog - } - - const result = { - models: rawModels.map((rawModel) => { - const model = toRecord(rawModel); - return { - id: toString(model.id, ""), - provider: toString(model.owned_by, toString(model.provider, args.provider || "unknown")), - capabilities: toStringArray(model.capabilities, ["chat"]), - status: toString(model.status, "available"), - pricing: model.pricing, - }; - }), - source, - ...(warning ? { warning } : {}), - }; + const result = await getMcpModelsCatalog(args); await logToolCall( "omniroute_list_models_catalog", diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index a0fb133473..a7297e3ee1 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -33,6 +33,7 @@ import { getModelSpec } from "@/shared/constants/modelSpecs"; import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth"; import { parseModel } from "@omniroute/open-sse/services/model"; import { getTokenLimit } from "@omniroute/open-sse/services/contextManager"; +import { extractApiKey } from "@/sse/services/auth"; import type { ComboModelStep } from "@/lib/combos/steps"; interface CustomModelEntry { @@ -189,13 +190,7 @@ function getOpenRouterDisplayName(model: { : name; } -function extractBearer(headers: Headers): string | null { - const authHeader = headers.get("authorization") || headers.get("Authorization"); - if (!authHeader?.trim().toLowerCase().startsWith("bearer ")) return null; - return authHeader.trim().slice(7).trim() || null; -} - -async function validateCatalogBearer(apiKey: string): Promise { +async function validateCatalogApiKey(apiKey: string): Promise { const { validateApiKey } = await import("@/lib/db/apiKeys"); return validateApiKey(apiKey); } @@ -207,9 +202,9 @@ async function getModelCatalogAuthRejection( ): Promise { if (settings.requireAuthForModels !== true || !(await isAuthRequired(request))) return null; - const bearer = extractBearer(request.headers); - if (bearer) { - if (await validateCatalogBearer(bearer)) return null; + const apiKey = extractApiKey(request); + if (apiKey) { + if (await validateCatalogApiKey(apiKey)) return null; return Response.json( { error: { @@ -1250,7 +1245,7 @@ export async function getUnifiedModelsResponse( } // Filter by API key permissions if requested - const apiKey = extractBearer(request.headers); + const apiKey = extractApiKey(request); let finalModels = models; if (apiKey) { const { isModelAllowedForKey, getApiKeyMetadata } = await import("@/lib/db/apiKeys"); diff --git a/src/lib/modelMetadataRegistry.ts b/src/lib/modelMetadataRegistry.ts index d184aee35d..ea69ecd478 100644 --- a/src/lib/modelMetadataRegistry.ts +++ b/src/lib/modelMetadataRegistry.ts @@ -272,6 +272,7 @@ export function enrichCatalogModelEntry( if (!metadata) return entry; const nextEntry: JsonRecord = { ...entry }; + const existingName = asNonEmptyString(entry.name); const capabilityFields = { ...(typeof metadata.capabilities.vision === "boolean" ? { vision: metadata.capabilities.vision } @@ -331,6 +332,9 @@ export function enrichCatalogModelEntry( if (typeof metadata.metadata.openWeights === "boolean") { nextEntry.open_weights = metadata.metadata.openWeights; } + if (!existingName && metadata.displayName) { + nextEntry.name = metadata.displayName; + } return nextEntry as T; } diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 273bc7190e..b921808d95 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -42,6 +42,15 @@ export const MODEL_SPECS: Record = { supportsVision: true, }, + "gpt-5.4": { + maxOutputTokens: 131072, + contextWindow: 409600, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + aliases: ["openai/gpt-5.4"], + }, + // ── GPT-4o family ────────────────────────────────────────────── "gpt-4o-mini": { maxOutputTokens: 16384, diff --git a/tests/unit/mcp-model-catalog.test.ts b/tests/unit/mcp-model-catalog.test.ts new file mode 100644 index 0000000000..0d4c677201 --- /dev/null +++ b/tests/unit/mcp-model-catalog.test.ts @@ -0,0 +1,147 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { getMcpModelsCatalog } from "../../open-sse/mcp-server/server.ts"; + +test("getMcpModelsCatalog aggregates only active connection model endpoints", async () => { + const calls: string[] = []; + + const result = await getMcpModelsCatalog( + {}, + { + listProviderConnections: async () => [ + { id: "conn-github", provider: "github", isActive: true }, + { id: "conn-codex", provider: "codex", isActive: false }, + ], + fetchJson: async (path: string) => { + calls.push(path); + if (path === "/api/providers/conn-github/models?excludeHidden=true") { + return { + source: "api", + models: [ + { id: "gpt-4.1", owned_by: "github", supportedEndpoints: ["chat"] }, + { + id: "text-embedding-3-small", + owned_by: "github", + supportedEndpoints: ["embeddings"], + }, + ], + }; + } + + throw new Error(`Unexpected path: ${path}`); + }, + } + ); + + assert.deepEqual(calls, ["/api/providers/conn-github/models?excludeHidden=true"]); + assert.deepEqual(result, { + models: [ + { + id: "gpt-4.1", + provider: "github", + capabilities: ["chat"], + status: "available", + pricing: undefined, + }, + { + id: "text-embedding-3-small", + provider: "github", + capabilities: ["embedding"], + status: "available", + pricing: undefined, + }, + ], + source: "api", + }); +}); + +test("getMcpModelsCatalog exposes codex default thinking effort when no override is stored", async () => { + const result = await getMcpModelsCatalog( + { provider: "codex" }, + { + listProviderConnections: async () => [ + { + id: "conn-codex", + provider: "codex", + isActive: true, + providerSpecificData: {}, + }, + ], + fetchJson: async () => ({ + source: "api", + models: [{ id: "gpt-5.5", owned_by: "codex", supportedEndpoints: ["chat"] }], + }), + } + ); + + assert.equal(result.models.length, 1); + assert.equal(result.models[0]?.thinkingEffort, "medium"); +}); + +test("getMcpModelsCatalog exposes stored thinking effort overrides", async () => { + const result = await getMcpModelsCatalog( + { provider: "chatgpt-web" }, + { + listProviderConnections: async () => [ + { + id: "conn-chatgpt", + provider: "chatgpt-web", + isActive: true, + providerSpecificData: { thinkingEffort: "extended" }, + }, + ], + fetchJson: async () => ({ + source: "api", + models: [{ id: "gpt-5", owned_by: "chatgpt-web", supportedEndpoints: ["chat"] }], + }), + } + ); + + assert.equal(result.models.length, 1); + assert.equal(result.models[0]?.thinkingEffort, "extended"); +}); + +test("getMcpModelsCatalog resolves provider aliases to active connection ids", async () => { + const calls: string[] = []; + + const result = await getMcpModelsCatalog( + { provider: "gh", capability: "chat" }, + { + listProviderConnections: async () => [ + { id: "conn-github", provider: "github", isActive: true }, + { id: "conn-codex", provider: "codex", isActive: true }, + ], + fetchJson: async (path: string) => { + calls.push(path); + return { + source: "api", + models: [{ id: "gpt-4.1", owned_by: "github", supportedEndpoints: ["chat"] }], + }; + }, + } + ); + + assert.deepEqual(calls, ["/api/providers/conn-github/models?excludeHidden=true"]); + assert.equal(result.models.length, 1); + assert.equal(result.models[0]?.provider, "github"); + assert.deepEqual(result.models[0]?.capabilities, ["chat"]); +}); + +test("getMcpModelsCatalog returns empty result when requested provider has no active connection", async () => { + const result = await getMcpModelsCatalog( + { provider: "github" }, + { + listProviderConnections: async () => [{ id: "conn-codex", provider: "codex", isActive: true }], + fetchJson: async () => { + throw new Error("fetchJson should not be called without a matching active provider"); + }, + } + ); + + assert.deepEqual(result, { + models: [], + source: "provider_connections", + warning: "No active connections found for provider 'github'.", + }); +}); \ No newline at end of file diff --git a/tests/unit/models-catalog-route.test.ts b/tests/unit/models-catalog-route.test.ts index 1157994208..fdc5cf09b7 100644 --- a/tests/unit/models-catalog-route.test.ts +++ b/tests/unit/models-catalog-route.test.ts @@ -122,6 +122,48 @@ test("v1 models catalog accepts bearer API keys and filters the list by allowed ); }); +test("v1 models catalog does NOT accept API keys supplied via query string (#3300 security follow-up)", async () => { + // Query-string token fallbacks (`?token=`/`?key=`/`?apiKey=`/`?api_key=`) were + // intentionally removed — a credential in the query string leaks into access + // logs / Referer headers. The VS Code integration uses the path-scoped + // `/vscode//…` form instead (covered by the next test). So a `?token=` + // on the catalog route is no longer a usable credential → auth fails. + await settingsDb.updateSettings({ + requireLogin: true, + password: "hashed-password", + requireAuthForModels: true, + }); + await seedConnection("openai", { name: "openai-query-auth" }); + + const key = await apiKeysDb.createApiKey("catalog-query-auth", "machine-catalog-query"); + + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request(`http://localhost/api/v1/models?token=${encodeURIComponent(key.key)}`) + ); + + assert.equal(response.status, 401); +}); + +test("v1 models catalog accepts API keys embedded in vscode path aliases when auth is required", async () => { + await settingsDb.updateSettings({ + requireLogin: true, + password: "hashed-password", + requireAuthForModels: true, + }); + await seedConnection("openai", { name: "openai-path-auth" }); + + const key = await apiKeysDb.createApiKey("catalog-path-auth", "machine-catalog-path"); + + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request(`http://localhost/api/v1/vscode/${encodeURIComponent(key.key)}/models`) + ); + const body = (await response.json()) as any; + + assert.equal(response.status, 200); + assert.ok(Array.isArray(body.data)); + assert.ok(body.data.length > 0); +}); + test("v1 models catalog hides models excluded by every active connection while keeping models served by at least one account", async () => { const first = await seedConnection("openai", { name: "openai-first", diff --git a/tests/unit/provider-models-route.test.ts b/tests/unit/provider-models-route.test.ts index d86ee3c2e0..15570c2be4 100644 --- a/tests/unit/provider-models-route.test.ts +++ b/tests/unit/provider-models-route.test.ts @@ -469,6 +469,27 @@ test("provider models route returns the updated local catalog for GitHub Copilot ); }); +test("provider models route returns codex gpt-5.4 effort variants in the local catalog", async () => { + const connection = await seedConnection("codex", { + authType: "oauth", + apiKey: null, + accessToken: "codex-access", + }); + + const response = await callRoute(connection.id); + const body = (await response.json()) as any; + const modelIds = new Set((body.models || []).map((model: any) => model.id)); + + assert.equal(response.status, 200); + assert.equal(body.provider, "codex"); + assert.equal(body.source, "local_catalog"); + assert.ok(modelIds.has("gpt-5.4")); + assert.ok(modelIds.has("gpt-5.4-low")); + assert.ok(modelIds.has("gpt-5.4-medium")); + assert.ok(modelIds.has("gpt-5.4-high")); + assert.ok(modelIds.has("gpt-5.4-xhigh")); +}); + test("provider models route returns the expanded local catalog for Kiro", async () => { const connection = await seedConnection("kiro", { authType: "oauth",