mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-02 08:25:00 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75ab5ceae9 | |||
| d00d274192 | |||
| 6007304aaa | |||
| f86fdb9712 | |||
| 40bd3fdfb2 | |||
| 3d0d15b4fe |
@@ -69,6 +69,7 @@ export const Model = Schema.Struct({
|
||||
temperature: Schema.Boolean,
|
||||
tool_call: Schema.Boolean,
|
||||
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
|
||||
variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.MutableJson))),
|
||||
interleaved: Schema.optional(
|
||||
Schema.Union([
|
||||
Schema.Literal(true),
|
||||
@@ -109,7 +110,13 @@ export const Model = Schema.Struct({
|
||||
),
|
||||
status: Schema.optional(CatalogModelStatus),
|
||||
provider: Schema.optional(
|
||||
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
|
||||
Schema.Struct({
|
||||
npm: Schema.optional(Schema.String),
|
||||
api: Schema.optional(Schema.String),
|
||||
variant: Schema.optional(Schema.String),
|
||||
body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)),
|
||||
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}),
|
||||
),
|
||||
})
|
||||
export type Model = Schema.Schema.Type<typeof Model>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { EventV2 } from "../event"
|
||||
import { ModelsDev } from "../models-dev"
|
||||
import { ModelV2 } from "../model"
|
||||
import { ProviderV2 } from "../provider"
|
||||
|
||||
function released(date: string) {
|
||||
@@ -102,7 +103,11 @@ function applyModel(
|
||||
input: [...(model.modalities?.input ?? [])],
|
||||
output: [...(model.modalities?.output ?? [])],
|
||||
}
|
||||
draft.variants = []
|
||||
draft.variants = Object.entries(model.variants ?? {}).map(([id, body]) => ({
|
||||
id: ModelV2.VariantID.make(id),
|
||||
headers: {},
|
||||
body: { ...body },
|
||||
}))
|
||||
draft.time.released = released(model.release_date)
|
||||
draft.cost = input.cost ?? cost(model.cost)
|
||||
draft.status = model.status ?? "active"
|
||||
@@ -112,6 +117,9 @@ function applyModel(
|
||||
input: model.limit.input,
|
||||
output: model.limit.output,
|
||||
}
|
||||
if (model.provider?.variant !== undefined) draft.request.variant = ModelV2.VariantID.make(model.provider.variant)
|
||||
Object.assign(draft.request.headers, model.provider?.headers ?? {})
|
||||
Object.assign(draft.request.body, model.provider?.body ?? {})
|
||||
Object.assign(draft.request.headers, input.request?.headers ?? {})
|
||||
Object.assign(draft.request.body, input.request?.body ?? {})
|
||||
}
|
||||
|
||||
@@ -48,8 +48,13 @@ describe("ModelsDevPlugin", () => {
|
||||
release_date: "2026-01-01",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "toggle" }],
|
||||
temperature: true,
|
||||
tool_call: true,
|
||||
variants: {
|
||||
none: { thinking: { type: "disabled" } },
|
||||
thinking: { thinking: { type: "adaptive" } },
|
||||
},
|
||||
cost: {
|
||||
input: 2.5,
|
||||
output: 15,
|
||||
@@ -64,6 +69,11 @@ describe("ModelsDevPlugin", () => {
|
||||
context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 },
|
||||
},
|
||||
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
|
||||
provider: {
|
||||
variant: "thinking",
|
||||
body: { thinking: { type: "adaptive" } },
|
||||
headers: { "x-model": "gpt-5.4" },
|
||||
},
|
||||
experimental: {
|
||||
modes: {
|
||||
fast: {
|
||||
@@ -93,18 +103,29 @@ describe("ModelsDevPlugin", () => {
|
||||
const base = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4"))
|
||||
const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
|
||||
|
||||
expect(base?.variants).toEqual([])
|
||||
expect(base?.request.body).toEqual({})
|
||||
expect(base?.variants).toEqual([
|
||||
{ id: ModelV2.VariantID.make("none"), headers: {}, body: { thinking: { type: "disabled" } } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), headers: {}, body: { thinking: { type: "adaptive" } } },
|
||||
])
|
||||
expect(base?.request).toEqual({
|
||||
variant: ModelV2.VariantID.make("thinking"),
|
||||
headers: { "x-model": "gpt-5.4" },
|
||||
body: { thinking: { type: "adaptive" } },
|
||||
})
|
||||
expect(fast).toMatchObject({
|
||||
id: "gpt-5.4-fast",
|
||||
providerID: "acme",
|
||||
name: "GPT-5.4 Fast",
|
||||
api: { id: "gpt-5.4" },
|
||||
request: {
|
||||
headers: { "x-mode": "fast" },
|
||||
body: { service_tier: "priority" },
|
||||
variant: ModelV2.VariantID.make("thinking"),
|
||||
headers: { "x-model": "gpt-5.4", "x-mode": "fast" },
|
||||
body: { thinking: { type: "adaptive" }, service_tier: "priority" },
|
||||
},
|
||||
variants: [],
|
||||
variants: [
|
||||
{ id: ModelV2.VariantID.make("none"), headers: {}, body: { thinking: { type: "disabled" } } },
|
||||
{ id: ModelV2.VariantID.make("thinking"), headers: {}, body: { thinking: { type: "adaptive" } } },
|
||||
],
|
||||
})
|
||||
expect(fast?.cost).toEqual([
|
||||
{ input: 5, output: 30, cache: { read: 0.5, write: 0 } },
|
||||
|
||||
@@ -22,7 +22,7 @@ type Api =
|
||||
}
|
||||
| { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
|
||||
|
||||
const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
|
||||
const model = (api: Api, variants: ModelV2.Info["variants"] = [], variant?: ModelV2.VariantID) =>
|
||||
ModelV2.Info.make({
|
||||
id: ModelV2.ID.make("test-model"),
|
||||
providerID: ProviderV2.ID.make("test-provider"),
|
||||
@@ -32,6 +32,7 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
|
||||
request: {
|
||||
headers: { "x-test": "header" },
|
||||
body: { apiKey: "secret", custom_extension: { enabled: true } },
|
||||
...(variant ? { variant } : {}),
|
||||
},
|
||||
variants,
|
||||
time: { released: 0 },
|
||||
@@ -175,6 +176,44 @@ describe("SessionRunnerModel", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the catalog default Session variant", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = model(
|
||||
{ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" },
|
||||
[
|
||||
{
|
||||
id: ModelV2.VariantID.make("none"),
|
||||
headers: {},
|
||||
body: { thinking: { type: "disabled" } },
|
||||
},
|
||||
{
|
||||
id: ModelV2.VariantID.make("thinking"),
|
||||
headers: {},
|
||||
body: { thinking: { type: "adaptive" } },
|
||||
},
|
||||
],
|
||||
ModelV2.VariantID.make("thinking"),
|
||||
)
|
||||
const session = SessionV2.Info.make({
|
||||
id: SessionV2.ID.make("ses_default_variant"),
|
||||
projectID: ProjectV2.ID.global,
|
||||
title: "test",
|
||||
model: { id: catalog.id, providerID: catalog.providerID },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
|
||||
location: { directory: AbsolutePath.make("/project") },
|
||||
})
|
||||
|
||||
const resolved = yield* SessionRunnerModel.resolve(session, catalog)
|
||||
|
||||
expect(resolved.route.defaults.http?.body).toEqual({
|
||||
custom_extension: { enabled: true },
|
||||
thinking: { type: "adaptive" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" })
|
||||
|
||||
@@ -55,7 +55,6 @@ const PROTOCOL_BODY_OVERLAY_DENYLIST = new Set([
|
||||
"systemInstruction",
|
||||
"system_instruction",
|
||||
"temperature",
|
||||
"thinking",
|
||||
"toolChoice",
|
||||
"toolConfig",
|
||||
"tool_choice",
|
||||
|
||||
@@ -156,6 +156,32 @@ describe("request option precedence", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("allows provider thinking body overlays", () =>
|
||||
LLMClient.generate(
|
||||
LLM.request({
|
||||
model: OpenAIChat.route
|
||||
.with({
|
||||
endpoint: { baseURL: "https://api.provider.test/v1/" },
|
||||
auth: Auth.bearer("test"),
|
||||
http: { body: { thinking: { type: "adaptive" } } },
|
||||
})
|
||||
.model({ id: "minimax-m3" }),
|
||||
prompt: "Say hello.",
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provide(
|
||||
dynamicResponse((input) => {
|
||||
expect(decodeJson(input.text)).toMatchObject({ thinking: { type: "adaptive" } })
|
||||
return Effect.succeed(
|
||||
input.respond(sseEvents(deltaChunk({}, "stop")), {
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("uses model output limits after route limits and before call maxTokens", () =>
|
||||
Effect.gen(function* () {
|
||||
const route = AnthropicMessages.route.with({
|
||||
|
||||
@@ -1041,6 +1041,7 @@ export const Model = Schema.Struct({
|
||||
options: Schema.Record(Schema.String, Schema.Any),
|
||||
headers: Schema.Record(Schema.String, Schema.String),
|
||||
release_date: Schema.String,
|
||||
variant: optional(ModelV2.VariantID),
|
||||
variants: optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))),
|
||||
}).annotate({ identifier: "Model" })
|
||||
export type Model = Types.DeepMutable<Schema.Schema.Type<typeof Model>>
|
||||
@@ -1216,8 +1217,8 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
|
||||
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
|
||||
},
|
||||
status: model.status ?? "active",
|
||||
headers: {},
|
||||
options: {},
|
||||
headers: { ...(model.provider?.headers ?? {}) },
|
||||
options: { ...(model.provider?.body ?? {}) },
|
||||
cost: cost(model.cost),
|
||||
limit: {
|
||||
context: model.limit.context,
|
||||
@@ -1246,10 +1247,12 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
|
||||
interleaved: model.interleaved ?? false,
|
||||
},
|
||||
release_date: model.release_date ?? "",
|
||||
variant: model.provider?.variant ? ModelV2.VariantID.make(model.provider.variant) : undefined,
|
||||
variants: {},
|
||||
}
|
||||
|
||||
const variants = ProviderTransform.reasoningVariants(model, base) ?? ProviderTransform.variants(base)
|
||||
const variants =
|
||||
model.variants ?? ProviderTransform.reasoningVariants(model, base) ?? ProviderTransform.variants(base)
|
||||
|
||||
return {
|
||||
...base,
|
||||
@@ -1498,6 +1501,7 @@ const layer = Layer.effect(
|
||||
headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
|
||||
family: model.family ?? existingModel?.family ?? "",
|
||||
release_date: model.release_date ?? existingModel?.release_date ?? "",
|
||||
variant: existingModel?.variant,
|
||||
variants: {},
|
||||
}
|
||||
const variants =
|
||||
|
||||
@@ -689,15 +689,6 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
const glm52 = ["glm-5.2", "glm-5-2", "glm-5p2"].some(
|
||||
(name) => id.includes(name) || model.api.id.toLowerCase().includes(name),
|
||||
)
|
||||
if (
|
||||
model.api.id.toLowerCase().includes("minimax-m3") &&
|
||||
["@ai-sdk/anthropic", "@ai-sdk/openai-compatible"].includes(model.api.npm)
|
||||
) {
|
||||
return {
|
||||
none: { thinking: { type: "disabled" } },
|
||||
thinking: { thinking: { type: "adaptive" } },
|
||||
}
|
||||
}
|
||||
const adaptiveThinkingOmitted = anthropicOmitsThinking(model.api.id)
|
||||
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
|
||||
if (glm52 && model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||
@@ -1188,11 +1179,6 @@ export function options(input: {
|
||||
|
||||
const modelId = input.model.api.id.toLowerCase()
|
||||
|
||||
// MiniMax's Anthropic interface defaults thinking off, unlike Chat Completions.
|
||||
if (modelId.includes("minimax-m3") && input.model.api.npm === "@ai-sdk/anthropic") {
|
||||
result["thinking"] = { type: "adaptive" }
|
||||
}
|
||||
|
||||
// Moonshot's Anthropic-compatible API uses adaptive effort rather than token budgets.
|
||||
// Request summaries so thinking content survives replay on subsequent turns.
|
||||
if (
|
||||
|
||||
@@ -77,10 +77,9 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
|
||||
system.push(header, rest.join("\n"))
|
||||
}
|
||||
|
||||
const variant =
|
||||
!input.small && input.model.variants && input.user.model.variant
|
||||
? input.model.variants[input.user.model.variant]
|
||||
: {}
|
||||
const variantID =
|
||||
input.user.model.variant === "default" ? input.model.variant : (input.user.model.variant ?? input.model.variant)
|
||||
const variant = !input.small && input.model.variants && variantID ? input.model.variants[variantID] : {}
|
||||
const base = input.small
|
||||
? ProviderTransform.smallOptions(input.model)
|
||||
: ProviderTransform.options({
|
||||
|
||||
@@ -1522,6 +1522,23 @@ test("models.dev reasoning options replace generated variants and unsupported to
|
||||
provider: { npm: "@ai-sdk/anthropic" },
|
||||
limit: { context: 1_048_576, output: 131_072 },
|
||||
},
|
||||
providerVariants: {
|
||||
id: "minimax-m3",
|
||||
name: "Provider Variants",
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "toggle" }],
|
||||
variants: {
|
||||
none: { thinking: { type: "disabled" } },
|
||||
thinking: { thinking: { type: "adaptive" } },
|
||||
},
|
||||
provider: {
|
||||
npm: "@ai-sdk/anthropic",
|
||||
variant: "thinking",
|
||||
body: { thinking: { type: "adaptive" } },
|
||||
headers: { "x-model": "minimax-m3" },
|
||||
},
|
||||
limit: { context: 1_000_000, output: 128_000 },
|
||||
},
|
||||
},
|
||||
} as unknown as ModelsDev.Provider
|
||||
|
||||
@@ -1539,6 +1556,13 @@ test("models.dev reasoning options replace generated variants and unsupported to
|
||||
high: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
|
||||
})
|
||||
expect(models.anthropicCompatible.variants).toEqual({ max: { effort: "max" } })
|
||||
expect(models.providerVariants.options).toEqual({ thinking: { type: "adaptive" } })
|
||||
expect(models.providerVariants.headers).toEqual({ "x-model": "minimax-m3" })
|
||||
expect(models.providerVariants.variant).toBe(ModelV2.VariantID.make("thinking"))
|
||||
expect(models.providerVariants.variants).toEqual({
|
||||
none: { thinking: { type: "disabled" } },
|
||||
thinking: { thinking: { type: "adaptive" } },
|
||||
})
|
||||
expect(models["gemini-3-pro-fast"].variants).toEqual(models.override.variants)
|
||||
})
|
||||
|
||||
|
||||
@@ -278,23 +278,57 @@ describe("ProviderTransform.options - minimax m3 thinking", () => {
|
||||
limit: { output: 64_000 },
|
||||
}) as any
|
||||
|
||||
test("explicitly enables adaptive thinking with the anthropic SDK", () => {
|
||||
test.each(["@ai-sdk/anthropic", "@ai-sdk/openai-compatible"])("does not synthesize options for %s", (npm) => {
|
||||
expect(
|
||||
ProviderTransform.options({
|
||||
model: createModel("@ai-sdk/anthropic"),
|
||||
sessionID: "test-session-123",
|
||||
}).thinking,
|
||||
).toEqual({ type: "adaptive" })
|
||||
})
|
||||
|
||||
test("uses the native default with the openai-compatible SDK", () => {
|
||||
expect(
|
||||
ProviderTransform.options({
|
||||
model: createModel("@ai-sdk/openai-compatible"),
|
||||
model: createModel(npm),
|
||||
sessionID: "test-session-123",
|
||||
}).thinking,
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
test("applies the model default variant without changing small requests", async () => {
|
||||
const model = {
|
||||
...createModel("@ai-sdk/anthropic"),
|
||||
variant: "thinking",
|
||||
variants: {
|
||||
none: { thinking: { type: "disabled" } },
|
||||
thinking: { thinking: { type: "adaptive" } },
|
||||
},
|
||||
}
|
||||
const prepare = (small: boolean) =>
|
||||
Effect.runPromise(
|
||||
LLMRequestPrep.prepare({
|
||||
user: {
|
||||
id: "msg_user-test",
|
||||
sessionID: "test-session-123",
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: "test",
|
||||
model: { providerID: "minimax", modelID: "minimax-m3" },
|
||||
} as any,
|
||||
sessionID: "test-session-123",
|
||||
model,
|
||||
agent: { name: "test", mode: "primary", options: {}, permission: [] } as any,
|
||||
system: [],
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
small,
|
||||
tools: {},
|
||||
provider: { id: "minimax", options: {} } as any,
|
||||
auth: undefined,
|
||||
plugin: {
|
||||
trigger: (_name: string, _input: unknown, output: unknown) => Effect.succeed(output),
|
||||
list: () => Effect.succeed([]),
|
||||
init: () => Effect.void,
|
||||
} as any,
|
||||
flags: { outputTokenMax: 32_000, client: "test" } as any,
|
||||
isWorkflow: false,
|
||||
}),
|
||||
)
|
||||
|
||||
expect((await prepare(false)).params.options.thinking).toEqual({ type: "adaptive" })
|
||||
expect((await prepare(true)).params.options.thinking).toEqual({ type: "disabled" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.options - google thinkingConfig gating", () => {
|
||||
@@ -3435,38 +3469,21 @@ describe("ProviderTransform.variants", () => {
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
test("minimax m3 using anthropic returns thinking toggles", () => {
|
||||
const model = createMockModel({
|
||||
id: "minimax/minimax-m3",
|
||||
providerID: "minimax",
|
||||
api: {
|
||||
id: "MiniMax-M3",
|
||||
url: "https://api.minimax.com/anthropic/v1",
|
||||
npm: "@ai-sdk/anthropic",
|
||||
},
|
||||
})
|
||||
const result = ProviderTransform.variants(model)
|
||||
expect(result).toEqual({
|
||||
none: { thinking: { type: "disabled" } },
|
||||
thinking: { thinking: { type: "adaptive" } },
|
||||
})
|
||||
})
|
||||
|
||||
test("minimax m3 using openai-compatible returns thinking toggles", () => {
|
||||
const model = createMockModel({
|
||||
id: "minimax/minimax-m3",
|
||||
providerID: "minimax",
|
||||
api: {
|
||||
id: "minimax-m3",
|
||||
url: "https://api.minimax.com/v1",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
},
|
||||
})
|
||||
expect(ProviderTransform.variants(model)).toEqual({
|
||||
none: { thinking: { type: "disabled" } },
|
||||
thinking: { thinking: { type: "adaptive" } },
|
||||
})
|
||||
})
|
||||
test.each(["@ai-sdk/anthropic", "@ai-sdk/openai-compatible"])(
|
||||
"does not synthesize minimax m3 variants for %s",
|
||||
(npm) => {
|
||||
const model = createMockModel({
|
||||
id: "minimax/minimax-m3",
|
||||
providerID: "minimax",
|
||||
api: {
|
||||
id: "minimax-m3",
|
||||
url: "https://api.minimax.com/v1",
|
||||
npm,
|
||||
},
|
||||
})
|
||||
expect(ProviderTransform.variants(model)).toEqual({})
|
||||
},
|
||||
)
|
||||
|
||||
test("glm returns empty object", () => {
|
||||
const model = createMockModel({
|
||||
|
||||
Reference in New Issue
Block a user