Compare commits
2 commits
dev
...
anthropic-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce0b726758 | ||
|
|
7c4c4c7239 |
13 changed files with 125 additions and 14 deletions
|
|
@ -1960,6 +1960,7 @@ export type ModelsListOutput = {
|
||||||
}
|
}
|
||||||
readonly capabilities: {
|
readonly capabilities: {
|
||||||
readonly tools: boolean
|
readonly tools: boolean
|
||||||
|
readonly reasoning: boolean
|
||||||
readonly input: ReadonlyArray<string>
|
readonly input: ReadonlyArray<string>
|
||||||
readonly output: ReadonlyArray<string>
|
readonly output: ReadonlyArray<string>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ export const Plugin = define({
|
||||||
if (config.capabilities !== undefined) {
|
if (config.capabilities !== undefined) {
|
||||||
model.capabilities = {
|
model.capabilities = {
|
||||||
tools: config.capabilities.tools,
|
tools: config.capabilities.tools,
|
||||||
|
reasoning: config.capabilities.reasoning,
|
||||||
input: [...config.capabilities.input],
|
input: [...config.capabilities.input],
|
||||||
output: [...config.capabilities.output],
|
output: [...config.capabilities.output],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ export const ModelsDevPlugin = define({
|
||||||
}
|
}
|
||||||
draft.capabilities = {
|
draft.capabilities = {
|
||||||
tools: model.tool_call,
|
tools: model.tool_call,
|
||||||
|
reasoning: model.reasoning,
|
||||||
input: [...(model.modalities?.input ?? [])],
|
input: [...(model.modalities?.input ?? [])],
|
||||||
output: [...(model.modalities?.output ?? [])],
|
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: {} }
|
: { id: model.api.id, type: "native", url: config.provider.api, settings: {} }
|
||||||
}
|
}
|
||||||
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
|
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?.input !== undefined) model.capabilities.input = [...config.modalities.input]
|
||||||
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
|
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
|
||||||
const packageName = config.provider?.npm ?? item.npm
|
const packageName = config.provider?.npm ?? item.npm
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,36 @@ export const Plugin = define({
|
||||||
})
|
})
|
||||||
|
|
||||||
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
|
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
|
||||||
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return []
|
if (model.api.type !== "aisdk") return []
|
||||||
const ids = `${model.id} ${model.api.id}`.toLowerCase()
|
|
||||||
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
|
if (model.api.package === "@ai-sdk/openai-compatible") {
|
||||||
return ["high", "max"].map((id) => ({
|
const ids = `${model.id} ${model.api.id}`.toLowerCase()
|
||||||
id,
|
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: {},
|
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 =
|
const capabilities =
|
||||||
info.tool_call !== undefined || info.modalities?.input !== undefined || info.modalities?.output !== undefined
|
info.tool_call !== undefined ||
|
||||||
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
|
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
|
: undefined
|
||||||
return {
|
return {
|
||||||
family: info.family,
|
family: info.family,
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
models: {
|
models: {
|
||||||
chat: {
|
chat: {
|
||||||
name: "First",
|
name: "First",
|
||||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
capabilities: { tools: true, reasoning: false, input: ["text"], output: ["text"] },
|
||||||
disabled: true,
|
disabled: true,
|
||||||
limit: { context: 100, output: 50 },
|
limit: { context: 100, output: 50 },
|
||||||
cost: { input: 1, output: 2 },
|
cost: { input: 1, output: 2 },
|
||||||
|
|
@ -251,7 +251,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
||||||
expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
|
expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
|
||||||
expect(model.name).toBe("Last")
|
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.enabled).toBe(false)
|
||||||
expect(model.limit).toEqual({ context: 100, output: 75 })
|
expect(model.limit).toEqual({ context: 100, output: 75 })
|
||||||
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
|
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ describe("OpencodePlugin", () => {
|
||||||
family: "remote",
|
family: "remote",
|
||||||
release_date: "2026-01-02",
|
release_date: "2026-01-02",
|
||||||
tool_call: true,
|
tool_call: true,
|
||||||
|
reasoning: true,
|
||||||
modalities: { input: ["text", "image"], output: ["text"] },
|
modalities: { input: ["text", "image"], output: ["text"] },
|
||||||
options: { apiKey: "model-secret", temperature: 0.5 },
|
options: { apiKey: "model-secret", temperature: 0.5 },
|
||||||
variants: { high: { apiKey: "variant-secret", temperature: 0.2 } },
|
variants: { high: { apiKey: "variant-secret", temperature: 0.2 } },
|
||||||
|
|
@ -170,7 +171,7 @@ describe("OpencodePlugin", () => {
|
||||||
expect(model).toMatchObject({
|
expect(model).toMatchObject({
|
||||||
name: "Remote Model",
|
name: "Remote Model",
|
||||||
family: "remote",
|
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 } }],
|
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
|
||||||
limit: { context: 1000, output: 100 },
|
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"),
|
providerID: ProviderV2.ID.make("test-provider"),
|
||||||
name: "Test model",
|
name: "Test model",
|
||||||
api: { id: ModelV2.ID.make("api-test-model"), ...api },
|
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: {
|
request: {
|
||||||
headers: { "x-test": "header" },
|
headers: { "x-test": "header" },
|
||||||
body: { apiKey: "secret", custom_extension: { enabled: true } },
|
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 interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {}
|
||||||
export const Capabilities = Schema.Struct({
|
export const Capabilities = Schema.Struct({
|
||||||
tools: Schema.Boolean,
|
tools: Schema.Boolean,
|
||||||
|
reasoning: Schema.Boolean,
|
||||||
input: Schema.Array(Schema.String),
|
input: Schema.Array(Schema.String),
|
||||||
output: Schema.Array(Schema.String),
|
output: Schema.Array(Schema.String),
|
||||||
}).annotate({ identifier: "Model.Capabilities" })
|
}).annotate({ identifier: "Model.Capabilities" })
|
||||||
|
|
@ -93,7 +94,7 @@ export const Info = Schema.Struct({
|
||||||
providerID,
|
providerID,
|
||||||
name: modelID,
|
name: modelID,
|
||||||
api: { id: modelID, type: "native", settings: {} },
|
api: { id: modelID, type: "native", settings: {} },
|
||||||
capabilities: { tools: false, input: [], output: [] },
|
capabilities: { tools: false, reasoning: false, input: [], output: [] },
|
||||||
request: { headers: {}, body: {} },
|
request: { headers: {}, body: {} },
|
||||||
variants: [],
|
variants: [],
|
||||||
time: { released: 0 },
|
time: { released: 0 },
|
||||||
|
|
|
||||||
|
|
@ -4787,6 +4787,7 @@ export type ModelApi =
|
||||||
|
|
||||||
export type ModelCapabilities = {
|
export type ModelCapabilities = {
|
||||||
tools: boolean
|
tools: boolean
|
||||||
|
reasoning: boolean
|
||||||
input: Array<string>
|
input: Array<string>
|
||||||
output: Array<string>
|
output: Array<string>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29767,6 +29767,9 @@
|
||||||
"tools": {
|
"tools": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
@ -29780,7 +29783,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["tools", "input", "output"],
|
"required": ["tools", "reasoning", "input", "output"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"ModelCost": {
|
"ModelCost": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue