fix(llm): preserve native provider options
This commit is contained in:
parent
7fe7b9f258
commit
6aaaac27d0
12 changed files with 630 additions and 107 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue