feat(core): sync models.dev reasoning options
This commit is contained in:
parent
80c0b06980
commit
a47ff5c746
9 changed files with 112 additions and 2 deletions
|
|
@ -48,6 +48,7 @@ export const Plugin = PluginV2.define({
|
|||
if (config.capabilities !== undefined) {
|
||||
model.capabilities = {
|
||||
tools: config.capabilities.tools,
|
||||
reasoningOptions: config.capabilities.reasoningOptions?.map((option) => ({ ...option })),
|
||||
input: [...config.capabilities.input],
|
||||
output: [...config.capabilities.output],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export type Family = typeof Family.Type
|
|||
|
||||
export const Capabilities = Schema.Struct({
|
||||
tools: Schema.Boolean,
|
||||
reasoningOptions: Schema.Array(Schema.Record(Schema.String, Schema.Any)).pipe(Schema.optional),
|
||||
// mime patterns, image, audio, video/*, text/*
|
||||
input: Schema.String.pipe(Schema.Array),
|
||||
output: Schema.String.pipe(Schema.Array),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ const Cost = Schema.Struct({
|
|||
),
|
||||
})
|
||||
|
||||
export const ReasoningOption = Schema.StructWithRest(Schema.Struct({ type: Schema.String }), [
|
||||
Schema.Record(Schema.String, Schema.MutableJson),
|
||||
])
|
||||
export type ReasoningOption = typeof ReasoningOption.Type
|
||||
|
||||
export const Model = Schema.Struct({
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
|
|
@ -50,6 +55,7 @@ export const Model = Schema.Struct({
|
|||
release_date: Schema.String,
|
||||
attachment: Schema.Boolean,
|
||||
reasoning: Schema.Boolean,
|
||||
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
|
||||
temperature: Schema.Boolean,
|
||||
tool_call: Schema.Boolean,
|
||||
interleaved: Schema.optional(
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export const ModelsDevPlugin = PluginV2.define({
|
|||
}
|
||||
draft.capabilities = {
|
||||
tools: model.tool_call,
|
||||
reasoningOptions: model.reasoning_options?.map((option) => ({ ...option })),
|
||||
input: [...(model.modalities?.input ?? [])],
|
||||
output: [...(model.modalities?.output ?? [])],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, beforeAll, beforeEach, afterAll } from "bun:test"
|
||||
import { Effect, Layer, Ref } from "effect"
|
||||
import { describe, expect, beforeAll, beforeEach, afterAll, test } from "bun:test"
|
||||
import { Effect, Layer, Ref, Schema } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
|
|
@ -125,6 +125,19 @@ const initialState: MockState = {
|
|||
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", () => {
|
||||
it.live("get() returns providers from disk when cache file exists", () =>
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
68
packages/core/test/plugin/models-dev.test.ts
Normal file
68
packages/core/test/plugin/models-dev.test.ts
Normal file
|
|
@ -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: "effort", values: [null, "low", "ultrathink"], default: "low" },
|
||||
{ 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("preserves reasoning options in V2 model 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(reasoningOptions)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -957,9 +957,12 @@ const ProviderInterleaved = Schema.Union([
|
|||
}),
|
||||
])
|
||||
|
||||
const ProviderReasoningOption = Schema.Record(Schema.String, Schema.Any)
|
||||
|
||||
const ProviderCapabilities = Schema.Struct({
|
||||
temperature: Schema.Boolean,
|
||||
reasoning: Schema.Boolean,
|
||||
reasoningOptions: optionalOmitUndefined(Schema.Array(ProviderReasoningOption)),
|
||||
attachment: Schema.Boolean,
|
||||
toolcall: Schema.Boolean,
|
||||
input: ProviderModalities,
|
||||
|
|
@ -1178,6 +1181,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
|
|||
capabilities: {
|
||||
temperature: model.temperature ?? false,
|
||||
reasoning: model.reasoning ?? false,
|
||||
reasoningOptions: model.reasoning_options?.map((option) => ({ ...option })),
|
||||
attachment: model.attachment ?? false,
|
||||
toolcall: model.tool_call ?? true,
|
||||
input: {
|
||||
|
|
@ -1400,6 +1404,7 @@ export const layer = Layer.effect(
|
|||
capabilities: {
|
||||
temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
|
||||
reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
|
||||
reasoningOptions: existingModel?.capabilities.reasoningOptions,
|
||||
attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
|
||||
toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
|
||||
input: {
|
||||
|
|
|
|||
|
|
@ -1286,6 +1286,10 @@ test("mode cost preserves over-200k pricing from base model", () => {
|
|||
})
|
||||
|
||||
test("models.dev normalization fills required response fields", () => {
|
||||
const reasoningOptions: ModelsDev.ReasoningOption[] = [
|
||||
{ type: "effort", values: [null, "low", "ultrathink"], default: "low" },
|
||||
{ type: "future_dynamic_budget", curve: { min: 1, max: 10 } },
|
||||
]
|
||||
const provider = {
|
||||
id: "gateway",
|
||||
name: "Gateway",
|
||||
|
|
@ -1295,6 +1299,7 @@ test("models.dev normalization fills required response fields", () => {
|
|||
id: "gpt-5.4",
|
||||
name: "GPT-5.4",
|
||||
family: "gpt",
|
||||
reasoning_options: reasoningOptions,
|
||||
cost: { input: 2.5, output: 15 },
|
||||
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
|
||||
},
|
||||
|
|
@ -1305,6 +1310,7 @@ test("models.dev normalization fills required response fields", () => {
|
|||
expect(model.api.url).toBe("")
|
||||
expect(model.capabilities.temperature).toBe(false)
|
||||
expect(model.capabilities.reasoning).toBe(false)
|
||||
expect(model.capabilities.reasoningOptions).toEqual(reasoningOptions)
|
||||
expect(model.capabilities.attachment).toBe(false)
|
||||
expect(model.capabilities.toolcall).toBe(true)
|
||||
expect(model.release_date).toBe("")
|
||||
|
|
|
|||
|
|
@ -2082,6 +2082,9 @@ export type Model = {
|
|||
capabilities: {
|
||||
temperature: boolean
|
||||
reasoning: boolean
|
||||
reasoningOptions?: Array<{
|
||||
[key: string]: unknown
|
||||
}>
|
||||
attachment: boolean
|
||||
toolcall: boolean
|
||||
input: {
|
||||
|
|
@ -2878,6 +2881,9 @@ export type ModelV2Info = {
|
|||
}
|
||||
capabilities: {
|
||||
tools: boolean
|
||||
reasoningOptions?: Array<{
|
||||
[key: string]: unknown
|
||||
}>
|
||||
input: Array<string>
|
||||
output: Array<string>
|
||||
}
|
||||
|
|
@ -4230,6 +4236,9 @@ export type ModelV2Info1 = {
|
|||
}
|
||||
capabilities: {
|
||||
tools: boolean
|
||||
reasoningOptions?: Array<{
|
||||
[key: string]: unknown
|
||||
}>
|
||||
input: Array<string>
|
||||
output: Array<string>
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue