diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 6db0f8255e64..70b6026acb69 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -2306,6 +2306,7 @@ export type ConfigEntry = models?: { [x: string]: { modelID?: string + type?: ModelType family?: string name?: string releaseDate?: string diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index a0b7d49adda5..75faaec5f27a 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -217,6 +217,7 @@ function overlay( if (item.package !== undefined) model.package = undefined if (item.settings?.baseURL !== undefined && model.settings) delete model.settings.baseURL } + if (config.type !== undefined) model.type = config.type if (config.family !== undefined) model.family = config.family if (config.name) model.name = config.name if (config.modelID !== undefined) model.modelID = config.modelID diff --git a/packages/core/src/plugin/provider/opencode-legacy.ts b/packages/core/src/plugin/provider/opencode-legacy.ts index 95453823e79d..a282981ca510 100644 --- a/packages/core/src/plugin/provider/opencode-legacy.ts +++ b/packages/core/src/plugin/provider/opencode-legacy.ts @@ -97,6 +97,7 @@ export const register = Effect.fn("OpencodeLegacy.register")(function* ( const base = source?.models.get(config.modelID ?? modelID) ?? source?.models.get(modelID) providers.models.update(providerID, modelID, (model) => { Object.assign(model, structuredClone(base ?? model)) + if (config.type !== undefined) model.type = config.type if (config.family !== undefined) model.family = config.family if (config.name !== undefined) model.name = config.name if (config.modelID !== undefined) model.modelID = config.modelID diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 9337ab2a0c3c..2acd4a9e3c88 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -397,6 +397,68 @@ describe("ConfigProviderPlugin.Plugin", () => { }), ) + it.effect("lists a custom model by its configured type and an untyped one as chat", () => + Effect.gen(function* () { + const models = yield* Model.Service + const entries = [ + new Document({ + type: "document", + info: decode({ + providers: { + custom: { + package: "aisdk:@ai-sdk/openai-compatible", + models: { chat: {}, painter: { type: "image" } }, + }, + }, + }), + }), + ] + + yield* addPlugin(entries) + + expect((yield* models.available()).map((model) => model.id)).toEqual([Model.ID.make("chat")]) + expect((yield* models.available(["image"])).map((model) => model.id)).toEqual([Model.ID.make("painter")]) + expect(yield* models.get(Provider.ID.make("custom"), Model.ID.make("painter"))).toBeUndefined() + }), + ) + + it.effect("lets config change a catalog model's type and keeps it when unset", () => + Effect.gen(function* () { + const providers = yield* Provider.Service + const models = yield* Model.Service + const providerID = Provider.ID.make("custom") + const retypedID = Model.ID.make("retyped") + const keptID = Model.ID.make("kept") + yield* providers.transform((editor) => { + editor.models.update(providerID, retypedID, () => {}) + editor.models.update(providerID, keptID, (model) => { + model.type = "decision" + }) + }) + const entries = [ + new Document({ + type: "document", + info: decode({ + providers: { + custom: { + package: "aisdk:@ai-sdk/openai-compatible", + models: { retyped: { type: "image" }, kept: { name: "Kept" } }, + }, + }, + }), + }), + ] + + yield* addPlugin(entries) + + expect(yield* models.available()).toEqual([]) + expect((yield* models.available(["image"])).map((model) => model.id)).toEqual([retypedID]) + expect((yield* models.available(["decision"])).map((model) => [model.id, model.name])).toEqual([ + [keptID, "Kept"], + ]) + }), + ) + it.effect("preserves catalog capabilities unless config overrides them", () => Effect.gen(function* () { const providers = yield* Provider.Service diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index eb575f88d56c..75101ecef1c3 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -15544,6 +15544,9 @@ "modelID": { "type": "string" }, + "type": { + "$ref": "#/components/schemas/Model.Type" + }, "family": { "type": "string" }, diff --git a/packages/schema/src/config/provider.ts b/packages/schema/src/config/provider.ts index e84cdd68a11f..11befcabb86d 100644 --- a/packages/schema/src/config/provider.ts +++ b/packages/schema/src/config/provider.ts @@ -2,7 +2,7 @@ export * as ConfigProvider from "./provider.js" import { Schema } from "effect" import { Money } from "../money.js" -import { Capabilities, Compatibility, Family, ID, Model, VariantID } from "../model.js" +import { Capabilities, Compatibility, Family, ID, Model, Type, VariantID } from "../model.js" import { ProviderCompatibility } from "../model-compatibility.js" import { Provider } from "../provider.js" import { optional } from "../schema.js" @@ -74,6 +74,7 @@ export type Reasoning = typeof Reasoning.Type class ModelConfig extends Schema.Class("Config.Model")({ modelID: ID.pipe(optional), + type: Type.pipe(optional), family: Family.pipe(optional), name: Schema.String.pipe(optional), releaseDate: Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}(-\d{2})?$/u)) diff --git a/services/www/src/docs/content/providers.mdx b/services/www/src/docs/content/providers.mdx index 77cfe9700905..b0de5785e5bc 100644 --- a/services/www/src/docs/content/providers.mdx +++ b/services/www/src/docs/content/providers.mdx @@ -293,6 +293,7 @@ ID sent to the provider. | `modelID` | Model or deployment ID sent to the provider. | | `name` | Display name. | | `family` | Model family used for grouping related models. | +| `type` | What the model produces, such as `chat`, `image` or `decision`. Defaults to `chat`; only chat models are offered for sessions. | | `releaseDate` | Release date as `YYYY-MM-DD` or `YYYY-MM`. | | `package` | Runtime override for this model. | | `settings` | Model-level OpenCode controls and package-specific JSON settings. |