Compare commits

...
7 changed files with 100 additions and 15 deletions
@@ -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
+2 -7
View File
@@ -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) {
+10
View File
@@ -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: {} },
{
+9 -2
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, 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,