refactor(llm): collapse client adapter injection
This commit is contained in:
@@ -30,12 +30,12 @@ const request = LLM.request({
|
||||
prompt: "Say hello.",
|
||||
})
|
||||
|
||||
const response = yield* LLMClient.make().generate(request)
|
||||
const response = yield* LLMClient.generate(request)
|
||||
```
|
||||
|
||||
`LLM.request(...)` builds an `LLMRequest`. `LLMClient.make(...)` selects an adapter from the model binding or explicit registry by `request.model.adapter`, prepares a typed provider payload, asks the adapter for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`.
|
||||
`LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` selects a registered adapter by `request.model.adapter`, prepares a typed provider payload, asks the adapter 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.make(...).stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.make(...).generate(request)` when callers want those same events collected into an `LLMResponse`. Use `LLMClient.make(...).prepare<Payload>(request)` to compile a request through the adapter pipeline without sending it — the optional `Payload` type argument narrows `.payload` to the adapter's native shape (e.g. `prepare<OpenAIChatPayload>(...)` returns a `PreparedRequestOf<OpenAIChatPayload>`). The runtime payload 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`. Use `LLMClient.prepare<Payload>(request)` to compile a request through the adapter pipeline without sending it — the optional `Payload` type argument narrows `.payload` to the adapter's native shape (e.g. `prepare<OpenAIChatPayload>(...)` returns a `PreparedRequestOf<OpenAIChatPayload>`). The runtime payload is identical; the generic is a type-level assertion.
|
||||
|
||||
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.
|
||||
|
||||
@@ -73,7 +73,7 @@ packages/llm/src/
|
||||
llm.ts // request constructors and convenience helpers
|
||||
adapter/
|
||||
index.ts // @opencode-ai/llm/adapter advanced barrel
|
||||
client.ts // Adapter.make + LLMClient.make
|
||||
client.ts // Adapter.make + LLMClient.prepare/stream/generate
|
||||
executor.ts // RequestExecutor service + transport error mapping
|
||||
protocol.ts // Protocol type + Protocol.define
|
||||
endpoint.ts // Endpoint type + Endpoint.baseURL
|
||||
@@ -131,7 +131,7 @@ Adapters lower this into provider-native assistant tool-call messages and tool-r
|
||||
|
||||
### Tool runtime
|
||||
|
||||
`ToolRuntime.run(client, options)` orchestrates the tool loop with full type safety:
|
||||
`ToolRuntime.run(options)` orchestrates the tool loop with full type safety:
|
||||
|
||||
```ts
|
||||
const get_weather = tool({
|
||||
@@ -147,7 +147,7 @@ const get_weather = tool({
|
||||
}),
|
||||
})
|
||||
|
||||
const events = yield* ToolRuntime.run(client, {
|
||||
const events = yield* ToolRuntime.run({
|
||||
request,
|
||||
tools: { get_weather, get_time, ... },
|
||||
maxSteps: 10,
|
||||
@@ -231,7 +231,7 @@ Do not blanket re-record an entire test file when adding one cassette. `RECORD=t
|
||||
|
||||
### Completed Foundation
|
||||
|
||||
- [x] Add an adapter registry so `LLMClient.make(...)` can choose an adapter by provider/protocol instead of requiring a single adapter.
|
||||
- [x] Add an adapter registry so `LLMClient` can choose an adapter by provider/protocol instead of requiring a single adapter.
|
||||
- [x] Add request/response convenience helpers where callsites still expose schema internals, but keep constructors returning canonical Schema class instances.
|
||||
- [x] Expand OpenAI Chat support for assistant tool-call messages followed by tool-result messages.
|
||||
- [x] Add OpenAI Chat recorded tests for tool-result follow-up and usage chunks.
|
||||
@@ -276,7 +276,7 @@ Do not blanket re-record an entire test file when adding one cassette. `RECORD=t
|
||||
- [x] Add a native event bridge that maps `LLMEvent` streams into the existing `SessionProcessor` event contract without creating a second processor.
|
||||
- [ ] Extract runtime-neutral OpenCode tool resolution from `SessionPrompt.resolveTools`, then build both existing-stream and native `@opencode-ai/llm` tool adapters from the same resolved shape.
|
||||
- [ ] Map `Permission.RejectedError`, `Permission.CorrectedError`, validation failures, thrown tool failures, and aborts into model-visible native tool error/results.
|
||||
- [ ] Wire a native stream producer behind an explicit local flag and provider allowlist; the producer should consume `nativeMessages`, call `LLMNative.request(...)`, stream through `LLMClient.make(...)`, and feed `LLMNativeEvents.mapper()` into `SessionProcessor`.
|
||||
- [ ] Wire a native stream producer behind an explicit local flag and provider allowlist; the producer should consume `nativeMessages`, call `LLMNative.request(...)`, stream through `LLMClient.stream(...)`, and feed `LLMNativeEvents.mapper()` into `SessionProcessor`.
|
||||
- [ ] Add end-to-end native stream tests through the actual session loop for text, reasoning, tool-call streaming, tool success, rejected permission, corrected permission, thrown tool error, abort, and provider-executed tool history.
|
||||
- [ ] Dogfood native streaming with the flag enabled for OpenAI first, then Anthropic, Gemini, OpenAI-compatible providers, Bedrock, and Copilot provider-by-provider.
|
||||
- [ ] Flip native streaming to default only after request parity, stream parity, tool execution, typecheck, focused provider tests, recorded cassettes, and manual dogfood pass for the enabled provider set.
|
||||
|
||||
@@ -124,7 +124,7 @@ The runtime pipeline is concentrated in [`src/adapter/client.ts`](./src/adapter/
|
||||
The important functions are:
|
||||
|
||||
- `Adapter.model`, which binds a user-facing model helper to the adapter that can run it.
|
||||
- `LLMClient.make`, which selects an adapter, builds the payload, sends HTTP, and parses the response.
|
||||
- `LLMClient`, which selects a registered adapter, builds the payload, sends HTTP, and parses the response.
|
||||
- `Adapter.make`, which composes protocol semantics with endpoint, auth, and framing.
|
||||
|
||||
At runtime, the flow is easier to read as a sequence of values. There are two levels to keep separate:
|
||||
@@ -185,19 +185,17 @@ const request: LLMRequest = LLM.request(input)
|
||||
|
||||
// The caller hands that request to the client and chooses one exit path:
|
||||
// inspect the compiled request, stream events, or collect a final response.
|
||||
const client: LLMClient = LLMClient.make()
|
||||
|
||||
// Alternative A: compile without sending HTTP. Useful for request-shape tests.
|
||||
// LLMRequest -> PreparedRequestOf<Payload>
|
||||
const prepared: PreparedRequestOf<Payload> = client.prepare<Payload>(request)
|
||||
const prepared: PreparedRequestOf<Payload> = LLMClient.prepare<Payload>(request)
|
||||
|
||||
// Alternative B: send HTTP and expose normalized stream events.
|
||||
// LLMRequest -> Stream<LLMEvent>
|
||||
const streamed: Stream.Stream<LLMEvent, LLMError> = client.stream(request)
|
||||
const streamed: Stream.Stream<LLMEvent, LLMError> = LLMClient.stream(request)
|
||||
|
||||
// Alternative C: send HTTP and collect those same events into one response.
|
||||
// LLMRequest -> LLMResponse
|
||||
const generated: LLMResponse = client.generate(request)
|
||||
const generated: LLMResponse = LLMClient.generate(request)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stage 3: Client Compiles The Request
|
||||
@@ -205,8 +203,7 @@ const generated: LLMResponse = client.generate(request)
|
||||
|
||||
// Internally, all three alternatives start by compiling the request. The client
|
||||
// first resolves model defaults plus request overrides, then selects the
|
||||
// runnable adapter from the model binding or an explicit registry keyed by
|
||||
// `request.model.adapter`.
|
||||
// runnable adapter from the registry keyed by `request.model.adapter`.
|
||||
const resolvedRequest: LLMRequest = resolveModelAndCallOptions(request)
|
||||
const adapter: Adapter<Payload> = resolveAdapter(request.model)
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ const FakeEcho = {
|
||||
// payload conversion, validation, endpoint, auth, and HTTP construction without
|
||||
// sending anything over the network.
|
||||
const inspectFakeProvider = Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make().prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: FakeEcho.model("tiny-echo"),
|
||||
prompt: "Show me the provider pipeline.",
|
||||
@@ -190,9 +190,9 @@ const inspectFakeProvider = Effect.gen(function* () {
|
||||
const program = Effect.gen(function* () {
|
||||
// yield* generateOnce
|
||||
// yield* inspectFakeProvider
|
||||
// yield* LLMClient.make().prepare(rawOverlayExample).pipe(Effect.andThen((prepared) => Effect.sync(() => console.log(prepared.payload))))
|
||||
// yield* LLMClient.prepare(rawOverlayExample).pipe(Effect.andThen((prepared) => Effect.sync(() => console.log(prepared.payload))))
|
||||
// yield* streamText
|
||||
yield* streamWithTools
|
||||
}).pipe(Effect.provide(Layer.mergeAll(LLM.layer(), RequestExecutor.defaultLayer)))
|
||||
}).pipe(Effect.provide(LLM.layer.pipe(Layer.provide(RequestExecutor.defaultLayer))))
|
||||
|
||||
Effect.runPromise(program)
|
||||
|
||||
@@ -52,20 +52,17 @@ export interface Adapter<Payload> {
|
||||
) => Stream.Stream<LLMEvent, LLMError>
|
||||
}
|
||||
|
||||
export type AdapterInput<Payload> = Adapter<Payload>
|
||||
|
||||
export interface AdapterDefinition<Payload> extends Adapter<Payload> {}
|
||||
|
||||
// Adapter registries intentionally erase payload generics after the typed
|
||||
// adapter is constructed. This keeps normal call sites on `OpenAIChat.adapter`
|
||||
// instead of leaking a separate runtime-adapter wrapper.
|
||||
// Adapter registries intentionally erase payload generics after construction.
|
||||
// Normal call sites use `OpenAIChat.adapter`; callers only need payload types
|
||||
// when preparing a request with a protocol-specific type assertion.
|
||||
// oxlint-disable-next-line typescript-eslint/no-explicit-any
|
||||
export type AnyAdapter = AdapterDefinition<any>
|
||||
export type AnyAdapter = Adapter<any>
|
||||
|
||||
const adapterRegistry = new Map<string, AnyAdapter>()
|
||||
|
||||
// The first adapter registered for an id is the package default. Tests and
|
||||
// advanced callers can still override per-client via `LLMClient.make({ adapters })`.
|
||||
// The first adapter registered for an id is the package default. Adapter lookup
|
||||
// is intentionally global: model refs name an adapter id, and importing the
|
||||
// provider/protocol/custom-adapter module registers the runnable implementation.
|
||||
const register = <Adapter extends AnyAdapter>(adapter: Adapter): Adapter => {
|
||||
if (!adapterRegistry.has(adapter.id)) adapterRegistry.set(adapter.id, adapter)
|
||||
return adapter
|
||||
@@ -202,10 +199,6 @@ export interface LLMClient {
|
||||
readonly generate: (request: LLMRequest) => Effect.Effect<LLMResponse, LLMError, RequestExecutor.Service>
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
readonly adapters?: ReadonlyArray<AnyAdapter>
|
||||
}
|
||||
|
||||
const noAdapter = (model: ModelRef) =>
|
||||
new NoAdapterError({ adapter: model.adapter, protocol: model.protocol, provider: model.provider, model: model.id })
|
||||
|
||||
@@ -254,7 +247,7 @@ export interface MakeInput<Payload, Frame, Chunk, State> {
|
||||
*/
|
||||
export function make<Payload, Frame, Chunk, State>(
|
||||
input: MakeInput<Payload, Frame, Chunk, State>,
|
||||
): AdapterDefinition<Payload> {
|
||||
): Adapter<Payload> {
|
||||
const auth = input.auth ?? authBearer
|
||||
const protocol = input.protocol
|
||||
const encodePayload = Schema.encodeSync(Schema.fromJsonString(protocol.payload))
|
||||
@@ -321,77 +314,68 @@ export function make<Payload, Frame, Chunk, State>(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the lower-level runtime. `compile` is the important boundary: it turns
|
||||
* a common `LLMRequest` into a validated provider payload plus HTTP request,
|
||||
* but does not execute transport.
|
||||
*/
|
||||
const makeClient = (options: ClientOptions = {}): LLMClient => {
|
||||
const adapters = new Map((options.adapters ?? []).map((adapter) => [adapter.id, adapter] as const))
|
||||
// `compile` is the important boundary: it turns a common `LLMRequest` into a
|
||||
// validated provider payload plus HTTP request, but does not execute transport.
|
||||
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
|
||||
const resolved = resolveRequestOptions(request)
|
||||
const adapter = registeredAdapter(resolved.model.adapter)
|
||||
if (!adapter) return yield* noAdapter(resolved.model)
|
||||
|
||||
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
|
||||
const resolved = resolveRequestOptions(request)
|
||||
const adapter = adapters.get(resolved.model.adapter) ?? registeredAdapter(resolved.model.adapter)
|
||||
if (!adapter) return yield* noAdapter(resolved.model)
|
||||
|
||||
const payload = yield* adapter.toPayload(resolved).pipe(
|
||||
Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(adapter.payloadSchema))),
|
||||
)
|
||||
const http = yield* adapter.toHttp(payload, {
|
||||
request: resolved,
|
||||
})
|
||||
|
||||
return {
|
||||
request: resolved,
|
||||
adapter,
|
||||
payload,
|
||||
http,
|
||||
}
|
||||
const payload = yield* adapter.toPayload(resolved).pipe(
|
||||
Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(adapter.payloadSchema))),
|
||||
)
|
||||
const http = yield* adapter.toHttp(payload, {
|
||||
request: resolved,
|
||||
})
|
||||
|
||||
const prepare = Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) {
|
||||
const compiled = yield* compile(request)
|
||||
return {
|
||||
request: resolved,
|
||||
adapter,
|
||||
payload,
|
||||
http,
|
||||
}
|
||||
})
|
||||
|
||||
return new PreparedRequest({
|
||||
id: compiled.request.id ?? "request",
|
||||
adapter: compiled.adapter.id,
|
||||
model: compiled.request.model,
|
||||
payload: compiled.payload,
|
||||
})
|
||||
const prepare = Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) {
|
||||
const compiled = yield* compile(request)
|
||||
|
||||
return new PreparedRequest({
|
||||
id: compiled.request.id ?? "request",
|
||||
adapter: compiled.adapter.id,
|
||||
model: compiled.request.model,
|
||||
payload: compiled.payload,
|
||||
})
|
||||
})
|
||||
|
||||
const stream = (request: LLMRequest) =>
|
||||
Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const compiled = yield* compile(request)
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const response = yield* executor.execute(compiled.http)
|
||||
const stream = (request: LLMRequest) =>
|
||||
Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const compiled = yield* compile(request)
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const response = yield* executor.execute(compiled.http)
|
||||
|
||||
return compiled.adapter.parse(response, { request: compiled.request })
|
||||
}),
|
||||
)
|
||||
return compiled.adapter.parse(response, { request: compiled.request })
|
||||
}),
|
||||
)
|
||||
|
||||
const generate = Effect.fn("LLM.generate")(function* (request: LLMRequest) {
|
||||
return new LLMResponse(
|
||||
yield* stream(request).pipe(
|
||||
Stream.runFold(
|
||||
() => ({ events: [] as LLMEvent[], usage: undefined as LLMResponse["usage"] }),
|
||||
(acc, event) => {
|
||||
acc.events.push(event)
|
||||
if ("usage" in event && event.usage !== undefined) acc.usage = event.usage
|
||||
return acc
|
||||
},
|
||||
),
|
||||
const generate = Effect.fn("LLM.generate")(function* (request: LLMRequest) {
|
||||
return new LLMResponse(
|
||||
yield* stream(request).pipe(
|
||||
Stream.runFold(
|
||||
() => ({ events: [] as LLMEvent[], usage: undefined as LLMResponse["usage"] }),
|
||||
(acc, event) => {
|
||||
acc.events.push(event)
|
||||
if ("usage" in event && event.usage !== undefined) acc.usage = event.usage
|
||||
return acc
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
// The runtime always emits a `PreparedRequest` (payload: unknown). Callers
|
||||
// who supply a `Payload` type argument assert the shape they expect from
|
||||
// their adapter; the cast hands them a typed view of the same payload.
|
||||
return { prepare: prepare as LLMClient["prepare"], stream, generate }
|
||||
}
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
export const Adapter = { make, model } as const
|
||||
|
||||
export const LLMClient = { make: makeClient }
|
||||
// The runtime always emits a `PreparedRequest` (payload: unknown). Callers who
|
||||
// supply a `Payload` type argument assert the shape they expect from their
|
||||
// adapter; the cast hands them a typed view of the same payload.
|
||||
export const LLMClient: LLMClient = { prepare: prepare as LLMClient["prepare"], stream, generate }
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
export { Adapter, LLMClient, modelCapabilities, modelLimits, modelRef } from "./client"
|
||||
export type {
|
||||
Adapter as AdapterShape,
|
||||
AdapterDefinition,
|
||||
AdapterInput,
|
||||
AdapterModelDefaults,
|
||||
AdapterModelInput,
|
||||
AdapterRoutedModelDefaults,
|
||||
AdapterRoutedModelInput,
|
||||
AnyAdapter,
|
||||
ClientOptions,
|
||||
HttpContext,
|
||||
LLMClient as LLMClientShape,
|
||||
ModelCapabilitiesInput,
|
||||
|
||||
@@ -2,7 +2,6 @@ export { LLMClient, modelCapabilities, modelLimits, modelRef } from "./adapter/c
|
||||
export type {
|
||||
AdapterModelInput,
|
||||
AdapterRoutedModelInput,
|
||||
ClientOptions,
|
||||
LLMClient as LLMClientShape,
|
||||
ModelCapabilitiesInput,
|
||||
ModelRefInput,
|
||||
|
||||
+21
-15
@@ -7,7 +7,7 @@ import {
|
||||
type ModelCapabilitiesInput,
|
||||
type ModelRefInput,
|
||||
} from "./adapter/client"
|
||||
import type { RequestExecutor } from "./adapter/executor"
|
||||
import { RequestExecutor } from "./adapter/executor"
|
||||
import { type Tools } from "./tool"
|
||||
import { ToolRuntime, type RunOptions } from "./tool-runtime"
|
||||
import {
|
||||
@@ -31,31 +31,37 @@ import type { LLMError } from "./schema"
|
||||
|
||||
export type StreamWithToolsInput<T extends Tools> = Omit<RequestInput, "tools"> & Omit<RunOptions<T>, "request">
|
||||
|
||||
export interface Runtime {
|
||||
readonly stream: (input: LLMRequest | RequestInput) => Stream.Stream<LLMEvent, LLMError, RequestExecutor.Service>
|
||||
readonly generate: (input: LLMRequest | RequestInput) => Effect.Effect<LLMResponse, LLMError, RequestExecutor.Service>
|
||||
export interface Interface {
|
||||
readonly stream: (input: LLMRequest | RequestInput) => Stream.Stream<LLMEvent, LLMError>
|
||||
readonly generate: (input: LLMRequest | RequestInput) => Effect.Effect<LLMResponse, LLMError>
|
||||
readonly streamWithTools: <T extends Tools>(
|
||||
input: StreamWithToolsInput<T>,
|
||||
) => Stream.Stream<LLMEvent, LLMError, RequestExecutor.Service>
|
||||
) => Stream.Stream<LLMEvent, LLMError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Runtime>()("@opencode/LLM") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LLM") {}
|
||||
|
||||
const requestOf = (input: LLMRequest | RequestInput) => (input instanceof LLMRequest ? input : request(input))
|
||||
|
||||
export const make = (): Runtime => {
|
||||
const client = LLMClient.make()
|
||||
return {
|
||||
stream: (input) => client.stream(requestOf(input)),
|
||||
generate: (input) => client.generate(requestOf(input)),
|
||||
export const make = (executor: RequestExecutor.Interface): Interface => ({
|
||||
stream: (input) =>
|
||||
LLMClient.stream(requestOf(input)).pipe(Stream.provideService(RequestExecutor.Service, executor)),
|
||||
generate: (input) =>
|
||||
LLMClient.generate(requestOf(input)).pipe(Effect.provideService(RequestExecutor.Service, executor)),
|
||||
streamWithTools: (input) => {
|
||||
const { maxSteps, concurrency, stopWhen, tools, ...rest } = input
|
||||
return ToolRuntime.run(client, { request: request(rest), tools, maxSteps, concurrency, stopWhen })
|
||||
return ToolRuntime.run({ request: request(rest), tools, maxSteps, concurrency, stopWhen }).pipe(
|
||||
Stream.provideService(RequestExecutor.Service, executor),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const layer = (): Layer.Layer<Service> => Layer.succeed(Service, Service.of(make()))
|
||||
export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
return Service.of(make(yield* RequestExecutor.Service))
|
||||
}),
|
||||
)
|
||||
|
||||
export const stream = (input: LLMRequest | RequestInput) =>
|
||||
Stream.unwrap(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect, Stream } from "effect"
|
||||
import type { Concurrency } from "effect/Types"
|
||||
import type { LLMClient } from "./adapter/client"
|
||||
import { LLMClient } from "./adapter/client"
|
||||
import type { RequestExecutor } from "./adapter/executor"
|
||||
import {
|
||||
type ContentPart,
|
||||
@@ -56,10 +56,7 @@ export interface RunOptions<T extends Tools> {
|
||||
* Tool handler dependencies are closed over at tool definition time, so the
|
||||
* runtime's only environment requirement is the `RequestExecutor.Service`.
|
||||
*/
|
||||
export const run = <T extends Tools>(
|
||||
client: LLMClient,
|
||||
options: RunOptions<T>,
|
||||
): Stream.Stream<LLMEvent, LLMError, RequestExecutor.Service> => {
|
||||
export const run = <T extends Tools>(options: RunOptions<T>): Stream.Stream<LLMEvent, LLMError, RequestExecutor.Service> => {
|
||||
const maxSteps = options.maxSteps ?? 10
|
||||
const concurrency = options.concurrency ?? 10
|
||||
const tools = options.tools as Tools
|
||||
@@ -80,7 +77,7 @@ export const run = <T extends Tools>(
|
||||
Effect.gen(function* () {
|
||||
const state: StepState = { assistantContent: [], toolCalls: [], finishReason: undefined }
|
||||
|
||||
const modelStream = client.stream(request).pipe(
|
||||
const modelStream = LLMClient.stream(request).pipe(
|
||||
Stream.tap((event) => Effect.sync(() => accumulate(state, event))),
|
||||
)
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ const it = testEffect(echoLayer)
|
||||
describe("llm adapter", () => {
|
||||
it.effect("stream and generate use the adapter pipeline", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [fake] })
|
||||
const llm = LLMClient
|
||||
const events = Array.from(yield* llm.stream(request).pipe(Stream.runCollect))
|
||||
const response = yield* llm.generate(request)
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("llm adapter", () => {
|
||||
|
||||
it.effect("selects adapters by request adapter", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [fake, gemini] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.updateRequest(request, { model: updateModel(request.model, { adapter: "gemini-fake" }) }),
|
||||
)
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("llm adapter", () => {
|
||||
|
||||
it.effect("uses registered adapters by model adapter id", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.updateRequest(request, { model: updateModel(request.model, { adapter: "gemini-fake" }) }),
|
||||
)
|
||||
|
||||
@@ -147,24 +147,6 @@ describe("llm adapter", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("explicit adapters override provider adapters", () =>
|
||||
Effect.gen(function* () {
|
||||
const override = Adapter.make({
|
||||
id: "fake",
|
||||
protocol: Protocol.define({
|
||||
...fakeProtocol,
|
||||
toPayload: () => Effect.succeed({ body: "override" }),
|
||||
}),
|
||||
endpoint: Endpoint.baseURL({ default: "https://fake.local", path: "/chat" }),
|
||||
framing: fakeFraming,
|
||||
})
|
||||
|
||||
const response = yield* LLMClient.make({ adapters: [override] }).generate(request)
|
||||
|
||||
expect(response.text).toBe('echo:{"body":"override"}')
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps the first registered adapter as the default", () =>
|
||||
Effect.gen(function* () {
|
||||
Adapter.make({
|
||||
@@ -177,7 +159,7 @@ describe("llm adapter", () => {
|
||||
framing: fakeFraming,
|
||||
})
|
||||
|
||||
const response = yield* LLMClient.make().generate(request)
|
||||
const response = yield* LLMClient.generate(request)
|
||||
|
||||
expect(response.text).toBe('echo:{"body":"hello"}')
|
||||
}),
|
||||
@@ -185,7 +167,7 @@ describe("llm adapter", () => {
|
||||
|
||||
it.effect("rejects missing adapter", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make({ adapters: [fake] })
|
||||
const error = yield* LLMClient
|
||||
.prepare(
|
||||
LLM.updateRequest(request, { model: updateModel(request.model, { adapter: "missing" }) }),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-message
|
||||
describe("public exports", () => {
|
||||
test("root exposes app-facing runtime APIs", () => {
|
||||
expect(LLM.generate).toBeFunction()
|
||||
expect(LLMClient.make).toBeFunction()
|
||||
expect(LLMClient.generate).toBeFunction()
|
||||
})
|
||||
|
||||
test("adapter barrel exposes adapter-authoring APIs", () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ const recorded = recordedTests({
|
||||
requires: ["ANTHROPIC_API_KEY"],
|
||||
options: { requestHeaders: ["content-type", "anthropic-version"] },
|
||||
})
|
||||
const anthropic = LLMClient.make({ adapters: [AnthropicMessages.adapter] })
|
||||
const anthropic = LLMClient
|
||||
|
||||
const malformedToolOrderRequest = LLM.request({
|
||||
id: "recorded_anthropic_malformed_tool_order",
|
||||
@@ -72,7 +72,7 @@ describe("Anthropic Messages recorded", () => {
|
||||
|
||||
recorded.effect.with("claude opus 4.7 drives a tool loop", { tags: ["tool", "tool-loop", "golden", "flagship"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(anthropic, flagshipToolLoopRequest))
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(flagshipToolLoopRequest))
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const it = testEffect(Layer.empty)
|
||||
describe("Anthropic Messages adapter", () => {
|
||||
it.effect("prepares Anthropic Messages target", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] }).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.payload).toEqual({
|
||||
model: "claude-sonnet-4-5",
|
||||
@@ -41,7 +41,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
|
||||
it.effect("prepares tool call and tool result messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result",
|
||||
model,
|
||||
@@ -80,7 +80,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 2 } },
|
||||
{ type: "message_stop" },
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(fixedResponse(body)))
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
{ type: "content_block_stop", index: 0 },
|
||||
{ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 1 } },
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
||||
@@ -130,7 +130,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
|
||||
it.effect("emits provider-error events for mid-stream provider errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
@@ -144,7 +144,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
|
||||
it.effect("fails HTTP provider errors before stream parsing", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] })
|
||||
const error = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
@@ -184,7 +184,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
{ type: "content_block_stop", index: 2 },
|
||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } },
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
tools: [{ name: "web_search", description: "Web search", inputSchema: { type: "object" } }],
|
||||
@@ -232,7 +232,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
{ type: "content_block_stop", index: 1 },
|
||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
tools: [{ name: "web_search", description: "Web search", inputSchema: { type: "object" } }],
|
||||
@@ -253,7 +253,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
|
||||
it.effect("round-trips provider-executed assistant content into server tool blocks", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_round_trip",
|
||||
model,
|
||||
@@ -304,7 +304,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
|
||||
it.effect("rejects round-trip for unknown server tool names", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] })
|
||||
const error = yield* LLMClient
|
||||
.prepare(
|
||||
LLM.request({
|
||||
id: "req_unknown_server_tool",
|
||||
@@ -330,7 +330,7 @@ describe("Anthropic Messages adapter", () => {
|
||||
|
||||
it.effect("rejects unsupported user media content", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] })
|
||||
const error = yield* LLMClient
|
||||
.prepare(
|
||||
LLM.request({
|
||||
id: "req_media",
|
||||
|
||||
@@ -64,7 +64,7 @@ const it = testEffect(Layer.empty)
|
||||
describe("Bedrock Converse adapter", () => {
|
||||
it.effect("prepares Converse target with system, inference config, and messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] }).prepare(baseRequest)
|
||||
const prepared = yield* LLMClient.prepare(baseRequest)
|
||||
|
||||
expect(prepared.payload).toEqual({
|
||||
modelId: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
@@ -77,7 +77,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
|
||||
it.effect("prepares tool config with toolSpec and toolChoice", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.updateRequest(baseRequest, {
|
||||
tools: [
|
||||
{
|
||||
@@ -111,7 +111,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
|
||||
it.effect("lowers assistant tool-call + tool-result message history", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_history",
|
||||
model,
|
||||
@@ -157,7 +157,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
["messageStop", { stopReason: "end_turn" }],
|
||||
["metadata", { usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 } }],
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(baseRequest)
|
||||
.pipe(Effect.provide(fixedBytes(body)))
|
||||
|
||||
@@ -192,7 +192,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
["contentBlockStop", { contentBlockIndex: 0 }],
|
||||
["messageStop", { stopReason: "tool_use" }],
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(baseRequest, {
|
||||
tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object" } }],
|
||||
@@ -223,7 +223,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
["contentBlockStop", { contentBlockIndex: 0 }],
|
||||
["messageStop", { stopReason: "end_turn" }],
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(baseRequest)
|
||||
.pipe(Effect.provide(fixedBytes(body)))
|
||||
|
||||
@@ -237,7 +237,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
["messageStart", { role: "assistant" }],
|
||||
["throttlingException", { message: "Slow down" }],
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(baseRequest)
|
||||
.pipe(Effect.provide(fixedBytes(body)))
|
||||
|
||||
@@ -255,7 +255,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
id: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
baseURL: "https://bedrock-runtime.test",
|
||||
})
|
||||
const error = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
const error = yield* LLMClient
|
||||
.generate(LLM.updateRequest(baseRequest, { model: unsignedModel }))
|
||||
.pipe(Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: "end_turn" }]))), Effect.flip)
|
||||
|
||||
@@ -274,7 +274,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
},
|
||||
})
|
||||
const prepared = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.updateRequest(baseRequest, { model: signed }),
|
||||
)
|
||||
|
||||
@@ -291,7 +291,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
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.make({ adapters: [BedrockConverse.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_cache",
|
||||
model,
|
||||
@@ -323,7 +323,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
|
||||
it.effect("does not emit cachePoint when no cache hint is set", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] }).prepare(baseRequest)
|
||||
const prepared = yield* LLMClient.prepare(baseRequest)
|
||||
expect(prepared.payload).toMatchObject({
|
||||
system: [{ text: "You are concise." }],
|
||||
messages: [{ role: "user", content: [{ text: "Say hello." }] }],
|
||||
@@ -333,7 +333,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
|
||||
it.effect("lowers image media into Bedrock image blocks", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_image",
|
||||
model,
|
||||
@@ -369,7 +369,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
|
||||
it.effect("base64-encodes Uint8Array image bytes", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_image_bytes",
|
||||
model,
|
||||
@@ -395,7 +395,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
|
||||
it.effect("lowers document media into Bedrock document blocks with format and name", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_doc",
|
||||
model,
|
||||
@@ -426,7 +426,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
|
||||
it.effect("rejects unsupported image media types", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
const error = yield* LLMClient
|
||||
.prepare(
|
||||
LLM.request({
|
||||
id: "req_bad_image",
|
||||
@@ -442,7 +442,7 @@ describe("Bedrock Converse adapter", () => {
|
||||
|
||||
it.effect("rejects unsupported document media types", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
const error = yield* LLMClient
|
||||
.prepare(
|
||||
LLM.request({
|
||||
id: "req_bad_doc",
|
||||
@@ -494,7 +494,7 @@ const recorded = recordedTests({
|
||||
describe("Bedrock Converse recorded", () => {
|
||||
recorded.effect("streams text", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
const llm = LLMClient
|
||||
const response = yield* llm.generate(
|
||||
LLM.request({
|
||||
id: "recorded_bedrock_text",
|
||||
@@ -514,7 +514,7 @@ describe("Bedrock Converse recorded", () => {
|
||||
|
||||
recorded.effect.with("streams a tool call", { tags: ["tool"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
const llm = LLMClient
|
||||
const response = yield* llm.generate(
|
||||
LLM.request({
|
||||
id: "recorded_bedrock_tool_call",
|
||||
@@ -536,8 +536,8 @@ describe("Bedrock Converse recorded", () => {
|
||||
|
||||
recorded.effect.with("drives a tool loop", { tags: ["tool", "tool-loop", "golden"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [BedrockConverse.adapter] })
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(llm, weatherToolLoopRequest({
|
||||
const llm = LLMClient
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(weatherToolLoopRequest({
|
||||
id: "recorded_bedrock_tool_loop",
|
||||
model: recordedModel(),
|
||||
})))
|
||||
|
||||
@@ -20,7 +20,7 @@ const recorded = recordedTests({
|
||||
protocol: "gemini",
|
||||
requires: ["GOOGLE_GENERATIVE_AI_API_KEY"],
|
||||
})
|
||||
const gemini = LLMClient.make({ adapters: [Gemini.adapter] })
|
||||
const gemini = LLMClient
|
||||
|
||||
describe("Gemini recorded", () => {
|
||||
recorded.effect("streams text", () =>
|
||||
|
||||
@@ -26,7 +26,7 @@ const it = testEffect(Layer.empty)
|
||||
describe("Gemini adapter", () => {
|
||||
it.effect("prepares Gemini target", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [Gemini.adapter] }).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.payload).toEqual({
|
||||
contents: [{ role: "user", parts: [{ text: "Say hello." }] }],
|
||||
@@ -38,7 +38,7 @@ describe("Gemini adapter", () => {
|
||||
|
||||
it.effect("prepares multimodal user input and tool history", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [Gemini.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result",
|
||||
model,
|
||||
@@ -91,7 +91,7 @@ describe("Gemini adapter", () => {
|
||||
|
||||
it.effect("omits tools when tool choice is none", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [Gemini.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_no_tools",
|
||||
model,
|
||||
@@ -109,7 +109,7 @@ describe("Gemini adapter", () => {
|
||||
|
||||
it.effect("sanitizes integer enums, dangling required, untyped arrays, and scalar object keys", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [Gemini.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_schema_patch",
|
||||
model,
|
||||
@@ -177,7 +177,7 @@ describe("Gemini adapter", () => {
|
||||
},
|
||||
},
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [Gemini.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(fixedResponse(body)))
|
||||
|
||||
@@ -230,7 +230,7 @@ describe("Gemini adapter", () => {
|
||||
usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 1 },
|
||||
},
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [Gemini.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
||||
@@ -266,7 +266,7 @@ describe("Gemini adapter", () => {
|
||||
}],
|
||||
},
|
||||
)
|
||||
const response = yield* LLMClient.make({ adapters: [Gemini.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
||||
@@ -284,14 +284,14 @@ describe("Gemini adapter", () => {
|
||||
|
||||
it.effect("maps length and content-filter finish reasons", () =>
|
||||
Effect.gen(function* () {
|
||||
const length = yield* LLMClient.make({ adapters: [Gemini.adapter] })
|
||||
const length = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(sseEvents({ candidates: [{ content: { role: "model", parts: [] }, finishReason: "MAX_TOKENS" }] })),
|
||||
),
|
||||
)
|
||||
const filtered = yield* LLMClient.make({ adapters: [Gemini.adapter] })
|
||||
const filtered = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
@@ -306,7 +306,7 @@ describe("Gemini adapter", () => {
|
||||
|
||||
it.effect("leaves total usage undefined when component counts are missing", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.make({ adapters: [Gemini.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(fixedResponse(sseEvents({ usageMetadata: { thoughtsTokenCount: 1 } }))))
|
||||
|
||||
@@ -317,7 +317,7 @@ describe("Gemini adapter", () => {
|
||||
|
||||
it.effect("fails invalid stream chunks", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make({ adapters: [Gemini.adapter] })
|
||||
const error = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(
|
||||
Effect.provide(fixedResponse(sseRaw("data: {not json}"))),
|
||||
@@ -331,7 +331,7 @@ describe("Gemini adapter", () => {
|
||||
|
||||
it.effect("rejects unsupported assistant media content", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make({ adapters: [Gemini.adapter] })
|
||||
const error = yield* LLMClient
|
||||
.prepare(
|
||||
LLM.request({
|
||||
id: "req_media",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { LLM } from "../../src"
|
||||
import { LLMClient } from "../../src/adapter"
|
||||
import * as OpenAIChat from "../../src/protocols/openai-chat"
|
||||
import { ToolRuntime } from "../../src/tool-runtime"
|
||||
import { eventSummary, weatherRuntimeTool } from "../recorded-scenarios"
|
||||
@@ -32,13 +31,11 @@ const recorded = recordedTests({
|
||||
protocol: "openai-chat",
|
||||
requires: ["OPENAI_API_KEY"],
|
||||
})
|
||||
const openai = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
|
||||
describe("OpenAI Chat tool-loop recorded", () => {
|
||||
recorded.effect.with("drives a tool loop end-to-end", { tags: ["tool", "tool-loop"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(openai, { request, tools: { get_weather: weatherRuntimeTool } }).pipe(Stream.runCollect),
|
||||
yield* ToolRuntime.run({ request, tools: { get_weather: weatherRuntimeTool } }).pipe(Stream.runCollect),
|
||||
)
|
||||
|
||||
expect(LLM.outputText({ events })).toContain("Paris")
|
||||
|
||||
@@ -36,7 +36,7 @@ const recorded = recordedTests({
|
||||
protocol: "openai-chat",
|
||||
requires: ["OPENAI_API_KEY"],
|
||||
})
|
||||
const openai = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
const openai = LLMClient
|
||||
|
||||
describe("OpenAI Chat recorded", () => {
|
||||
recorded.effect("streams text", () =>
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
// Pass the OpenAIChat payload type so `prepared.payload` is statically
|
||||
// typed to the adapter's native shape — the assertions below read field
|
||||
// names without `unknown` casts.
|
||||
const prepared = yield* LLMClient.make().prepare<OpenAIChat.OpenAIChatPayload>(request)
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatPayload>(request)
|
||||
const _typed: { readonly model: string; readonly stream: true } = prepared.payload
|
||||
|
||||
expect(prepared.payload).toEqual({
|
||||
@@ -56,7 +56,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
|
||||
it.effect("maps OpenAI provider options to Chat options", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make().prepare<OpenAIChat.OpenAIChatPayload>(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatPayload>(
|
||||
LLM.request({
|
||||
model: OpenAI.chat("gpt-4o-mini", { baseURL: "https://api.openai.test/v1/" }),
|
||||
prompt: "think",
|
||||
@@ -70,7 +70,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
)
|
||||
|
||||
it.effect("adds native query params to the Chat Completions URL", () =>
|
||||
LLMClient.make()
|
||||
LLMClient
|
||||
.generate(LLM.updateRequest(request, { model: OpenAIChat.model({ ...model, queryParams: { "api-version": "v1" } }) }))
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
@@ -88,7 +88,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
)
|
||||
|
||||
it.effect("uses Azure api-key header for static OpenAI Chat keys", () =>
|
||||
LLMClient.make()
|
||||
LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
model: Azure.model("gpt-4o-mini", {
|
||||
@@ -116,7 +116,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
)
|
||||
|
||||
it.effect("applies serializable HTTP overlays after payload lowering", () =>
|
||||
LLMClient.make()
|
||||
LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
model: OpenAIChat.model({ ...model, apiKey: "fresh-key", headers: { authorization: "Bearer stale" } }),
|
||||
@@ -151,7 +151,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
|
||||
it.effect("prepares assistant tool-call and tool-result messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make().prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result",
|
||||
model,
|
||||
@@ -188,7 +188,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
|
||||
it.effect("rejects unsupported user media content", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make()
|
||||
const error = yield* LLMClient
|
||||
.prepare(
|
||||
LLM.request({
|
||||
id: "req_media",
|
||||
@@ -204,7 +204,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
|
||||
it.effect("rejects unsupported assistant reasoning content", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make()
|
||||
const error = yield* LLMClient
|
||||
.prepare(
|
||||
LLM.request({
|
||||
id: "req_reasoning",
|
||||
@@ -232,7 +232,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
completion_tokens_details: { reasoning_tokens: 0 },
|
||||
}),
|
||||
)
|
||||
const response = yield* LLMClient.make()
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(fixedResponse(body)))
|
||||
|
||||
@@ -272,7 +272,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
deltaChunk({ tool_calls: [{ index: 0, function: { arguments: ':"weather"}' } }] }),
|
||||
deltaChunk({}, "tool_calls"),
|
||||
)
|
||||
const response = yield* LLMClient.make()
|
||||
const response = yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
||||
@@ -298,7 +298,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
}),
|
||||
deltaChunk({ tool_calls: [{ index: 0, function: { arguments: ':"weather"}' } }] }),
|
||||
)
|
||||
const response = yield* LLMClient.make()
|
||||
const response = yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
||||
@@ -317,7 +317,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
it.effect("fails on malformed stream chunks", () =>
|
||||
Effect.gen(function* () {
|
||||
const body = sseEvents(deltaChunk({ content: 123 }))
|
||||
const error = yield* LLMClient.make()
|
||||
const error = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(fixedResponse(body)), Effect.flip)
|
||||
|
||||
@@ -330,7 +330,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
const layer = truncatedStream([
|
||||
`data: ${JSON.stringify(deltaChunk({ role: "assistant", content: "Hello" }))}\n\n`,
|
||||
])
|
||||
const error = yield* LLMClient.make()
|
||||
const error = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(layer), Effect.flip)
|
||||
|
||||
@@ -340,7 +340,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
|
||||
it.effect("fails HTTP provider errors before stream parsing", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make()
|
||||
const error = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
@@ -360,7 +360,7 @@ describe("OpenAI Chat adapter", () => {
|
||||
|
||||
it.effect("short-circuits the upstream stream when the consumer takes a prefix", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make()
|
||||
const llm = LLMClient
|
||||
// The body has more chunks than we'll consume. If `Stream.take(1)` did
|
||||
// not interrupt the upstream HTTP body the test would hang waiting for
|
||||
// the rest of the stream to drain.
|
||||
|
||||
@@ -55,7 +55,7 @@ const xaiRequest = textRequest({ id: "recorded_xai_text", model: xaiModel })
|
||||
const xaiToolRequest = weatherToolRequest({ id: "recorded_xai_tool_call", model: xaiModel })
|
||||
|
||||
const recorded = recordedTests({ prefix: "openai-compatible-chat", protocol: "openai-compatible-chat" })
|
||||
const llm = LLMClient.make({ adapters: [OpenAICompatibleChat.adapter, ...OpenRouter.adapters] })
|
||||
const llm = LLMClient
|
||||
|
||||
const openrouterToolLoops = [
|
||||
{
|
||||
@@ -128,7 +128,7 @@ describe("OpenAI-compatible Chat recorded", () => {
|
||||
|
||||
recorded.effect.with("groq llama 3.3 70b drives a tool loop", { provider: "groq", requires: ["GROQ_API_KEY"], tags: ["tool", "tool-loop", "golden"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(llm, weatherToolLoopRequest({
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(weatherToolLoopRequest({
|
||||
id: "recorded_groq_llama_3_3_70b_tool_loop",
|
||||
model: groqModel,
|
||||
})))
|
||||
@@ -158,7 +158,7 @@ describe("OpenAI-compatible Chat recorded", () => {
|
||||
openrouterToolLoops.forEach((scenario) =>
|
||||
recorded.effect.with(scenario.name, { provider: "openrouter", requires: ["OPENROUTER_API_KEY"], tags: scenario.tags }, () =>
|
||||
Effect.gen(function* () {
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(llm, weatherToolLoopRequest({
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(weatherToolLoopRequest({
|
||||
id: scenario.id,
|
||||
model: scenario.model,
|
||||
system: "Use the get_weather tool exactly once, then answer in one short sentence.",
|
||||
@@ -188,7 +188,7 @@ describe("OpenAI-compatible Chat recorded", () => {
|
||||
|
||||
recorded.effect.with("xai grok 4.3 drives a tool loop", { provider: "xai", requires: ["XAI_API_KEY"], tags: ["tool", "tool-loop", "golden", "flagship"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(llm, weatherToolLoopRequest({
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(weatherToolLoopRequest({
|
||||
id: "recorded_xai_grok_4_3_tool_loop",
|
||||
model: xaiFlagshipModel,
|
||||
})))
|
||||
|
||||
@@ -54,7 +54,7 @@ const providerFamilies = [
|
||||
describe("OpenAI-compatible Chat adapter", () => {
|
||||
it.effect("prepares generic Chat target", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [OpenAICompatibleChat.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.updateRequest(request, {
|
||||
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
||||
toolChoice: { type: "required" },
|
||||
@@ -127,7 +127,7 @@ describe("OpenAI-compatible Chat adapter", () => {
|
||||
|
||||
it.effect("matches AI SDK compatible basic request body fixture", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [OpenAICompatibleChat.adapter] }).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.payload).toEqual({
|
||||
model: "deepseek-chat",
|
||||
@@ -145,7 +145,7 @@ describe("OpenAI-compatible Chat adapter", () => {
|
||||
|
||||
it.effect("matches AI SDK compatible tool request body fixture", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: [OpenAICompatibleChat.adapter] }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_parity",
|
||||
model,
|
||||
@@ -195,7 +195,7 @@ describe("OpenAI-compatible Chat adapter", () => {
|
||||
|
||||
it.effect("posts to the configured compatible endpoint and parses text usage", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.make({ adapters: [OpenAICompatibleChat.adapter] })
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
|
||||
@@ -41,7 +41,7 @@ const recorded = recordedTests({
|
||||
protocol: "openai-responses",
|
||||
requires: ["OPENAI_API_KEY"],
|
||||
})
|
||||
const openai = LLMClient.make({ adapters: [OpenAIResponses.adapter] })
|
||||
const openai = LLMClient
|
||||
|
||||
describe("OpenAI Responses recorded", () => {
|
||||
recorded.effect.with("gpt-5.5 streams text", { tags: ["flagship"] }, () =>
|
||||
@@ -71,7 +71,7 @@ describe("OpenAI Responses recorded", () => {
|
||||
|
||||
recorded.effect.with("gpt-5.5 drives a tool loop", { tags: ["tool", "tool-loop", "golden", "flagship"] }, () =>
|
||||
Effect.gen(function* () {
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(openai, loopRequest))
|
||||
expectWeatherToolLoop(yield* runWeatherToolLoop(loopRequest))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ const it = testEffect(Layer.empty)
|
||||
describe("OpenAI Responses adapter", () => {
|
||||
it.effect("prepares OpenAI Responses target", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make().prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.payload).toEqual({
|
||||
model: "gpt-4.1-mini",
|
||||
@@ -46,7 +46,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
|
||||
it.effect("adds native query params to the Responses URL", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* LLMClient.make()
|
||||
yield* LLMClient
|
||||
.generate(LLM.updateRequest(request, { model: OpenAIResponses.model({ ...model, queryParams: { "api-version": "v1" } }) }))
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
@@ -66,7 +66,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
|
||||
it.effect("uses Azure api-key header for static OpenAI Responses keys", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* LLMClient.make()
|
||||
yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
model: Azure.model("gpt-4.1-mini", {
|
||||
@@ -95,7 +95,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
|
||||
it.effect("prepares function call and function output input items", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make().prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result",
|
||||
model,
|
||||
@@ -121,7 +121,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
|
||||
it.effect("maps OpenAI provider options to Responses options", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make().prepare<OpenAIResponses.OpenAIResponsesPayload>(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesPayload>(
|
||||
LLM.request({
|
||||
model: OpenAI.model("gpt-5.2", { baseURL: "https://api.openai.test/v1/" }),
|
||||
prompt: "think",
|
||||
@@ -146,7 +146,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
|
||||
it.effect("request OpenAI provider options override model defaults", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make().prepare<OpenAIResponses.OpenAIResponsesPayload>(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesPayload>(
|
||||
LLM.request({
|
||||
model: OpenAI.model("gpt-4.1-mini", {
|
||||
baseURL: "https://api.openai.test/v1/",
|
||||
@@ -179,7 +179,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
},
|
||||
},
|
||||
)
|
||||
const response = yield* LLMClient.make()
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(fixedResponse(body)))
|
||||
|
||||
@@ -230,7 +230,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
},
|
||||
{ type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } },
|
||||
)
|
||||
const response = yield* LLMClient.make()
|
||||
const response = yield* LLMClient
|
||||
.generate(
|
||||
LLM.updateRequest(request, {
|
||||
tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }],
|
||||
@@ -264,7 +264,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
{ type: "response.output_item.done", item },
|
||||
{ type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } },
|
||||
)
|
||||
const response = yield* LLMClient.make()
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(fixedResponse(body)))
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
{ type: "response.output_item.done", item },
|
||||
{ type: "response.completed", response: { usage: { input_tokens: 5, output_tokens: 1 } } },
|
||||
)
|
||||
const response = yield* LLMClient.make()
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(fixedResponse(body)))
|
||||
|
||||
@@ -327,7 +327,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
|
||||
it.effect("rejects unsupported user media content", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make()
|
||||
const error = yield* LLMClient
|
||||
.prepare(
|
||||
LLM.request({
|
||||
id: "req_media",
|
||||
@@ -343,7 +343,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
|
||||
it.effect("emits provider-error events for mid-stream provider errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.make()
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
@@ -357,7 +357,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
|
||||
it.effect("falls back to error code when no message is present", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.make()
|
||||
const response = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(Effect.provide(fixedResponse(sseEvents({ type: "error", code: "internal_error" }))))
|
||||
|
||||
@@ -367,7 +367,7 @@ describe("OpenAI Responses adapter", () => {
|
||||
|
||||
it.effect("fails HTTP provider errors before stream parsing", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.make()
|
||||
const error = yield* LLMClient
|
||||
.generate(request)
|
||||
.pipe(
|
||||
Effect.provide(
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("OpenRouter", () => {
|
||||
apiKey: "test-key",
|
||||
})
|
||||
|
||||
const prepared = yield* LLMClient.make({ adapters: OpenRouter.adapters }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({ model, prompt: "Say hello." }),
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("OpenRouter", () => {
|
||||
|
||||
it.effect("applies OpenRouter payload options from the model helper", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.make({ adapters: OpenRouter.adapters }).prepare(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: OpenRouter.model("anthropic/claude-3.7-sonnet:thinking", {
|
||||
providerOptions: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { LLM, LLMEvent, type LLMRequest, type LLMResponse, type ModelRef } from "../src"
|
||||
import type { LLMClient } from "../src/adapter"
|
||||
import { tool } from "../src/tool"
|
||||
import { ToolRuntime } from "../src/tool-runtime"
|
||||
|
||||
@@ -76,8 +75,8 @@ export const weatherToolLoopRequest = (input: {
|
||||
: { maxTokens: input.maxTokens ?? 80, temperature: input.temperature ?? 0 },
|
||||
})
|
||||
|
||||
export const runWeatherToolLoop = (client: LLMClient, request: LLMRequest) =>
|
||||
ToolRuntime.run(client, { request, tools: { [weatherToolName]: weatherRuntimeTool } }).pipe(
|
||||
export const runWeatherToolLoop = (request: LLMRequest) =>
|
||||
ToolRuntime.run({ request, tools: { [weatherToolName]: weatherRuntimeTool } }).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.map((events) => Array.from(events)),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer, Schema, Stream } from "effect"
|
||||
import { LLM, LLMEvent, LLMRequest } from "../src"
|
||||
import { LLMClient, RequestExecutor } from "../src/adapter"
|
||||
import { LLMClient } from "../src/adapter"
|
||||
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||
import { tool, ToolFailure } from "../src/tool"
|
||||
import { ToolRuntime } from "../src/tool-runtime"
|
||||
@@ -40,11 +41,10 @@ const get_weather = tool({
|
||||
describe("ToolRuntime", () => {
|
||||
it.effect("uses the registered model adapter when adding runtime tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make()
|
||||
const layer = scriptedResponses([sseEvents(deltaChunk({ role: "assistant", content: "Done." }), finishChunk("stop"))])
|
||||
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(llm, { request: baseRequest, tools: { get_weather } }).pipe(
|
||||
yield* ToolRuntime.run({ request: baseRequest, tools: { get_weather } }).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.provide(layer),
|
||||
),
|
||||
@@ -70,7 +70,7 @@ describe("ToolRuntime", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
yield* ToolRuntime.run(LLMClient.make(), {
|
||||
yield* ToolRuntime.run({
|
||||
request: LLMRequest.update(baseRequest, {
|
||||
generation: LLM.generation({ maxTokens: 50 }),
|
||||
toolChoice: LLM.toolChoice("auto"),
|
||||
@@ -104,14 +104,13 @@ describe("ToolRuntime", () => {
|
||||
|
||||
it.effect("dispatches a tool call, appends results, and resumes streaming", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
const layer = scriptedResponses([
|
||||
sseEvents(toolCallChunk("call_1", "get_weather", '{"city":"Paris"}'), finishChunk("tool_calls")),
|
||||
sseEvents(deltaChunk({ role: "assistant", content: "It's sunny in Paris." }), finishChunk("stop")),
|
||||
])
|
||||
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(llm, { request: baseRequest, tools: { get_weather } }).pipe(
|
||||
yield* ToolRuntime.run({ request: baseRequest, tools: { get_weather } }).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.provide(layer),
|
||||
),
|
||||
@@ -131,14 +130,13 @@ describe("ToolRuntime", () => {
|
||||
|
||||
it.effect("emits tool-error for unknown tools so the model can self-correct", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
const layer = scriptedResponses([
|
||||
sseEvents(toolCallChunk("call_1", "missing_tool", "{}"), finishChunk("tool_calls")),
|
||||
sseEvents(deltaChunk({ role: "assistant", content: "Sorry." }), finishChunk("stop")),
|
||||
])
|
||||
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(llm, { request: baseRequest, tools: { get_weather } }).pipe(
|
||||
yield* ToolRuntime.run({ request: baseRequest, tools: { get_weather } }).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.provide(layer),
|
||||
),
|
||||
@@ -158,14 +156,13 @@ describe("ToolRuntime", () => {
|
||||
|
||||
it.effect("emits tool-error when the LLM input fails the parameters schema", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
const layer = scriptedResponses([
|
||||
sseEvents(toolCallChunk("call_1", "get_weather", '{"city":42}'), finishChunk("tool_calls")),
|
||||
sseEvents(deltaChunk({ role: "assistant", content: "Done." }), finishChunk("stop")),
|
||||
])
|
||||
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(llm, { request: baseRequest, tools: { get_weather } }).pipe(
|
||||
yield* ToolRuntime.run({ request: baseRequest, tools: { get_weather } }).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.provide(layer),
|
||||
),
|
||||
@@ -179,14 +176,13 @@ describe("ToolRuntime", () => {
|
||||
|
||||
it.effect("emits tool-error when the handler returns a ToolFailure", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
const layer = scriptedResponses([
|
||||
sseEvents(toolCallChunk("call_1", "get_weather", '{"city":"FAIL"}'), finishChunk("tool_calls")),
|
||||
sseEvents(deltaChunk({ role: "assistant", content: "Sorry." }), finishChunk("stop")),
|
||||
])
|
||||
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(llm, { request: baseRequest, tools: { get_weather } }).pipe(
|
||||
yield* ToolRuntime.run({ request: baseRequest, tools: { get_weather } }).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.provide(layer),
|
||||
),
|
||||
@@ -200,11 +196,10 @@ describe("ToolRuntime", () => {
|
||||
|
||||
it.effect("stops when the model finishes without requesting more tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
const layer = scriptedResponses([sseEvents(deltaChunk({ role: "assistant", content: "Done." }), finishChunk("stop"))])
|
||||
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(llm, { request: baseRequest, tools: { get_weather } }).pipe(
|
||||
yield* ToolRuntime.run({ request: baseRequest, tools: { get_weather } }).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.provide(layer),
|
||||
),
|
||||
@@ -217,7 +212,6 @@ describe("ToolRuntime", () => {
|
||||
|
||||
it.effect("respects maxSteps and stops the loop", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
// Every script entry asks for another tool call. With maxSteps: 2 the
|
||||
// runtime should run at most two model rounds and then exit even though
|
||||
// the model still wants to keep going.
|
||||
@@ -225,7 +219,7 @@ describe("ToolRuntime", () => {
|
||||
const layer = scriptedResponses([toolCallStep, toolCallStep, toolCallStep])
|
||||
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(llm, { request: baseRequest, tools: { get_weather }, maxSteps: 2 }).pipe(
|
||||
yield* ToolRuntime.run({ request: baseRequest, tools: { get_weather }, maxSteps: 2 }).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.provide(layer),
|
||||
),
|
||||
@@ -237,14 +231,13 @@ describe("ToolRuntime", () => {
|
||||
|
||||
it.effect("stops when stopWhen returns true after the first step", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
const layer = scriptedResponses([
|
||||
sseEvents(toolCallChunk("call_1", "get_weather", '{"city":"Paris"}'), finishChunk("tool_calls")),
|
||||
sseEvents(deltaChunk({ role: "assistant", content: "Should not run." }), finishChunk("stop")),
|
||||
])
|
||||
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(llm, {
|
||||
yield* ToolRuntime.run({
|
||||
request: baseRequest,
|
||||
tools: { get_weather },
|
||||
stopWhen: (state) => state.step >= 0,
|
||||
@@ -258,47 +251,42 @@ describe("ToolRuntime", () => {
|
||||
|
||||
it.effect("does not dispatch provider-executed tool calls", () =>
|
||||
Effect.gen(function* () {
|
||||
// Stub client emits a provider-executed tool-call followed by its
|
||||
// tool-result and a stop. The runtime must not dispatch a handler (no
|
||||
// tool-error for unknown name) and must not loop (no second stream).
|
||||
let streams = 0
|
||||
const stub: LLMClient = {
|
||||
prepare: () => Effect.die("not used"),
|
||||
generate: () => Effect.die("not used"),
|
||||
stream: () => {
|
||||
const layer = dynamicResponse((input) =>
|
||||
Effect.sync(() => {
|
||||
streams++
|
||||
return Stream.fromIterable<LLMEvent>([
|
||||
{ type: "request-start", id: "req_1", model: baseRequest.model },
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "srvtoolu_abc",
|
||||
name: "web_search",
|
||||
input: { query: "x" },
|
||||
providerExecuted: true,
|
||||
},
|
||||
{
|
||||
type: "tool-result",
|
||||
id: "srvtoolu_abc",
|
||||
name: "web_search",
|
||||
result: { type: "json", value: { results: [] } },
|
||||
providerExecuted: true,
|
||||
},
|
||||
{ type: "text-delta", text: "Done." },
|
||||
{ type: "request-finish", reason: "stop" },
|
||||
])
|
||||
},
|
||||
}
|
||||
|
||||
// The runtime's stream type carries `RequestExecutor.Service` because
|
||||
// adapters use it. Our stub never executes HTTP, but the type still
|
||||
// demands the service — provide a noop so the test compiles.
|
||||
const noopExecutor = Layer.succeed(RequestExecutor.Service, {
|
||||
execute: () => Effect.die("stub client never executes HTTP"),
|
||||
})
|
||||
return input.respond(
|
||||
sseEvents(
|
||||
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||
{ type: "content_block_start", index: 0, content_block: { type: "server_tool_use", id: "srvtoolu_abc", name: "web_search" } },
|
||||
{ type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: '{"query":"x"}' } },
|
||||
{ type: "content_block_stop", index: 0 },
|
||||
{
|
||||
type: "content_block_start",
|
||||
index: 1,
|
||||
content_block: {
|
||||
type: "web_search_tool_result",
|
||||
tool_use_id: "srvtoolu_abc",
|
||||
content: [{ type: "web_search_result", url: "https://example.com", title: "Example" }],
|
||||
},
|
||||
},
|
||||
{ type: "content_block_stop", index: 1 },
|
||||
{ type: "content_block_start", index: 2, content_block: { type: "text", text: "" } },
|
||||
{ type: "content_block_delta", index: 2, delta: { type: "text_delta", text: "Done." } },
|
||||
{ type: "content_block_stop", index: 2 },
|
||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 8 } },
|
||||
),
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
)
|
||||
}),
|
||||
)
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(stub, { request: baseRequest, tools: {} }).pipe(
|
||||
yield* ToolRuntime.run({
|
||||
request: LLM.updateRequest(baseRequest, { model: AnthropicMessages.model({ id: "claude-sonnet-4-5", apiKey: "test" }) }),
|
||||
tools: {},
|
||||
}).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.provide(noopExecutor),
|
||||
Effect.provide(layer),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -319,7 +307,6 @@ describe("ToolRuntime", () => {
|
||||
|
||||
it.effect("dispatches multiple tool calls in one step concurrently", () =>
|
||||
Effect.gen(function* () {
|
||||
const llm = LLMClient.make({ adapters: [OpenAIChat.adapter] })
|
||||
const layer = scriptedResponses([
|
||||
sseEvents(
|
||||
deltaChunk({
|
||||
@@ -335,7 +322,7 @@ describe("ToolRuntime", () => {
|
||||
])
|
||||
|
||||
const events = Array.from(
|
||||
yield* ToolRuntime.run(llm, { request: baseRequest, tools: { get_weather } }).pipe(
|
||||
yield* ToolRuntime.run({ request: baseRequest, tools: { get_weather } }).pipe(
|
||||
Stream.runCollect,
|
||||
Effect.provide(layer),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
LLM,
|
||||
type LLMClient,
|
||||
LLMClient,
|
||||
type LLMError,
|
||||
type LLMEvent,
|
||||
type LLMRequest,
|
||||
@@ -129,7 +129,6 @@ const dispatchTool = (
|
||||
// `done` resolves with the accumulated state so the multi-round driver can
|
||||
// decide whether to recurse.
|
||||
const runOneRound = (
|
||||
client: LLMClient,
|
||||
request: LLMRequest,
|
||||
tools: Record<string, Tool>,
|
||||
abort: AbortSignal,
|
||||
@@ -149,7 +148,7 @@ const runOneRound = (
|
||||
|
||||
yield* Effect.forkScoped(
|
||||
Effect.gen(function* () {
|
||||
yield* client.stream(request).pipe(
|
||||
yield* LLMClient.stream(request).pipe(
|
||||
Stream.runForEach((event) =>
|
||||
Effect.gen(function* () {
|
||||
accumulate(state, event)
|
||||
@@ -219,7 +218,6 @@ const continuationRequest = (request: LLMRequest, state: RoundState): LLMRequest
|
||||
* interrupted (e.g. via the abort signal).
|
||||
*/
|
||||
export const runWithTools = (input: {
|
||||
readonly client: LLMClient
|
||||
readonly request: LLMRequest
|
||||
readonly tools: Record<string, Tool>
|
||||
readonly abort: AbortSignal
|
||||
@@ -229,7 +227,7 @@ export const runWithTools = (input: {
|
||||
const round = (request: LLMRequest, step: number): Stream.Stream<LLMEvent, LLMError, RequestExecutor.Service> =>
|
||||
Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const { events, done } = yield* runOneRound(input.client, request, input.tools, input.abort)
|
||||
const { events, done } = yield* runOneRound(request, input.tools, input.abort)
|
||||
const continuation = Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const state = yield* Deferred.await(done)
|
||||
|
||||
@@ -11,14 +11,7 @@ import {
|
||||
type ProtocolID,
|
||||
} from "@opencode-ai/llm"
|
||||
import { RequestExecutor } from "@opencode-ai/llm/adapter"
|
||||
import {
|
||||
AnthropicMessages,
|
||||
BedrockConverse,
|
||||
Gemini,
|
||||
OpenAIChat,
|
||||
OpenAICompatibleChat,
|
||||
OpenAIResponses,
|
||||
} from "@opencode-ai/llm/protocols"
|
||||
import "@opencode-ai/llm/protocols"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { Config } from "@/config/config"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
@@ -499,19 +492,6 @@ const live: Layer.Layer<
|
||||
// existing AI SDK path. The return shape is deliberately narrow — we are
|
||||
// not yet committed to native-by-default for any provider.
|
||||
const NATIVE_PROTOCOLS = new Set<ProtocolID>(["anthropic-messages"])
|
||||
const NATIVE_ADAPTERS = [
|
||||
AnthropicMessages.adapter,
|
||||
OpenAIChat.adapter,
|
||||
OpenAIResponses.adapter,
|
||||
Gemini.adapter,
|
||||
OpenAICompatibleChat.adapter,
|
||||
BedrockConverse.adapter,
|
||||
]
|
||||
|
||||
const nativeClient = LLMClient.make({
|
||||
adapters: NATIVE_ADAPTERS,
|
||||
})
|
||||
|
||||
const runNative = Effect.fn("LLM.runNative")(function* (input: StreamRequest, prepared: PreparedStream) {
|
||||
if (!Flag.OPENCODE_EXPERIMENTAL_LLM_NATIVE) return undefined
|
||||
if (!input.nativeMessages || input.nativeMessages.length === 0) return undefined
|
||||
@@ -599,13 +579,12 @@ const live: Layer.Layer<
|
||||
// subsequent tool-call streaming.
|
||||
const map = LLMNativeEvents.mapper()
|
||||
const upstream = filteredNativeTools && filteredNativeTools.length > 0
|
||||
? LLMNativeTools.runWithTools({
|
||||
client: nativeClient,
|
||||
request: llmRequest,
|
||||
tools: filteredAITools,
|
||||
abort: input.abort,
|
||||
})
|
||||
: nativeClient.stream(llmRequest)
|
||||
? LLMNativeTools.runWithTools({
|
||||
request: llmRequest,
|
||||
tools: filteredAITools,
|
||||
abort: input.abort,
|
||||
})
|
||||
: LLMClient.stream(llmRequest)
|
||||
return upstream.pipe(
|
||||
Stream.flatMap((event) => Stream.fromIterable(map.map(event))),
|
||||
Stream.concat(Stream.unwrap(Effect.sync(() => Stream.fromIterable(map.flush())))),
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import {
|
||||
LLMClient,
|
||||
} from "@opencode-ai/llm"
|
||||
import { LLMClient } from "@opencode-ai/llm"
|
||||
import { RequestExecutor } from "@opencode-ai/llm/adapter"
|
||||
import {
|
||||
AnthropicMessages,
|
||||
BedrockConverse,
|
||||
Gemini,
|
||||
OpenAIChat,
|
||||
OpenAICompatibleChat,
|
||||
OpenAIResponses,
|
||||
} from "@opencode-ai/llm/protocols"
|
||||
import "@opencode-ai/llm/protocols"
|
||||
import { Effect, Layer, Ref, Schema, Stream } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { tool, jsonSchema } from "ai"
|
||||
@@ -100,17 +91,6 @@ const userMessage = (mdl: Provider.Model, id: MessageID, parts: MessageV2.Part[]
|
||||
parts,
|
||||
})
|
||||
|
||||
// What `runNative` builds. Kept in sync with `session/llm.ts`'s
|
||||
// NATIVE_ADAPTERS list — if a protocol is added there, add it here.
|
||||
const adapters = [
|
||||
AnthropicMessages.adapter,
|
||||
OpenAIChat.adapter,
|
||||
OpenAIResponses.adapter,
|
||||
Gemini.adapter,
|
||||
OpenAICompatibleChat.adapter,
|
||||
BedrockConverse.adapter,
|
||||
]
|
||||
|
||||
const it = testEffect(Layer.empty)
|
||||
|
||||
describe("LLMNative stream wire-up (audit gap #4 phase 1)", () => {
|
||||
@@ -128,7 +108,6 @@ describe("LLMNative stream wire-up (audit gap #4 phase 1)", () => {
|
||||
messages: [userMessage(mdl, userID, [userPart(userID, "Say hello.")])],
|
||||
})
|
||||
|
||||
const client = LLMClient.make({ adapters })
|
||||
const map = LLMNativeEvents.mapper()
|
||||
|
||||
const body = sseBody([
|
||||
@@ -141,7 +120,7 @@ describe("LLMNative stream wire-up (audit gap #4 phase 1)", () => {
|
||||
{ type: "message_stop" },
|
||||
])
|
||||
|
||||
const events = yield* client.stream(llmRequest).pipe(
|
||||
const events = yield* LLMClient.stream(llmRequest).pipe(
|
||||
Stream.flatMap((event) => Stream.fromIterable(map.map(event))),
|
||||
Stream.concat(Stream.unwrap(Effect.sync(() => Stream.fromIterable(map.flush())))),
|
||||
Stream.runCollect,
|
||||
@@ -246,11 +225,9 @@ describe("LLMNative stream wire-up (audit gap #4 phase 1)", () => {
|
||||
{ type: "message_stop" },
|
||||
])
|
||||
|
||||
const client = LLMClient.make({ adapters })
|
||||
const map = LLMNativeEvents.mapper()
|
||||
|
||||
const events = yield* LLMNativeTools.runWithTools({
|
||||
client,
|
||||
request: llmRequest,
|
||||
tools: { lookup: aiTool },
|
||||
abort: new AbortController().signal,
|
||||
@@ -323,7 +300,7 @@ describe("LLMNative stream wire-up (audit gap #4 phase 1)", () => {
|
||||
tools: [lookupTool],
|
||||
})
|
||||
|
||||
const prepared = yield* LLMClient.make({ adapters }).prepare(llmRequest)
|
||||
const prepared = yield* LLMClient.prepare(llmRequest)
|
||||
expect(prepared.payload).toMatchObject({
|
||||
tools: [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { LLMClient } from "@opencode-ai/llm"
|
||||
import { AnthropicMessages, BedrockConverse, Gemini, OpenAICompatibleChat, OpenAIResponses } from "@opencode-ai/llm/protocols"
|
||||
import "@opencode-ai/llm/protocols"
|
||||
import { Cause, Effect, Exit, Layer, Schema } from "effect"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { LLMNative } from "../../src/session/llm-native"
|
||||
@@ -598,7 +598,7 @@ describe("LLMNative.request", () => {
|
||||
tools: [lookupTool],
|
||||
toolChoice: "lookup",
|
||||
})
|
||||
const prepared = yield* LLMClient.make({ adapters: [OpenAIResponses.adapter] }).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.payload).toMatchObject({
|
||||
model: "gpt-5",
|
||||
@@ -657,7 +657,7 @@ describe("LLMNative.request", () => {
|
||||
tools: [lookupTool],
|
||||
toolChoice: "lookup",
|
||||
})
|
||||
const prepared = yield* LLMClient.make({ adapters: [AnthropicMessages.adapter] }).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(request.model).toMatchObject({
|
||||
provider: "anthropic",
|
||||
@@ -726,7 +726,7 @@ describe("LLMNative.request", () => {
|
||||
tools: [lookupTool],
|
||||
toolChoice: "lookup",
|
||||
})
|
||||
const prepared = yield* LLMClient.make({ adapters: [OpenAICompatibleChat.adapter] }).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(request.model).toMatchObject({
|
||||
provider: "togetherai",
|
||||
@@ -857,7 +857,7 @@ describe("LLMNative.request", () => {
|
||||
tools: [lookupTool],
|
||||
toolChoice: "lookup",
|
||||
})
|
||||
const prepared = yield* LLMClient.make({ adapters: [Gemini.adapter] }).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(request.model).toMatchObject({
|
||||
provider: "google",
|
||||
@@ -929,9 +929,7 @@ describe("LLMNative.request", () => {
|
||||
system: ["First", "Second", "Third"],
|
||||
messages: [userMessage(mdl, userID, [textPart(userID, "hello")])],
|
||||
})
|
||||
const prepared = yield* LLMClient.make({
|
||||
adapters: [AnthropicMessages.adapter],
|
||||
}).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.payload).toMatchObject({
|
||||
system: [
|
||||
@@ -953,9 +951,7 @@ describe("LLMNative.request", () => {
|
||||
model: mdl,
|
||||
messages: messageIds.map((id, index) => userMessage(mdl, id, [textPart(id, `m${index}`)])),
|
||||
})
|
||||
const prepared = yield* LLMClient.make({
|
||||
adapters: [AnthropicMessages.adapter],
|
||||
}).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.payload).toMatchObject({
|
||||
messages: [
|
||||
@@ -979,9 +975,7 @@ describe("LLMNative.request", () => {
|
||||
system: ["You are concise."],
|
||||
messages: [userMessage(mdl, userID, [textPart(userID, "hello")])],
|
||||
})
|
||||
const prepared = yield* LLMClient.make({
|
||||
adapters: [BedrockConverse.adapter],
|
||||
}).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.payload).toMatchObject({
|
||||
system: [{ text: "You are concise." }, { cachePoint: { type: "default" } }],
|
||||
@@ -1006,9 +1000,7 @@ describe("LLMNative.request", () => {
|
||||
system: ["A", "B", "C"],
|
||||
messages: ids.map((id, index) => userMessage(mdl, id, [textPart(id, `m${index}`)])),
|
||||
})
|
||||
const prepared = yield* LLMClient.make({
|
||||
adapters: [OpenAIResponses.adapter],
|
||||
}).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
// The serialized OpenAI Responses payload has no cache concept; the
|
||||
// assertion is that nothing in the payload carries a cache marker.
|
||||
@@ -1084,9 +1076,7 @@ describe("LLMNative.request", () => {
|
||||
]),
|
||||
],
|
||||
})
|
||||
const prepared = yield* LLMClient.make({
|
||||
adapters: [AnthropicMessages.adapter],
|
||||
}).prepare(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.payload).toMatchObject({
|
||||
messages: [
|
||||
|
||||
Reference in New Issue
Block a user