feat(core): let custom models in config set a model type (#59388)

This commit is contained in:
Aiden Cline 2026-10-26 15:29:09 -05:00 • committed by GitHub
parent 804a7f3c01
commit a697f53611
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 71 additions and 1 deletions

View file

@ -2306,6 +2306,7 @@ export type ConfigEntry =
models?: {
[x: string]: {
modelID?: string
type?: ModelType
family?: string
name?: string
releaseDate?: string

View file

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

View file

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

View file

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

View file

@ -15544,6 +15544,9 @@
"modelID": {
"type": "string"
},
"type": {
"$ref": "#/components/schemas/Model.Type"
},
"family": {
"type": "string"
},

View file

@ -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<ModelConfig>("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))

View file

@ -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. |