New:Socket for Asana Is Now Available.Learn more
Get Started

@opencode-ai/ai

Package Overview
Dependencies
Maintainers
2
Versions
1089
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@opencode-ai/ai - npm Package Compare versions

Comparing version
0.0.0-dev-18562
to
0.0.0-dev-18564
+25
-2
dist/testing.d.ts
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));

{
"$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 @@