fix(llm): preserve native provider options

This commit is contained in:
Aiden Cline 2026-05-23 15:42:28 -05:00
commit 6aaaac27d0
12 changed files with 630 additions and 107 deletions

View file

@ -209,6 +209,80 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("lowers Anthropic thinking provider option (enabled)", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
prompt: "think",
providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 12345 } } },
}),
)
expect(prepared.body).toMatchObject({ thinking: { type: "enabled", budget_tokens: 12345 } })
}),
)
it.effect("lowers Anthropic adaptive thinking with effort sibling", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
prompt: "think",
providerOptions: { anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "max" } },
}),
)
expect(prepared.body).toMatchObject({
thinking: { type: "adaptive", display: "summarized" },
effort: "max",
})
}),
)
it.effect("sets per-tool eager_input_streaming only when toolStreaming is true", () =>
Effect.gen(function* () {
const off = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({
model,
prompt: "use tool",
providerOptions: { anthropic: { toolStreaming: false } },
tools: [{ name: "lookup", description: "lookup", inputSchema: { type: "object" } }],
}),
)
expect(off.body.tools?.[0]?.eager_input_streaming).toBeUndefined()
const on = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({
model,
prompt: "use tool",
providerOptions: { anthropic: { toolStreaming: true } },
tools: [{ name: "lookup", description: "lookup", inputSchema: { type: "object" } }],
}),
)
expect(on.body.tools?.[0]?.eager_input_streaming).toBe(true)
}),
)
it.effect("passes unknown Anthropic provider options through with snake-cased keys", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
prompt: "go",
providerOptions: {
anthropic: {
anthropicBeta: ["claude-2024-07-15"],
customField: { keepCamelCase: true },
},
},
}),
)
expect(prepared.body).toMatchObject({
anthropic_beta: ["claude-2024-07-15"],
custom_field: { keepCamelCase: true },
})
}),
)
it.effect("lowers preserved Anthropic reasoning signature metadata", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(

View file

@ -6,7 +6,7 @@ import { Auth, LLMClient } from "../../src/route"
import * as OpenAICompatible from "../../src/providers/openai-compatible"
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
import { it } from "../lib/effect"
import { dynamicResponse } from "../lib/http"
import { dynamicResponse, fixedResponse } from "../lib/http"
import { sseEvents } from "../lib/sse"
const Json = Schema.fromJsonString(Schema.Unknown)
@ -199,6 +199,134 @@ describe("OpenAI-compatible Chat route", () => {
}),
)
it.effect("passes through compatible options and prior reasoning for tool continuations", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
providerOptions: {
deepseek: {
reasoningEffort: "max",
textVerbosity: "low",
promptCacheKey: "session_123",
strictJsonSchema: false,
enable_thinking: true,
},
},
messages: [
Message.user("Audit the site"),
Message.make({
role: "assistant",
native: { openaiCompatible: { reasoning_content: "I should inspect the page." } },
content: [ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "page" } })],
}),
],
}),
)
expect(prepared.body).toMatchObject({
reasoning_effort: "max",
verbosity: "low",
prompt_cache_key: "session_123",
enable_thinking: true,
messages: [
{ role: "user", content: "Audit the site" },
{
role: "assistant",
reasoning_content: "I should inspect the page.",
tool_calls: [
{
id: "call_1",
function: { name: "lookup", arguments: '{"query":"page"}' },
},
],
},
],
})
expect(prepared.body).not.toHaveProperty("strictJsonSchema")
}),
)
it.effect("preserves structured reasoning_details on compatible continuations", () =>
Effect.gen(function* () {
const details = [
{ type: "reasoning.text", text: "Let me work through this.", format: "anthropic-claude-v1", index: 0 },
{ type: "reasoning.encrypted", data: "sha256:abc123", format: "anthropic-claude-v1", index: 1 },
]
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
messages: [
Message.make({
role: "assistant",
native: { openaiCompatible: { reasoning_details: details } },
content: [ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })],
}),
],
}),
)
expect(prepared.body).toMatchObject({
messages: [{ role: "assistant", reasoning_details: details }],
})
}),
)
it.effect("resolves dot-scoped compatible provider options", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model: OpenAICompatibleChat.route
.with({ provider: "opencode.internal", endpoint: { baseURL: "https://api.example.test/v1" } })
.model({ id: "reasoning-model" }),
prompt: "Think.",
providerOptions: { opencode: { reasoningEffort: "max", enable_thinking: true } },
}),
)
expect(prepared.body).toMatchObject({ reasoning_effort: "max", enable_thinking: true })
}),
)
it.effect("does not apply OpenAI effort limits to compatible providers", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model: OpenAICompatibleChat.route
.with({ provider: "openai", endpoint: { baseURL: "https://compatible.example.test/v1" } })
.model({ id: "reasoning-model" }),
prompt: "Think.",
providerOptions: { openai: { reasoningEffort: "max" } },
}),
)
expect(prepared.body).toMatchObject({ reasoning_effort: "max" })
}),
)
it.effect("parses compatible reasoning field variants", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
deltaChunk({ reasoning: "fallback" }),
deltaChunk({
reasoning_details: [
{ type: "reasoning.text", text: " text-detail", format: "anthropic-claude-v1", index: 0 },
{ type: "reasoning.summary", summary: " summary-detail", format: "anthropic-claude-v1", index: 1 },
],
}),
deltaChunk({}, "stop"),
),
),
),
)
expect(response.reasoning).toBe("fallback text-detail summary-detail")
}),
)
it.effect("posts to the configured compatible endpoint and parses text usage", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(

View file

@ -407,6 +407,30 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("passes unknown OpenAI provider options through with snake-cased keys", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
prompt: "passthrough",
providerOptions: {
openai: {
customCamelCaseField: "value",
already_snake_case: 42,
nested: { keepCamelCase: true },
},
},
}),
)
expect(prepared.body).toMatchObject({
custom_camel_case_field: "value",
already_snake_case: 42,
nested: { keepCamelCase: true },
})
}),
)
it.effect("request OpenAI provider options override route defaults", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(