366 lines
12 KiB
TypeScript
366 lines
12 KiB
TypeScript
import { describe, expect } from "bun:test"
|
|
import { Effect, Schema } from "effect"
|
|
import { HttpClientRequest } from "effect/unstable/http"
|
|
import { LLM, Message, ToolCallPart } from "../../src"
|
|
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, fixedResponse } from "../lib/http"
|
|
import { sseEvents } from "../lib/sse"
|
|
|
|
const Json = Schema.fromJsonString(Schema.Unknown)
|
|
const decodeJson = Schema.decodeUnknownSync(Json)
|
|
|
|
const model = OpenAICompatibleChat.route
|
|
.with({
|
|
provider: "deepseek",
|
|
endpoint: { baseURL: "https://api.deepseek.test/v1/", query: { "api-version": "2026-01-01" } },
|
|
auth: Auth.bearer("test-key"),
|
|
})
|
|
.model({ id: "deepseek-chat" })
|
|
|
|
const request = LLM.request({
|
|
id: "req_1",
|
|
model,
|
|
system: "You are concise.",
|
|
prompt: "Say hello.",
|
|
generation: { maxTokens: 20, temperature: 0 },
|
|
})
|
|
|
|
const deltaChunk = (delta: object, finishReason: string | null = null) => ({
|
|
id: "chatcmpl_fixture",
|
|
choices: [{ delta, finish_reason: finishReason }],
|
|
usage: null,
|
|
})
|
|
|
|
const usageChunk = (usage: object) => ({
|
|
id: "chatcmpl_fixture",
|
|
choices: [],
|
|
usage,
|
|
})
|
|
|
|
const providerFamilies = [
|
|
["baseten", OpenAICompatible.baseten, "https://inference.baseten.co/v1"],
|
|
["cerebras", OpenAICompatible.cerebras, "https://api.cerebras.ai/v1"],
|
|
["deepinfra", OpenAICompatible.deepinfra, "https://api.deepinfra.com/v1/openai"],
|
|
["deepseek", OpenAICompatible.deepseek, "https://api.deepseek.com/v1"],
|
|
["fireworks", OpenAICompatible.fireworks, "https://api.fireworks.ai/inference/v1"],
|
|
["togetherai", OpenAICompatible.togetherai, "https://api.together.xyz/v1"],
|
|
] as const
|
|
|
|
describe("OpenAI-compatible Chat route", () => {
|
|
it.effect("prepares generic Chat target", () =>
|
|
Effect.gen(function* () {
|
|
const prepared = yield* LLMClient.prepare(
|
|
LLM.updateRequest(request, {
|
|
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
|
toolChoice: { type: "required" },
|
|
}),
|
|
)
|
|
|
|
expect(prepared.route).toBe("openai-compatible-chat")
|
|
expect(prepared.model).toMatchObject({
|
|
id: "deepseek-chat",
|
|
provider: "deepseek",
|
|
route: { id: "openai-compatible-chat" },
|
|
})
|
|
expect(prepared.model.route.endpoint).toMatchObject({
|
|
baseURL: "https://api.deepseek.test/v1/",
|
|
query: { "api-version": "2026-01-01" },
|
|
})
|
|
expect(prepared.body).toEqual({
|
|
model: "deepseek-chat",
|
|
messages: [
|
|
{ role: "system", content: "You are concise." },
|
|
{ role: "user", content: "Say hello." },
|
|
],
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
function: { name: "lookup", description: "Lookup data", parameters: { type: "object" } },
|
|
},
|
|
],
|
|
tool_choice: "required",
|
|
stream: true,
|
|
stream_options: { include_usage: true },
|
|
max_tokens: 20,
|
|
temperature: 0,
|
|
})
|
|
}),
|
|
)
|
|
|
|
it.effect("provides model helpers for compatible provider families", () =>
|
|
Effect.gen(function* () {
|
|
expect(
|
|
providerFamilies.map(([provider, family]) => {
|
|
const model = family.configure({ apiKey: "test-key" }).model(`${provider}-model`)
|
|
return {
|
|
id: String(model.id),
|
|
provider: String(model.provider),
|
|
route: model.route.id,
|
|
baseURL: model.route.endpoint.baseURL,
|
|
}
|
|
}),
|
|
).toEqual(
|
|
providerFamilies.map(([provider, _, baseURL]) => ({
|
|
id: `${provider}-model`,
|
|
provider,
|
|
route: "openai-compatible-chat",
|
|
baseURL,
|
|
})),
|
|
)
|
|
|
|
const custom = OpenAICompatible.deepseek
|
|
.configure({
|
|
apiKey: "test-key",
|
|
baseURL: "https://custom.deepseek.test/v1",
|
|
})
|
|
.model("deepseek-chat")
|
|
expect(custom).toMatchObject({
|
|
provider: "deepseek",
|
|
route: { id: "openai-compatible-chat" },
|
|
})
|
|
expect(custom.route.endpoint.baseURL).toBe("https://custom.deepseek.test/v1")
|
|
}),
|
|
)
|
|
|
|
it.effect("matches AI SDK compatible basic request body fixture", () =>
|
|
Effect.gen(function* () {
|
|
const prepared = yield* LLMClient.prepare(request)
|
|
|
|
expect(prepared.body).toEqual({
|
|
model: "deepseek-chat",
|
|
messages: [
|
|
{ role: "system", content: "You are concise." },
|
|
{ role: "user", content: "Say hello." },
|
|
],
|
|
stream: true,
|
|
stream_options: { include_usage: true },
|
|
max_tokens: 20,
|
|
temperature: 0,
|
|
})
|
|
}),
|
|
)
|
|
|
|
it.effect("matches AI SDK compatible tool request body fixture", () =>
|
|
Effect.gen(function* () {
|
|
const prepared = yield* LLMClient.prepare(
|
|
LLM.request({
|
|
id: "req_tool_parity",
|
|
model,
|
|
tools: [
|
|
{
|
|
name: "lookup",
|
|
description: "Lookup data",
|
|
inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
|
|
},
|
|
],
|
|
toolChoice: "lookup",
|
|
messages: [
|
|
Message.user("What is the weather?"),
|
|
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })]),
|
|
Message.tool({ id: "call_1", name: "lookup", result: { forecast: "sunny" } }),
|
|
],
|
|
}),
|
|
)
|
|
|
|
expect(prepared.body).toEqual({
|
|
model: "deepseek-chat",
|
|
messages: [
|
|
{ role: "user", content: "What is the weather?" },
|
|
{
|
|
role: "assistant",
|
|
content: null,
|
|
tool_calls: [
|
|
{
|
|
id: "call_1",
|
|
type: "function",
|
|
function: { name: "lookup", arguments: '{"query":"weather"}' },
|
|
},
|
|
],
|
|
},
|
|
{ role: "tool", tool_call_id: "call_1", content: '{"forecast":"sunny"}' },
|
|
],
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "lookup",
|
|
description: "Lookup data",
|
|
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
|
|
},
|
|
},
|
|
],
|
|
tool_choice: { type: "function", function: { name: "lookup" } },
|
|
stream: true,
|
|
stream_options: { include_usage: true },
|
|
})
|
|
}),
|
|
)
|
|
|
|
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(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
expect(web.url).toBe("https://api.deepseek.test/v1/chat/completions?api-version=2026-01-01")
|
|
expect(web.headers.get("authorization")).toBe("Bearer test-key")
|
|
expect(decodeJson(input.text)).toMatchObject({
|
|
model: "deepseek-chat",
|
|
stream: true,
|
|
messages: [
|
|
{ role: "system", content: "You are concise." },
|
|
{ role: "user", content: "Say hello." },
|
|
],
|
|
})
|
|
return input.respond(
|
|
sseEvents(
|
|
deltaChunk({ role: "assistant", content: "Hello" }),
|
|
deltaChunk({ content: "!" }),
|
|
deltaChunk({}, "stop"),
|
|
usageChunk({ prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 }),
|
|
),
|
|
{ headers: { "content-type": "text/event-stream" } },
|
|
)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
expect(response.text).toBe("Hello!")
|
|
expect(response.usage).toMatchObject({ inputTokens: 5, outputTokens: 2, totalTokens: 7 })
|
|
expect(response.events.at(-1)).toMatchObject({ type: "finish", reason: "stop" })
|
|
}),
|
|
)
|
|
})
|