mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-31 15:34:02 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce0b726758 | |||
| 7c4c4c7239 |
@@ -1960,6 +1960,7 @@ export type ModelsListOutput = {
|
||||
}
|
||||
readonly capabilities: {
|
||||
readonly tools: boolean
|
||||
readonly reasoning: boolean
|
||||
readonly input: ReadonlyArray<string>
|
||||
readonly output: ReadonlyArray<string>
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export const Plugin = define({
|
||||
if (config.capabilities !== undefined) {
|
||||
model.capabilities = {
|
||||
tools: config.capabilities.tools,
|
||||
reasoning: config.capabilities.reasoning,
|
||||
input: [...config.capabilities.input],
|
||||
output: [...config.capabilities.output],
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ export const ModelsDevPlugin = define({
|
||||
}
|
||||
draft.capabilities = {
|
||||
tools: model.tool_call,
|
||||
reasoning: model.reasoning,
|
||||
input: [...(model.modalities?.input ?? [])],
|
||||
output: [...(model.modalities?.output ?? [])],
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
||||
: { id: model.api.id, type: "native", url: config.provider.api, settings: {} }
|
||||
}
|
||||
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
|
||||
if (config.reasoning !== undefined) model.capabilities.reasoning = config.reasoning
|
||||
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
|
||||
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
|
||||
const packageName = config.provider?.npm ?? item.npm
|
||||
|
||||
@@ -28,12 +28,36 @@ export const Plugin = define({
|
||||
})
|
||||
|
||||
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
|
||||
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return []
|
||||
const ids = `${model.id} ${model.api.id}`.toLowerCase()
|
||||
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
|
||||
return ["high", "max"].map((id) => ({
|
||||
id,
|
||||
if (model.api.type !== "aisdk") return []
|
||||
|
||||
if (model.api.package === "@ai-sdk/openai-compatible") {
|
||||
const ids = `${model.id} ${model.api.id}`.toLowerCase()
|
||||
if (["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name)))
|
||||
return ["high", "max"].map((id) => ({
|
||||
id,
|
||||
headers: {},
|
||||
body: { reasoning_effort: id },
|
||||
}))
|
||||
return []
|
||||
}
|
||||
|
||||
if (model.api.package === "@ai-sdk/anthropic") return anthropicThinking(model)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// Anthropic thinking levels lower to the Messages API `thinking` body, which the
|
||||
// v2 Anthropic protocol only accepts as `{ type: "enabled", budget_tokens }`.
|
||||
// Budgets scale with the model output limit so the larger tier stays under the cap.
|
||||
function anthropicThinking(model: ModelV2Info): ModelV2Info["variants"] {
|
||||
if (!model.capabilities.reasoning) return []
|
||||
const budget = (target: number) => Math.max(1024, Math.min(target, model.limit.output - 1))
|
||||
return [
|
||||
{ id: "high", target: 16_000 },
|
||||
{ id: "max", target: 31_999 },
|
||||
].map((tier) => ({
|
||||
id: tier.id,
|
||||
headers: {},
|
||||
body: { reasoning_effort: id },
|
||||
body: { thinking: { type: "enabled", budget_tokens: budget(tier.target) } },
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -211,8 +211,16 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
|
||||
: []),
|
||||
]
|
||||
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 ?? [] }
|
||||
info.tool_call !== undefined ||
|
||||
info.reasoning !== undefined ||
|
||||
info.modalities?.input !== undefined ||
|
||||
info.modalities?.output !== undefined
|
||||
? {
|
||||
tools: info.tool_call ?? false,
|
||||
reasoning: info.reasoning ?? false,
|
||||
input: info.modalities?.input ?? [],
|
||||
output: info.modalities?.output ?? [],
|
||||
}
|
||||
: undefined
|
||||
return {
|
||||
family: info.family,
|
||||
|
||||
@@ -174,7 +174,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
models: {
|
||||
chat: {
|
||||
name: "First",
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
capabilities: { tools: true, reasoning: false, input: ["text"], output: ["text"] },
|
||||
disabled: true,
|
||||
limit: { context: 100, output: 50 },
|
||||
cost: { input: 1, output: 2 },
|
||||
@@ -251,7 +251,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
||||
expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
|
||||
expect(model.name).toBe("Last")
|
||||
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
|
||||
expect(model.capabilities).toEqual({ tools: true, reasoning: false, input: ["text"], output: ["text"] })
|
||||
expect(model.enabled).toBe(false)
|
||||
expect(model.limit).toEqual({ context: 100, output: 75 })
|
||||
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
|
||||
|
||||
@@ -111,6 +111,7 @@ describe("OpencodePlugin", () => {
|
||||
family: "remote",
|
||||
release_date: "2026-01-02",
|
||||
tool_call: true,
|
||||
reasoning: true,
|
||||
modalities: { input: ["text", "image"], output: ["text"] },
|
||||
options: { apiKey: "model-secret", temperature: 0.5 },
|
||||
variants: { high: { apiKey: "variant-secret", temperature: 0.2 } },
|
||||
@@ -170,7 +171,7 @@ describe("OpencodePlugin", () => {
|
||||
expect(model).toMatchObject({
|
||||
name: "Remote Model",
|
||||
family: "remote",
|
||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||
capabilities: { tools: true, reasoning: true, input: ["text", "image"], output: ["text"] },
|
||||
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
|
||||
limit: { context: 1000, output: 100 },
|
||||
})
|
||||
|
||||
@@ -76,4 +76,73 @@ describe("VariantPlugin", () => {
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("adds Anthropic thinking variants for reasoning models", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Catalog.Service
|
||||
yield* service.transform((catalog) => {
|
||||
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("claude-opus-4-8"), (model) => {
|
||||
model.api = {
|
||||
id: ModelV2.ID.make("claude-opus-4-8"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/anthropic",
|
||||
}
|
||||
model.capabilities.reasoning = true
|
||||
model.limit = { context: 200_000, output: 64_000 }
|
||||
})
|
||||
})
|
||||
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
|
||||
|
||||
expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("claude-opus-4-8")))?.variants).toEqual([
|
||||
expect.objectContaining({ id: "high", body: { thinking: { type: "enabled", budget_tokens: 16_000 } } }),
|
||||
expect.objectContaining({ id: "max", body: { thinking: { type: "enabled", budget_tokens: 31_999 } } }),
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("clamps Anthropic thinking budgets to the output limit", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Catalog.Service
|
||||
yield* service.transform((catalog) => {
|
||||
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("claude-haiku-4-5"), (model) => {
|
||||
model.api = {
|
||||
id: ModelV2.ID.make("claude-haiku-4-5"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/anthropic",
|
||||
}
|
||||
model.capabilities.reasoning = true
|
||||
model.limit = { context: 200_000, output: 8_000 }
|
||||
})
|
||||
})
|
||||
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
|
||||
|
||||
expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("claude-haiku-4-5")))?.variants).toEqual(
|
||||
[
|
||||
expect.objectContaining({ id: "high", body: { thinking: { type: "enabled", budget_tokens: 7_999 } } }),
|
||||
expect.objectContaining({ id: "max", body: { thinking: { type: "enabled", budget_tokens: 7_999 } } }),
|
||||
],
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("skips Anthropic models without reasoning capability", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Catalog.Service
|
||||
yield* service.transform((catalog) => {
|
||||
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("claude-3-5-haiku"), (model) => {
|
||||
model.api = {
|
||||
id: ModelV2.ID.make("claude-3-5-haiku"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/anthropic",
|
||||
}
|
||||
model.capabilities.reasoning = false
|
||||
})
|
||||
})
|
||||
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
|
||||
|
||||
expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("claude-3-5-haiku")))?.variants).toEqual(
|
||||
[],
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
|
||||
providerID: ProviderV2.ID.make("test-provider"),
|
||||
name: "Test model",
|
||||
api: { id: ModelV2.ID.make("api-test-model"), ...api },
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
capabilities: { tools: true, reasoning: false, input: ["text"], output: ["text"] },
|
||||
request: {
|
||||
headers: { "x-test": "header" },
|
||||
body: { apiKey: "secret", custom_extension: { enabled: true } },
|
||||
|
||||
@@ -24,6 +24,7 @@ export type Family = typeof Family.Type
|
||||
export interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {}
|
||||
export const Capabilities = Schema.Struct({
|
||||
tools: Schema.Boolean,
|
||||
reasoning: Schema.Boolean,
|
||||
input: Schema.Array(Schema.String),
|
||||
output: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "Model.Capabilities" })
|
||||
@@ -93,7 +94,7 @@ export const Info = Schema.Struct({
|
||||
providerID,
|
||||
name: modelID,
|
||||
api: { id: modelID, type: "native", settings: {} },
|
||||
capabilities: { tools: false, input: [], output: [] },
|
||||
capabilities: { tools: false, reasoning: false, input: [], output: [] },
|
||||
request: { headers: {}, body: {} },
|
||||
variants: [],
|
||||
time: { released: 0 },
|
||||
|
||||
@@ -4787,6 +4787,7 @@ export type ModelApi =
|
||||
|
||||
export type ModelCapabilities = {
|
||||
tools: boolean
|
||||
reasoning: boolean
|
||||
input: Array<string>
|
||||
output: Array<string>
|
||||
}
|
||||
|
||||
@@ -29767,6 +29767,9 @@
|
||||
"tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"input": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -29780,7 +29783,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["tools", "input", "output"],
|
||||
"required": ["tools", "reasoning", "input", "output"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ModelCost": {
|
||||
|
||||
Reference in New Issue
Block a user