fix(core): type models.dev reasoning options
This commit is contained in:
parent
a47ff5c746
commit
e8c63dbf83
8 changed files with 122 additions and 29 deletions
|
|
@ -48,7 +48,9 @@ export const Plugin = PluginV2.define({
|
|||
if (config.capabilities !== undefined) {
|
||||
model.capabilities = {
|
||||
tools: config.capabilities.tools,
|
||||
reasoningOptions: config.capabilities.reasoningOptions?.map((option) => ({ ...option })),
|
||||
reasoningOptions: config.capabilities.reasoningOptions?.map((option) =>
|
||||
option.type === "effort" ? { ...option, values: [...option.values] } : { ...option },
|
||||
),
|
||||
input: [...config.capabilities.input],
|
||||
output: [...config.capabilities.output],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { DateTime, Schema } from "effect"
|
|||
import { DateTimeUtcFromMillis } from "effect/Schema"
|
||||
import { ProviderV2 } from "./provider"
|
||||
import { ModelRequest } from "./model-request"
|
||||
import { ModelsDev } from "./models-dev"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
|
|
@ -15,7 +16,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),
|
||||
reasoningOptions: Schema.Array(ModelsDev.ResolvedReasoningOption).pipe(Schema.optional),
|
||||
// mime patterns, image, audio, video/*, text/*
|
||||
input: Schema.String.pipe(Schema.Array),
|
||||
output: Schema.String.pipe(Schema.Array),
|
||||
|
|
|
|||
|
|
@ -43,11 +43,64 @@ const Cost = Schema.Struct({
|
|||
),
|
||||
})
|
||||
|
||||
export const ReasoningOption = Schema.StructWithRest(Schema.Struct({ type: Schema.String }), [
|
||||
Schema.Record(Schema.String, Schema.MutableJson),
|
||||
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({
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export const ModelsDevPlugin = PluginV2.define({
|
|||
}
|
||||
draft.capabilities = {
|
||||
tools: model.tool_call,
|
||||
reasoningOptions: model.reasoning_options?.map((option) => ({ ...option })),
|
||||
reasoningOptions: ModelsDev.resolveReasoningOptions(model.reasoning_options),
|
||||
input: [...(model.modalities?.input ?? [])],
|
||||
output: [...(model.modalities?.output ?? [])],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ 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 },
|
||||
]
|
||||
|
||||
|
|
@ -47,22 +49,20 @@ 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 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", () =>
|
||||
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"),
|
||||
)
|
||||
const model = yield* (yield* Catalog.Service).model.get(ProviderV2.ID.make("acme"), ModelV2.ID.make("acme-1"))
|
||||
|
||||
expect(model.capabilities.reasoningOptions).toEqual(reasoningOptions)
|
||||
expect(model.capabilities.reasoningOptions).toEqual([
|
||||
{ type: "toggle" },
|
||||
{ type: "effort", values: ["low", "ultrathink"] },
|
||||
{ type: "budget_tokens", min: 1024 },
|
||||
])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -957,12 +957,10 @@ 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)),
|
||||
reasoningOptions: optionalOmitUndefined(Schema.Array(ModelsDev.ResolvedReasoningOption)),
|
||||
attachment: Schema.Boolean,
|
||||
toolcall: Schema.Boolean,
|
||||
input: ProviderModalities,
|
||||
|
|
@ -1181,7 +1179,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 })),
|
||||
reasoningOptions: ModelsDev.resolveReasoningOptions(model.reasoning_options),
|
||||
attachment: model.attachment ?? false,
|
||||
toolcall: model.tool_call ?? true,
|
||||
input: {
|
||||
|
|
|
|||
|
|
@ -1287,7 +1287,9 @@ test("mode cost preserves over-200k pricing from base model", () => {
|
|||
|
||||
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 = {
|
||||
|
|
@ -1310,7 +1312,11 @@ 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.reasoningOptions).toEqual([
|
||||
{ type: "toggle" },
|
||||
{ type: "effort", values: ["low", "ultrathink"] },
|
||||
{ type: "budget_tokens", min: 1024 },
|
||||
])
|
||||
expect(model.capabilities.attachment).toBe(false)
|
||||
expect(model.capabilities.toolcall).toBe(true)
|
||||
expect(model.release_date).toBe("")
|
||||
|
|
|
|||
|
|
@ -2082,9 +2082,20 @@ export type Model = {
|
|||
capabilities: {
|
||||
temperature: boolean
|
||||
reasoning: boolean
|
||||
reasoningOptions?: Array<{
|
||||
[key: string]: unknown
|
||||
}>
|
||||
reasoningOptions?: Array<
|
||||
| {
|
||||
type: "toggle"
|
||||
}
|
||||
| {
|
||||
type: "effort"
|
||||
values: Array<string>
|
||||
}
|
||||
| {
|
||||
type: "budget_tokens"
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
>
|
||||
attachment: boolean
|
||||
toolcall: boolean
|
||||
input: {
|
||||
|
|
@ -2881,9 +2892,20 @@ export type ModelV2Info = {
|
|||
}
|
||||
capabilities: {
|
||||
tools: boolean
|
||||
reasoningOptions?: Array<{
|
||||
[key: string]: unknown
|
||||
}>
|
||||
reasoningOptions?: Array<
|
||||
| {
|
||||
type: "toggle"
|
||||
}
|
||||
| {
|
||||
type: "effort"
|
||||
values: Array<string>
|
||||
}
|
||||
| {
|
||||
type: "budget_tokens"
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
>
|
||||
input: Array<string>
|
||||
output: Array<string>
|
||||
}
|
||||
|
|
@ -4236,9 +4258,20 @@ export type ModelV2Info1 = {
|
|||
}
|
||||
capabilities: {
|
||||
tools: boolean
|
||||
reasoningOptions?: Array<{
|
||||
[key: string]: unknown
|
||||
}>
|
||||
reasoningOptions?: Array<
|
||||
| {
|
||||
type: "toggle"
|
||||
}
|
||||
| {
|
||||
type: "effort"
|
||||
values: Array<string>
|
||||
}
|
||||
| {
|
||||
type: "budget_tokens"
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
>
|
||||
input: Array<string>
|
||||
output: Array<string>
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue