mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(core): accept partial model capabilities in native provider config
This commit is contained in:
@@ -2130,7 +2130,7 @@ export type ConfigEntry =
|
||||
settings?: ConfigModelSettings
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: JsonValue }
|
||||
capabilities?: ModelCapabilities
|
||||
capabilities?: { tools?: boolean; input?: Array<string>; output?: Array<string> }
|
||||
variants?: Array<{
|
||||
id: string
|
||||
settings?: ConfigModelSettings
|
||||
|
||||
@@ -117,13 +117,8 @@ export const Plugin = define({
|
||||
if (config.settings !== undefined) model.settings = Provider.mergeOverlay(model.settings, config.settings)
|
||||
if (config.headers !== undefined) model.headers = Provider.mergeHeaders(model.headers, config.headers)
|
||||
if (config.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.body)
|
||||
if (config.capabilities !== undefined) {
|
||||
model.capabilities = {
|
||||
tools: config.capabilities.tools,
|
||||
input: [...config.capabilities.input],
|
||||
output: [...config.capabilities.output],
|
||||
}
|
||||
}
|
||||
if (config.capabilities !== undefined)
|
||||
model.capabilities = Model.mergeCapabilities(config.capabilities, model.capabilities)
|
||||
if (config.variants !== undefined) {
|
||||
model.variants ??= []
|
||||
for (const variant of config.variants) {
|
||||
|
||||
@@ -28,6 +28,16 @@ export type Compatibility = Model.Compatibility
|
||||
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()
|
||||
return {
|
||||
tools: config.tools ?? fallback.tools,
|
||||
input: [...(config.input ?? fallback.input)],
|
||||
output: [...(config.output ?? fallback.output)],
|
||||
}
|
||||
}
|
||||
|
||||
export const Cost = Model.Cost
|
||||
|
||||
export const Ref = Model.Ref
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Credential } from "../../credential.js"
|
||||
import { Integration } from "../../integration.js"
|
||||
import { IntegrationConnection } from "../../integration/connection.js"
|
||||
import { ManagedPolicy } from "../../managed-policy.js"
|
||||
import { Model } from "../../model.js"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { WebSearch } from "../../websearch.js"
|
||||
import { ConfigPolicy } from "@opencode/schema/config/policy"
|
||||
@@ -223,11 +224,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Manag
|
||||
model.package = config.package ?? (item.package !== undefined ? undefined : model.package)
|
||||
if (item.settings?.baseURL !== undefined && model.settings) delete model.settings.baseURL
|
||||
if (config.capabilities !== undefined)
|
||||
model.capabilities = {
|
||||
...config.capabilities,
|
||||
input: [...config.capabilities.input],
|
||||
output: [...config.capabilities.output],
|
||||
}
|
||||
model.capabilities = Model.mergeCapabilities(config.capabilities, model.capabilities)
|
||||
model.settings = Provider.mergeOverlay(
|
||||
withoutCredentials(model.settings),
|
||||
withoutCredentials(config.settings),
|
||||
|
||||
@@ -203,6 +203,18 @@ describe("ConfigNormalize", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("accepts partial model capabilities without reporting a diagnostic", () => {
|
||||
const result = normalized({
|
||||
providers: {
|
||||
demo: { name: "Demo", models: { demo: { name: "Demo", capabilities: { input: ["text"], output: ["text"] } } } },
|
||||
},
|
||||
})
|
||||
expect(result.diagnostics).toEqual([])
|
||||
expect(result.encoded.providers).toEqual({
|
||||
demo: { name: "Demo", models: { demo: { name: "Demo", capabilities: { input: ["text"], output: ["text"] } } } },
|
||||
})
|
||||
})
|
||||
|
||||
test("uses a valid retired provider alias when the canonical legacy entry is malformed", () => {
|
||||
const result = normalized({
|
||||
provider: {
|
||||
|
||||
@@ -255,6 +255,70 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("merges partial config capabilities onto the base model's existing fields", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
const models = yield* Model.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
const modelID = Model.ID.make("partial")
|
||||
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: { partial: { capabilities: { input: ["text", "image"] } } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(entries)
|
||||
|
||||
expect((yield* models.get(providerID, modelID))?.capabilities).toEqual({
|
||||
tools: false,
|
||||
input: ["text", "image"],
|
||||
output: ["text"],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("resolves a config model with only input/output capabilities using default tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const models = yield* Model.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
const modelID = Model.ID.make("demo")
|
||||
const entries = [
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
models: { demo: { name: "Demo", capabilities: { input: ["text"], output: ["text"] } } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(entries)
|
||||
|
||||
expect((yield* models.get(providerID, modelID))?.capabilities).toEqual({
|
||||
tools: true,
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
for (const scenario of [
|
||||
{ name: "omitted capabilities", legacy: {}, overrides: {} },
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as ConfigProvider from "./provider.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Money } from "../money.js"
|
||||
import { Capabilities, Compatibility, Family, ID, VariantID } from "../model.js"
|
||||
import { Compatibility, Family, ID, VariantID } from "../model.js"
|
||||
import { Provider } from "../provider.js"
|
||||
import { optional } from "../schema.js"
|
||||
|
||||
@@ -59,6 +59,13 @@ class Cost extends Schema.Class<Cost>("Config.Model.Cost")({
|
||||
cache: Cache.pipe(optional),
|
||||
}) {}
|
||||
|
||||
// Partial: unset fields fall back to the existing/base model's capabilities or Model.Capabilities.default().
|
||||
class ModelCapabilities extends Schema.Class<ModelCapabilities>("Config.Model.Capabilities")({
|
||||
tools: Schema.Boolean.pipe(optional),
|
||||
input: Schema.Array(Schema.String).pipe(optional),
|
||||
output: Schema.Array(Schema.String).pipe(optional),
|
||||
}) {}
|
||||
|
||||
class Limit extends Schema.Class<Limit>("Config.Model.Limit")({
|
||||
context: Schema.Int.pipe(optional),
|
||||
input: Schema.Int.pipe(optional),
|
||||
@@ -72,7 +79,7 @@ class Model extends Schema.Class<Model>("Config.Model")({
|
||||
compatibility: Compatibility.pipe(optional),
|
||||
package: Schema.String.pipe(optional),
|
||||
...ModelOverlays,
|
||||
capabilities: Capabilities.pipe(optional),
|
||||
capabilities: ModelCapabilities.pipe(optional),
|
||||
variants: Schema.Struct({
|
||||
id: VariantID,
|
||||
...ModelOverlays,
|
||||
|
||||
Reference in New Issue
Block a user