mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 17:19:49 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8c63dbf83 | |||
| a47ff5c746 |
@@ -48,6 +48,9 @@ export const Plugin = PluginV2.define({
|
|||||||
if (config.capabilities !== undefined) {
|
if (config.capabilities !== undefined) {
|
||||||
model.capabilities = {
|
model.capabilities = {
|
||||||
tools: config.capabilities.tools,
|
tools: config.capabilities.tools,
|
||||||
|
reasoningOptions: config.capabilities.reasoningOptions?.map((option) =>
|
||||||
|
option.type === "effort" ? { ...option, values: [...option.values] } : { ...option },
|
||||||
|
),
|
||||||
input: [...config.capabilities.input],
|
input: [...config.capabilities.input],
|
||||||
output: [...config.capabilities.output],
|
output: [...config.capabilities.output],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { DateTime, Schema } from "effect"
|
|||||||
import { DateTimeUtcFromMillis } from "effect/Schema"
|
import { DateTimeUtcFromMillis } from "effect/Schema"
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
import { ModelRequest } from "./model-request"
|
import { ModelRequest } from "./model-request"
|
||||||
|
import { ModelsDev } from "./models-dev"
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
|
export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
@@ -15,6 +16,7 @@ export type Family = typeof Family.Type
|
|||||||
|
|
||||||
export const Capabilities = Schema.Struct({
|
export const Capabilities = Schema.Struct({
|
||||||
tools: Schema.Boolean,
|
tools: Schema.Boolean,
|
||||||
|
reasoningOptions: Schema.Array(ModelsDev.ResolvedReasoningOption).pipe(Schema.optional),
|
||||||
// mime patterns, image, audio, video/*, text/*
|
// mime patterns, image, audio, video/*, text/*
|
||||||
input: Schema.String.pipe(Schema.Array),
|
input: Schema.String.pipe(Schema.Array),
|
||||||
output: Schema.String.pipe(Schema.Array),
|
output: Schema.String.pipe(Schema.Array),
|
||||||
|
|||||||
@@ -43,6 +43,64 @@ const Cost = Schema.Struct({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const reasoningOption = <Fields extends Schema.Struct.Fields>(fields: Fields) =>
|
||||||
|
Schema.StructWithRest(Schema.Struct(fields), [Schema.Record(Schema.String, Schema.MutableJson)])
|
||||||
|
|
||||||
|
export const ReasoningOption = Schema.Union([
|
||||||
|
reasoningOption({ type: Schema.Literal("toggle") }),
|
||||||
|
reasoningOption({
|
||||||
|
type: Schema.Literal("effort"),
|
||||||
|
values: Schema.Array(Schema.NullOr(Schema.String)),
|
||||||
|
}),
|
||||||
|
reasoningOption({
|
||||||
|
type: Schema.Literal("budget_tokens"),
|
||||||
|
min: Schema.optional(Schema.Finite),
|
||||||
|
max: Schema.optional(Schema.Finite),
|
||||||
|
}),
|
||||||
|
reasoningOption({ type: Schema.String }),
|
||||||
|
])
|
||||||
|
export type ReasoningOption = typeof ReasoningOption.Type
|
||||||
|
|
||||||
|
export const ResolvedReasoningOption = Schema.Union([
|
||||||
|
Schema.Struct({ type: Schema.Literal("toggle") }),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("effort"),
|
||||||
|
values: Schema.Array(Schema.String),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("budget_tokens"),
|
||||||
|
min: Schema.optional(Schema.Finite),
|
||||||
|
max: Schema.optional(Schema.Finite),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
export type ResolvedReasoningOption =
|
||||||
|
| { type: "toggle" }
|
||||||
|
| { type: "effort"; values: string[] }
|
||||||
|
| { type: "budget_tokens"; min?: number; max?: number }
|
||||||
|
|
||||||
|
export function resolveReasoningOptions(
|
||||||
|
options: readonly ReasoningOption[] | undefined,
|
||||||
|
): ResolvedReasoningOption[] | undefined {
|
||||||
|
if (!options) return
|
||||||
|
return options
|
||||||
|
.map((option): ResolvedReasoningOption | undefined => {
|
||||||
|
if (option.type === "toggle") return { type: "toggle" }
|
||||||
|
if (option.type === "effort" && Array.isArray(option.values)) {
|
||||||
|
return {
|
||||||
|
type: "effort",
|
||||||
|
values: option.values.filter((value): value is string => typeof value === "string"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (option.type !== "budget_tokens") return
|
||||||
|
return {
|
||||||
|
type: "budget_tokens",
|
||||||
|
...(typeof option.min === "number" && Number.isFinite(option.min) ? { min: option.min } : {}),
|
||||||
|
...(typeof option.max === "number" && Number.isFinite(option.max) ? { max: option.max } : {}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((option): option is ResolvedReasoningOption => option !== undefined)
|
||||||
|
}
|
||||||
|
|
||||||
export const Model = Schema.Struct({
|
export const Model = Schema.Struct({
|
||||||
id: Schema.String,
|
id: Schema.String,
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
@@ -50,6 +108,7 @@ export const Model = Schema.Struct({
|
|||||||
release_date: Schema.String,
|
release_date: Schema.String,
|
||||||
attachment: Schema.Boolean,
|
attachment: Schema.Boolean,
|
||||||
reasoning: Schema.Boolean,
|
reasoning: Schema.Boolean,
|
||||||
|
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
|
||||||
temperature: Schema.Boolean,
|
temperature: Schema.Boolean,
|
||||||
tool_call: Schema.Boolean,
|
tool_call: Schema.Boolean,
|
||||||
interleaved: Schema.optional(
|
interleaved: Schema.optional(
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export const ModelsDevPlugin = PluginV2.define({
|
|||||||
}
|
}
|
||||||
draft.capabilities = {
|
draft.capabilities = {
|
||||||
tools: model.tool_call,
|
tools: model.tool_call,
|
||||||
|
reasoningOptions: ModelsDev.resolveReasoningOptions(model.reasoning_options),
|
||||||
input: [...(model.modalities?.input ?? [])],
|
input: [...(model.modalities?.input ?? [])],
|
||||||
output: [...(model.modalities?.output ?? [])],
|
output: [...(model.modalities?.output ?? [])],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
|
import { describe, expect, beforeAll, beforeEach, afterAll, test } from "bun:test"
|
||||||
import { Effect, Layer, Ref } from "effect"
|
import { Effect, Layer, Ref, Schema } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
@@ -125,6 +125,19 @@ const initialState: MockState = {
|
|||||||
calls: [],
|
calls: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("reasoning options preserve unknown types and fields", () => {
|
||||||
|
const options: ModelsDev.ReasoningOption[] = [
|
||||||
|
{ type: "effort", values: [null, "low", "ultrathink"], default: "low" },
|
||||||
|
{ type: "future_dynamic_budget", curve: { min: 1, max: 10 }, enabled: true },
|
||||||
|
]
|
||||||
|
const model = Schema.decodeUnknownSync(ModelsDev.Model)({
|
||||||
|
...fixture.acme.models["acme-1"],
|
||||||
|
reasoning_options: options,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(model.reasoning_options).toEqual(options)
|
||||||
|
})
|
||||||
|
|
||||||
describe("ModelsDev Service", () => {
|
describe("ModelsDev Service", () => {
|
||||||
it.live("get() returns providers from disk when cache file exists", () =>
|
it.live("get() returns providers from disk when cache file exists", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
|
import { Location } from "@opencode-ai/core/location"
|
||||||
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||||
|
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
|
||||||
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
import { location } from "../fixture/location"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
|
const reasoningOptions: ModelsDev.ReasoningOption[] = [
|
||||||
|
{ type: "toggle" },
|
||||||
|
{ type: "effort", values: [null, "low", "ultrathink"], default: "low" },
|
||||||
|
{ type: "budget_tokens", min: 1024, future: true },
|
||||||
|
{ type: "future_dynamic_budget", curve: { min: 1, max: 10 }, enabled: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
const modelsDev = Layer.succeed(
|
||||||
|
ModelsDev.Service,
|
||||||
|
ModelsDev.Service.of({
|
||||||
|
get: () =>
|
||||||
|
Effect.succeed({
|
||||||
|
acme: {
|
||||||
|
id: "acme",
|
||||||
|
name: "Acme",
|
||||||
|
env: [],
|
||||||
|
models: {
|
||||||
|
"acme-1": {
|
||||||
|
id: "acme-1",
|
||||||
|
name: "Acme One",
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
attachment: false,
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: reasoningOptions,
|
||||||
|
temperature: true,
|
||||||
|
tool_call: true,
|
||||||
|
limit: { context: 128_000, output: 8_192 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
refresh: () => Effect.void,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const locationLayer = Layer.succeed(
|
||||||
|
Location.Service,
|
||||||
|
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||||
|
)
|
||||||
|
const catalog = Catalog.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer))
|
||||||
|
const it = testEffect(Layer.merge(catalog, modelsDev))
|
||||||
|
|
||||||
|
describe("ModelsDevPlugin", () => {
|
||||||
|
it.effect("maps known reasoning options into typed V2 capabilities", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* ModelsDevPlugin.effect
|
||||||
|
const model = yield* (yield* Catalog.Service).model.get(ProviderV2.ID.make("acme"), ModelV2.ID.make("acme-1"))
|
||||||
|
|
||||||
|
expect(model.capabilities.reasoningOptions).toEqual([
|
||||||
|
{ type: "toggle" },
|
||||||
|
{ type: "effort", values: ["low", "ultrathink"] },
|
||||||
|
{ type: "budget_tokens", min: 1024 },
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -960,6 +960,7 @@ const ProviderInterleaved = Schema.Union([
|
|||||||
const ProviderCapabilities = Schema.Struct({
|
const ProviderCapabilities = Schema.Struct({
|
||||||
temperature: Schema.Boolean,
|
temperature: Schema.Boolean,
|
||||||
reasoning: Schema.Boolean,
|
reasoning: Schema.Boolean,
|
||||||
|
reasoningOptions: optionalOmitUndefined(Schema.Array(ModelsDev.ResolvedReasoningOption)),
|
||||||
attachment: Schema.Boolean,
|
attachment: Schema.Boolean,
|
||||||
toolcall: Schema.Boolean,
|
toolcall: Schema.Boolean,
|
||||||
input: ProviderModalities,
|
input: ProviderModalities,
|
||||||
@@ -1178,6 +1179,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: model.temperature ?? false,
|
temperature: model.temperature ?? false,
|
||||||
reasoning: model.reasoning ?? false,
|
reasoning: model.reasoning ?? false,
|
||||||
|
reasoningOptions: ModelsDev.resolveReasoningOptions(model.reasoning_options),
|
||||||
attachment: model.attachment ?? false,
|
attachment: model.attachment ?? false,
|
||||||
toolcall: model.tool_call ?? true,
|
toolcall: model.tool_call ?? true,
|
||||||
input: {
|
input: {
|
||||||
@@ -1400,6 +1402,7 @@ export const layer = Layer.effect(
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
|
temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
|
||||||
reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
|
reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
|
||||||
|
reasoningOptions: existingModel?.capabilities.reasoningOptions,
|
||||||
attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
|
attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
|
||||||
toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
|
toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -1286,6 +1286,12 @@ test("mode cost preserves over-200k pricing from base model", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("models.dev normalization fills required response fields", () => {
|
test("models.dev normalization fills required response fields", () => {
|
||||||
|
const reasoningOptions: ModelsDev.ReasoningOption[] = [
|
||||||
|
{ type: "toggle" },
|
||||||
|
{ type: "effort", values: [null, "low", "ultrathink"], default: "low" },
|
||||||
|
{ type: "budget_tokens", min: 1024, future: true },
|
||||||
|
{ type: "future_dynamic_budget", curve: { min: 1, max: 10 } },
|
||||||
|
]
|
||||||
const provider = {
|
const provider = {
|
||||||
id: "gateway",
|
id: "gateway",
|
||||||
name: "Gateway",
|
name: "Gateway",
|
||||||
@@ -1295,6 +1301,7 @@ test("models.dev normalization fills required response fields", () => {
|
|||||||
id: "gpt-5.4",
|
id: "gpt-5.4",
|
||||||
name: "GPT-5.4",
|
name: "GPT-5.4",
|
||||||
family: "gpt",
|
family: "gpt",
|
||||||
|
reasoning_options: reasoningOptions,
|
||||||
cost: { input: 2.5, output: 15 },
|
cost: { input: 2.5, output: 15 },
|
||||||
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
|
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
|
||||||
},
|
},
|
||||||
@@ -1305,6 +1312,11 @@ test("models.dev normalization fills required response fields", () => {
|
|||||||
expect(model.api.url).toBe("")
|
expect(model.api.url).toBe("")
|
||||||
expect(model.capabilities.temperature).toBe(false)
|
expect(model.capabilities.temperature).toBe(false)
|
||||||
expect(model.capabilities.reasoning).toBe(false)
|
expect(model.capabilities.reasoning).toBe(false)
|
||||||
|
expect(model.capabilities.reasoningOptions).toEqual([
|
||||||
|
{ type: "toggle" },
|
||||||
|
{ type: "effort", values: ["low", "ultrathink"] },
|
||||||
|
{ type: "budget_tokens", min: 1024 },
|
||||||
|
])
|
||||||
expect(model.capabilities.attachment).toBe(false)
|
expect(model.capabilities.attachment).toBe(false)
|
||||||
expect(model.capabilities.toolcall).toBe(true)
|
expect(model.capabilities.toolcall).toBe(true)
|
||||||
expect(model.release_date).toBe("")
|
expect(model.release_date).toBe("")
|
||||||
|
|||||||
@@ -2082,6 +2082,20 @@ export type Model = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: boolean
|
temperature: boolean
|
||||||
reasoning: boolean
|
reasoning: boolean
|
||||||
|
reasoningOptions?: Array<
|
||||||
|
| {
|
||||||
|
type: "toggle"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "effort"
|
||||||
|
values: Array<string>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "budget_tokens"
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
>
|
||||||
attachment: boolean
|
attachment: boolean
|
||||||
toolcall: boolean
|
toolcall: boolean
|
||||||
input: {
|
input: {
|
||||||
@@ -2878,6 +2892,20 @@ export type ModelV2Info = {
|
|||||||
}
|
}
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: boolean
|
tools: boolean
|
||||||
|
reasoningOptions?: Array<
|
||||||
|
| {
|
||||||
|
type: "toggle"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "effort"
|
||||||
|
values: Array<string>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "budget_tokens"
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
>
|
||||||
input: Array<string>
|
input: Array<string>
|
||||||
output: Array<string>
|
output: Array<string>
|
||||||
}
|
}
|
||||||
@@ -4230,6 +4258,20 @@ export type ModelV2Info1 = {
|
|||||||
}
|
}
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: boolean
|
tools: boolean
|
||||||
|
reasoningOptions?: Array<
|
||||||
|
| {
|
||||||
|
type: "toggle"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "effort"
|
||||||
|
values: Array<string>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "budget_tokens"
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
>
|
||||||
input: Array<string>
|
input: Array<string>
|
||||||
output: Array<string>
|
output: Array<string>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user