@opencode-ai/ai
Advanced tools
+25
-2
| export * as TestLLM from "./testing.js"; | ||
| import { type Interface as LLMClientShape } from "./route/client.js"; | ||
| import { LLMClient } from "./route/client.js"; | ||
| import { LLMEvent, type FinishReasonDetails, type AIError, type LLMRequest, type ProviderMetadata, type UsageInput } from "./schema/index.js"; | ||
@@ -10,2 +10,20 @@ import { Context, Effect, Layer, Scope, Stream } from "effect"; | ||
| }>; | ||
| type ClientInterface = Context.Service.Shape<typeof LLMClient.Service>; | ||
| export type Responder = (request: LLMRequest) => Response; | ||
| export interface TestInterface extends ClientInterface { | ||
| /** Returns a snapshot of requests observed at execution time. */ | ||
| readonly requests: () => Effect.Effect<readonly LLMRequest[]>; | ||
| readonly push: (...responses: readonly Response[]) => Effect.Effect<void>; | ||
| /** Replaces the fallback without changing queued responses. */ | ||
| readonly always: (response: Response) => Effect.Effect<void>; | ||
| /** Answers requests after the one-shot queue is exhausted; receives the original request. */ | ||
| readonly serve: (responder: Responder) => Effect.Effect<void>; | ||
| /** Waits for request arrivals, not output or completion. */ | ||
| readonly wait: (count: number) => Effect.Effect<void>; | ||
| readonly gate: () => Effect.Effect<Gate, never, Scope.Scope>; | ||
| } | ||
| declare const Test_base: Context.ServiceClass<Test, "@opencode/ai/TestLLM/Test", TestInterface>; | ||
| export declare class Test extends Test_base { | ||
| } | ||
| /** @deprecated Use TestInterface through Test and testLayer. */ | ||
| export interface Interface { | ||
@@ -17,3 +35,3 @@ readonly requests: LLMRequest[]; | ||
| readonly gate: Effect.Effect<Gate, never, Scope.Scope>; | ||
| readonly client: LLMClientShape; | ||
| readonly client: ClientInterface; | ||
| } | ||
@@ -26,2 +44,3 @@ export interface LayerOptions { | ||
| declare const Service_base: Context.ServiceClass<Service, "@opencode/ai/TestLLM", Interface>; | ||
| /** @deprecated Use Test and testLayer for normal client methods and test controls. */ | ||
| export declare class Service extends Service_base { | ||
@@ -1513,3 +1532,7 @@ } | ||
| }, never, never>; | ||
| /** Provides one shared implementation under the normal client and test-control tags. */ | ||
| export declare const testLayer: (options?: LayerOptions) => Layer.Layer<import("./route/client.js").Service | Test, never, never>; | ||
| /** @deprecated Use testLayer; retained for published callers of the legacy control interface. */ | ||
| export declare const layer: (options?: LayerOptions) => Layer.Layer<Service, never, never>; | ||
| /** @deprecated testLayer provides LLMClient.Service directly. */ | ||
| export declare const clientLayer: Layer.Layer<import("./route/client.js").Service, never, Service>; | ||
@@ -1516,0 +1539,0 @@ export declare const push: (...responses: readonly Response[]) => Effect.Effect<void, never, Service>; |
+39
-18
@@ -5,2 +5,5 @@ export * as TestLLM from "./testing.js"; | ||
| import { Context, Deferred, Effect, Latch, Layer, Queue, Scope, Stream } from "effect"; | ||
| export class Test extends Context.Service()("@opencode/ai/TestLLM/Test") { | ||
| } | ||
| /** @deprecated Use Test and testLayer for normal client methods and test controls. */ | ||
| export class Service extends Context.Service()("@opencode/ai/TestLLM") { | ||
@@ -32,3 +35,3 @@ } | ||
| const toStream = (response) => (Stream.isStream(response) ? response : Stream.fromIterable(response)); | ||
| export const layer = (options = {}) => Layer.effect(Service, Effect.gen(function* () { | ||
| const make = (options) => Effect.sync(() => { | ||
| const requests = []; | ||
@@ -40,17 +43,22 @@ const responses = []; | ||
| const wait = (count) => Effect.suspend(() => requests.length >= count ? Effect.void : Deferred.await(started).pipe(Effect.andThen(wait(count)))); | ||
| const stream = ((request) => { | ||
| requests.push(options.transformRequest?.(request) ?? request); | ||
| const stream = (request) => Stream.suspend(() => { | ||
| const count = requests.push(options.transformRequest?.(request) ?? request); | ||
| const waiting = started; | ||
| started = Deferred.makeUnsafe(); | ||
| Deferred.doneUnsafe(waiting, Effect.void); | ||
| const response = responses.shift() ?? fallback; | ||
| if (!response) | ||
| return Stream.die(new Error(`TestLLM has no response for request ${requests.length}`)); | ||
| const streamed = toStream(response); | ||
| const gate = activeGate; | ||
| if (!gate) | ||
| return streamed; | ||
| return Stream.unwrap(Queue.offer(gate.started, undefined).pipe(Effect.andThen(gate.release.await), Effect.as(streamed))); | ||
| try { | ||
| const response = responses.shift() ?? (typeof fallback === "function" ? fallback(request) : fallback); | ||
| if (!response) | ||
| return Stream.die(new Error(`TestLLM has no response for request ${count}`)); | ||
| const streamed = toStream(response); | ||
| if (!gate) | ||
| return streamed; | ||
| return Stream.unwrap(Queue.offer(gate.started, undefined).pipe(Effect.andThen(gate.release.await), Effect.as(streamed))); | ||
| } | ||
| finally { | ||
| // Waiters can resume synchronously; assign the reply and gate before notifying them. | ||
| Deferred.doneUnsafe(waiting, Effect.void); | ||
| } | ||
| }); | ||
| const client = LLMClient.Service.of({ | ||
| const test = Test.of({ | ||
| stream, | ||
@@ -63,5 +71,3 @@ generate: (request) => stream(request).pipe(Stream.runFold(LLMResponse.empty, LLMResponse.reduce), Effect.flatMap((state) => { | ||
| })), | ||
| }); | ||
| return Service.of({ | ||
| requests, | ||
| requests: () => Effect.sync(() => [...requests]), | ||
| push: (...input) => Effect.sync(() => { | ||
@@ -73,4 +79,7 @@ responses.push(...input); | ||
| }), | ||
| serve: (responder) => Effect.sync(() => { | ||
| fallback = responder; | ||
| }), | ||
| wait, | ||
| gate: Effect.gen(function* () { | ||
| gate: () => Effect.gen(function* () { | ||
| const gate = { | ||
@@ -91,5 +100,17 @@ started: yield* Effect.acquireRelease(Queue.unbounded(), Queue.shutdown), | ||
| }), | ||
| client, | ||
| }); | ||
| })); | ||
| return { test, requests }; | ||
| }); | ||
| /** Provides one shared implementation under the normal client and test-control tags. */ | ||
| export const testLayer = (options = {}) => Layer.effectContext(Effect.map(make(options), (implementation) => Context.make(LLMClient.Service, implementation.test).pipe(Context.add(Test, implementation.test)))); | ||
| /** @deprecated Use testLayer; retained for published callers of the legacy control interface. */ | ||
| export const layer = (options = {}) => Layer.effect(Service, Effect.map(make(options), (implementation) => Service.of({ | ||
| requests: implementation.requests, | ||
| push: implementation.test.push, | ||
| always: implementation.test.always, | ||
| wait: implementation.test.wait, | ||
| gate: implementation.test.gate(), | ||
| client: implementation.test, | ||
| }))); | ||
| /** @deprecated testLayer provides LLMClient.Service directly. */ | ||
| export const clientLayer = Layer.effect(LLMClient.Service, Effect.map(Service, (service) => service.client)); | ||
@@ -96,0 +117,0 @@ export const push = (...responses) => Service.use((service) => service.push(...responses)); |
+3
-3
| { | ||
| "$schema": "https://json.schemastore.org/package.json", | ||
| "version": "0.0.0-dev-18562", | ||
| "version": "0.0.0-dev-18564", | ||
| "name": "@opencode-ai/ai", | ||
@@ -33,3 +33,3 @@ "type": "module", | ||
| "@effect/platform-node": "4.0.0-rc.112", | ||
| "@opencode-ai/http-recorder": "0.0.0-dev-18562", | ||
| "@opencode-ai/http-recorder": "0.0.0-dev-18564", | ||
| "@tsconfig/bun": "1.0.9", | ||
@@ -43,3 +43,3 @@ "@types/bun": "1.3.13", | ||
| "@smithy/util-utf8": "4.2.2", | ||
| "@opencode-ai/schema": "0.0.0-dev-18562", | ||
| "@opencode-ai/schema": "0.0.0-dev-18564", | ||
| "aws4fetch": "1.0.20", | ||
@@ -46,0 +46,0 @@ "effect": "4.0.0-rc.112", |
+29
-11
@@ -217,19 +217,37 @@ # @opencode-ai/ai | ||
| const testLLM = TestLLM.layer({ | ||
| fallback: TestLLM.text("Hello from the test model", "text-1"), | ||
| }) | ||
| // TestLLM.clientLayer provides LLMClient.Service and consumes TestLLM.Service. | ||
| const programWithTestClient = Effect.gen(function* () { | ||
| const test = yield* TestLLM.Test | ||
| yield* test.push(TestLLM.text("Hello from the test model", "text-1")) | ||
| const result = yield* program | ||
| const test = yield* TestLLM.Service | ||
| console.log(test.requests) | ||
| console.log(yield* test.requests()) | ||
| return result | ||
| }).pipe(Effect.provide(TestLLM.clientLayer), Effect.provide(testLLM)) | ||
| }).pipe(Effect.provide(TestLLM.testLayer())) | ||
| ``` | ||
| `TestLLM.push(...)` scripts one-shot responses, `TestLLM.always(...)` changes the fallback, and | ||
| `TestLLM.wait(...)` lets concurrent tests wait until a request has arrived. Every received canonical request is | ||
| available on the yielded `TestLLM.Service`. | ||
| `testLayer()` provides the same object under `LLMClient.Service` and `TestLLM.Test`. Production consumes the | ||
| normal client; tests use the additional controls. Each layer build has fresh state. | ||
| - `test.push(...)` queues one-shot responses in execution order. Each argument is one response. | ||
| - `test.always(response)` installs a repeatable fallback. The layer's `fallback` option sets its initial value. | ||
| - `test.serve(request => response)` installs a request-dependent fallback. `always` and `serve` replace each | ||
| other without changing queued replies; queued replies take precedence. | ||
| - `test.requests()` returns an array snapshot. `transformRequest` changes only the recorded observation; | ||
| `serve` receives the original canonical request. | ||
| - `test.wait(count)` waits for request arrivals, not output or completion, and supports concurrent waiters. | ||
| - `test.gate()` returns a scoped gate with countable `started` notifications and a `release` Effect. Release | ||
| unblocks all requests captured by that gate; closing its scope also releases it. Effect-aware test runners | ||
| already provide Scope. | ||
| Constructing `stream()` or `generate()` does not record a request, invoke a responder, or consume a script. | ||
| Each execution does. An exhausted queue without a fallback defects immediately rather than waiting for a | ||
| future reply. | ||
| Responses remain canonical event arrays or arbitrary `Stream<LLMEvent, AIError>` values. The client consumes | ||
| supplied streams directly, preserving failure identity, finalizers, incomplete output, and post-finish tails; | ||
| it does not repair or truncate them. | ||
| The published legacy `Service`, `layer`, `clientLayer`, and module-level controls remain available as adapters | ||
| over the same implementation, including the legacy live `requests` array. New tests should use `Test` and | ||
| `testLayer`. | ||
| ## Caching | ||
@@ -236,0 +254,0 @@ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1479648
0.29%34069
0.13%422
4.46%+ Added
- Removed