diff --git a/apps/x/packages/core/src/models/gateway.ts b/apps/x/packages/core/src/models/gateway.ts index ee14a7fa..bdaa7ab5 100644 --- a/apps/x/packages/core/src/models/gateway.ts +++ b/apps/x/packages/core/src/models/gateway.ts @@ -3,6 +3,7 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { getAccessToken } from '../auth/tokens.js'; import { getCurrentUseCase } from '../analytics/use_case.js'; import { API_URL } from '../config/env.js'; +import { annotateReasoningFlags } from './models-dev.js'; const authedFetch: typeof fetch = async (input, init) => { const token = await getAccessToken(); @@ -30,6 +31,7 @@ type ProviderSummary = { id: string; name?: string; release_date?: string; + reasoning?: boolean; }>; }; @@ -42,7 +44,9 @@ export async function listGatewayModels(): Promise<{ providers: ProviderSummary[ throw new Error(`Gateway /v1/models failed: ${response.status}`); } const body = await response.json() as { data: Array<{ id: string }> }; - const models = body.data.map((m) => ({ id: m.id })); + // The gateway returns bare "vendor/model" ids; the models.dev cache + // supplies the reasoning capability the composer's effort control needs. + const models = await annotateReasoningFlags(body.data.map((m) => ({ id: m.id }))); return { providers: [{ id: 'rowboat', diff --git a/apps/x/packages/core/src/models/models-dev.ts b/apps/x/packages/core/src/models/models-dev.ts index 5be56c9f..14286a08 100644 --- a/apps/x/packages/core/src/models/models-dev.ts +++ b/apps/x/packages/core/src/models/models-dev.ts @@ -85,6 +85,8 @@ type ProviderSummary = { id: string; name?: string; release_date?: string; + // Supports reasoning/extended thinking per models.dev; absent = unknown. + reasoning?: boolean; }>; }; @@ -195,10 +197,11 @@ function normalizeModels(models: Record>) name: model.name, release_date: model.release_date, tool_call: model.tool_call, + reasoning: model.reasoning, status: model.status, })) .filter((model) => isStableModel(model) && supportsToolCall(model)) - .map(({ id, name, release_date }) => ({ id, name, release_date })); + .map(({ id, name, release_date, reasoning }) => ({ id, name, release_date, reasoning })); list.sort((a, b) => { const aDate = a.release_date ? Date.parse(a.release_date) : 0; @@ -268,6 +271,44 @@ export async function isReasoningModel( } } +/** + * Annotate gateway-style "vendor/model" ids with the models.dev reasoning + * flag. Reads the cache once for the whole batch; ids whose vendor or model + * is unknown keep `reasoning` absent (= unknown). Cache-only, like + * isReasoningModel. + */ +export async function annotateReasoningFlags( + models: T[], +): Promise> { + let catalog: z.infer | null = null; + try { + const cached = await readCache(); + if (cached) { + const parsed = ModelsDevResponse.safeParse(cached.data); + if (parsed.success) catalog = parsed.data; + } + } catch { + catalog = null; + } + if (!catalog) return models; + + const flags = new Map(); + for (const vendor of ["openai", "anthropic", "google"] as const) { + const provider = pickProvider(catalog, vendor); + if (!provider) continue; + for (const [key, model] of Object.entries(provider.models)) { + if (typeof model.reasoning === "boolean") { + flags.set(`${vendor}/${model.id ?? key}`, model.reasoning); + } + } + } + + return models.map((model) => { + const reasoning = flags.get(model.id); + return reasoning === undefined ? model : { ...model, reasoning }; + }); +} + export async function getChatModelIds( flavor: "openai" | "anthropic" | "google", ): Promise> { diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 77b573f9..17872e88 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -586,6 +586,9 @@ const ipcSchemas = { id: z.string(), name: z.string().optional(), release_date: z.string().optional(), + // models.dev "supports reasoning/extended thinking" flag; absent = + // unknown. Gates the composer's reasoning-effort control. + reasoning: z.boolean().optional(), })), })), lastUpdated: z.string().optional(),