@opencode-ai/ai 0.0.0-dev-18562 → 0.0.0-dev-18565
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -11
- package/dist/testing.d.ts +25 -2
- package/dist/testing.js +39 -18
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -214,22 +214,40 @@ the requests sent by code under test:
|
|
|
214
214
|
import { Effect } from "effect"
|
|
215
215
|
import { TestLLM } from "@opencode-ai/ai/testing"
|
|
216
216
|
|
|
217
|
-
const testLLM = TestLLM.layer({
|
|
218
|
-
fallback: TestLLM.text("Hello from the test model", "text-1"),
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
// TestLLM.clientLayer provides LLMClient.Service and consumes TestLLM.Service.
|
|
222
217
|
const programWithTestClient = Effect.gen(function* () {
|
|
218
|
+
const test = yield* TestLLM.Test
|
|
219
|
+
yield* test.push(TestLLM.text("Hello from the test model", "text-1"))
|
|
223
220
|
const result = yield* program
|
|
224
|
-
|
|
225
|
-
console.log(test.requests)
|
|
221
|
+
console.log(yield* test.requests())
|
|
226
222
|
return result
|
|
227
|
-
}).pipe(Effect.provide(TestLLM.
|
|
223
|
+
}).pipe(Effect.provide(TestLLM.testLayer()))
|
|
228
224
|
```
|
|
229
225
|
|
|
230
|
-
`
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
`testLayer()` provides the same object under `LLMClient.Service` and `TestLLM.Test`. Production consumes the
|
|
227
|
+
normal client; tests use the additional controls. Each layer build has fresh state.
|
|
228
|
+
|
|
229
|
+
- `test.push(...)` queues one-shot responses in execution order. Each argument is one response.
|
|
230
|
+
- `test.always(response)` installs a repeatable fallback. The layer's `fallback` option sets its initial value.
|
|
231
|
+
- `test.serve(request => response)` installs a request-dependent fallback. `always` and `serve` replace each
|
|
232
|
+
other without changing queued replies; queued replies take precedence.
|
|
233
|
+
- `test.requests()` returns an array snapshot. `transformRequest` changes only the recorded observation;
|
|
234
|
+
`serve` receives the original canonical request.
|
|
235
|
+
- `test.wait(count)` waits for request arrivals, not output or completion, and supports concurrent waiters.
|
|
236
|
+
- `test.gate()` returns a scoped gate with countable `started` notifications and a `release` Effect. Release
|
|
237
|
+
unblocks all requests captured by that gate; closing its scope also releases it. Effect-aware test runners
|
|
238
|
+
already provide Scope.
|
|
239
|
+
|
|
240
|
+
Constructing `stream()` or `generate()` does not record a request, invoke a responder, or consume a script.
|
|
241
|
+
Each execution does. An exhausted queue without a fallback defects immediately rather than waiting for a
|
|
242
|
+
future reply.
|
|
243
|
+
|
|
244
|
+
Responses remain canonical event arrays or arbitrary `Stream<LLMEvent, AIError>` values. The client consumes
|
|
245
|
+
supplied streams directly, preserving failure identity, finalizers, incomplete output, and post-finish tails;
|
|
246
|
+
it does not repair or truncate them.
|
|
247
|
+
|
|
248
|
+
The published legacy `Service`, `layer`, `clientLayer`, and module-level controls remain available as adapters
|
|
249
|
+
over the same implementation, including the legacy live `requests` array. New tests should use `Test` and
|
|
250
|
+
`testLayer`.
|
|
233
251
|
|
|
234
252
|
## Caching
|
|
235
253
|
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * as TestLLM from "./testing.js";
|
|
2
|
-
import {
|
|
2
|
+
import { LLMClient } from "./route/client.js";
|
|
3
3
|
import { LLMEvent, type FinishReasonDetails, type AIError, type LLMRequest, type ProviderMetadata, type UsageInput } from "./schema/index.js";
|
|
4
4
|
import { Context, Effect, Layer, Scope, Stream } from "effect";
|
|
5
5
|
export type Response = readonly LLMEvent[] | Stream.Stream<LLMEvent, AIError>;
|
|
@@ -7,13 +7,31 @@ export type Gate = Readonly<{
|
|
|
7
7
|
started: Effect.Effect<void>;
|
|
8
8
|
release: Effect.Effect<void>;
|
|
9
9
|
}>;
|
|
10
|
+
type ClientInterface = Context.Service.Shape<typeof LLMClient.Service>;
|
|
11
|
+
export type Responder = (request: LLMRequest) => Response;
|
|
12
|
+
export interface TestInterface extends ClientInterface {
|
|
13
|
+
/** Returns a snapshot of requests observed at execution time. */
|
|
14
|
+
readonly requests: () => Effect.Effect<readonly LLMRequest[]>;
|
|
15
|
+
readonly push: (...responses: readonly Response[]) => Effect.Effect<void>;
|
|
16
|
+
/** Replaces the fallback without changing queued responses. */
|
|
17
|
+
readonly always: (response: Response) => Effect.Effect<void>;
|
|
18
|
+
/** Answers requests after the one-shot queue is exhausted; receives the original request. */
|
|
19
|
+
readonly serve: (responder: Responder) => Effect.Effect<void>;
|
|
20
|
+
/** Waits for request arrivals, not output or completion. */
|
|
21
|
+
readonly wait: (count: number) => Effect.Effect<void>;
|
|
22
|
+
readonly gate: () => Effect.Effect<Gate, never, Scope.Scope>;
|
|
23
|
+
}
|
|
24
|
+
declare const Test_base: Context.ServiceClass<Test, "@opencode/ai/TestLLM/Test", TestInterface>;
|
|
25
|
+
export declare class Test extends Test_base {
|
|
26
|
+
}
|
|
27
|
+
/** @deprecated Use TestInterface through Test and testLayer. */
|
|
10
28
|
export interface Interface {
|
|
11
29
|
readonly requests: LLMRequest[];
|
|
12
30
|
readonly push: (...responses: readonly Response[]) => Effect.Effect<void>;
|
|
13
31
|
readonly always: (response: Response) => Effect.Effect<void>;
|
|
14
32
|
readonly wait: (count: number) => Effect.Effect<void>;
|
|
15
33
|
readonly gate: Effect.Effect<Gate, never, Scope.Scope>;
|
|
16
|
-
readonly client:
|
|
34
|
+
readonly client: ClientInterface;
|
|
17
35
|
}
|
|
18
36
|
export interface LayerOptions {
|
|
19
37
|
readonly transformRequest?: (request: LLMRequest) => LLMRequest;
|
|
@@ -21,6 +39,7 @@ export interface LayerOptions {
|
|
|
21
39
|
readonly fallback?: Response;
|
|
22
40
|
}
|
|
23
41
|
declare const Service_base: Context.ServiceClass<Service, "@opencode/ai/TestLLM", Interface>;
|
|
42
|
+
/** @deprecated Use Test and testLayer for normal client methods and test controls. */
|
|
24
43
|
export declare class Service extends Service_base {
|
|
25
44
|
}
|
|
26
45
|
export declare const complete: (options: {
|
|
@@ -1507,7 +1526,11 @@ export declare const hangAfter: (...events: readonly LLMEvent[]) => Stream.Strea
|
|
|
1507
1526
|
} | undefined;
|
|
1508
1527
|
readonly classification?: "context-overflow" | "payload-too-large" | undefined;
|
|
1509
1528
|
}, never, never>;
|
|
1529
|
+
/** Provides one shared implementation under the normal client and test-control tags. */
|
|
1530
|
+
export declare const testLayer: (options?: LayerOptions) => Layer.Layer<import("./route/client.js").Service | Test, never, never>;
|
|
1531
|
+
/** @deprecated Use testLayer; retained for published callers of the legacy control interface. */
|
|
1510
1532
|
export declare const layer: (options?: LayerOptions) => Layer.Layer<Service, never, never>;
|
|
1533
|
+
/** @deprecated testLayer provides LLMClient.Service directly. */
|
|
1511
1534
|
export declare const clientLayer: Layer.Layer<import("./route/client.js").Service, never, Service>;
|
|
1512
1535
|
export declare const push: (...responses: readonly Response[]) => Effect.Effect<void, never, Service>;
|
|
1513
1536
|
export declare const always: (response: Response) => Effect.Effect<void, never, Service>;
|
package/dist/testing.js
CHANGED
|
@@ -2,6 +2,9 @@ export * as TestLLM from "./testing.js";
|
|
|
2
2
|
import { LLMClient } from "./route/client.js";
|
|
3
3
|
import { LLMEvent, LLMResponse, } from "./schema/index.js";
|
|
4
4
|
import { Context, Deferred, Effect, Latch, Layer, Queue, Scope, Stream } from "effect";
|
|
5
|
+
export class Test extends Context.Service()("@opencode/ai/TestLLM/Test") {
|
|
6
|
+
}
|
|
7
|
+
/** @deprecated Use Test and testLayer for normal client methods and test controls. */
|
|
5
8
|
export class Service extends Context.Service()("@opencode/ai/TestLLM") {
|
|
6
9
|
}
|
|
7
10
|
export const complete = (options, ...events) => [
|
|
@@ -28,28 +31,33 @@ export const tool = (id, name, input) => toolCalls(LLMEvent.toolCall({ id, name,
|
|
|
28
31
|
export const failAfter = (error, ...events) => Stream.fromIterable(events).pipe(Stream.concat(Stream.fail(error)));
|
|
29
32
|
export const hangAfter = (...events) => Stream.concat(Stream.fromIterable(events), Stream.never);
|
|
30
33
|
const toStream = (response) => (Stream.isStream(response) ? response : Stream.fromIterable(response));
|
|
31
|
-
|
|
34
|
+
const make = (options) => Effect.sync(() => {
|
|
32
35
|
const requests = [];
|
|
33
36
|
const responses = [];
|
|
34
37
|
let started = Deferred.makeUnsafe();
|
|
35
38
|
let fallback = options.fallback;
|
|
36
39
|
let activeGate;
|
|
37
40
|
const wait = (count) => Effect.suspend(() => requests.length >= count ? Effect.void : Deferred.await(started).pipe(Effect.andThen(wait(count))));
|
|
38
|
-
const stream = (
|
|
39
|
-
requests.push(options.transformRequest?.(request) ?? request);
|
|
41
|
+
const stream = (request) => Stream.suspend(() => {
|
|
42
|
+
const count = requests.push(options.transformRequest?.(request) ?? request);
|
|
40
43
|
const waiting = started;
|
|
41
44
|
started = Deferred.makeUnsafe();
|
|
42
|
-
Deferred.doneUnsafe(waiting, Effect.void);
|
|
43
|
-
const response = responses.shift() ?? fallback;
|
|
44
|
-
if (!response)
|
|
45
|
-
return Stream.die(new Error(`TestLLM has no response for request ${requests.length}`));
|
|
46
|
-
const streamed = toStream(response);
|
|
47
45
|
const gate = activeGate;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
try {
|
|
47
|
+
const response = responses.shift() ?? (typeof fallback === "function" ? fallback(request) : fallback);
|
|
48
|
+
if (!response)
|
|
49
|
+
return Stream.die(new Error(`TestLLM has no response for request ${count}`));
|
|
50
|
+
const streamed = toStream(response);
|
|
51
|
+
if (!gate)
|
|
52
|
+
return streamed;
|
|
53
|
+
return Stream.unwrap(Queue.offer(gate.started, undefined).pipe(Effect.andThen(gate.release.await), Effect.as(streamed)));
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
// Waiters can resume synchronously; assign the reply and gate before notifying them.
|
|
57
|
+
Deferred.doneUnsafe(waiting, Effect.void);
|
|
58
|
+
}
|
|
51
59
|
});
|
|
52
|
-
const
|
|
60
|
+
const test = Test.of({
|
|
53
61
|
stream,
|
|
54
62
|
generate: (request) => stream(request).pipe(Stream.runFold(LLMResponse.empty, LLMResponse.reduce), Effect.flatMap((state) => {
|
|
55
63
|
const response = LLMResponse.complete(state);
|
|
@@ -57,17 +65,18 @@ export const layer = (options = {}) => Layer.effect(Service, Effect.gen(function
|
|
|
57
65
|
return Effect.succeed(response);
|
|
58
66
|
return Effect.die("TestLLM response ended without a terminal finish event");
|
|
59
67
|
})),
|
|
60
|
-
|
|
61
|
-
return Service.of({
|
|
62
|
-
requests,
|
|
68
|
+
requests: () => Effect.sync(() => [...requests]),
|
|
63
69
|
push: (...input) => Effect.sync(() => {
|
|
64
70
|
responses.push(...input);
|
|
65
71
|
}),
|
|
66
72
|
always: (response) => Effect.sync(() => {
|
|
67
73
|
fallback = response;
|
|
68
74
|
}),
|
|
75
|
+
serve: (responder) => Effect.sync(() => {
|
|
76
|
+
fallback = responder;
|
|
77
|
+
}),
|
|
69
78
|
wait,
|
|
70
|
-
gate: Effect.gen(function* () {
|
|
79
|
+
gate: () => Effect.gen(function* () {
|
|
71
80
|
const gate = {
|
|
72
81
|
started: yield* Effect.acquireRelease(Queue.unbounded(), Queue.shutdown),
|
|
73
82
|
release: yield* Latch.make(),
|
|
@@ -83,9 +92,21 @@ export const layer = (options = {}) => Layer.effect(Service, Effect.gen(function
|
|
|
83
92
|
release,
|
|
84
93
|
};
|
|
85
94
|
}),
|
|
86
|
-
client,
|
|
87
95
|
});
|
|
88
|
-
}
|
|
96
|
+
return { test, requests };
|
|
97
|
+
});
|
|
98
|
+
/** Provides one shared implementation under the normal client and test-control tags. */
|
|
99
|
+
export const testLayer = (options = {}) => Layer.effectContext(Effect.map(make(options), (implementation) => Context.make(LLMClient.Service, implementation.test).pipe(Context.add(Test, implementation.test))));
|
|
100
|
+
/** @deprecated Use testLayer; retained for published callers of the legacy control interface. */
|
|
101
|
+
export const layer = (options = {}) => Layer.effect(Service, Effect.map(make(options), (implementation) => Service.of({
|
|
102
|
+
requests: implementation.requests,
|
|
103
|
+
push: implementation.test.push,
|
|
104
|
+
always: implementation.test.always,
|
|
105
|
+
wait: implementation.test.wait,
|
|
106
|
+
gate: implementation.test.gate(),
|
|
107
|
+
client: implementation.test,
|
|
108
|
+
})));
|
|
109
|
+
/** @deprecated testLayer provides LLMClient.Service directly. */
|
|
89
110
|
export const clientLayer = Layer.effect(LLMClient.Service, Effect.map(Service, (service) => service.client));
|
|
90
111
|
export const push = (...responses) => Service.use((service) => service.push(...responses));
|
|
91
112
|
export const always = (response) => Service.use((service) => service.always(response));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-18565",
|
|
4
4
|
"name": "@opencode-ai/ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@clack/prompts": "1.0.0-alpha.1",
|
|
32
32
|
"@effect/platform-node": "4.0.0-rc.112",
|
|
33
|
-
"@opencode-ai/http-recorder": "0.0.0-dev-
|
|
33
|
+
"@opencode-ai/http-recorder": "0.0.0-dev-18565",
|
|
34
34
|
"@tsconfig/bun": "1.0.9",
|
|
35
35
|
"@types/bun": "1.3.13",
|
|
36
36
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@smithy/eventstream-codec": "4.2.14",
|
|
41
41
|
"@smithy/util-utf8": "4.2.2",
|
|
42
|
-
"@opencode-ai/schema": "0.0.0-dev-
|
|
42
|
+
"@opencode-ai/schema": "0.0.0-dev-18565",
|
|
43
43
|
"aws4fetch": "1.0.20",
|
|
44
44
|
"effect": "4.0.0-rc.112",
|
|
45
45
|
"google-auth-library": "10.5.0"
|