| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- import { describe, expect, test } from "bun:test"
- import { Effect, Ref, Schema } from "effect"
- import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
- import { LLM, mergeProviderOptions } from "../src"
- import { AnthropicMessages, OpenAIChat } from "../src/protocols"
- import { Auth, LLMClient } from "../src/route"
- import { compileRequest } from "../src/route/client"
- import { it } from "./lib/effect"
- import { dynamicResponse } from "./lib/http"
- import { deltaChunk } from "./lib/openai-chunks"
- import { sseEvents } from "./lib/sse"
- const TargetJson = Schema.fromJsonString(Schema.Unknown)
- const decodeJson = Schema.decodeUnknownSync(TargetJson)
- describe("request option precedence", () => {
- test("deep-merges provider option records and replaces arrays, primitives, and null", () => {
- const merged = mergeProviderOptions(
- {
- openai: {
- include: ["route"],
- metadata: { route: true, shared: "route" },
- nullable: "route",
- primitive: "route",
- },
- },
- {
- openai: {
- include: ["model"],
- metadata: { model: true, shared: "model" },
- nullable: null,
- primitive: "model",
- },
- },
- { openai: { metadata: { request: true }, primitive: false } },
- )
- expect(merged).toEqual({
- openai: {
- include: ["model"],
- metadata: { route: true, model: true, request: true, shared: "model" },
- nullable: null,
- primitive: false,
- },
- })
- })
- it.effect("compiles bodies with route defaults, model defaults, and call options in order", () =>
- Effect.gen(function* () {
- const route = OpenAIChat.route.with({
- endpoint: { baseURL: "https://api.openai.test/v1/" },
- auth: Auth.bearer("test"),
- generation: { maxTokens: 10, temperature: 1, stop: ["route"] },
- providerOptions: { openai: { store: false, reasoningEffort: "low" } },
- })
- const model = route.model({
- id: "gpt-4o-mini",
- defaults: {
- generation: { maxTokens: 20, temperature: 0.5, frequencyPenalty: 0.25, stop: ["model"] },
- providerOptions: { openai: { reasoningEffort: "medium" } },
- },
- })
- const prepared = yield* compileRequest(
- LLM.request({
- model,
- prompt: "Say hello.",
- generation: { maxTokens: 30, topP: 0.9, stop: ["request"] },
- providerOptions: { openai: { store: true } },
- }),
- )
- expect(prepared.body).toMatchObject({
- model: "gpt-4o-mini",
- stream: true,
- max_tokens: 30,
- temperature: 0.5,
- top_p: 0.9,
- frequency_penalty: 0.25,
- store: true,
- reasoning_effort: "medium",
- })
- expect(prepared.body.stop).toEqual(["request"])
- }),
- )
- it.effect("applies model HTTP defaults before request HTTP overlays", () =>
- LLMClient.generate(
- LLM.request({
- model: OpenAIChat.route
- .with({
- endpoint: { baseURL: "https://api.openai.test/v1/" },
- auth: Auth.bearer("fresh-key"),
- http: {
- body: { metadata: { route: true, shared: "route" }, value: "route" },
- headers: { "x-route": "route", "x-shared": "route" },
- query: { route: "1", shared: "route" },
- },
- })
- .model({
- id: "gpt-4o-mini",
- defaults: {
- http: {
- body: { metadata: { model: true, shared: "model" }, value: "model" },
- headers: { "x-model": "model", "x-shared": "model" },
- query: { model: "1", shared: "model" },
- },
- },
- }),
- prompt: "Say hello.",
- http: {
- body: { metadata: { request: true }, value: null },
- headers: { "x-request": "request" },
- query: { request: "1" },
- },
- }),
- ).pipe(
- Effect.provide(
- dynamicResponse((input) =>
- Effect.gen(function* () {
- const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
- expect(web.url).toBe("https://api.openai.test/v1/chat/completions?route=1&shared=model&model=1&request=1")
- expect(web.headers.get("authorization")).toBe("Bearer fresh-key")
- expect(web.headers.get("x-route")).toBe("route")
- expect(web.headers.get("x-model")).toBe("model")
- expect(web.headers.get("x-request")).toBe("request")
- expect(web.headers.get("x-shared")).toBe("model")
- expect(decodeJson(input.text)).toMatchObject({
- metadata: { route: true, model: true, request: true, shared: "model" },
- value: null,
- })
- return input.respond(sseEvents(deltaChunk({}, "stop")), {
- headers: { "content-type": "text/event-stream" },
- })
- }),
- ),
- ),
- ),
- )
- it.effect("transforms the final HTTP request after serialization and authentication", () =>
- LLMClient.generate(
- LLM.request({
- model: OpenAIChat.route
- .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("fresh-key") })
- .model({ id: "gpt-4o-mini" }),
- prompt: "Say hello.",
- }),
- {
- http: (request, handler) =>
- Effect.gen(function* () {
- return yield* handler(
- request.pipe(
- HttpClientRequest.setUrl("https://proxy.test/v1/chat/completions"),
- HttpClientRequest.setMethod("PUT"),
- HttpClientRequest.setHeader("x-plugin", "transformed"),
- HttpClientRequest.bodyText(JSON.stringify({ transformed: true }), "application/custom+json"),
- ),
- )
- }),
- },
- ).pipe(
- Effect.provide(
- dynamicResponse((input) =>
- Effect.gen(function* () {
- const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
- expect(web.url).toBe("https://proxy.test/v1/chat/completions")
- expect(web.method).toBe("PUT")
- expect(web.headers.get("x-plugin")).toBe("transformed")
- expect(web.headers.get("content-type")).toBe("application/custom+json")
- expect(decodeJson(input.text)).toEqual({ transformed: true })
- return input.respond(sseEvents(deltaChunk({}, "stop")), {
- headers: { "content-type": "text/event-stream" },
- })
- }),
- ),
- ),
- ),
- )
- it.effect("transforms the HTTP response before protocol decoding", () =>
- Effect.gen(function* () {
- const response = yield* LLMClient.generate(
- LLM.request({
- model: OpenAIChat.route
- .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
- .model({ id: "gpt-4o-mini" }),
- prompt: "Say hello.",
- }),
- {
- http: (request, handler) =>
- Effect.gen(function* () {
- const response = yield* handler(request)
- return HttpClientResponse.fromWeb(
- response.request,
- new Response((yield* response.text).replace("network", "hooked"), {
- status: response.status,
- headers: response.headers,
- }),
- )
- }),
- },
- ).pipe(
- Effect.provide(
- dynamicResponse((input) =>
- Effect.succeed(
- input.respond(sseEvents(deltaChunk({ content: "network" }, "stop")), {
- headers: { "content-type": "text/event-stream" },
- }),
- ),
- ),
- ),
- )
- expect(response.text).toBe("hooked")
- }),
- )
- it.effect("can inspect an error response and retry the native request", () =>
- Effect.gen(function* () {
- const attempts = yield* Ref.make(0)
- const response = yield* LLMClient.generate(
- LLM.request({
- model: OpenAIChat.route
- .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("stale") })
- .model({ id: "gpt-4o-mini" }),
- prompt: "Say hello.",
- }),
- {
- http: (request, handler) =>
- Effect.gen(function* () {
- const response = yield* handler(request)
- expect(response.status).toBe(401)
- return yield* handler(HttpClientRequest.setHeader(request, "authorization", "Bearer refreshed"))
- }),
- },
- ).pipe(
- Effect.provide(
- dynamicResponse((input) =>
- Effect.gen(function* () {
- yield* Ref.update(attempts, (value) => value + 1)
- if (input.request.headers.authorization !== "Bearer refreshed")
- return input.respond("unauthorized", { status: 401 })
- return input.respond(sseEvents(deltaChunk({ content: "retried" }, "stop")), {
- headers: { "content-type": "text/event-stream" },
- })
- }),
- ),
- ),
- )
- expect(response.text).toBe("retried")
- expect(yield* Ref.get(attempts)).toBe(2)
- }),
- )
- it.effect("applies raw body overlays after protocol lowering", () =>
- LLMClient.generate(
- LLM.request({
- model: OpenAIChat.route
- .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
- .model({ id: "gpt-4o-mini" }),
- prompt: "Say hello.",
- http: { body: { model: "gpt-5", messages: [], tools: [] } },
- }),
- ).pipe(
- Effect.provide(
- dynamicResponse((input) =>
- Effect.gen(function* () {
- expect(decodeJson(input.text)).toMatchObject({ model: "gpt-5", messages: [], tools: [] })
- return input.respond(sseEvents(deltaChunk({}, "stop")), {
- headers: { "content-type": "text/event-stream" },
- })
- }),
- ),
- ),
- ),
- )
- it.effect("uses model output limits after route limits and before call maxTokens", () =>
- Effect.gen(function* () {
- const route = AnthropicMessages.route.with({
- endpoint: { baseURL: "https://api.anthropic.test/v1/" },
- auth: Auth.header("x-api-key", "test"),
- limits: { output: 128 },
- })
- const model = route.model({ id: "claude-sonnet-4-5", defaults: { limits: { output: 64 } } })
- const withoutMaxTokens = yield* compileRequest(LLM.request({ model, prompt: "Say hello.", cache: "none" }))
- const withMaxTokens = yield* compileRequest(
- LLM.request({ model, prompt: "Say hello.", cache: "none", generation: { maxTokens: 32 } }),
- )
- expect(withoutMaxTokens.body.max_tokens).toBe(64)
- expect(withMaxTokens.body.max_tokens).toBe(32)
- }),
- )
- })
|