fix(provider): loosen models.dev reasoning options
This commit is contained in:
parent
6e3c76e86e
commit
6413d66aa2
4 changed files with 101 additions and 8 deletions
|
|
@ -44,10 +44,7 @@ const Cost = Schema.Struct({
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ReasoningEffortValue = Schema.Union([
|
const ReasoningEffortValue = Schema.Union([Schema.Null, Schema.String])
|
||||||
Schema.Null,
|
|
||||||
Schema.Literals(["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"]),
|
|
||||||
])
|
|
||||||
|
|
||||||
export const ReasoningOption = Schema.Union([
|
export const ReasoningOption = Schema.Union([
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
|
|
@ -72,7 +69,9 @@ 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)),
|
// models.dev is external metadata and reasoning controls are expected to evolve.
|
||||||
|
// Keep the fetched value opaque here; provider normalization extracts the known subset.
|
||||||
|
reasoning_options: Schema.optional(Schema.Unknown),
|
||||||
temperature: Schema.Boolean,
|
temperature: Schema.Boolean,
|
||||||
tool_call: Schema.Boolean,
|
tool_call: Schema.Boolean,
|
||||||
interleaved: Schema.optional(
|
interleaved: Schema.optional(
|
||||||
|
|
|
||||||
|
|
@ -1186,11 +1186,37 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReasoningOption = NonNullable<Model["reasoning_options"]>[number]
|
||||||
|
|
||||||
function reasoningOptions(model: ModelsDev.Model): Model["reasoning_options"] {
|
function reasoningOptions(model: ModelsDev.Model): Model["reasoning_options"] {
|
||||||
return model.reasoning_options?.map((option) => {
|
if (!Array.isArray(model.reasoning_options)) return undefined
|
||||||
if (option.type === "effort") return { ...option, values: [...option.values] }
|
const normalized = model.reasoning_options.flatMap((option) => {
|
||||||
return { ...option }
|
const normalized = normalizeReasoningOption(option)
|
||||||
|
return normalized ? [normalized] : []
|
||||||
})
|
})
|
||||||
|
return normalized.length > 0 || model.reasoning_options.length === 0 ? normalized : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReasoningOption(option: unknown): ReasoningOption | undefined {
|
||||||
|
if (!isRecord(option) || typeof option.type !== "string") return undefined
|
||||||
|
if (option.type === "effort") {
|
||||||
|
if (!Array.isArray(option.values)) return undefined
|
||||||
|
return {
|
||||||
|
type: "effort",
|
||||||
|
values: option.values.filter((value): value is string | null => value === null || typeof value === "string"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (option.type === "toggle") return { type: "toggle" }
|
||||||
|
if (option.type === "budget_tokens") {
|
||||||
|
const min = typeof option.min === "number" && Number.isFinite(option.min) ? option.min : undefined
|
||||||
|
const max = typeof option.max === "number" && Number.isFinite(option.max) ? option.max : undefined
|
||||||
|
return {
|
||||||
|
type: "budget_tokens",
|
||||||
|
...(min === undefined ? {} : { min }),
|
||||||
|
...(max === undefined ? {} : { max }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
||||||
|
|
|
||||||
|
|
@ -58,4 +58,23 @@ describe("provider model status schemas", () => {
|
||||||
}).status,
|
}).status,
|
||||||
).toBe("active")
|
).toBe("active")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("keeps models.dev reasoning options opaque for forward compatibility", () => {
|
||||||
|
expect(() =>
|
||||||
|
Schema.decodeUnknownSync(ModelsDev.Model)({
|
||||||
|
id: "test-model",
|
||||||
|
name: "Test Model",
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
attachment: false,
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: {
|
||||||
|
future: "shape",
|
||||||
|
values: [{ type: "new_control", values: ["turbo"] }],
|
||||||
|
},
|
||||||
|
temperature: true,
|
||||||
|
tool_call: true,
|
||||||
|
limit: { context: 128000, output: 8192 },
|
||||||
|
}),
|
||||||
|
).not.toThrow()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1450,6 +1450,55 @@ test("models.dev normalization preserves reasoning options for variant generatio
|
||||||
expect(model.variants!.xhigh).toEqual({ reasoningEffort: "xhigh" })
|
expect(model.variants!.xhigh).toEqual({ reasoningEffort: "xhigh" })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("models.dev normalization ignores unknown reasoning options and passes new effort strings", () => {
|
||||||
|
const provider = {
|
||||||
|
id: "custom",
|
||||||
|
name: "Custom",
|
||||||
|
env: [],
|
||||||
|
npm: "@ai-sdk/openai-compatible",
|
||||||
|
models: {
|
||||||
|
reasoner: {
|
||||||
|
id: "reasoner",
|
||||||
|
name: "Reasoner",
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: [
|
||||||
|
{ type: "future_control", values: ["turbo"] },
|
||||||
|
{ type: "effort", values: ["turbo", 123, null] },
|
||||||
|
{ type: "budget_tokens", min: "1024", max: 16_000 },
|
||||||
|
"invalid",
|
||||||
|
],
|
||||||
|
cost: { input: 1, output: 2 },
|
||||||
|
limit: { context: 128_000, output: 32_000 },
|
||||||
|
},
|
||||||
|
changed: {
|
||||||
|
id: "changed",
|
||||||
|
name: "Changed",
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: { type: "effort", values: ["turbo"] },
|
||||||
|
cost: { input: 1, output: 2 },
|
||||||
|
limit: { context: 128_000, output: 32_000 },
|
||||||
|
},
|
||||||
|
future: {
|
||||||
|
id: "future",
|
||||||
|
name: "Future",
|
||||||
|
reasoning: true,
|
||||||
|
reasoning_options: [{ type: "future_control", values: ["turbo"] }],
|
||||||
|
cost: { input: 1, output: 2 },
|
||||||
|
limit: { context: 128_000, output: 32_000 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as ModelsDev.Provider
|
||||||
|
|
||||||
|
const models = Provider.fromModelsDevProvider(provider).models
|
||||||
|
expect(models.reasoner.reasoning_options).toEqual([
|
||||||
|
{ type: "effort", values: ["turbo", null] },
|
||||||
|
{ type: "budget_tokens", max: 16_000 },
|
||||||
|
])
|
||||||
|
expect(models.reasoner.variants).toEqual({ turbo: { reasoningEffort: "turbo" } })
|
||||||
|
expect(models.changed.reasoning_options).toBeUndefined()
|
||||||
|
expect(models.future.reasoning_options).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it.instance("model variants are generated for reasoning models", () =>
|
it.instance("model variants are generated for reasoning models", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
yield* set("ANTHROPIC_API_KEY", "test-api-key")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue