diff --git a/packages/app/src/runtime/server/global-sync/utils.test.ts b/packages/app/src/runtime/server/global-sync/utils.test.ts index 2a39fce84f0c..ec8ee8dc0168 100644 --- a/packages/app/src/runtime/server/global-sync/utils.test.ts +++ b/packages/app/src/runtime/server/global-sync/utils.test.ts @@ -48,7 +48,7 @@ describe("normalizeProviderList", () => { modelID: "gpt-5", providerID: "openai", name: "GPT-5", - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + capabilities: { tools: true, reasoning: true, input: ["text", "image"], output: ["text"] }, variants: [{ id: "high" }], time: { released: 1 }, cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0.2 } }], @@ -69,6 +69,19 @@ describe("normalizeProviderList", () => { enabled: true, limit: { context: 1, output: 1 }, }, + { + id: "gpt-classic", + modelID: "gpt-classic", + providerID: "openai", + name: "GPT Classic", + capabilities: { tools: true, input: ["text"], output: ["text"] }, + variants: [], + time: { released: 0 }, + cost: [], + status: "active", + enabled: true, + limit: { context: 1, output: 1 }, + }, ] as ModelListOutput["data"], ) @@ -78,10 +91,12 @@ describe("normalizeProviderList", () => { expect(result.all.get("openai")?.models["gpt-5"]).toMatchObject({ id: "gpt-5", providerID: "openai", - capabilities: { toolcall: true, attachment: true }, + capabilities: { toolcall: true, attachment: true, reasoning: true }, cost: { input: 1, output: 2 }, variants: { high: {} }, }) + // Models without a catalog reasoning flag map to false instead of being hardcoded + expect(result.all.get("openai")?.models["gpt-classic"]?.capabilities.reasoning).toBe(false) }) }) diff --git a/packages/app/src/runtime/server/global-sync/utils.ts b/packages/app/src/runtime/server/global-sync/utils.ts index af36458c609d..652693aa6c62 100644 --- a/packages/app/src/runtime/server/global-sync/utils.ts +++ b/packages/app/src/runtime/server/global-sync/utils.ts @@ -79,7 +79,7 @@ export function normalizeProviderList( family: model.family, capabilities: { temperature: false, - reasoning: false, + reasoning: model.capabilities.reasoning ?? false, attachment: model.capabilities.input.some((item) => item !== "text"), toolcall: model.capabilities.tools, input: { diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index f5db7ce7e535..0c59a085f52e 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -186,7 +186,13 @@ export type ModelCacheFormat = "anthropic" | "alibaba" export type ModelMediaInput = "assets" | "state" | "root" -export type ModelCapabilities = { tools: boolean; input: Array; output: Array; temperature?: boolean } +export type ModelCapabilities = { + tools: boolean + reasoning?: boolean + input: Array + output: Array + temperature?: boolean +} export type MoneyUSDPerMillionTokens = number diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 0640ba8a01b4..e597d0a19c32 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -29,13 +29,19 @@ export const Capabilities = Model.Capabilities export type Capabilities = Model.Capabilities /** Merges partial config capabilities onto a base model's capabilities, defaulting unset fields. */ -export const mergeCapabilities = (config: Partial, base: Capabilities | undefined) => { - const fallback = base ?? Capabilities.default() +export const mergeCapabilities = ( + config: Partial> & { reasoning?: boolean | { readonly supported: boolean } }, + base: Capabilities | undefined, +) => { + const fallback: Capabilities = base ?? Capabilities.default() + // Config may carry the rich reasoning object; the runtime capability flag is its `supported` boolean. + const reasoning = typeof config.reasoning === "object" ? config.reasoning.supported : config.reasoning return { tools: config.tools ?? fallback.tools, input: [...(config.input ?? fallback.input)], output: [...(config.output ?? fallback.output)], temperature: config.temperature ?? base?.temperature, + reasoning: reasoning ?? fallback.reasoning, } } diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 79d85807a953..bf18885b807e 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -441,6 +441,7 @@ function modelInfo( settings: Object.keys(settings).length === 0 ? undefined : settings, capabilities: { tools: model.capabilities.tools.supported, + reasoning: model.capabilities.reasoning.supported, input: modalities(model.modalities.input), output: modalities(model.modalities.output), temperature: model.capabilities.temperature, diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index b64cbea4758c..c7f9a87c811f 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -225,9 +225,13 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, npm: string | un ] const defaults = Model.Capabilities.default() const capabilities = - info.tool_call !== undefined || info.modalities?.input !== undefined || info.modalities?.output !== undefined + info.tool_call !== undefined || + info.modalities?.input !== undefined || + info.modalities?.output !== undefined || + info.reasoning !== undefined ? { tools: info.tool_call ?? defaults.tools, + ...(info.reasoning === undefined ? {} : { reasoning: { supported: info.reasoning } }), input: info.modalities?.input ?? defaults.input, output: info.modalities?.output ?? defaults.output, } diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index e28cce00d3a5..cdee953c954f 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -1229,6 +1229,34 @@ describe("Config", () => { }) }) + test("preserves a model's declared reasoning through the v1 provider migration", () => { + const migrated = migrateV1({ + provider: { + localtest: { + npm: "@ai-sdk/openai-compatible", + models: { + "glm-flash": { name: "GLM Flash", reasoning: true }, + "plain-model": { name: "Plain Model", tool_call: true }, + silent: { name: "Silent" }, + }, + }, + }, + }) + + expect(migrated.providers?.localtest?.models?.["glm-flash"]?.capabilities).toEqual({ + tools: true, + reasoning: { supported: true }, + input: ["text", "image"], + output: ["text"], + }) + expect(migrated.providers?.localtest?.models?.["plain-model"]?.capabilities).toEqual({ + tools: true, + input: ["text", "image"], + output: ["text"], + }) + expect(migrated.providers?.localtest?.models?.silent?.capabilities).toBeUndefined() + }) + test("renames old provider IDs while migrating v1 configuration", () => { const migrated = migrateV1({ model: "azure-cognitive-services/deployment", diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 2acd4a9e3c88..ef4ea642bffd 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -543,6 +543,42 @@ describe("ConfigProviderPlugin.Plugin", () => { }), ) + it.effect("keeps a config-declared reasoning capability", () => + Effect.gen(function* () { + const providers = yield* Provider.Service + const models = yield* Model.Service + const providerID = Provider.ID.make("custom") + const modelID = Model.ID.make("reasoning") + yield* providers.transform((editor) => { + editor.models.update(providerID, modelID, (model) => { + model.capabilities = { tools: false, input: ["text"], output: ["text"] } + }) + }) + const entries = [ + new Document({ + type: "document", + info: decode({ + providers: { + custom: { + package: "aisdk:@ai-sdk/openai-compatible", + models: { reasoning: { capabilities: { reasoning: { supported: true } } } }, + }, + }, + }), + }), + ] + + yield* addPlugin(entries) + + expect((yield* models.get(providerID, modelID))?.capabilities).toEqual({ + tools: false, + reasoning: true, + input: ["text"], + output: ["text"], + }) + }), + ) + it.effect("resolves a config model with only input/output capabilities using default tools", () => Effect.gen(function* () { const models = yield* Model.Service diff --git a/packages/core/test/models.test.ts b/packages/core/test/models.test.ts index b9a3801a2f5e..9952e5021aca 100644 --- a/packages/core/test/models.test.ts +++ b/packages/core/test/models.test.ts @@ -92,7 +92,7 @@ const fixtureSnapshot = [ family: undefined, package: undefined, settings: { provider: "acme" }, - capabilities: { tools: true, input: ["text"], output: ["text"], temperature: true }, + capabilities: { tools: true, reasoning: false, input: ["text"], output: ["text"], temperature: true }, variants: [], time: { released: Date.parse("2026-01-01") }, cost: zeroCost, @@ -143,7 +143,7 @@ const fixture2Snapshot = [ family: undefined, package: undefined, settings: { provider: "beta" }, - capabilities: { tools: false, input: ["text"], output: ["text"], temperature: false }, + capabilities: { tools: false, reasoning: false, input: ["text"], output: ["text"], temperature: false }, variants: [], time: { released: Date.parse("2026-02-01") }, cost: zeroCost, diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index 1789c459277b..05c944da129a 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -18952,6 +18952,9 @@ "tools": { "type": "boolean" }, + "reasoning": { + "type": "boolean" + }, "input": { "type": "array", "items": { diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 8d198338b1f4..d43da3c3c10d 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -77,6 +77,7 @@ export const Overlays = { export interface Capabilities extends Schema.Schema.Type {} export const Capabilities = Schema.Struct({ tools: Schema.Boolean, + reasoning: optional(Schema.Boolean), input: Schema.Array(Schema.String), output: Schema.Array(Schema.String), /** Whether the model accepts a sampling temperature. Unknown when unset. */