feat(x): thread the models.dev reasoning flag to the renderer

Widens the models.dev schema to keep the per-model `reasoning` boolean
and carries it through normalizeModels → ProviderSummary → models:list,
so the composer can gate the reasoning-effort control on actual model
capability. Gateway model lists (bare "vendor/model" ids from the
server) are annotated from the models.dev cache in one batched,
cache-only read; unknown models keep the flag absent, which the UI
treats as "hide the control".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-10 09:59:48 +05:30
parent 125d39ee0b
commit 1ffe29db9d
3 changed files with 50 additions and 2 deletions

View file

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

View file

@ -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<string, z.infer<typeof ModelsDevModel>>)
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<T extends { id: string }>(
models: T[],
): Promise<Array<T & { reasoning?: boolean }>> {
let catalog: z.infer<typeof ModelsDevResponse> | 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<string, boolean>();
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<Set<string>> {

View file

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