fix(core): share native model defaults with v1 config migration (#45760)

Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-08-27 19:25:56 -05:00
committed by GitHub
Unverified
parent 7b6b0f2717
commit d5be4cf888
3 changed files with 180 additions and 3 deletions
+6 -1
View File
@@ -313,9 +313,14 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
]
: []),
]
const defaults = Model.Capabilities.default()
const capabilities =
info.tool_call !== undefined || info.modalities?.input !== undefined || info.modalities?.output !== undefined
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
? {
tools: info.tool_call ?? defaults.tools,
input: info.modalities?.input ?? defaults.input,
output: info.modalities?.output ?? defaults.output,
}
: undefined
return {
modelID: info.id,
+166
View File
@@ -5,8 +5,10 @@ import { Effect, Schema, Stream } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Config } from "@opencode-ai/core/config"
import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider"
import { ConfigNormalize } from "@opencode-ai/core/config/normalize"
import { Integration } from "@opencode-ai/core/integration"
import { Model } from "@opencode-ai/core/model"
import { ModelResolver } from "@opencode-ai/core/model-resolver"
import { Plugin } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { Provider } from "@opencode-ai/core/provider"
@@ -152,6 +154,170 @@ describe("ConfigProviderPlugin.Plugin", () => {
}),
)
for (const scenario of [
{ name: "omitted capabilities", legacy: {}, overrides: {} },
{
name: "input-only modalities",
legacy: { modalities: { input: ["text", "image", "pdf"] } },
overrides: { input: ["text", "image", "pdf"] },
},
{
name: "output-only modalities",
legacy: { modalities: { output: ["audio"] } },
overrides: { output: ["audio"] },
},
{ name: "enabled tools", legacy: { tool_call: true }, overrides: { tools: true } },
{ name: "disabled tools", legacy: { tool_call: false }, overrides: { tools: false } },
{
name: "explicit empty modalities",
legacy: { modalities: { input: [], output: [] } },
overrides: { input: [], output: [] },
},
{
name: "fully specified capabilities",
legacy: { tool_call: false, modalities: { input: ["audio"], output: ["text"] } },
overrides: { tools: false, input: ["audio"], output: ["text"] },
},
]) {
it.effect(`uses native model defaults for migrated ${scenario.name}`, () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = Provider.ID.make("custom")
const result = ConfigNormalize.normalize({ provider: { custom: { models: { migrated: scenario.legacy } } } })
if (result.type !== "normalized") throw new Error("Expected normalized config")
expect(result.diagnostics).toEqual([])
yield* addPlugin([
new Document({
type: "document",
info: decode({ providers: { custom: { models: { native: {} } } } }),
}),
new Document({ type: "document", info: decode(result.encoded) }),
])
const native = required(yield* catalog.model.get(providerID, Model.ID.make("native")))
const migrated = required(yield* catalog.model.get(providerID, Model.ID.make("migrated")))
expect(migrated).toEqual({
...native,
id: migrated.id,
modelID: migrated.id,
name: migrated.id,
capabilities: { ...native.capabilities, ...scenario.overrides },
})
}),
)
}
for (const scenario of [
{
name: "provider package and request defaults",
legacy: {
npm: "@ai-sdk/openai",
api: "https://proxy.example/v1",
options: { headers: { "x-provider": "default" }, body: { store: false } },
models: { chat: {} },
},
native: {
package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://proxy.example/v1" },
headers: { "x-provider": "default" },
body: { store: false },
models: { chat: {} },
},
},
{
name: "model package, limits, costs, and variant overrides",
legacy: {
npm: "@ai-sdk/openai",
api: "https://proxy.example/v1",
models: {
chat: {
id: "vendor/chat",
name: "Custom chat",
provider: { npm: "@ai-sdk/anthropic", api: "https://model.example/v1" },
limit: { context: 100000, input: 80000, output: 16000 },
cost: { input: 1, output: 2, cache_read: 0.1, cache_write: 0.2 },
options: { temperature: 0.2 },
variants: { high: { effort: "high" } },
},
},
},
native: {
package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://proxy.example/v1" },
models: {
chat: {
modelID: "vendor/chat",
name: "Custom chat",
package: "aisdk:@ai-sdk/anthropic",
settings: { baseURL: "https://model.example/v1", temperature: 0.2 },
limit: { context: 100000, input: 80000, output: 16000 },
cost: { input: 1, output: 2, cache: { read: 0.1, write: 0.2 } },
variants: [{ id: "high", settings: { effort: "high" } }],
},
},
},
},
{
name: "zero costs and omitted cache costs",
legacy: { models: { chat: { cost: { input: 0, output: 0 } } } },
native: { models: { chat: { cost: { input: 0, output: 0 } } } },
},
]) {
it.effect(`matches native configuration for migrated ${scenario.name}`, () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const result = ConfigNormalize.normalize({ provider: { legacy: scenario.legacy } })
if (result.type !== "normalized") throw new Error("Expected normalized config")
expect(result.diagnostics).toEqual([])
yield* addPlugin([
new Document({ type: "document", info: decode({ providers: { native: scenario.native } }) }),
new Document({ type: "document", info: decode(result.encoded) }),
])
const native = required(yield* catalog.model.get(Provider.ID.make("native"), Model.ID.make("chat")))
const migrated = required(yield* catalog.model.get(Provider.ID.make("legacy"), Model.ID.make("chat")))
expect({ ...migrated, providerID: native.providerID }).toEqual(native)
for (const variant of native.variants) {
const expected = yield* ModelResolver.withVariant(native, variant.id)
const actual = yield* ModelResolver.withVariant(migrated, variant.id)
expect({ ...actual, providerID: expected.providerID }).toEqual(expected)
}
}),
)
}
it.effect("preserves existing catalog metadata when migrated fields are omitted", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = Provider.ID.make("custom")
const modelID = Model.ID.make("chat")
yield* catalog.transform((draft) => {
draft.model.update(providerID, modelID, (model) => {
model.package = "aisdk:@ai-sdk/anthropic"
model.settings = { baseURL: "https://catalog.example/v1" }
model.capabilities = { tools: false, input: ["audio"], output: ["audio"] }
model.limit = { context: 100000, input: 80000, output: 16000 }
model.cost = [
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(2),
cache: { read: Money.USDPerMillionTokens.zero, write: Money.USDPerMillionTokens.zero },
},
]
model.variants = [{ id: Model.VariantID.make("high"), settings: { effort: "high" } }]
})
})
const before = required(yield* catalog.model.get(providerID, modelID))
const result = ConfigNormalize.normalize({ provider: { custom: { models: { chat: {} } } } })
if (result.type !== "normalized") throw new Error("Expected normalized config")
expect(result.diagnostics).toEqual([])
yield* addPlugin([new Document({ type: "document", info: decode(result.encoded) })])
expect(yield* catalog.model.get(providerID, modelID)).toEqual(before)
}),
)
it.effect("keeps configured model variant bodies unchanged", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
+8 -2
View File
@@ -68,7 +68,13 @@ export const Capabilities = Schema.Struct({
input: Schema.Array(Schema.String),
output: Schema.Array(Schema.String),
responsesWebsockets: Schema.Boolean.pipe(optional),
}).annotate({ identifier: "Model.Capabilities" })
})
.annotate({ identifier: "Model.Capabilities" })
.pipe(
statics(() => ({
default: () => ({ tools: true, input: ["text", "image"], output: ["text"] }) satisfies Capabilities,
})),
)
export interface Cost extends Schema.Schema.Type<typeof Cost> {}
export const Cost = Schema.Struct({
@@ -123,7 +129,7 @@ export const Info = Schema.Struct({
modelID: id,
providerID,
name: id,
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
capabilities: Capabilities.default(),
variants: [],
time: { released: 0 },
cost: [],