feat(api): model catalog enrichment + MCP model-catalog tools (#3306)

Integrated into release/v3.8.13 — model catalog enrichment + MCP model-catalog tools, reconciled with #3309 URL-token hardening
This commit is contained in:
Felipe Almeman
2026-06-06 13:49:34 -03:00
committed by GitHub
parent 8ecf056fe2
commit 6e7ea0a680
10 changed files with 471 additions and 60 deletions

View File

@@ -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`).

View File

@@ -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<string, RegistryModel[]> = {
deepinfra: buildModels([
"anthropic/claude-4-opus",
@@ -866,9 +876,27 @@ const _REGISTRY_EAGER: Record<string, RegistryEntry> = {
{
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<string, RegistryEntry> = {
{ 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",

View File

@@ -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(),

View File

@@ -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<string, string> {
const aliasMap: Record<string, string> = {};
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<unknown>;
listProviderConnections?: () => Promise<ProviderConnectionLike[]>;
} = {}
): Promise<McpCatalogResponse> {
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<string, McpCatalogResponse["models"][number]>();
const warnings = new Set<string>();
const sources = new Set<string>();
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<TextToolResult>,
@@ -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",

View File

@@ -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<boolean> {
async function validateCatalogApiKey(apiKey: string): Promise<boolean> {
const { validateApiKey } = await import("@/lib/db/apiKeys");
return validateApiKey(apiKey);
}
@@ -207,9 +202,9 @@ async function getModelCatalogAuthRejection(
): Promise<Response | null> {
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");

View File

@@ -272,6 +272,7 @@ export function enrichCatalogModelEntry<T extends JsonRecord>(
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<T extends JsonRecord>(
if (typeof metadata.metadata.openWeights === "boolean") {
nextEntry.open_weights = metadata.metadata.openWeights;
}
if (!existingName && metadata.displayName) {
nextEntry.name = metadata.displayName;
}
return nextEntry as T;
}

View File

@@ -42,6 +42,15 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
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,

View File

@@ -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'.",
});
});

View File

@@ -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/<token>/…` 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",

View File

@@ -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",