Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/app/src/runtime/server/global-sync/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }],
Expand All @@ -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"],
)

Expand All @@ -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)
})
})

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/runtime/server/global-sync/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
8 changes: 7 additions & 1 deletion packages/client/src/promise/generated/types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions packages/core/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Capabilities>, base: Capabilities | undefined) => {
const fallback = base ?? Capabilities.default()
export const mergeCapabilities = (
config: Partial<Omit<Capabilities, "reasoning">> & { 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,
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/models-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/v1/config/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
28 changes: 28 additions & 0 deletions packages/core/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 36 additions & 0 deletions packages/core/test/config/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/protocol/openapi.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/schema/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const Overlays = {
export interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {}
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. */
Expand Down
Loading