diff --git a/packages/ai/AGENTS.md b/packages/ai/AGENTS.md
index 6c35cafc34..ad4a625385 100644
--- a/packages/ai/AGENTS.md
+++ b/packages/ai/AGENTS.md
@@ -46,7 +46,7 @@ const response = yield * LLMClient.generate(request)
`LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` reads the executable route carried by `request.model.route`, builds the provider-native body, asks the route's transport for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`.
-Use `LLMClient.stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.generate(request)` when callers want those same events collected into an `LLMResponse`. Use `LLMClient.prepare
(request)` to compile a request through the route pipeline without sending it — the optional `Body` type argument narrows `.body` to the route's native shape (e.g. `prepare(...)` returns a `PreparedRequestOf`). The runtime body is identical; the generic is a type-level assertion.
+Use `LLMClient.stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.generate(request)` when callers want those same events collected into an `LLMResponse`.
Filter or narrow `LLMEvent` streams with `LLMEvent.is.*` (camelCase guards, e.g. `events.filter(LLMEvent.is.toolCall)`). The kebab-case `LLMEvent.guards["tool-call"]` form also works but prefer `is.*` in new code.
@@ -138,13 +138,13 @@ packages/ai/src/
ids.ts branded IDs, literal types, ProviderMetadata
options.ts Generation/Provider/Http options, Limits, Model, cache policy
messages.ts content parts, Message, ToolDefinition, LLMRequest
- events.ts Usage, individual events, LLMEvent, PreparedRequest, LLMResponse
+ events.ts Usage, individual events, LLMEvent, LLMResponse
errors.ts error reasons, LLMError, ToolFailure
index.ts barrel
llm.ts request constructors and convenience helpers
route/
index.ts @opencode-ai/ai/route advanced barrel
- client.ts Route.make + LLMClient.prepare/stream/generate
+ client.ts Route.make + LLMClient.stream/generate
executor.ts RequestExecutor service + transport error mapping
protocol.ts Protocol type + Protocol.make
endpoint.ts Endpoint type + Endpoint.path
diff --git a/packages/ai/README.md b/packages/ai/README.md
index 377e09306f..6d89fa317b 100644
--- a/packages/ai/README.md
+++ b/packages/ai/README.md
@@ -196,7 +196,6 @@ The hosted result is represented as a provider-executed tool call and tool resul
- **`LLM.generate` / `LLM.stream`** — re-exported from `LLMClient` for one-import use.
- **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model.
- **`Model.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model.
-- **`LLMClient.prepare(request)`** — compile a request through protocol body construction, validation, and HTTP preparation without sending. Useful for inspection and testing.
- **`LLMEvent.is.*`** — typed guards (`is.textDelta`, `is.toolCall`, `is.finish`, …) for filtering streams.
- **`Image.generate({...})`** — generate images through a provider-neutral image request and response model.
- **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.
diff --git a/packages/ai/example/call-sites.md b/packages/ai/example/call-sites.md
index 0b33d28c48..397e0cc09f 100644
--- a/packages/ai/example/call-sites.md
+++ b/packages/ai/example/call-sites.md
@@ -568,7 +568,7 @@ App boundary = explicit durable-config -> typed-provider call
calling `.model(...)`.
- [x] Remove request-shaping defaults from `Model`; selected models now carry only
id, provider, and configured route while defaults live on routes or requests.
-- [x] Rework `LLMClient.prepare` / `stream` / `generate` to read
+- [x] Rework `LLMClient.stream` / `generate` to read
`request.model.route` directly instead of calling `registeredRoute(...)`.
- [x] Remove `Route.make(...)` global registration from the normal execution
path; keep route ids only as diagnostics/provider API labels.
diff --git a/packages/ai/example/tutorial.ts b/packages/ai/example/tutorial.ts
index 7ee4abb146..3924a57dd2 100644
--- a/packages/ai/example/tutorial.ts
+++ b/packages/ai/example/tutorial.ts
@@ -50,18 +50,6 @@ const request = LLM.request({
},
})
-// `http` is intentionally not needed for normal calls. This shows the shape for
-// newly released provider fields before they deserve a typed provider option.
-const rawOverlayExample = LLM.request({
- model,
- prompt: "Show the final HTTP overlay shape.",
- http: {
- body: { metadata: { example: "tutorial" } },
- headers: { "x-opencode-tutorial": "1" },
- query: { debug: "1" },
- },
-})
-
// 3. `generate` sends the request and collects the event stream into one
// response object. `response.text` is the collected text output.
const generateOnce = Effect.gen(function* () {
@@ -222,33 +210,15 @@ const FakeEcho = {
}),
}
-// `LLMClient.prepare` is the lower-level inspection hook: it compiles through
-// body conversion, validation, endpoint, auth, and HTTP construction without
-// sending anything over the network.
-const inspectFakeProvider = Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
- LLM.request({
- model: FakeEcho.configure().model("tiny-echo"),
- prompt: "Show me the provider pipeline.",
- }),
- )
-
- console.log("\n== fake provider prepare ==")
- console.log("route:", prepared.route)
- console.log("body:", Formatter.formatJson(prepared.body, { space: 2 }))
-})
-
// Provide the LLM runtime and the HTTP request executor once. Keep one path
-// enabled at a time so the tutorial can demonstrate generate, prepare, stream,
-// or tool-loop behavior without spending tokens on every example.
+// enabled at a time so the tutorial can demonstrate generate, stream, or
+// tool-loop behavior without spending tokens on every example.
const requestExecutorLayer = RequestExecutor.fetchLayer
const llmDeps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer)
const llmClientLayer = LLMClient.layer.pipe(Layer.provide(llmDeps))
const program = Effect.gen(function* () {
// yield* generateOnce
- // yield* inspectFakeProvider
- // yield* LLMClient.prepare(rawOverlayExample).pipe(Effect.andThen((prepared) => Effect.sync(() => console.log(prepared.body))))
// yield* streamText
// yield* generateStructuredObject
// yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object))))
diff --git a/packages/ai/src/route/client.ts b/packages/ai/src/route/client.ts
index 067292329b..048f82f554 100644
--- a/packages/ai/src/route/client.ts
+++ b/packages/ai/src/route/client.ts
@@ -10,7 +10,7 @@ import { WebSocketExecutor } from "./transport"
import type { Protocol } from "./protocol"
import { applyCachePolicy } from "../cache-policy"
import * as ProviderShared from "../protocols/shared"
-import type { LLMError, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema"
+import type { LLMError, ProtocolID, ProviderOptions } from "../schema"
import {
GenerationOptions,
HttpOptions,
@@ -20,7 +20,6 @@ import {
ModelLimits,
LLMError as LLMErrorClass,
LLMEvent,
- PreparedRequest,
ProviderID,
mergeGenerationOptions,
mergeHttpOptions,
@@ -142,17 +141,6 @@ export const httpOptions = (input: HttpOptionsInput | undefined) => {
}
export interface Interface {
- /**
- * Compile a request through protocol body construction, validation, and HTTP
- * preparation without sending it. Returns the prepared request including the
- * provider-native body.
- *
- * Pass a `Body` type argument to statically expose the route's body
- * shape (e.g. `prepare(...)`) — the runtime body is
- * identical, so this is a type-level assertion the caller makes about which
- * route the request will resolve to.
- */
- readonly prepare: (request: LLMRequest) => Effect.Effect, LLMError>
readonly stream: StreamMethod
readonly generate: GenerateMethod
}
@@ -370,9 +358,6 @@ export function make(
})
}
-// `compile` is the important boundary: it turns a common `LLMRequest` into a
-// validated provider body plus transport-private prepared data, but does not
-// execute transport.
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
const resolved = applyCachePolicy(resolveRequestOptions(request))
const route = resolved.model.route
@@ -390,17 +375,17 @@ const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
}
})
-const prepareWith = Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) {
+/** @internal Test-only projection of the execution compiler; not exported from package barrels. */
+export const compileRequest = Effect.fn("LLM.compileRequest")(function* (request: LLMRequest) {
const compiled = yield* compile(request)
-
- return new PreparedRequest({
+ return {
id: compiled.request.id ?? "request",
route: compiled.route.id,
protocol: compiled.route.protocol,
model: compiled.request.model,
body: compiled.body,
metadata: { transport: compiled.route.transport.id },
- })
+ }
})
const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) =>
@@ -422,9 +407,6 @@ const generateWith = (stream: Interface["stream"]) =>
)
})
-export const prepare = (request: LLMRequest) =>
- prepareWith(request) as Effect.Effect, LLMError>
-
export function stream(request: LLMRequest): Stream.Stream {
return Stream.unwrap(
Effect.gen(function* () {
@@ -453,7 +435,7 @@ export const layer: Layer.Layer = Layer
http: yield* RequestExecutor.Service,
webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)),
})
- return Service.of({ prepare: prepareWith as Interface["prepare"], stream, generate: generateWith(stream) })
+ return Service.of({ stream, generate: generateWith(stream) })
}),
)
@@ -462,7 +444,6 @@ export const Route = { make } as const
export const LLMClient = {
Service,
layer,
- prepare,
stream,
generate,
} as const
diff --git a/packages/ai/src/schema/events.ts b/packages/ai/src/schema/events.ts
index 654a892859..e1b9251c02 100644
--- a/packages/ai/src/schema/events.ts
+++ b/packages/ai/src/schema/events.ts
@@ -1,6 +1,5 @@
import { Schema } from "effect"
-import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids"
-import { ModelSchema } from "./options"
+import { ContentBlockID, FinishReason, ProviderMetadata, ToolCallID } from "./ids"
import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages"
import { ProviderFailureClassification } from "./errors"
@@ -314,29 +313,6 @@ export const LLMEvent = Object.assign(llmEventTagged, {
})
export type LLMEvent = Schema.Schema.Type
-export class PreparedRequest extends Schema.Class("LLM.PreparedRequest")({
- id: Schema.String,
- route: RouteID,
- protocol: ProtocolID,
- model: ModelSchema,
- body: Schema.Unknown,
- metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
-}) {}
-
-/**
- * A `PreparedRequest` whose `body` is typed as `Body`. Use with the generic
- * on `LLMClient.prepare(...)` when the caller knows which route their
- * request will resolve to and wants its native shape statically exposed
- * (debug UIs, request previews, plan rendering).
- *
- * The runtime body is identical — the route still emits `body: unknown` — so
- * this is a type-level assertion the caller makes about what they expect to
- * find. The prepare runtime does not validate the assertion.
- */
-export type PreparedRequestOf = Omit & {
- readonly body: Body
-}
-
const responseText = (events: ReadonlyArray) =>
events
.filter(LLMEvent.is.textDelta)
diff --git a/packages/ai/src/testing.ts b/packages/ai/src/testing.ts
index 69619b96e5..dbddb8d988 100644
--- a/packages/ai/src/testing.ts
+++ b/packages/ai/src/testing.ts
@@ -99,7 +99,6 @@ export const layer = (options: LayerOptions = {}) =>
)
}) as LLMClientShape["stream"]
const client = LLMClient.Service.of({
- prepare: () => Effect.die("TestLLM does not prepare provider-native requests"),
stream,
generate: (request) =>
stream(request).pipe(
diff --git a/packages/ai/test/adapter.test.ts b/packages/ai/test/adapter.test.ts
index b2b180a2b2..9409d04a17 100644
--- a/packages/ai/test/adapter.test.ts
+++ b/packages/ai/test/adapter.test.ts
@@ -2,6 +2,7 @@ import { describe, expect } from "bun:test"
import { Effect, Schema, Stream } from "effect"
import { LLM, LLMRequest, LLMResponse } from "../src"
import { Route, Endpoint, LLMClient, Protocol, type FramingDef } from "../src/route"
+import { compileRequest } from "../src/route/client"
import { Model } from "../src/schema"
import { testEffect } from "./lib/effect"
import { dynamicResponse } from "./lib/http"
@@ -139,8 +140,7 @@ describe("llm route", () => {
it.effect("selects routes by model route value", () =>
Effect.gen(function* () {
- const llm = yield* LLMClient.Service
- const prepared = yield* llm.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(request, { model: updateModel(request.model, { route: configuredGemini }) }),
)
@@ -173,7 +173,7 @@ describe("llm route", () => {
framing: fakeFraming,
})
- const prepared = yield* (yield* LLMClient.Service).prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(request, { model: updateModel(request.model, { route: duplicate }) }),
)
diff --git a/packages/ai/test/cache-policy.test.ts b/packages/ai/test/cache-policy.test.ts
index 8e2535b54b..a862b8b65d 100644
--- a/packages/ai/test/cache-policy.test.ts
+++ b/packages/ai/test/cache-policy.test.ts
@@ -1,7 +1,8 @@
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CacheHint, LLM, Message } from "../src"
-import { Auth, LLMClient } from "../src/route"
+import { Auth } from "../src/route"
+import { compileRequest } from "../src/route/client"
import { AmazonBedrock } from "../src/providers"
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
import * as Gemini from "../src/protocols/gemini"
@@ -31,7 +32,7 @@ const geminiModel = Gemini.route
describe("applyCachePolicy", () => {
it.effect("undefined cache resolves to 'auto' (the recommended default)", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: anthropicModel,
system: "You are concise.",
@@ -50,7 +51,7 @@ describe("applyCachePolicy", () => {
it.effect("'auto' marks the last tool, first and last system parts, and final message boundary on Anthropic", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: anthropicModel,
system: [
@@ -87,7 +88,7 @@ describe("applyCachePolicy", () => {
it.effect("'auto' is a no-op on OpenAI (implicit caching protocol)", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: openaiModel,
system: "Sys",
@@ -106,7 +107,7 @@ describe("applyCachePolicy", () => {
it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: geminiModel,
system: "Sys",
@@ -123,7 +124,7 @@ describe("applyCachePolicy", () => {
it.effect("'auto' on Bedrock emits cachePoint markers in the right places", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: bedrockModel,
system: [
@@ -157,7 +158,7 @@ describe("applyCachePolicy", () => {
it.effect("'none' disables auto placement even when manual hints exist", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: anthropicModel,
system: "Sys",
@@ -176,7 +177,7 @@ describe("applyCachePolicy", () => {
it.effect("granular object form: tools-only marks just tools", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: anthropicModel,
system: "Sys",
@@ -195,7 +196,7 @@ describe("applyCachePolicy", () => {
it.effect("auto policy preserves manual CacheHints on other parts", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: anthropicModel,
system: [
@@ -241,7 +242,7 @@ describe("applyCachePolicy", () => {
expect("cache" in tail ? tail.cache : undefined).toBeUndefined()
expect(applyCachePolicy(applied)).toBe(applied)
- const prepared = yield* LLMClient.prepare(request)
+ const prepared = yield* compileRequest(request)
const body = prepared.body as {
tools: Array<{ cache_control?: unknown }>
@@ -261,7 +262,7 @@ describe("applyCachePolicy", () => {
it.effect("ttlSeconds in the policy flows through to wire markers", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: anthropicModel,
system: "Sys",
@@ -278,7 +279,7 @@ describe("applyCachePolicy", () => {
it.effect("messages: { tail: 2 } marks the last 2 message boundaries", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: anthropicModel,
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2"), Message.assistant("a2")],
@@ -296,7 +297,7 @@ describe("applyCachePolicy", () => {
it.effect("'latest-assistant' marks the last assistant message", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: anthropicModel,
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2")],
diff --git a/packages/ai/test/prepare.test.ts b/packages/ai/test/compile.test.ts
similarity index 93%
rename from packages/ai/test/prepare.test.ts
rename to packages/ai/test/compile.test.ts
index 6923c5a678..b64b173b0e 100644
--- a/packages/ai/test/prepare.test.ts
+++ b/packages/ai/test/compile.test.ts
@@ -4,6 +4,7 @@ import { HttpClientRequest } 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"
@@ -44,7 +45,7 @@ describe("request option precedence", () => {
})
})
- it.effect("prepares bodies with route defaults, model defaults, and call options in order", () =>
+ 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/" },
@@ -59,7 +60,7 @@ describe("request option precedence", () => {
providerOptions: { openai: { reasoningEffort: "medium" } },
},
})
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Say hello.",
@@ -141,7 +142,7 @@ describe("request option precedence", () => {
const model = OpenAIChat.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
.model({ id: "gpt-4o-mini" })
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
model,
prompt: "Say hello.",
@@ -164,10 +165,8 @@ describe("request option precedence", () => {
limits: { output: 128 },
})
const model = route.model({ id: "claude-sonnet-4-5", defaults: { limits: { output: 64 } } })
- const withoutMaxTokens = yield* LLMClient.prepare(
- LLM.request({ model, prompt: "Say hello.", cache: "none" }),
- )
- const withMaxTokens = yield* LLMClient.prepare(
+ 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 } }),
)
diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts
index af1b3d99c8..920978258c 100644
--- a/packages/ai/test/provider/anthropic-messages.test.ts
+++ b/packages/ai/test/provider/anthropic-messages.test.ts
@@ -3,6 +3,7 @@ import { Effect } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
import { CacheHint, LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
import { Auth, LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import * as AnthropicMessages from "../../src/protocols/anthropic-messages"
import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios"
import { it } from "../lib/effect"
@@ -44,7 +45,7 @@ const expectToolResult = (body: AnthropicMessages.AnthropicMessagesBody): Anthro
describe("Anthropic Messages route", () => {
it.effect("prepares Anthropic Messages target", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(request)
+ const prepared = yield* compileRequest(request)
expect(prepared.body).toEqual({
model: "claude-sonnet-4-5",
@@ -59,7 +60,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers adaptive thinking settings with effort", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(request, {
providerOptions: {
anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
@@ -76,17 +77,17 @@ describe("Anthropic Messages route", () => {
it.effect("normalizes enabled and disabled thinking settings", () =>
Effect.gen(function* () {
- const enabled = yield* LLMClient.prepare(
+ const enabled = yield* compileRequest(
LLMRequest.update(request, {
providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 } } },
}),
)
- const legacy = yield* LLMClient.prepare(
+ const legacy = yield* compileRequest(
LLMRequest.update(request, {
providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2_048 } } },
}),
)
- const disabled = yield* LLMClient.prepare(
+ const disabled = yield* compileRequest(
LLMRequest.update(request, {
providerOptions: { anthropic: { thinking: { type: "disabled" } } },
}),
@@ -100,7 +101,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects enabled thinking without a budget", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLMRequest.update(request, {
providerOptions: { anthropic: { thinking: { type: "enabled" } } },
}),
@@ -112,7 +113,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: opus48,
messages: [
@@ -137,7 +138,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers chronological system updates to wrapped user text for unsupported Anthropic models", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -164,7 +165,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects non-text chronological system update content before send", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
model: opus48,
messages: [
@@ -181,7 +182,7 @@ describe("Anthropic Messages route", () => {
it.effect("falls back for unsupported native chronological system update placement", () =>
Effect.gen(function* () {
expect(
- (yield* LLMClient.prepare(
+ (yield* compileRequest(
LLM.request({
model: opus48,
messages: [Message.assistant("Plain."), Message.system("After plain assistant.")],
@@ -196,12 +197,11 @@ describe("Anthropic Messages route", () => {
},
])
expect(
- (yield* LLMClient.prepare(
- LLM.request({ model: opus48, messages: [Message.system("First.")], cache: "none" }),
- )).body.messages,
+ (yield* compileRequest(LLM.request({ model: opus48, messages: [Message.system("First.")], cache: "none" })))
+ .body.messages,
).toEqual([{ role: "user", content: [{ type: "text", text: "\nFirst.\n" }] }])
expect(
- (yield* LLMClient.prepare(
+ (yield* compileRequest(
LLM.request({
model: opus48,
messages: [Message.user("Before."), Message.system("One."), Message.system("Two.")],
@@ -223,7 +223,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects a system update between a local tool call and its result", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
model: opus48,
messages: [
@@ -242,7 +242,7 @@ describe("Anthropic Messages route", () => {
it.effect("prepares tool call and tool result messages", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result",
model,
@@ -273,7 +273,7 @@ describe("Anthropic Messages route", () => {
it.effect("keeps tools and sends tool_choice none", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_choice_none",
model,
@@ -303,7 +303,7 @@ describe("Anthropic Messages route", () => {
// not JSON-stringified into `tool_result.content`.
it.effect("lowers media tool-result content as structured blocks", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result_image",
model,
@@ -335,7 +335,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers single-image tool-result content as a structured image block", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result_image_only",
model,
@@ -360,7 +360,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects unsupported media in tool-result content with a clear error", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
id: "req_tool_result_unsupported_media",
model,
@@ -384,7 +384,7 @@ describe("Anthropic Messages route", () => {
it.effect("prepares the composed native continuation request", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
continuationRequest({
id: "req_native_continuation_anthropic",
model,
@@ -428,7 +428,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers preserved Anthropic reasoning signature metadata", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -447,7 +447,7 @@ describe("Anthropic Messages route", () => {
it.effect("round-trips redacted thinking as redacted_thinking blocks", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -628,9 +628,7 @@ describe("Anthropic Messages route", () => {
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
])
- const prepared = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message], cache: "none" }),
- )
+ const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message], cache: "none" }))
expect(prepared.body.messages).toEqual([
{ role: "assistant", content: [{ type: "thinking", thinking: "", signature: "sig_1" }] },
])
@@ -773,7 +771,7 @@ describe("Anthropic Messages route", () => {
),
),
)
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -1133,7 +1131,7 @@ describe("Anthropic Messages route", () => {
it.effect("round-trips provider-executed assistant content into server tool blocks", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_round_trip",
model,
@@ -1184,7 +1182,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects round-trip for unknown server tool names", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
id: "req_unknown_server_tool",
model,
@@ -1261,7 +1259,7 @@ describe("Anthropic Messages route", () => {
it.effect("maps ttlSeconds >= 3600 to cache_control ttl: '1h'", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
system: { type: "text", text: "system", cache: new CacheHint({ type: "ephemeral", ttlSeconds: 3600 }) },
@@ -1277,7 +1275,7 @@ describe("Anthropic Messages route", () => {
it.effect("emits cache_control on tool definitions and tool-result blocks", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
tools: [
@@ -1318,7 +1316,7 @@ describe("Anthropic Messages route", () => {
it.effect("drops cache_control breakpoints past the 4-per-request cap", () =>
Effect.gen(function* () {
const hint = new CacheHint({ type: "ephemeral" })
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
system: [
@@ -1344,7 +1342,7 @@ describe("Anthropic Messages route", () => {
it.effect("spends breakpoint budget on tools before system before messages", () =>
Effect.gen(function* () {
const hint = new CacheHint({ type: "ephemeral" })
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
tools: [
diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts
index 5cc8af1378..0bca4b9c5c 100644
--- a/packages/ai/test/provider/bedrock-converse.test.ts
+++ b/packages/ai/test/provider/bedrock-converse.test.ts
@@ -13,6 +13,7 @@ import {
ToolDefinition,
} from "../../src"
import { LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import { AmazonBedrock } from "../../src/providers"
import * as BedrockConverse from "../../src/protocols/bedrock-converse"
import { it } from "../lib/effect"
@@ -101,7 +102,7 @@ const baseRequest = LLM.request({
describe("Bedrock Converse route", () => {
it.effect("prepares Converse target with system, inference config, and messages", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(baseRequest)
+ const prepared = yield* compileRequest(baseRequest)
expect(prepared.body).toEqual({
modelId: "anthropic.claude-3-5-sonnet-20240620-v1:0",
@@ -114,7 +115,7 @@ describe("Bedrock Converse route", () => {
it.effect("passes topK through additionalModelRequestFields as top_k", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(baseRequest, {
generation: GenerationOptions.make({ maxTokens: 64, temperature: 0, topK: 40 }),
}),
@@ -129,14 +130,14 @@ describe("Bedrock Converse route", () => {
it.effect("omits additionalModelRequestFields when topK is unset", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(baseRequest)
+ const prepared = yield* compileRequest(baseRequest)
expect(prepared.body.additionalModelRequestFields).toBeUndefined()
}),
)
it.effect("lowers chronological system updates to wrapped user text in order", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")],
@@ -153,7 +154,7 @@ describe("Bedrock Converse route", () => {
it.effect("prepares tool config with toolSpec and toolChoice", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(baseRequest, {
tools: [
ToolDefinition.make({
@@ -187,7 +188,7 @@ describe("Bedrock Converse route", () => {
it.effect("keeps tools and omits the unsupported choice when tool choice is none", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(baseRequest, {
tools: [
ToolDefinition.make({
@@ -217,7 +218,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers assistant tool-call + tool-result message history", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_history",
model,
@@ -256,7 +257,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers image content in tool-result messages", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_image",
model,
@@ -491,7 +492,7 @@ describe("Bedrock Converse route", () => {
providerMetadata: { bedrock: { signature: "sig_1" } },
})
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -546,9 +547,7 @@ describe("Bedrock Converse route", () => {
},
])
- const prepared = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message], cache: "none" }),
- )
+ const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message], cache: "none" }))
expect(prepared.body.messages).toEqual([
{
role: "assistant",
@@ -639,7 +638,7 @@ describe("Bedrock Converse route", () => {
text: "",
providerMetadata: { bedrock: { redactedData } },
})
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -754,7 +753,7 @@ describe("Bedrock Converse route", () => {
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
},
}).model("anthropic.claude-3-5-sonnet-20240620-v1:0")
- const prepared = yield* LLMClient.prepare(LLMRequest.update(baseRequest, { model: signed }))
+ const prepared = yield* compileRequest(LLMRequest.update(baseRequest, { model: signed }))
expect(prepared.route).toBe("bedrock-converse")
expect(prepared.model).toBe(signed)
@@ -764,7 +763,7 @@ describe("Bedrock Converse route", () => {
it.effect("emits cachePoint markers after system, user-text, and assistant-text with cache hints", () =>
Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral" })
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_cache",
model,
@@ -796,7 +795,7 @@ describe("Bedrock Converse route", () => {
it.effect("does not emit cachePoint when no cache hint is set", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(baseRequest)
+ const prepared = yield* compileRequest(baseRequest)
expect(prepared.body).toMatchObject({
system: [{ text: "You are concise." }],
messages: [{ role: "user", content: [{ text: "Say hello." }] }],
@@ -806,7 +805,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers image media into Bedrock image blocks", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_image",
model,
@@ -843,7 +842,7 @@ describe("Bedrock Converse route", () => {
it.effect("base64-encodes Uint8Array image bytes", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_image_bytes",
model,
@@ -865,7 +864,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers document media into Bedrock document blocks with format and name", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_doc",
model,
@@ -897,7 +896,7 @@ describe("Bedrock Converse route", () => {
it.effect("requires names for document media", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
model,
messages: [Message.user({ type: "media", mediaType: "application/pdf", data: "UERGREFUQQ==" })],
@@ -910,7 +909,7 @@ describe("Bedrock Converse route", () => {
it.effect("passes named document-only messages through for provider validation", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
cache: "none",
@@ -936,7 +935,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers document media in tool results", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
cache: "none",
@@ -988,7 +987,7 @@ describe("Bedrock Converse route", () => {
it.effect("rejects unsupported image media types", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
id: "req_bad_image",
model,
@@ -1002,7 +1001,7 @@ describe("Bedrock Converse route", () => {
it.effect("rejects unsupported document media types", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
id: "req_bad_doc",
model,
@@ -1017,7 +1016,7 @@ describe("Bedrock Converse route", () => {
it.effect("maps ttlSeconds >= 3600 to cachePoint ttl: '1h'", () =>
Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral", ttlSeconds: 3600 })
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
system: [{ type: "text", text: "system", cache }],
@@ -1034,7 +1033,7 @@ describe("Bedrock Converse route", () => {
it.effect("appends cachePoint after marked tool definitions and tool-result blocks", () =>
Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral" })
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
tools: [{ name: "lookup", description: "lookup", inputSchema: { type: "object", properties: {} }, cache }],
@@ -1066,7 +1065,7 @@ describe("Bedrock Converse route", () => {
it.effect("drops cachePoint markers past the 4-per-request cap", () =>
Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral" })
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
system: [
diff --git a/packages/ai/test/provider/cloudflare.test.ts b/packages/ai/test/provider/cloudflare.test.ts
index 309f5db0ff..e09e321873 100644
--- a/packages/ai/test/provider/cloudflare.test.ts
+++ b/packages/ai/test/provider/cloudflare.test.ts
@@ -3,7 +3,7 @@ import { ConfigProvider, Effect, Schema } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
import { LLM, LLMEvent } from "../../src"
import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare"
-import { LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import { it } from "../lib/effect"
import { dynamicResponse } from "../lib/http"
import { sseEvents } from "../lib/sse"
@@ -34,7 +34,7 @@ describe("Cloudflare", () => {
})
expect(model.route.endpoint.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat")
- const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
+ const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." }))
expect(prepared.route).toBe("cloudflare-ai-gateway")
expect(prepared.body).toMatchObject({
@@ -129,7 +129,7 @@ describe("Cloudflare", () => {
openai: { reasoningField: "reasoning", reasoningDetails: merged },
})
- const replay = yield* LLMClient.prepare(LLM.request({ model, messages: [response.message] }))
+ const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([
{ role: "assistant", content: "Hello", reasoning: "Thinking", reasoning_details: merged },
])
@@ -180,7 +180,7 @@ describe("Cloudflare", () => {
it.effect("allows a fully configured baseURL override", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: CloudflareAIGateway.configure({
baseURL: "https://gateway.proxy.test/v1/custom/compat",
@@ -208,7 +208,7 @@ describe("Cloudflare", () => {
})
expect(model.route.endpoint.baseURL).toBe("https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1")
- const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
+ const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." }))
expect(prepared.route).toBe("cloudflare-workers-ai")
expect(prepared.body).toMatchObject({
diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts
index 2b22401e0a..07325b9702 100644
--- a/packages/ai/test/provider/gemini.test.ts
+++ b/packages/ai/test/provider/gemini.test.ts
@@ -2,6 +2,7 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
import { Auth, LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import * as Gemini from "../../src/protocols/gemini"
import { ProviderShared } from "../../src/protocols/shared"
import { it } from "../lib/effect"
@@ -26,7 +27,7 @@ const request = LLM.request({
describe("Gemini route", () => {
it.effect("prepares Gemini target", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(request)
+ const prepared = yield* compileRequest(request)
expect(prepared.body).toEqual({
contents: [{ role: "user", parts: [{ text: "Say hello." }] }],
@@ -38,12 +39,12 @@ describe("Gemini route", () => {
it.effect("normalizes Gemini thinking options", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(request, {
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } },
}),
)
- const filtered = yield* LLMClient.prepare(
+ const filtered = yield* compileRequest(
LLMRequest.update(request, {
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } } },
}),
@@ -59,7 +60,7 @@ describe("Gemini route", () => {
it.effect("lowers chronological system updates to wrapped user text in order", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")],
@@ -75,7 +76,7 @@ describe("Gemini route", () => {
it.effect("prepares multimodal user input and tool history", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result",
model,
@@ -143,7 +144,7 @@ describe("Gemini route", () => {
it.effect("continues media tool results as inline model input without base64 text", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -188,7 +189,7 @@ describe("Gemini route", () => {
it.effect("strips matching data URLs to raw base64 inlineData", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -229,7 +230,7 @@ describe("Gemini route", () => {
] as const)
it.effect(`rejects ${name}`, () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }),
).pipe(Effect.flip)
expect(error.message).toMatch(/does not support|does not match|valid base64/)
@@ -238,7 +239,7 @@ describe("Gemini route", () => {
it.effect("rejects oversized image input", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -256,7 +257,7 @@ describe("Gemini route", () => {
it.effect("keeps tools and sends function calling mode NONE", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_choice_none",
model,
@@ -276,7 +277,7 @@ describe("Gemini route", () => {
it.effect("sanitizes integer enums, dangling required, untyped arrays, and scalar object keys", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_schema_patch",
model,
@@ -457,7 +458,7 @@ describe("Gemini route", () => {
response.events.findIndex((event) => event.type === "tool-call"),
)
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -691,7 +692,7 @@ describe("Gemini route", () => {
it.effect("rejects unsupported assistant media content", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
id: "req_media",
model,
diff --git a/packages/ai/test/provider/google-vertex.test.ts b/packages/ai/test/provider/google-vertex.test.ts
index 395d983ffe..8c031d473c 100644
--- a/packages/ai/test/provider/google-vertex.test.ts
+++ b/packages/ai/test/provider/google-vertex.test.ts
@@ -4,6 +4,7 @@ import { HttpClientRequest } from "effect/unstable/http"
import { LLM } from "../../src"
import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers"
import { LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import { it } from "../lib/effect"
import { dynamicResponse } from "../lib/http"
import { deltaChunk, finishChunk } from "../lib/openai-chunks"
@@ -182,7 +183,7 @@ describe("Google Vertex providers", () => {
it.effect("protects the Vertex Messages API version from body overlays", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
model: GoogleVertexMessages.configure({
accessToken: "vertex-token",
diff --git a/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts b/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts
index a8b40346cf..4c881d20fb 100644
--- a/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts
+++ b/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts
@@ -5,6 +5,7 @@ import { OpenAIChat } from "../../src/protocols/openai-chat"
import * as OpenAICompatible from "../../src/providers/openai-compatible"
import * as OpenRouter from "../../src/providers/openrouter"
import { LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import { recordedTests } from "../recorded-test"
import { expectWeatherToolLoop, goldenWeatherToolLoopRequest, runWeatherToolLoop } from "../recorded-scenarios"
@@ -84,9 +85,7 @@ for (const item of cases) {
),
).toBe(true)
- const replay = yield* LLMClient.prepare(
- LLM.request({ model: item.model, messages: [response.message] }),
- )
+ const replay = yield* compileRequest(LLM.request({ model: item.model, messages: [response.message] }))
expect(replay.body.messages).toMatchObject([
{ role: "assistant", content: response.text, reasoning: response.reasoning },
])
diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts
index 57ae8e8653..27d1f296dd 100644
--- a/packages/ai/test/provider/openai-chat.test.ts
+++ b/packages/ai/test/provider/openai-chat.test.ts
@@ -18,6 +18,7 @@ import * as OpenAI from "../../src/providers/openai"
import * as OpenAIChat from "../../src/protocols/openai-chat"
import { ProviderShared } from "../../src/protocols/shared"
import { Auth, LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import { it } from "../lib/effect"
import { dynamicResponse, fixedResponse, truncatedStream } from "../lib/http"
import { deltaChunk, usageChunk } from "../lib/openai-chunks"
@@ -42,11 +43,7 @@ const request = LLM.request({
describe("OpenAI Chat route", () => {
it.effect("prepares OpenAI Chat payload", () =>
Effect.gen(function* () {
- // Pass the OpenAIChat payload type so `prepared.body` is statically
- // typed to the route's native shape — the assertions below read field
- // names without `unknown` casts.
- const prepared = yield* LLMClient.prepare(request)
- const _typed: { readonly model: string; readonly stream: true } = prepared.body
+ const prepared = yield* compileRequest(request)
expect(prepared.body).toEqual({
model: "gpt-4o-mini",
@@ -64,7 +61,7 @@ describe("OpenAI Chat route", () => {
it.effect("lowers chronological system updates to escaped user wrappers in order", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -87,7 +84,7 @@ describe("OpenAI Chat route", () => {
it.effect("replays canonical reasoning as OpenAI-compatible reasoning_content", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -105,7 +102,7 @@ describe("OpenAI Chat route", () => {
it.effect("writes reasoning to a configured custom field on every assistant message", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }),
messages: [
@@ -131,7 +128,7 @@ describe("OpenAI Chat route", () => {
it.effect("rejects reasoning fields that conflict with assistant message fields", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
model: Model.update(model, { compatibility: { reasoningField: "content" } }),
messages: [Message.assistant([{ type: "reasoning", text: "thinking" }])],
@@ -144,7 +141,7 @@ describe("OpenAI Chat route", () => {
it.effect("maps OpenAI provider options to Chat options", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"),
prompt: "think",
@@ -159,7 +156,7 @@ describe("OpenAI Chat route", () => {
it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "think",
@@ -253,7 +250,7 @@ describe("OpenAI Chat route", () => {
it.effect("prepares assistant tool-call and tool-result messages", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result",
model,
@@ -291,7 +288,7 @@ describe("OpenAI Chat route", () => {
it.effect("preserves structured tool errors for the model", () =>
Effect.gen(function* () {
const error = { error: { type: "unknown", message: "Tool execution interrupted" } }
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -311,7 +308,7 @@ describe("OpenAI Chat route", () => {
it.effect("continues image tool results as vision input without base64 text", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -355,7 +352,7 @@ describe("OpenAI Chat route", () => {
it.effect("orders parallel tool responses before one aggregated vision message", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -405,7 +402,7 @@ describe("OpenAI Chat route", () => {
it.effect("aggregates consecutive tool images with a following system update", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -446,7 +443,7 @@ describe("OpenAI Chat route", () => {
it.effect("appends system updates without replacing multipart user content", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -474,7 +471,7 @@ describe("OpenAI Chat route", () => {
] as const)
it.effect(`rejects ${name}`, () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }),
).pipe(Effect.flip)
expect(error.message).toMatch(/does not support|does not match|valid base64/)
@@ -483,7 +480,7 @@ describe("OpenAI Chat route", () => {
it.effect("rejects oversized image input", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -501,7 +498,7 @@ describe("OpenAI Chat route", () => {
it.effect("prepares raw and data URL image media as vision input", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_media",
model,
@@ -528,7 +525,7 @@ describe("OpenAI Chat route", () => {
it.effect("lowers reasoning-only assistant history", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_reasoning",
model,
@@ -619,9 +616,7 @@ describe("OpenAI Chat route", () => {
openai: { reasoningField: field },
})
- const replay = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message] }),
- )
+ const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", [field]: "thinking" }])
}
}),
@@ -647,9 +642,7 @@ describe("OpenAI Chat route", () => {
openai: { reasoningField: "vendor_reasoning" },
})
- const replay = yield* LLMClient.prepare(
- LLM.request({ model: custom, messages: [response.message] }),
- )
+ const replay = yield* compileRequest(LLM.request({ model: custom, messages: [response.message] }))
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", vendor_reasoning: "thinking" }])
}),
)
@@ -692,9 +685,7 @@ describe("OpenAI Chat route", () => {
openai: { reasoningField: "reasoning", reasoningDetails: details },
})
- const replay = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message] }),
- )
+ const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([
{
role: "assistant",
@@ -737,9 +728,7 @@ describe("OpenAI Chat route", () => {
openai: { reasoningDetails: details },
})
- const replay = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message] }),
- )
+ const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", reasoning_details: details }])
}),
)
@@ -764,9 +753,7 @@ describe("OpenAI Chat route", () => {
openai: { reasoningField: "reasoning", reasoningDetails: details },
})
- const replay = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message] }),
- )
+ const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([
{ role: "assistant", content: "Hello", reasoning: "thinking", reasoning_details: details },
])
@@ -839,9 +826,7 @@ describe("OpenAI Chat route", () => {
openai: { reasoningDetails: [] },
})
- const replay = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message] }),
- )
+ const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", reasoning_details: [] }])
}),
)
@@ -889,9 +874,7 @@ describe("OpenAI Chat route", () => {
response.events.findIndex(LLMEvent.is.textStart),
)
- const replay = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message] }),
- )
+ const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([
{ role: "assistant", content: "Hello", reasoning: "thinking", reasoning_details: merged },
])
@@ -918,9 +901,7 @@ describe("OpenAI Chat route", () => {
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
- const replay = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message] }),
- )
+ const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([{ role: "assistant", content: null, reasoning_details: details }])
}),
)
@@ -950,7 +931,7 @@ describe("OpenAI Chat route", () => {
Effect.gen(function* () {
const first = { type: "reasoning.text", text: "first", signature: "signed-0", index: 0 }
const second = { type: "reasoning.text", text: "second", signature: "signed-1", index: 1 }
- const replay = yield* LLMClient.prepare(
+ const replay = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -979,7 +960,7 @@ describe("OpenAI Chat route", () => {
it.effect("retains scalar replay for mixed structured reasoning parts", () =>
Effect.gen(function* () {
const detail = { type: "reasoning.encrypted", data: "opaque", index: 0 }
- const replay = yield* LLMClient.prepare(
+ const replay = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -1004,7 +985,7 @@ describe("OpenAI Chat route", () => {
it.effect("replays native scalar reasoning alongside native details", () =>
Effect.gen(function* () {
const details = [{ type: "reasoning.encrypted", data: "opaque", index: 0 }]
- const replay = yield* LLMClient.prepare(
+ const replay = yield* compileRequest(
LLM.request({
model,
messages: [
diff --git a/packages/ai/test/provider/openai-compatible-chat.test.ts b/packages/ai/test/provider/openai-compatible-chat.test.ts
index 6565820af8..397c64c8b3 100644
--- a/packages/ai/test/provider/openai-compatible-chat.test.ts
+++ b/packages/ai/test/provider/openai-compatible-chat.test.ts
@@ -3,6 +3,7 @@ import { Effect, Schema } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src"
import { Auth, LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import * as OpenAICompatible from "../../src/providers/openai-compatible"
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
import { it } from "../lib/effect"
@@ -52,7 +53,7 @@ const providerFamilies = [
describe("OpenAI-compatible Chat route", () => {
it.effect("prepares generic Chat target", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
toolChoice: ToolChoice.make({ type: "required" }),
@@ -127,7 +128,7 @@ describe("OpenAI-compatible Chat route", () => {
it.effect("matches AI SDK compatible basic request body fixture", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(request)
+ const prepared = yield* compileRequest(request)
expect(prepared.body).toEqual({
model: "deepseek-chat",
@@ -145,7 +146,7 @@ describe("OpenAI-compatible Chat route", () => {
it.effect("matches AI SDK compatible tool request body fixture", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_parity",
model,
diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts
index f6591f50d2..7d52b10cd1 100644
--- a/packages/ai/test/provider/openai-compatible-responses.test.ts
+++ b/packages/ai/test/provider/openai-compatible-responses.test.ts
@@ -7,6 +7,7 @@ import { OpenResponses } from "../../src/protocols/open-responses"
import { OpenAICompatibleResponses } from "../../src/protocols/openai-compatible-responses"
import { OpenAIResponses } from "../../src/protocols/openai-responses"
import { LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import { it } from "../lib/effect"
import { fixedResponse } from "../lib/http"
import { sseEvents } from "../lib/sse"
@@ -23,7 +24,7 @@ describe("Open Responses-compatible route", () => {
baseURL: "https://responses.example.test/v1",
provider: "example",
}).model("example-model")
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
system: "You are concise.",
@@ -61,7 +62,7 @@ describe("Open Responses-compatible route", () => {
apiKey: "test-key",
baseURL: "https://responses.example.test/v1",
}).model("example-model")
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({ model, prompt: "Draw.", tools: [OpenAI.imageGeneration()] }),
).pipe(Effect.flip)
@@ -76,7 +77,7 @@ describe("Open Responses-compatible route", () => {
apiKey: "test-key",
baseURL: "https://responses.example.test/v1",
}).model("example-model")
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -102,7 +103,7 @@ describe("Open Responses-compatible route", () => {
baseURL: "https://responses.example.test/v1",
providerOptions: { openresponses: { reasoningEffort: "low", store: true } },
}).model("example-model")
- const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Think." }))
+ const prepared = yield* compileRequest(LLM.request({ model, prompt: "Think." }))
expect(prepared.body).toMatchObject({
reasoning: { effort: "low" },
diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts
index a3d75a7eeb..3b9a0b76aa 100644
--- a/packages/ai/test/provider/openai-responses.test.ts
+++ b/packages/ai/test/provider/openai-responses.test.ts
@@ -14,6 +14,7 @@ import {
Usage,
} from "../../src"
import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import * as Azure from "../../src/providers/azure"
import * as OpenAI from "../../src/providers/openai"
import * as XAI from "../../src/providers/xai"
@@ -56,7 +57,7 @@ const expectToolOutput = (body: OpenAIResponses.OpenAIResponsesBody): OpenAITool
describe("OpenAI Responses route", () => {
it.effect("prepares OpenAI Responses target", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(request)
+ const prepared = yield* compileRequest(request)
expect(prepared.body).toEqual({
model: "gpt-4.1-mini",
@@ -74,7 +75,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers the hosted OpenAI image generation tool", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Show me a rooftop garden.",
@@ -92,7 +93,7 @@ describe("OpenAI Responses route", () => {
it.effect("rejects invalid hosted image generation options locally", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
model,
prompt: "Show me a rooftop garden.",
@@ -109,7 +110,7 @@ describe("OpenAI Responses route", () => {
Effect.gen(function* () {
const input = LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "priority" } } })
expect(input.providerOptions).toEqual({ openai: { serviceTier: "priority" } })
- const prepared = yield* LLMClient.prepare(input)
+ const prepared = yield* compileRequest(input)
expect(prepared.body).toMatchObject({ service_tier: "priority" })
expect(prepared.body).not.toHaveProperty("serviceTier")
@@ -118,7 +119,7 @@ describe("OpenAI Responses route", () => {
it.effect("passes through custom OpenAI reasoning effort strings", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }),
)
@@ -128,7 +129,7 @@ describe("OpenAI Responses route", () => {
it.effect("omits unsupported semantic service tiers", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "unsupported" } } }),
)
@@ -138,7 +139,7 @@ describe("OpenAI Responses route", () => {
it.effect("flattens top-level object unions in function schemas", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(request, {
tools: [
ToolDefinition.make({
@@ -191,7 +192,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers chronological system updates to escaped user wrappers in order", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -217,7 +218,7 @@ describe("OpenAI Responses route", () => {
it.effect("prepares OpenAI Responses WebSocket target", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLMRequest.update(request, {
model: OpenAIResponses.webSocketRoute
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
@@ -395,7 +396,7 @@ describe("OpenAI Responses route", () => {
it.effect("prepares function call and function output input items", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result",
model,
@@ -432,7 +433,7 @@ describe("OpenAI Responses route", () => {
content: [],
structured: {},
}
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -453,7 +454,7 @@ describe("OpenAI Responses route", () => {
it.effect("keeps primitive tool errors as plain text", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -469,7 +470,7 @@ describe("OpenAI Responses route", () => {
it.effect("keeps non-JSON tool errors as plain text", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -487,7 +488,7 @@ describe("OpenAI Responses route", () => {
// image data is not JSON-stringified into `function_call_output.output`.
it.effect("lowers image tool-result content as structured input_image items", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result_image",
model,
@@ -516,7 +517,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers single-image tool-result content as structured input_image array", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result_image_only",
model,
@@ -540,7 +541,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers PDF tool-result content as structured input_file array", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_tool_result_pdf",
model,
@@ -575,7 +576,7 @@ describe("OpenAI Responses route", () => {
it.effect("uses xAI inline file encoding for PDF tool results", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: xaiModel,
messages: [
@@ -610,7 +611,7 @@ describe("OpenAI Responses route", () => {
it.effect("rejects unsupported media in tool-result content with a clear error", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
id: "req_tool_result_unsupported_media",
model,
@@ -633,7 +634,7 @@ describe("OpenAI Responses route", () => {
it.effect("prepares the composed native continuation request", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
continuationRequest({
id: "req_native_continuation_openai",
model,
@@ -675,7 +676,7 @@ describe("OpenAI Responses route", () => {
it.effect("maps OpenAI provider options to Responses options", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
prompt: "think",
@@ -700,7 +701,7 @@ describe("OpenAI Responses route", () => {
it.effect("accepts the full ResponseIncludable union", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "hi",
@@ -722,7 +723,7 @@ describe("OpenAI Responses route", () => {
it.effect("filters unknown includable values out of the include array", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "hi",
@@ -739,7 +740,7 @@ describe("OpenAI Responses route", () => {
it.effect("treats an explicit empty include as no include at all", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: [] } } }),
)
@@ -749,7 +750,7 @@ describe("OpenAI Responses route", () => {
it.effect("treats an all-invalid include as no include at all", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }),
)
@@ -759,7 +760,7 @@ describe("OpenAI Responses route", () => {
it.effect("omits include when no include is set", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { store: false } } }),
)
@@ -773,7 +774,7 @@ describe("OpenAI Responses route", () => {
// reasoningSummary: "auto" by default. Without `include`, a follow-up
// turn cannot replay reasoning state, so the facade also opts into
// `reasoning.encrypted_content` automatically.
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
prompt: "hi",
@@ -788,7 +789,7 @@ describe("OpenAI Responses route", () => {
it.effect("lets callers opt out of the GPT-5 default include", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
prompt: "hi",
@@ -802,7 +803,7 @@ describe("OpenAI Responses route", () => {
it.effect("request OpenAI provider options override route defaults", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenAI.configure({
baseURL: "https://api.openai.test/v1/",
@@ -934,9 +935,7 @@ describe("OpenAI Responses route", () => {
},
])
- const prepared = yield* LLMClient.prepare(
- LLM.request({ model, messages: [response.message] }),
- )
+ const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(prepared.body.input).toEqual([
{
role: "assistant",
@@ -1270,7 +1269,7 @@ describe("OpenAI Responses route", () => {
it.effect("preserves assistant content order around reasoning items", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_reasoning_order",
model,
@@ -1308,7 +1307,7 @@ describe("OpenAI Responses route", () => {
it.effect("references stored reasoning items by id", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -1330,7 +1329,7 @@ describe("OpenAI Responses route", () => {
it.effect("references stored provider-executed hosted tool results by id", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -1367,7 +1366,7 @@ describe("OpenAI Responses route", () => {
it.effect("continues stateless hosted image generation with the generated image", () =>
Effect.gen(function* () {
const imageTool = OpenAI.imageGeneration({ action: "edit" })
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
@@ -1408,7 +1407,7 @@ describe("OpenAI Responses route", () => {
it.effect("joins streamed summary blocks into one continuation reasoning item", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_multi_summary_continuation",
model,
@@ -1445,7 +1444,7 @@ describe("OpenAI Responses route", () => {
it.effect("skips non-persisted reasoning ids without encrypted state", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_reasoning_without_encrypted_state",
model,
@@ -1762,7 +1761,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers user image and PDF content", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
id: "req_media",
model,
@@ -1793,7 +1792,7 @@ describe("OpenAI Responses route", () => {
it.effect("uses xAI inline file encoding for user PDFs", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: xaiModel,
messages: [
@@ -1825,7 +1824,7 @@ describe("OpenAI Responses route", () => {
it.effect("rejects unsupported user media content", () =>
Effect.gen(function* () {
- const error = yield* LLMClient.prepare(
+ const error = yield* compileRequest(
LLM.request({
id: "req_media",
model,
diff --git a/packages/ai/test/provider/openrouter.test.ts b/packages/ai/test/provider/openrouter.test.ts
index b4ac2fe2e5..e2a61c250e 100644
--- a/packages/ai/test/provider/openrouter.test.ts
+++ b/packages/ai/test/provider/openrouter.test.ts
@@ -2,6 +2,7 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, Message } from "../../src"
import { LLMClient } from "../../src/route"
+import { compileRequest } from "../../src/route/client"
import * as OpenRouter from "../../src/providers/openrouter"
import { it } from "../lib/effect"
import { fixedResponse } from "../lib/http"
@@ -19,7 +20,7 @@ describe("OpenRouter", () => {
})
expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1")
- const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
+ const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." }))
expect(prepared.route).toBe("openrouter")
expect(prepared.body).toMatchObject({
@@ -32,7 +33,7 @@ describe("OpenRouter", () => {
it.effect("applies OpenRouter payload options from the model helper", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({
apiKey: "test-key",
@@ -100,7 +101,7 @@ describe("OpenRouter", () => {
{ type: "reasoning.text", signature: "signed", format: "anthropic-claude-v1", index: 0 },
{ type: "reasoning.encrypted", data: "opaque", format: "openai-responses-v1", index: 1 },
]
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
messages: [
@@ -133,7 +134,7 @@ describe("OpenRouter", () => {
{ type: "reasoning.encrypted", id: "state", data: "opaque" },
{ type: "reasoning.encrypted", id: "state", data: "opaque" },
]
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
messages: [
@@ -158,7 +159,7 @@ describe("OpenRouter", () => {
{ type: "reasoning.text", id: "first", index: 0, text: "A", opaque: "first" },
{ type: "reasoning.text", id: "second", index: 1, text: "B", opaque: "second" },
]
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
messages: [
@@ -179,7 +180,7 @@ describe("OpenRouter", () => {
it.effect("omits scalar reasoning without continuation details", () =>
Effect.gen(function* () {
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
messages: [Message.assistant({ type: "reasoning", text: "Thinking" })],
diff --git a/packages/ai/test/tool-schema-projection.test.ts b/packages/ai/test/tool-schema-projection.test.ts
index a9df815daf..4f83942a66 100644
--- a/packages/ai/test/tool-schema-projection.test.ts
+++ b/packages/ai/test/tool-schema-projection.test.ts
@@ -3,7 +3,8 @@ import { Effect } from "effect"
import { LLM } from "../src"
import { OpenAIChat } from "../src/protocols"
import { ToolSchemaProjection } from "../src/protocols/utils/tool-schema"
-import { Auth, LLMClient } from "../src/route"
+import { Auth } from "../src/route"
+import { compileRequest } from "../src/route/client"
import { it } from "./lib/effect"
describe("tool schema projections", () => {
@@ -79,7 +80,7 @@ describe("tool schema projections", () => {
const model = OpenAIChat.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
.model({ id: "kimi-k2", compatibility: { toolSchema: "moonshot" } })
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Use the tool.",
diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts
index 87a02d1b3c..e3dfbe8d80 100644
--- a/packages/core/test/aisdk.test.ts
+++ b/packages/core/test/aisdk.test.ts
@@ -1,9 +1,10 @@
-import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart } from "@ai-sdk/provider"
+import type { LanguageModelV3, LanguageModelV3StreamPart } from "@ai-sdk/provider"
import { AISDK } from "@opencode-ai/core/aisdk"
import { Model } from "@opencode-ai/core/model"
import { Provider } from "@opencode-ai/core/provider"
import { LLM, LLMError, LLMEvent, Message } from "@opencode-ai/ai"
import { LLMClient, RequestExecutor } from "@opencode-ai/ai/route"
+import { compileRequest } from "@opencode-ai/ai/route/client"
import { expect } from "bun:test"
import { Effect, Layer } from "effect"
import { testEffect } from "./lib/effect"
@@ -87,9 +88,7 @@ it.effect("projects request settings, headers, and body overlays", () =>
headers: { "x-test": "header" },
body: { safety_setting: "strict" },
})
- const prepared = yield* LLMClient.prepare(
- LLM.request({ model: resolved, prompt: "Hello" }),
- )
+ const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
expect(prepared.body.providerOptions).toEqual({
google: { thinkingConfig: { thinkingBudget: 1024 } },
@@ -112,9 +111,7 @@ it.effect("maps pro reasoning bodies to AI SDK provider options", () =>
...model("@ai-sdk/openai"),
body: { reasoning: { mode: "pro" } },
})
- const prepared = yield* LLMClient.prepare(
- LLM.request({ model: resolved, prompt: "Hello" }),
- )
+ const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
expect(body).toBeUndefined()
expect(prepared.body.providerOptions).toEqual({
@@ -139,9 +136,7 @@ it.effect("maps package-specific AI SDK provider option keys", () =>
] as const
for (const [packageName, key, settings] of cases) {
const resolved = yield* aisdk.model(model(packageName, { reasoningEffort: "high" }))
- const prepared = yield* LLMClient.prepare(
- LLM.request({ model: resolved, prompt: "Hello" }),
- )
+ const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
expect(prepared.body.providerOptions).toEqual({ [key]: settings })
}
}),
@@ -155,17 +150,13 @@ it.effect("forces reasoning and projects both Azure AI SDK namespaces", () =>
})
const openai = yield* aisdk.model(model("@ai-sdk/openai", { reasoningEffort: "high" }))
- const openaiPrepared = yield* LLMClient.prepare(
- LLM.request({ model: openai, prompt: "Hello" }),
- )
+ const openaiPrepared = yield* compileRequest(LLM.request({ model: openai, prompt: "Hello" }))
expect(openaiPrepared.body.providerOptions).toEqual({
openai: { reasoningEffort: "high", forceReasoning: true },
})
const azure = yield* aisdk.model(model("@ai-sdk/azure", { reasoningEffort: "high" }))
- const azurePrepared = yield* LLMClient.prepare(
- LLM.request({ model: azure, prompt: "Hello" }),
- )
+ const azurePrepared = yield* compileRequest(LLM.request({ model: azure, prompt: "Hello" }))
expect(azurePrepared.body.providerOptions).toEqual({
openai: { reasoningEffort: "high", forceReasoning: true },
azure: { reasoningEffort: "high", forceReasoning: true },
@@ -187,9 +178,7 @@ it.effect("routes AI Gateway model options by upstream prefix", () =>
}),
modelID: Model.ID.make("anthropic/claude-sonnet-5"),
})
- const anthropicPrepared = yield* LLMClient.prepare(
- LLM.request({ model: anthropic, prompt: "Hello" }),
- )
+ const anthropicPrepared = yield* compileRequest(LLM.request({ model: anthropic, prompt: "Hello" }))
expect(anthropicPrepared.body.providerOptions).toEqual({
gateway: { order: ["anthropic"] },
anthropic: { thinking: { type: "adaptive" } },
@@ -199,9 +188,7 @@ it.effect("routes AI Gateway model options by upstream prefix", () =>
...model("@ai-sdk/gateway", { reasoningConfig: { type: "enabled" } }),
modelID: Model.ID.make("amazon/nova-2-lite"),
})
- const bedrockPrepared = yield* LLMClient.prepare(
- LLM.request({ model: bedrock, prompt: "Hello" }),
- )
+ const bedrockPrepared = yield* compileRequest(LLM.request({ model: bedrock, prompt: "Hello" }))
expect(bedrockPrepared.body.providerOptions).toEqual({
bedrock: { reasoningConfig: { type: "enabled" } },
})
@@ -210,9 +197,7 @@ it.effect("routes AI Gateway model options by upstream prefix", () =>
...model("@ai-sdk/gateway", { reasoningEffort: "high" }),
modelID: Model.ID.make("deepseek/deepseek-v4"),
})
- const fallbackPrepared = yield* LLMClient.prepare(
- LLM.request({ model: fallback, prompt: "Hello" }),
- )
+ const fallbackPrepared = yield* compileRequest(LLM.request({ model: fallback, prompt: "Hello" }))
expect(fallbackPrepared.body.providerOptions).toEqual({
deepseek: { reasoningEffort: "high" },
})
@@ -228,7 +213,7 @@ it.effect("projects replay metadata onto AI SDK prompt parts", () =>
const resolved = yield* aisdk.model(model("@ai-sdk/anthropic"))
expect(resolved.route.providerMetadataKey).toBe("anthropic")
- const prepared = yield* LLMClient.prepare(
+ const prepared = yield* compileRequest(
LLM.request({
model: resolved,
messages: [
diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts
index 547893a088..273c15a8c6 100644
--- a/packages/core/test/model-resolver.test.ts
+++ b/packages/core/test/model-resolver.test.ts
@@ -1,6 +1,6 @@
import { describe, expect } from "bun:test"
import { LLM, Model } from "@opencode-ai/ai"
-import { LLMClient } from "@opencode-ai/ai/route"
+import { compileRequest } from "@opencode-ai/ai/route/client"
import { Effect } from "effect"
import { Headers } from "effect/unstable/http"
import { Credential } from "@opencode-ai/core/credential"
@@ -69,7 +69,7 @@ describe("ModelResolver", () => {
settings: { apiKey: "secret", baseURL: "https://openai.example/v1" },
}),
)
- const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
+ const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
expect(JSON.stringify(prepared.body)).not.toContain("apiKey")
expect(JSON.stringify(prepared.body)).not.toContain("secret")
diff --git a/packages/core/test/session-compact.test.ts b/packages/core/test/session-compact.test.ts
index b8f46d91af..a64f2367d5 100644
--- a/packages/core/test/session-compact.test.ts
+++ b/packages/core/test/session-compact.test.ts
@@ -41,7 +41,6 @@ const projects = Layer.succeed(
)
let requests: LLMRequest[] = []
const client = Layer.mock(LLMClient.Service)({
- prepare: () => Effect.die("unused"),
stream: (request: LLMRequest) => {
requests.push(request)
return Stream.make(LLMEvent.textDelta({ id: "summary", text: "manual session summary" }))
diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts
index 89f1f18d39..aa781f40da 100644
--- a/packages/core/test/session-compaction.test.ts
+++ b/packages/core/test/session-compaction.test.ts
@@ -42,7 +42,6 @@ const cost = [
},
]
const client = Layer.mock(LLMClient.Service)({
- prepare: () => Effect.die("unused"),
stream: (request: LLMRequest) => {
requests.push(request)
return Stream.make(
diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts
index ea12b528f2..7ca2a9b18f 100644
--- a/packages/core/test/session-generate.test.ts
+++ b/packages/core/test/session-generate.test.ts
@@ -49,7 +49,6 @@ const sessionID = SessionSchema.ID.make("ses_generate_test")
const model = Model.make({ id: "generate-model", provider: "test", route: OpenAIChat.route })
const client = Layer.mock(LLMClient.Service)({
- prepare: () => Effect.die(new Error("unused")),
stream: () => Stream.die(new Error("unused")),
generate: (request) =>
Effect.sync(() => {
diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts
index b69abb77bc..6c95cdb009 100644
--- a/packages/core/test/session-title.test.ts
+++ b/packages/core/test/session-title.test.ts
@@ -40,7 +40,6 @@ const cost = [
},
]
const client = Layer.mock(LLMClient.Service)({
- prepare: () => Effect.die("unused"),
stream: (request: LLMRequest) => {
requests.push(request)
return Stream.make(