phantomllm 0.2.3 → 0.3.0

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 CHANGED
@@ -32,6 +32,7 @@ await mock.stop();
32
32
  - [Streaming](#streaming)
33
33
  - [Embeddings](#embeddings)
34
34
  - [Error Simulation](#error-simulation)
35
+ - [API Key Validation](#api-key-validation)
35
36
  - [Stub Matching](#stub-matching)
36
37
  - [Integration Examples](#integration-examples)
37
38
  - [OpenAI Node.js SDK](#openai-nodejs-sdk)
@@ -56,7 +57,7 @@ await mock.stop();
56
57
  - **Works with any client** — OpenAI SDK, Vercel AI SDK, LangChain, opencode, Python, curl — anything that speaks the OpenAI API protocol.
57
58
  - **Streaming support** — SSE chunked responses work exactly like the real OpenAI API.
58
59
  - **Fast** — ~1s container cold start, sub-millisecond response latency, 4,000+ req/s throughput.
59
- - **Simple API** — fluent `given/when` pattern: `mock.given.chatCompletion.forModel('gpt-4').willReturn('Hello')`.
60
+ - **Simple API** — fluent `given`/`require` pattern: `mock.given.chatCompletion.forModel('gpt-4').willReturn('Hello')`.
60
61
 
61
62
  ## Prerequisites
62
63
 
@@ -151,8 +152,9 @@ const mock = new MockLLM({
151
152
  | `await mock.stop()` | `void` | Stop and remove the container. Idempotent. |
152
153
  | `mock.baseUrl` | `string` | Server URL without `/v1`, e.g. `http://localhost:55123`. |
153
154
  | `mock.apiBaseUrl` | `string` | Server URL with `/v1`, e.g. `http://localhost:55123/v1`. Pass this to SDK clients. |
154
- | `mock.given` | `GivenStubs` | Entry point for the fluent stubbing API. |
155
- | `await mock.clear()` | `void` | Remove all registered stubs. Call between tests. |
155
+ | `mock.given` | `GivenStubs` | Entry point for stubbing responses. |
156
+ | `mock.expect` | `ExpectConditions` | Entry point for configuring server behavior (API key validation, etc.). |
157
+ | `await mock.clear()` | `void` | Remove all stubs and reset server config (including API key). Call between tests. |
156
158
 
157
159
  `MockLLM` implements `Symbol.asyncDispose` for automatic cleanup:
158
160
 
@@ -262,6 +264,53 @@ Error responses follow the OpenAI error format:
262
264
  }
263
265
  ```
264
266
 
267
+ ### API Key Validation
268
+
269
+ Test that your code sends the correct API key by requiring authentication on the mock server.
270
+
271
+ ```typescript
272
+ mock.expect.apiKey('sk-test-key-123');
273
+
274
+ // Requests without a key or with the wrong key get 401
275
+ // { error: { message: "...", type: "authentication_error", code: "invalid_api_key" } }
276
+
277
+ // Only requests with the correct key succeed
278
+ const openai = new OpenAI({
279
+ baseURL: mock.apiBaseUrl,
280
+ apiKey: 'sk-test-key-123', // must match exactly
281
+ });
282
+ ```
283
+
284
+ `mock.expect` configures server constraints at runtime — no container restart needed. API key validation applies to all `/v1/*` endpoints (chat completions, embeddings, models). Admin endpoints (`/_admin/*`) are always accessible.
285
+
286
+ Calling `mock.clear()` resets the API key requirement along with all stubs.
287
+
288
+ | Method | Description |
289
+ |---|---|
290
+ | `mock.expect.apiKey(key)` | Require `Authorization: Bearer <key>` on all `/v1/*` requests. Single call, no chaining needed. |
291
+
292
+ **Testing auth error handling:**
293
+
294
+ ```typescript
295
+ it('handles invalid API key', async () => {
296
+ mock.expect.apiKey('correct-key');
297
+ mock.given.chatCompletion.willReturn('Hello');
298
+
299
+ // Client configured with wrong key
300
+ const badClient = new OpenAI({
301
+ baseURL: mock.apiBaseUrl,
302
+ apiKey: 'wrong-key',
303
+ });
304
+
305
+ await expect(
306
+ badClient.chat.completions.create({
307
+ model: 'gpt-4o',
308
+ messages: [{ role: 'user', content: 'Hi' }],
309
+ }),
310
+ ).rejects.toThrow();
311
+ });
312
+ ```
313
+
265
314
  ### Stub Matching
266
315
 
267
316
  When multiple stubs are registered, the most specific match wins:
package/dist/index.cjs CHANGED
@@ -19,10 +19,10 @@ var ContainerManager = class {
19
19
  }
20
20
  container = null;
21
21
  async start() {
22
- const container = new testcontainers.GenericContainer(this.config.image).withExposedPorts(this.config.containerPort).withWaitStrategy(
22
+ const builder = new testcontainers.GenericContainer(this.config.image).withExposedPorts(this.config.containerPort).withWaitStrategy(
23
23
  testcontainers.Wait.forHttp("/_admin/health", this.config.containerPort).forStatusCode(200)
24
24
  ).withStartupTimeout(this.config.startupTimeout).withLabels({ "com.phantomllm": "true" });
25
- this.container = await container.start();
25
+ this.container = await builder.start();
26
26
  return {
27
27
  host: this.container.getHost(),
28
28
  port: this.container.getMappedPort(this.config.containerPort)
@@ -83,13 +83,24 @@ var AdminClient = class {
83
83
  this.baseUrl = baseUrl;
84
84
  }
85
85
  pendingStubs = [];
86
+ pendingConfigs = [];
86
87
  enqueueStub(stub) {
87
88
  this.pendingStubs.push(stub);
88
89
  }
90
+ enqueueConfig(config) {
91
+ this.pendingConfigs.push(config);
92
+ }
89
93
  async flush() {
90
- if (this.pendingStubs.length === 0) return;
91
- const stubs = this.pendingStubs.splice(0);
92
- await this.post("/_admin/stubs/batch", { stubs });
94
+ if (this.pendingConfigs.length > 0) {
95
+ const configs = this.pendingConfigs.splice(0);
96
+ for (const config of configs) {
97
+ await this.post("/_admin/config", config);
98
+ }
99
+ }
100
+ if (this.pendingStubs.length > 0) {
101
+ const stubs = this.pendingStubs.splice(0);
102
+ await this.post("/_admin/stubs/batch", { stubs });
103
+ }
93
104
  }
94
105
  async clearStubs() {
95
106
  await this.flush();
@@ -242,6 +253,16 @@ var GivenStubs = class {
242
253
  }
243
254
  };
244
255
 
256
+ // src/stubs/expect.ts
257
+ var ExpectConditions = class {
258
+ constructor(adminClient) {
259
+ this.adminClient = adminClient;
260
+ }
261
+ apiKey(key) {
262
+ this.adminClient.enqueueConfig({ apiKey: key });
263
+ }
264
+ };
265
+
245
266
  // src/errors/lifecycle.errors.ts
246
267
  var ContainerNotStartedError = class extends MockLLMError {
247
268
  code = "CONTAINER_NOT_STARTED";
@@ -260,6 +281,7 @@ var MockLLM = class {
260
281
  containerManager;
261
282
  adminClient = null;
262
283
  _given = null;
284
+ _expect = null;
263
285
  _baseUrl = null;
264
286
  constructor(options) {
265
287
  const config = resolveContainerConfig(options);
@@ -277,6 +299,7 @@ var MockLLM = class {
277
299
  this._baseUrl = `http://${host}:${port}`;
278
300
  this.adminClient = new AdminClient(this._baseUrl);
279
301
  this._given = new GivenStubs(this.adminClient);
302
+ this._expect = new ExpectConditions(this.adminClient);
280
303
  this.state = "running";
281
304
  }
282
305
  async stop() {
@@ -288,6 +311,7 @@ var MockLLM = class {
288
311
  this.state = "stopped";
289
312
  this.adminClient = null;
290
313
  this._given = null;
314
+ this._expect = null;
291
315
  this._baseUrl = null;
292
316
  }
293
317
  }
@@ -302,6 +326,10 @@ var MockLLM = class {
302
326
  this.assertRunning();
303
327
  return this._given;
304
328
  }
329
+ get expect() {
330
+ this.assertRunning();
331
+ return this._expect;
332
+ }
305
333
  async clear() {
306
334
  this.assertRunning();
307
335
  await this.adminClient.clearStubs();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/driver/container.config.ts","../src/driver/container.manager.ts","../src/errors/base.ts","../src/errors/admin.errors.ts","../src/driver/admin.client.ts","../src/stubs/chat.builder.ts","../src/stubs/embedding.builder.ts","../src/stubs/models.builder.ts","../src/stubs/given.ts","../src/errors/lifecycle.errors.ts","../src/driver/mock-llm.ts","../src/errors/stub.errors.ts"],"names":["GenericContainer","Wait"],"mappings":";;;;;AAEA,IAAM,aAAA,GAAgB,0BAAA;AACtB,IAAM,YAAA,GAAe,IAAA;AAEd,SAAS,uBAAuB,OAAA,EAA2C;AAChF,EAAA,OAAO;AAAA,IACL,OAAO,OAAA,EAAS,KAAA,IAAS,OAAA,CAAQ,GAAA,CAAI,kBAAkB,CAAA,IAAK,aAAA;AAAA,IAC5D,aAAA,EAAe,SAAS,aAAA,IAAiB,YAAA;AAAA,IACzC,KAAA,EAAO,SAAS,KAAA,IAAS,IAAA;AAAA,IACzB,cAAA,EAAgB,SAAS,cAAA,IAAkB;AAAA,GAC7C;AACF;ACRO,IAAM,mBAAN,MAAuB;AAAA,EAG5B,YAA6B,MAAA,EAAyB;AAAzB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAA0B;AAAA,EAF/C,SAAA,GAAyC,IAAA;AAAA,EAIjD,MAAM,KAAA,GAAiD;AACrD,IAAA,MAAM,SAAA,GAAY,IAAIA,+BAAA,CAAiB,IAAA,CAAK,MAAA,CAAO,KAAK,CAAA,CACrD,gBAAA,CAAiB,IAAA,CAAK,MAAA,CAAO,aAAa,CAAA,CAC1C,gBAAA;AAAA,MACCC,mBAAA,CAAK,QAAQ,gBAAA,EAAkB,IAAA,CAAK,OAAO,aAAa,CAAA,CACrD,cAAc,GAAG;AAAA,KACtB,CACC,kBAAA,CAAmB,IAAA,CAAK,MAAA,CAAO,cAAc,EAC7C,UAAA,CAAW,EAAE,gBAAA,EAAkB,MAAA,EAAQ,CAAA;AAE1C,IAAA,IAAA,CAAK,SAAA,GAAY,MAAM,SAAA,CAAU,KAAA,EAAM;AAEvC,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,OAAA,EAAQ;AAAA,MAC7B,MAAM,IAAA,CAAK,SAAA,CAAU,aAAA,CAAc,IAAA,CAAK,OAAO,aAAa;AAAA,KAC9D;AAAA,EACF;AAAA,EAEA,MAAM,IAAA,GAAsB;AAC1B,IAAA,IAAI,KAAK,SAAA,EAAW;AAClB,MAAA,MAAM,IAAA,CAAK,UAAU,IAAA,EAAK;AAC1B,MAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AAAA,IACnB;AAAA,EACF;AACF,CAAA;;;ACjCO,IAAe,YAAA,GAAf,cAAoC,KAAA,CAAM;AAAA,EAE/C,WAAA,CAAY,SAAiB,OAAA,EAA+B;AAC1D,IAAA,KAAA,CAAM,SAAS,OAAO,CAAA;AACtB,IAAA,IAAA,CAAK,IAAA,GAAO,KAAK,WAAA,CAAY,IAAA;AAAA,EAC/B;AACF;;;ACJO,IAAM,aAAA,GAAN,cAA4B,YAAA,CAAa;AAAA,EAE9C,WAAA,CACkB,UAAA,EACA,YAAA,EAChB,OAAA,EACA;AACA,IAAA,KAAA;AAAA,MACE,CAAA,wBAAA,EAA2B,UAAU,CAAA,EAAA,EAAK,YAAY,CAAA,CAAA;AAAA,MACtD;AAAA,KACF;AAPgB,IAAA,IAAA,CAAA,UAAA,GAAA,UAAA;AACA,IAAA,IAAA,CAAA,YAAA,GAAA,YAAA;AAAA,EAOlB;AAAA,EAVS,IAAA,GAAO,iBAAA;AAWlB,CAAA;AAEO,IAAM,2BAAA,GAAN,cAA0C,YAAA,CAAa;AAAA,EACnD,IAAA,GAAO,0BAAA;AAAA,EAChB,YAAY,OAAA,EAA+B;AACzC,IAAA,KAAA;AAAA,MACE,sEAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AACF,CAAA;AAEO,IAAM,iBAAA,GAAN,cAAgC,YAAA,CAAa;AAAA,EAElD,WAAA,CACkB,WAChB,OAAA,EACA;AACA,IAAA,KAAA;AAAA,MACE,oCAAoC,SAAS,CAAA,GAAA,CAAA;AAAA,MAC7C;AAAA,KACF;AANgB,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AAAA,EAOlB;AAAA,EATS,IAAA,GAAO,eAAA;AAUlB,CAAA;;;AC1BA,IAAM,kBAAA,GAAqB,GAAA;AAEpB,IAAM,cAAN,MAAkB;AAAA,EAGvB,YAA6B,OAAA,EAAiB;AAAjB,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA,EAAkB;AAAA,EAFvC,eAAsC,EAAC;AAAA,EAI/C,YAAY,IAAA,EAAiC;AAC3C,IAAA,IAAA,CAAK,YAAA,CAAa,KAAK,IAAI,CAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,KAAW,CAAA,EAAG;AACpC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,YAAA,CAAa,MAAA,CAAO,CAAC,CAAA;AACxC,IAAA,MAAM,IAAA,CAAK,IAAA,CAAK,qBAAA,EAAuB,EAAE,OAAO,CAAA;AAAA,EAClD;AAAA,EAEA,MAAM,UAAA,GAA4B;AAChC,IAAA,MAAM,KAAK,KAAA,EAAM;AACjB,IAAA,MAAM,IAAA,CAAK,OAAO,eAAe,CAAA;AAAA,EACnC;AAAA,EAEA,MAAM,SAAA,GAAyC;AAC7C,IAAA,OAAO,IAAA,CAAK,IAAwB,gBAAgB,CAAA;AAAA,EACtD;AAAA,EAEA,MAAM,WAAA,GAAgE;AACpE,IAAA,MAAM,KAAK,KAAA,EAAM;AACjB,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,GAAA,CAAiC,kBAAkB,CAAA;AAC3E,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA,EAEA,MAAc,IAAO,IAAA,EAA0B;AAC7C,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,KAAA,CAAM,MAAM,EAAE,MAAA,EAAQ,OAAO,CAAA;AACzD,IAAA,OAAQ,MAAM,SAAS,IAAA,EAAK;AAAA,EAC9B;AAAA,EAEA,MAAc,IAAA,CAAQ,IAAA,EAAc,IAAA,EAA2B;AAC7D,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM;AAAA,MACtC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI;AAAA,KAC1B,CAAA;AACD,IAAA,OAAQ,MAAM,SAAS,IAAA,EAAK;AAAA,EAC9B;AAAA,EAEA,MAAc,OAAO,IAAA,EAA6B;AAChD,IAAA,MAAM,KAAK,KAAA,CAAM,IAAA,EAAM,EAAE,MAAA,EAAQ,UAAU,CAAA;AAAA,EAC7C;AAAA,EAEA,MAAc,KAAA,CAAM,IAAA,EAAc,IAAA,EAAsC;AACtE,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAA,CAAK,OAAO,GAAG,IAAI,CAAA,CAAA;AAClC,IAAA,IAAI,QAAA;AAEJ,IAAA,IAAI;AACF,MAAA,QAAA,GAAW,MAAM,MAAM,GAAA,EAAK;AAAA,QAC1B,GAAG,IAAA;AAAA,QACH,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,kBAAkB;AAAA,OAC/C,CAAA;AAAA,IACH,SAAS,KAAA,EAAgB;AACvB,MAAA,IAAI,iBAAiB,SAAA,EAAW;AAC9B,QAAA,MAAM,QAAQ,KAAA,CAAM,KAAA;AACpB,QAAA,IAAI,KAAA,EAAO,SAAS,cAAA,EAAgB;AAClC,UAAA,MAAM,IAAI,2BAAA,CAA4B,EAAE,KAAA,EAAO,OAAO,CAAA;AAAA,QACxD;AAAA,MACF;AACA,MAAA,IAAI,KAAA,YAAiB,YAAA,IAAgB,KAAA,CAAM,IAAA,KAAS,cAAA,EAAgB;AAClE,QAAA,MAAM,IAAI,iBAAA,CAAkB,kBAAA,EAAoB,EAAE,KAAA,EAAO,OAAO,CAAA;AAAA,MAClE;AACA,MAAA,MAAM,KAAA;AAAA,IACR;AAEA,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AACjC,MAAA,MAAM,IAAI,aAAA,CAAc,QAAA,CAAS,MAAA,EAAQ,IAAI,CAAA;AAAA,IAC/C;AAEA,IAAA,OAAO,QAAA;AAAA,EACT;AACF,CAAA;;;ACvFO,IAAM,4BAAN,MAAgC;AAAA,EAGrC,YAA6B,WAAA,EAA0B;AAA1B,IAAA,IAAA,CAAA,WAAA,GAAA,WAAA;AAAA,EAA2B;AAAA,EAFvC,OAAA,GAA4B,EAAE,QAAA,EAAU,MAAA,EAAO;AAAA,EAIhE,SAAS,KAAA,EAAqB;AAC5B,IAAA,IAAA,CAAK,QAAQ,KAAA,GAAQ,KAAA;AACrB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,sBAAsB,SAAA,EAAyB;AAC7C,IAAA,IAAA,CAAK,QAAQ,OAAA,GAAU,SAAA;AACvB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,WAAW,OAAA,EAAuB;AAChC,IAAA,IAAA,CAAK,YAAY,WAAA,CAAY;AAAA,MAC3B,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,MAAM,OAAA;AAAQ,KACzC,CAAA;AAAA,EACH;AAAA,EAEA,WAAW,MAAA,EAAwB;AACjC,IAAA,IAAA,CAAK,YAAY,WAAA,CAAY;AAAA,MAC3B,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,QAAA,EAAU,EAAE,IAAA,EAAM,gBAAA,EAAkB,MAAA;AAAO,KAC5C,CAAA;AAAA,EACH;AAAA,EAEA,SAAA,CAAU,YAAoB,OAAA,EAAuB;AACnD,IAAA,IAAA,CAAK,YAAY,WAAA,CAAY;AAAA,MAC3B,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,QAAA,EAAU;AAAA,QACR,IAAA,EAAM,OAAA;AAAA,QACN,MAAA,EAAQ,UAAA;AAAA,QACR,OAAO,EAAE,OAAA,EAAS,IAAA,EAAM,WAAA,EAAa,MAAM,IAAA;AAAK;AAClD,KACD,CAAA;AAAA,EACH;AACF,CAAA;;;ACvCO,IAAM,uBAAN,MAA2B;AAAA,EAGhC,YAA6B,WAAA,EAA0B;AAA1B,IAAA,IAAA,CAAA,WAAA,GAAA,WAAA;AAAA,EAA2B;AAAA,EAFvC,OAAA,GAA4B,EAAE,QAAA,EAAU,YAAA,EAAa;AAAA,EAItE,SAAS,KAAA,EAAqB;AAC5B,IAAA,IAAA,CAAK,QAAQ,KAAA,GAAQ,KAAA;AACrB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,WAAW,MAAA,EAAqC;AAC9C,IAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAC,CAAA,GAClC,MAAA,GACD,CAAC,MAAkB,CAAA;AAEvB,IAAA,IAAA,CAAK,YAAY,WAAA,CAAY;AAAA,MAC3B,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,QAAA,EAAU,EAAE,IAAA,EAAM,WAAA,EAAa,OAAA;AAAQ,KACxC,CAAA;AAAA,EACH;AAAA,EAEA,SAAA,CAAU,YAAoB,OAAA,EAAuB;AACnD,IAAA,IAAA,CAAK,YAAY,WAAA,CAAY;AAAA,MAC3B,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,QAAA,EAAU;AAAA,QACR,IAAA,EAAM,OAAA;AAAA,QACN,MAAA,EAAQ,UAAA;AAAA,QACR,OAAO,EAAE,OAAA,EAAS,IAAA,EAAM,WAAA,EAAa,MAAM,IAAA;AAAK;AAClD,KACD,CAAA;AAAA,EACH;AACF,CAAA;;;AChCO,IAAM,oBAAN,MAAwB;AAAA,EAC7B,YAA6B,WAAA,EAA0B;AAA1B,IAAA,IAAA,CAAA,WAAA,GAAA,WAAA;AAAA,EAA2B;AAAA,EAExD,WAAW,MAAA,EAAuD;AAChE,IAAA,IAAA,CAAK,YAAY,WAAA,CAAY;AAAA,MAC3B,SAAS,EAAC;AAAA,MACV,QAAA,EAAU,EAAE,IAAA,EAAM,QAAA,EAAU,MAAA;AAAO,KACpC,CAAA;AAAA,EACH;AACF,CAAA;;;ACNO,IAAM,aAAN,MAAiB;AAAA,EACtB,YAA6B,WAAA,EAA0B;AAA1B,IAAA,IAAA,CAAA,WAAA,GAAA,WAAA;AAAA,EAA2B;AAAA,EAExD,IAAI,cAAA,GAA4C;AAC9C,IAAA,OAAO,IAAI,yBAAA,CAA0B,IAAA,CAAK,WAAW,CAAA;AAAA,EACvD;AAAA,EAEA,IAAI,SAAA,GAAkC;AACpC,IAAA,OAAO,IAAI,oBAAA,CAAqB,IAAA,CAAK,WAAW,CAAA;AAAA,EAClD;AAAA,EAEA,IAAI,MAAA,GAA4B;AAC9B,IAAA,OAAO,IAAI,iBAAA,CAAkB,IAAA,CAAK,WAAW,CAAA;AAAA,EAC/C;AACF,CAAA;;;ACjBO,IAAM,wBAAA,GAAN,cAAuC,YAAA,CAAa;AAAA,EAChD,IAAA,GAAO,uBAAA;AAAA,EAChB,YAAY,OAAA,EAA+B;AACzC,IAAA,KAAA;AAAA,MACE,uFAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AACF;;;ACDO,IAAM,UAAN,MAAc;AAAA,EACX,KAAA,GAAsB,MAAA;AAAA,EACtB,YAAA,GAAqC,IAAA;AAAA,EACrC,gBAAA;AAAA,EACA,WAAA,GAAkC,IAAA;AAAA,EAClC,MAAA,GAA4B,IAAA;AAAA,EAC5B,QAAA,GAA0B,IAAA;AAAA,EAElC,YAAY,OAAA,EAA0B;AACpC,IAAA,MAAM,MAAA,GAAS,uBAAuB,OAAO,CAAA;AAC7C,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAI,gBAAA,CAAiB,MAAM,CAAA;AAAA,EACrD;AAAA,EAEA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAI,IAAA,CAAK,UAAU,SAAA,EAAW;AAC9B,IAAA,IAAI,KAAK,KAAA,KAAU,UAAA,IAAc,IAAA,CAAK,YAAA,SAAqB,IAAA,CAAK,YAAA;AAEhE,IAAA,IAAA,CAAK,KAAA,GAAQ,UAAA;AACb,IAAA,IAAA,CAAK,YAAA,GAAe,KAAK,OAAA,EAAQ;AACjC,IAAA,OAAO,IAAA,CAAK,YAAA;AAAA,EACd;AAAA,EAEA,MAAc,OAAA,GAAyB;AACrC,IAAA,MAAM,EAAE,IAAA,EAAM,IAAA,KAAS,MAAM,IAAA,CAAK,iBAAiB,KAAA,EAAM;AACzD,IAAA,IAAA,CAAK,QAAA,GAAW,CAAA,OAAA,EAAU,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;AACtC,IAAA,IAAA,CAAK,WAAA,GAAc,IAAI,WAAA,CAAY,IAAA,CAAK,QAAQ,CAAA;AAChD,IAAA,IAAA,CAAK,MAAA,GAAS,IAAI,UAAA,CAAW,IAAA,CAAK,WAAW,CAAA;AAC7C,IAAA,IAAA,CAAK,KAAA,GAAQ,SAAA;AAAA,EACf;AAAA,EAEA,MAAM,IAAA,GAAsB;AAC1B,IAAA,IAAI,IAAA,CAAK,KAAA,KAAU,SAAA,IAAa,IAAA,CAAK,UAAU,MAAA,EAAQ;AACvD,IAAA,IAAA,CAAK,KAAA,GAAQ,UAAA;AACb,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,iBAAiB,IAAA,EAAK;AAAA,IACnC,CAAA,SAAE;AACA,MAAA,IAAA,CAAK,KAAA,GAAQ,SAAA;AACb,MAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AACnB,MAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AACd,MAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,IAAI,OAAA,GAAkB;AACpB,IAAA,IAAA,CAAK,aAAA,EAAc;AACnB,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA,EAEA,IAAI,UAAA,GAAqB;AACvB,IAAA,OAAO,CAAA,EAAG,KAAK,OAAO,CAAA,GAAA,CAAA;AAAA,EACxB;AAAA,EAEA,IAAI,KAAA,GAAoB;AACtB,IAAA,IAAA,CAAK,aAAA,EAAc;AACnB,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA,EAEA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAA,CAAK,aAAA,EAAc;AACnB,IAAA,MAAM,IAAA,CAAK,YAAa,UAAA,EAAW;AAAA,EACrC;AAAA,EAEA,OAAO,MAAA,CAAO,YAAY,CAAA,GAAmB;AAC3C,IAAA,MAAM,KAAK,IAAA,EAAK;AAAA,EAClB;AAAA,EAEQ,aAAA,GAAsB;AAC5B,IAAA,IAAI,IAAA,CAAK,UAAU,SAAA,EAAW;AAC5B,MAAA,MAAM,IAAI,wBAAA,EAAyB;AAAA,IACrC;AAAA,EACF;AACF;;;AC9EO,IAAM,sBAAA,GAAN,cAAqC,YAAA,CAAa;AAAA,EAC9C,IAAA,GAAO,4BAAA;AAAA,EAChB,WAAA,CAAY,SAAiB,OAAA,EAA+B;AAC1D,IAAA,KAAA,CAAM,SAAS,OAAO,CAAA;AAAA,EACxB;AACF","file":"index.cjs","sourcesContent":["import type { MockLLMOptions, ContainerConfig } from \"../types/config.js\";\n\nconst DEFAULT_IMAGE = \"phantomllm-server:latest\";\nconst DEFAULT_PORT = 8080;\n\nexport function resolveContainerConfig(options?: MockLLMOptions): ContainerConfig {\n return {\n image: options?.image ?? process.env[\"PHANTOMLLM_IMAGE\"] ?? DEFAULT_IMAGE,\n containerPort: options?.containerPort ?? DEFAULT_PORT,\n reuse: options?.reuse ?? true,\n startupTimeout: options?.startupTimeout ?? 30_000,\n };\n}\n","import { GenericContainer, Wait } from \"testcontainers\";\nimport type { StartedTestContainer } from \"testcontainers\";\nimport type { ContainerConfig } from \"../types/config.js\";\n\nexport class ContainerManager {\n private container: StartedTestContainer | null = null;\n\n constructor(private readonly config: ContainerConfig) {}\n\n async start(): Promise<{ host: string; port: number }> {\n const container = new GenericContainer(this.config.image)\n .withExposedPorts(this.config.containerPort)\n .withWaitStrategy(\n Wait.forHttp(\"/_admin/health\", this.config.containerPort)\n .forStatusCode(200),\n )\n .withStartupTimeout(this.config.startupTimeout)\n .withLabels({ \"com.phantomllm\": \"true\" });\n\n this.container = await container.start();\n\n return {\n host: this.container.getHost(),\n port: this.container.getMappedPort(this.config.containerPort),\n };\n }\n\n async stop(): Promise<void> {\n if (this.container) {\n await this.container.stop();\n this.container = null;\n }\n }\n}\n","export abstract class MockLLMError extends Error {\n abstract readonly code: string;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = this.constructor.name;\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class AdminAPIError extends MockLLMError {\n readonly code = 'ADMIN_API_ERROR' as const;\n constructor(\n public readonly statusCode: number,\n public readonly responseBody: string,\n options?: { cause?: unknown },\n ) {\n super(\n `Admin API returned HTTP ${statusCode}: ${responseBody}`,\n options,\n );\n }\n}\n\nexport class AdminConnectionRefusedError extends MockLLMError {\n readonly code = 'ADMIN_CONNECTION_REFUSED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'Cannot connect to MockLLM admin API. The container may have crashed.',\n options,\n );\n }\n}\n\nexport class AdminTimeoutError extends MockLLMError {\n readonly code = 'ADMIN_TIMEOUT' as const;\n constructor(\n public readonly timeoutMs: number,\n options?: { cause?: unknown },\n ) {\n super(\n `Admin API did not respond within ${timeoutMs}ms.`,\n options,\n );\n }\n}\n","import {\n AdminConnectionRefusedError,\n AdminTimeoutError,\n AdminAPIError,\n} from \"../errors/admin.errors.js\";\nimport type {\n AdminStubDefinition,\n AdminHealthPayload,\n AdminRecordedRequestPayload,\n} from \"./admin.client.types.js\";\n\nconst REQUEST_TIMEOUT_MS = 5_000;\n\nexport class AdminClient {\n private pendingStubs: AdminStubDefinition[] = [];\n\n constructor(private readonly baseUrl: string) {}\n\n enqueueStub(stub: AdminStubDefinition): void {\n this.pendingStubs.push(stub);\n }\n\n async flush(): Promise<void> {\n if (this.pendingStubs.length === 0) return;\n const stubs = this.pendingStubs.splice(0);\n await this.post(\"/_admin/stubs/batch\", { stubs });\n }\n\n async clearStubs(): Promise<void> {\n await this.flush();\n await this.delete(\"/_admin/stubs\");\n }\n\n async getHealth(): Promise<AdminHealthPayload> {\n return this.get<AdminHealthPayload>(\"/_admin/health\");\n }\n\n async getRequests(): Promise<AdminRecordedRequestPayload[\"requests\"]> {\n await this.flush();\n const data = await this.get<AdminRecordedRequestPayload>(\"/_admin/requests\");\n return data.requests;\n }\n\n private async get<T>(path: string): Promise<T> {\n const response = await this.fetch(path, { method: \"GET\" });\n return (await response.json()) as T;\n }\n\n private async post<T>(path: string, body: unknown): Promise<T> {\n const response = await this.fetch(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n return (await response.json()) as T;\n }\n\n private async delete(path: string): Promise<void> {\n await this.fetch(path, { method: \"DELETE\" });\n }\n\n private async fetch(path: string, init: RequestInit): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n let response: Response;\n\n try {\n response = await fetch(url, {\n ...init,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (error: unknown) {\n if (error instanceof TypeError) {\n const cause = error.cause as { code?: string } | undefined;\n if (cause?.code === \"ECONNREFUSED\") {\n throw new AdminConnectionRefusedError({ cause: error });\n }\n }\n if (error instanceof DOMException && error.name === \"TimeoutError\") {\n throw new AdminTimeoutError(REQUEST_TIMEOUT_MS, { cause: error });\n }\n throw error;\n }\n\n if (!response.ok) {\n const body = await response.text();\n throw new AdminAPIError(response.status, body);\n }\n\n return response;\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport type { AdminStubMatcher } from \"../driver/admin.client.types.js\";\n\nexport class ChatCompletionStubBuilder {\n private readonly matcher: AdminStubMatcher = { endpoint: \"chat\" };\n\n constructor(private readonly adminClient: AdminClient) {}\n\n forModel(model: string): this {\n this.matcher.model = model;\n return this;\n }\n\n withMessageContaining(substring: string): this {\n this.matcher.content = substring;\n return this;\n }\n\n willReturn(content: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"chat\", body: content },\n });\n }\n\n willStream(chunks: string[]): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"streaming-chat\", chunks },\n });\n }\n\n willError(statusCode: number, message: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: {\n type: \"error\",\n status: statusCode,\n error: { message, type: \"api_error\", code: null },\n },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport type { AdminStubMatcher } from \"../driver/admin.client.types.js\";\n\nexport class EmbeddingStubBuilder {\n private readonly matcher: AdminStubMatcher = { endpoint: \"embeddings\" };\n\n constructor(private readonly adminClient: AdminClient) {}\n\n forModel(model: string): this {\n this.matcher.model = model;\n return this;\n }\n\n willReturn(vector: number[] | number[][]): void {\n const vectors = Array.isArray(vector[0])\n ? (vector as number[][])\n : [vector as number[]];\n\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"embedding\", vectors },\n });\n }\n\n willError(statusCode: number, message: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: {\n type: \"error\",\n status: statusCode,\n error: { message, type: \"api_error\", code: null },\n },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\n\nexport class ModelsStubBuilder {\n constructor(private readonly adminClient: AdminClient) {}\n\n willReturn(models: Array<{ id: string; ownedBy?: string }>): void {\n this.adminClient.enqueueStub({\n matcher: {},\n response: { type: \"models\", models },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport { ChatCompletionStubBuilder } from \"./chat.builder.js\";\nimport { EmbeddingStubBuilder } from \"./embedding.builder.js\";\nimport { ModelsStubBuilder } from \"./models.builder.js\";\n\nexport class GivenStubs {\n constructor(private readonly adminClient: AdminClient) {}\n\n get chatCompletion(): ChatCompletionStubBuilder {\n return new ChatCompletionStubBuilder(this.adminClient);\n }\n\n get embedding(): EmbeddingStubBuilder {\n return new EmbeddingStubBuilder(this.adminClient);\n }\n\n get models(): ModelsStubBuilder {\n return new ModelsStubBuilder(this.adminClient);\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class ContainerNotStartedError extends MockLLMError {\n readonly code = 'CONTAINER_NOT_STARTED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'MockLLM container is not running. Call `await mock.start()` before configuring stubs.',\n options,\n );\n }\n}\n\nexport class ContainerAlreadyStartedError extends MockLLMError {\n readonly code = 'CONTAINER_ALREADY_STARTED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'MockLLM container is already running. Call `await mock.stop()` first if you need to restart.',\n options,\n );\n }\n}\n","import type { MockLLMOptions } from \"../types/config.js\";\nimport { resolveContainerConfig } from \"./container.config.js\";\nimport { ContainerManager } from \"./container.manager.js\";\nimport { AdminClient } from \"./admin.client.js\";\nimport { GivenStubs } from \"../stubs/given.js\";\nimport { ContainerNotStartedError } from \"../errors/lifecycle.errors.js\";\n\ntype MockLLMState = \"idle\" | \"starting\" | \"running\" | \"stopping\" | \"stopped\";\n\nexport class MockLLM {\n private state: MockLLMState = \"idle\";\n private startPromise: Promise<void> | null = null;\n private containerManager: ContainerManager;\n private adminClient: AdminClient | null = null;\n private _given: GivenStubs | null = null;\n private _baseUrl: string | null = null;\n\n constructor(options?: MockLLMOptions) {\n const config = resolveContainerConfig(options);\n this.containerManager = new ContainerManager(config);\n }\n\n async start(): Promise<void> {\n if (this.state === \"running\") return;\n if (this.state === \"starting\" && this.startPromise) return this.startPromise;\n\n this.state = \"starting\";\n this.startPromise = this.doStart();\n return this.startPromise;\n }\n\n private async doStart(): Promise<void> {\n const { host, port } = await this.containerManager.start();\n this._baseUrl = `http://${host}:${port}`;\n this.adminClient = new AdminClient(this._baseUrl);\n this._given = new GivenStubs(this.adminClient);\n this.state = \"running\";\n }\n\n async stop(): Promise<void> {\n if (this.state === \"stopped\" || this.state === \"idle\") return;\n this.state = \"stopping\";\n try {\n await this.containerManager.stop();\n } finally {\n this.state = \"stopped\";\n this.adminClient = null;\n this._given = null;\n this._baseUrl = null;\n }\n }\n\n get baseUrl(): string {\n this.assertRunning();\n return this._baseUrl!;\n }\n\n get apiBaseUrl(): string {\n return `${this.baseUrl}/v1`;\n }\n\n get given(): GivenStubs {\n this.assertRunning();\n return this._given!;\n }\n\n async clear(): Promise<void> {\n this.assertRunning();\n await this.adminClient!.clearStubs();\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.stop();\n }\n\n private assertRunning(): void {\n if (this.state !== \"running\") {\n throw new ContainerNotStartedError();\n }\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class StubConfigurationError extends MockLLMError {\n readonly code = 'STUB_CONFIGURATION_INVALID' as const;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/driver/container.config.ts","../src/driver/container.manager.ts","../src/errors/base.ts","../src/errors/admin.errors.ts","../src/driver/admin.client.ts","../src/stubs/chat.builder.ts","../src/stubs/embedding.builder.ts","../src/stubs/models.builder.ts","../src/stubs/given.ts","../src/stubs/expect.ts","../src/errors/lifecycle.errors.ts","../src/driver/mock-llm.ts","../src/errors/stub.errors.ts"],"names":["GenericContainer","Wait"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { MockLLMOptions, ContainerConfig } from \"../types/config.js\";\n\nconst DEFAULT_IMAGE = \"phantomllm-server:latest\";\nconst DEFAULT_PORT = 8080;\n\nexport function resolveContainerConfig(options?: MockLLMOptions): ContainerConfig {\n return {\n image: options?.image ?? process.env[\"PHANTOMLLM_IMAGE\"] ?? DEFAULT_IMAGE,\n containerPort: options?.containerPort ?? DEFAULT_PORT,\n reuse: options?.reuse ?? true,\n startupTimeout: options?.startupTimeout ?? 30_000,\n };\n}\n","import { GenericContainer, Wait } from \"testcontainers\";\nimport type { StartedTestContainer } from \"testcontainers\";\nimport type { ContainerConfig } from \"../types/config.js\";\n\nexport class ContainerManager {\n private container: StartedTestContainer | null = null;\n\n constructor(private readonly config: ContainerConfig) {}\n\n async start(): Promise<{ host: string; port: number }> {\n const builder = new GenericContainer(this.config.image)\n .withExposedPorts(this.config.containerPort)\n .withWaitStrategy(\n Wait.forHttp(\"/_admin/health\", this.config.containerPort)\n .forStatusCode(200),\n )\n .withStartupTimeout(this.config.startupTimeout)\n .withLabels({ \"com.phantomllm\": \"true\" });\n\n this.container = await builder.start();\n\n return {\n host: this.container.getHost(),\n port: this.container.getMappedPort(this.config.containerPort),\n };\n }\n\n async stop(): Promise<void> {\n if (this.container) {\n await this.container.stop();\n this.container = null;\n }\n }\n}\n","export abstract class MockLLMError extends Error {\n abstract readonly code: string;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = this.constructor.name;\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class AdminAPIError extends MockLLMError {\n readonly code = 'ADMIN_API_ERROR' as const;\n constructor(\n public readonly statusCode: number,\n public readonly responseBody: string,\n options?: { cause?: unknown },\n ) {\n super(\n `Admin API returned HTTP ${statusCode}: ${responseBody}`,\n options,\n );\n }\n}\n\nexport class AdminConnectionRefusedError extends MockLLMError {\n readonly code = 'ADMIN_CONNECTION_REFUSED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'Cannot connect to MockLLM admin API. The container may have crashed.',\n options,\n );\n }\n}\n\nexport class AdminTimeoutError extends MockLLMError {\n readonly code = 'ADMIN_TIMEOUT' as const;\n constructor(\n public readonly timeoutMs: number,\n options?: { cause?: unknown },\n ) {\n super(\n `Admin API did not respond within ${timeoutMs}ms.`,\n options,\n );\n }\n}\n","import {\n AdminConnectionRefusedError,\n AdminTimeoutError,\n AdminAPIError,\n} from \"../errors/admin.errors.js\";\nimport type {\n AdminStubDefinition,\n AdminHealthPayload,\n AdminRecordedRequestPayload,\n} from \"./admin.client.types.js\";\n\nconst REQUEST_TIMEOUT_MS = 5_000;\n\nexport class AdminClient {\n private pendingStubs: AdminStubDefinition[] = [];\n private pendingConfigs: Array<{ apiKey: string }> = [];\n\n constructor(private readonly baseUrl: string) {}\n\n enqueueStub(stub: AdminStubDefinition): void {\n this.pendingStubs.push(stub);\n }\n\n enqueueConfig(config: { apiKey: string }): void {\n this.pendingConfigs.push(config);\n }\n\n async flush(): Promise<void> {\n if (this.pendingConfigs.length > 0) {\n const configs = this.pendingConfigs.splice(0);\n for (const config of configs) {\n await this.post(\"/_admin/config\", config);\n }\n }\n\n if (this.pendingStubs.length > 0) {\n const stubs = this.pendingStubs.splice(0);\n await this.post(\"/_admin/stubs/batch\", { stubs });\n }\n }\n\n async clearStubs(): Promise<void> {\n await this.flush();\n await this.delete(\"/_admin/stubs\");\n }\n\n async getHealth(): Promise<AdminHealthPayload> {\n return this.get<AdminHealthPayload>(\"/_admin/health\");\n }\n\n async getRequests(): Promise<AdminRecordedRequestPayload[\"requests\"]> {\n await this.flush();\n const data = await this.get<AdminRecordedRequestPayload>(\"/_admin/requests\");\n return data.requests;\n }\n\n private async get<T>(path: string): Promise<T> {\n const response = await this.fetch(path, { method: \"GET\" });\n return (await response.json()) as T;\n }\n\n private async post<T>(path: string, body: unknown): Promise<T> {\n const response = await this.fetch(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n return (await response.json()) as T;\n }\n\n private async delete(path: string): Promise<void> {\n await this.fetch(path, { method: \"DELETE\" });\n }\n\n private async fetch(path: string, init: RequestInit): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n let response: Response;\n\n try {\n response = await fetch(url, {\n ...init,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (error: unknown) {\n if (error instanceof TypeError) {\n const cause = error.cause as { code?: string } | undefined;\n if (cause?.code === \"ECONNREFUSED\") {\n throw new AdminConnectionRefusedError({ cause: error });\n }\n }\n if (error instanceof DOMException && error.name === \"TimeoutError\") {\n throw new AdminTimeoutError(REQUEST_TIMEOUT_MS, { cause: error });\n }\n throw error;\n }\n\n if (!response.ok) {\n const body = await response.text();\n throw new AdminAPIError(response.status, body);\n }\n\n return response;\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport type { AdminStubMatcher } from \"../driver/admin.client.types.js\";\n\nexport class ChatCompletionStubBuilder {\n private readonly matcher: AdminStubMatcher = { endpoint: \"chat\" };\n\n constructor(private readonly adminClient: AdminClient) {}\n\n forModel(model: string): this {\n this.matcher.model = model;\n return this;\n }\n\n withMessageContaining(substring: string): this {\n this.matcher.content = substring;\n return this;\n }\n\n willReturn(content: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"chat\", body: content },\n });\n }\n\n willStream(chunks: string[]): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"streaming-chat\", chunks },\n });\n }\n\n willError(statusCode: number, message: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: {\n type: \"error\",\n status: statusCode,\n error: { message, type: \"api_error\", code: null },\n },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport type { AdminStubMatcher } from \"../driver/admin.client.types.js\";\n\nexport class EmbeddingStubBuilder {\n private readonly matcher: AdminStubMatcher = { endpoint: \"embeddings\" };\n\n constructor(private readonly adminClient: AdminClient) {}\n\n forModel(model: string): this {\n this.matcher.model = model;\n return this;\n }\n\n willReturn(vector: number[] | number[][]): void {\n const vectors = Array.isArray(vector[0])\n ? (vector as number[][])\n : [vector as number[]];\n\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"embedding\", vectors },\n });\n }\n\n willError(statusCode: number, message: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: {\n type: \"error\",\n status: statusCode,\n error: { message, type: \"api_error\", code: null },\n },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\n\nexport class ModelsStubBuilder {\n constructor(private readonly adminClient: AdminClient) {}\n\n willReturn(models: Array<{ id: string; ownedBy?: string }>): void {\n this.adminClient.enqueueStub({\n matcher: {},\n response: { type: \"models\", models },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport { ChatCompletionStubBuilder } from \"./chat.builder.js\";\nimport { EmbeddingStubBuilder } from \"./embedding.builder.js\";\nimport { ModelsStubBuilder } from \"./models.builder.js\";\n\nexport class GivenStubs {\n constructor(private readonly adminClient: AdminClient) {}\n\n get chatCompletion(): ChatCompletionStubBuilder {\n return new ChatCompletionStubBuilder(this.adminClient);\n }\n\n get embedding(): EmbeddingStubBuilder {\n return new EmbeddingStubBuilder(this.adminClient);\n }\n\n get models(): ModelsStubBuilder {\n return new ModelsStubBuilder(this.adminClient);\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\n\nexport class ExpectConditions {\n constructor(private readonly adminClient: AdminClient) {}\n\n apiKey(key: string): void {\n this.adminClient.enqueueConfig({ apiKey: key });\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class ContainerNotStartedError extends MockLLMError {\n readonly code = 'CONTAINER_NOT_STARTED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'MockLLM container is not running. Call `await mock.start()` before configuring stubs.',\n options,\n );\n }\n}\n\nexport class ContainerAlreadyStartedError extends MockLLMError {\n readonly code = 'CONTAINER_ALREADY_STARTED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'MockLLM container is already running. Call `await mock.stop()` first if you need to restart.',\n options,\n );\n }\n}\n","import type { MockLLMOptions } from \"../types/config.js\";\nimport { resolveContainerConfig } from \"./container.config.js\";\nimport { ContainerManager } from \"./container.manager.js\";\nimport { AdminClient } from \"./admin.client.js\";\nimport { GivenStubs } from \"../stubs/given.js\";\nimport { ExpectConditions } from \"../stubs/expect.js\";\nimport { ContainerNotStartedError } from \"../errors/lifecycle.errors.js\";\n\ntype MockLLMState = \"idle\" | \"starting\" | \"running\" | \"stopping\" | \"stopped\";\n\nexport class MockLLM {\n private state: MockLLMState = \"idle\";\n private startPromise: Promise<void> | null = null;\n private containerManager: ContainerManager;\n private adminClient: AdminClient | null = null;\n private _given: GivenStubs | null = null;\n private _expect: ExpectConditions | null = null;\n private _baseUrl: string | null = null;\n\n constructor(options?: MockLLMOptions) {\n const config = resolveContainerConfig(options);\n this.containerManager = new ContainerManager(config);\n }\n\n async start(): Promise<void> {\n if (this.state === \"running\") return;\n if (this.state === \"starting\" && this.startPromise) return this.startPromise;\n\n this.state = \"starting\";\n this.startPromise = this.doStart();\n return this.startPromise;\n }\n\n private async doStart(): Promise<void> {\n const { host, port } = await this.containerManager.start();\n this._baseUrl = `http://${host}:${port}`;\n this.adminClient = new AdminClient(this._baseUrl);\n this._given = new GivenStubs(this.adminClient);\n this._expect = new ExpectConditions(this.adminClient);\n this.state = \"running\";\n }\n\n async stop(): Promise<void> {\n if (this.state === \"stopped\" || this.state === \"idle\") return;\n this.state = \"stopping\";\n try {\n await this.containerManager.stop();\n } finally {\n this.state = \"stopped\";\n this.adminClient = null;\n this._given = null;\n this._expect = null;\n this._baseUrl = null;\n }\n }\n\n get baseUrl(): string {\n this.assertRunning();\n return this._baseUrl!;\n }\n\n get apiBaseUrl(): string {\n return `${this.baseUrl}/v1`;\n }\n\n get given(): GivenStubs {\n this.assertRunning();\n return this._given!;\n }\n\n get expect(): ExpectConditions {\n this.assertRunning();\n return this._expect!;\n }\n\n async clear(): Promise<void> {\n this.assertRunning();\n await this.adminClient!.clearStubs();\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.stop();\n }\n\n private assertRunning(): void {\n if (this.state !== \"running\") {\n throw new ContainerNotStartedError();\n }\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class StubConfigurationError extends MockLLMError {\n readonly code = 'STUB_CONFIGURATION_INVALID' as const;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -64,8 +64,12 @@ interface AdminRecordedRequestPayload {
64
64
  declare class AdminClient {
65
65
  private readonly baseUrl;
66
66
  private pendingStubs;
67
+ private pendingConfigs;
67
68
  constructor(baseUrl: string);
68
69
  enqueueStub(stub: AdminStubDefinition): void;
70
+ enqueueConfig(config: {
71
+ apiKey: string;
72
+ }): void;
69
73
  flush(): Promise<void>;
70
74
  clearStubs(): Promise<void>;
71
75
  getHealth(): Promise<AdminHealthPayload>;
@@ -113,12 +117,19 @@ declare class GivenStubs {
113
117
  get models(): ModelsStubBuilder;
114
118
  }
115
119
 
120
+ declare class ExpectConditions {
121
+ private readonly adminClient;
122
+ constructor(adminClient: AdminClient);
123
+ apiKey(key: string): void;
124
+ }
125
+
116
126
  declare class MockLLM {
117
127
  private state;
118
128
  private startPromise;
119
129
  private containerManager;
120
130
  private adminClient;
121
131
  private _given;
132
+ private _expect;
122
133
  private _baseUrl;
123
134
  constructor(options?: MockLLMOptions);
124
135
  start(): Promise<void>;
@@ -127,6 +138,7 @@ declare class MockLLM {
127
138
  get baseUrl(): string;
128
139
  get apiBaseUrl(): string;
129
140
  get given(): GivenStubs;
141
+ get expect(): ExpectConditions;
130
142
  clear(): Promise<void>;
131
143
  [Symbol.asyncDispose](): Promise<void>;
132
144
  private assertRunning;
package/dist/index.d.ts CHANGED
@@ -64,8 +64,12 @@ interface AdminRecordedRequestPayload {
64
64
  declare class AdminClient {
65
65
  private readonly baseUrl;
66
66
  private pendingStubs;
67
+ private pendingConfigs;
67
68
  constructor(baseUrl: string);
68
69
  enqueueStub(stub: AdminStubDefinition): void;
70
+ enqueueConfig(config: {
71
+ apiKey: string;
72
+ }): void;
69
73
  flush(): Promise<void>;
70
74
  clearStubs(): Promise<void>;
71
75
  getHealth(): Promise<AdminHealthPayload>;
@@ -113,12 +117,19 @@ declare class GivenStubs {
113
117
  get models(): ModelsStubBuilder;
114
118
  }
115
119
 
120
+ declare class ExpectConditions {
121
+ private readonly adminClient;
122
+ constructor(adminClient: AdminClient);
123
+ apiKey(key: string): void;
124
+ }
125
+
116
126
  declare class MockLLM {
117
127
  private state;
118
128
  private startPromise;
119
129
  private containerManager;
120
130
  private adminClient;
121
131
  private _given;
132
+ private _expect;
122
133
  private _baseUrl;
123
134
  constructor(options?: MockLLMOptions);
124
135
  start(): Promise<void>;
@@ -127,6 +138,7 @@ declare class MockLLM {
127
138
  get baseUrl(): string;
128
139
  get apiBaseUrl(): string;
129
140
  get given(): GivenStubs;
141
+ get expect(): ExpectConditions;
130
142
  clear(): Promise<void>;
131
143
  [Symbol.asyncDispose](): Promise<void>;
132
144
  private assertRunning;
package/dist/index.js CHANGED
@@ -17,10 +17,10 @@ var ContainerManager = class {
17
17
  }
18
18
  container = null;
19
19
  async start() {
20
- const container = new GenericContainer(this.config.image).withExposedPorts(this.config.containerPort).withWaitStrategy(
20
+ const builder = new GenericContainer(this.config.image).withExposedPorts(this.config.containerPort).withWaitStrategy(
21
21
  Wait.forHttp("/_admin/health", this.config.containerPort).forStatusCode(200)
22
22
  ).withStartupTimeout(this.config.startupTimeout).withLabels({ "com.phantomllm": "true" });
23
- this.container = await container.start();
23
+ this.container = await builder.start();
24
24
  return {
25
25
  host: this.container.getHost(),
26
26
  port: this.container.getMappedPort(this.config.containerPort)
@@ -81,13 +81,24 @@ var AdminClient = class {
81
81
  this.baseUrl = baseUrl;
82
82
  }
83
83
  pendingStubs = [];
84
+ pendingConfigs = [];
84
85
  enqueueStub(stub) {
85
86
  this.pendingStubs.push(stub);
86
87
  }
88
+ enqueueConfig(config) {
89
+ this.pendingConfigs.push(config);
90
+ }
87
91
  async flush() {
88
- if (this.pendingStubs.length === 0) return;
89
- const stubs = this.pendingStubs.splice(0);
90
- await this.post("/_admin/stubs/batch", { stubs });
92
+ if (this.pendingConfigs.length > 0) {
93
+ const configs = this.pendingConfigs.splice(0);
94
+ for (const config of configs) {
95
+ await this.post("/_admin/config", config);
96
+ }
97
+ }
98
+ if (this.pendingStubs.length > 0) {
99
+ const stubs = this.pendingStubs.splice(0);
100
+ await this.post("/_admin/stubs/batch", { stubs });
101
+ }
91
102
  }
92
103
  async clearStubs() {
93
104
  await this.flush();
@@ -240,6 +251,16 @@ var GivenStubs = class {
240
251
  }
241
252
  };
242
253
 
254
+ // src/stubs/expect.ts
255
+ var ExpectConditions = class {
256
+ constructor(adminClient) {
257
+ this.adminClient = adminClient;
258
+ }
259
+ apiKey(key) {
260
+ this.adminClient.enqueueConfig({ apiKey: key });
261
+ }
262
+ };
263
+
243
264
  // src/errors/lifecycle.errors.ts
244
265
  var ContainerNotStartedError = class extends MockLLMError {
245
266
  code = "CONTAINER_NOT_STARTED";
@@ -258,6 +279,7 @@ var MockLLM = class {
258
279
  containerManager;
259
280
  adminClient = null;
260
281
  _given = null;
282
+ _expect = null;
261
283
  _baseUrl = null;
262
284
  constructor(options) {
263
285
  const config = resolveContainerConfig(options);
@@ -275,6 +297,7 @@ var MockLLM = class {
275
297
  this._baseUrl = `http://${host}:${port}`;
276
298
  this.adminClient = new AdminClient(this._baseUrl);
277
299
  this._given = new GivenStubs(this.adminClient);
300
+ this._expect = new ExpectConditions(this.adminClient);
278
301
  this.state = "running";
279
302
  }
280
303
  async stop() {
@@ -286,6 +309,7 @@ var MockLLM = class {
286
309
  this.state = "stopped";
287
310
  this.adminClient = null;
288
311
  this._given = null;
312
+ this._expect = null;
289
313
  this._baseUrl = null;
290
314
  }
291
315
  }
@@ -300,6 +324,10 @@ var MockLLM = class {
300
324
  this.assertRunning();
301
325
  return this._given;
302
326
  }
327
+ get expect() {
328
+ this.assertRunning();
329
+ return this._expect;
330
+ }
303
331
  async clear() {
304
332
  this.assertRunning();
305
333
  await this.adminClient.clearStubs();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/driver/container.config.ts","../src/driver/container.manager.ts","../src/errors/base.ts","../src/errors/admin.errors.ts","../src/driver/admin.client.ts","../src/stubs/chat.builder.ts","../src/stubs/embedding.builder.ts","../src/stubs/models.builder.ts","../src/stubs/given.ts","../src/errors/lifecycle.errors.ts","../src/driver/mock-llm.ts","../src/errors/stub.errors.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { MockLLMOptions, ContainerConfig } from \"../types/config.js\";\n\nconst DEFAULT_IMAGE = \"phantomllm-server:latest\";\nconst DEFAULT_PORT = 8080;\n\nexport function resolveContainerConfig(options?: MockLLMOptions): ContainerConfig {\n return {\n image: options?.image ?? process.env[\"PHANTOMLLM_IMAGE\"] ?? DEFAULT_IMAGE,\n containerPort: options?.containerPort ?? DEFAULT_PORT,\n reuse: options?.reuse ?? true,\n startupTimeout: options?.startupTimeout ?? 30_000,\n };\n}\n","import { GenericContainer, Wait } from \"testcontainers\";\nimport type { StartedTestContainer } from \"testcontainers\";\nimport type { ContainerConfig } from \"../types/config.js\";\n\nexport class ContainerManager {\n private container: StartedTestContainer | null = null;\n\n constructor(private readonly config: ContainerConfig) {}\n\n async start(): Promise<{ host: string; port: number }> {\n const container = new GenericContainer(this.config.image)\n .withExposedPorts(this.config.containerPort)\n .withWaitStrategy(\n Wait.forHttp(\"/_admin/health\", this.config.containerPort)\n .forStatusCode(200),\n )\n .withStartupTimeout(this.config.startupTimeout)\n .withLabels({ \"com.phantomllm\": \"true\" });\n\n this.container = await container.start();\n\n return {\n host: this.container.getHost(),\n port: this.container.getMappedPort(this.config.containerPort),\n };\n }\n\n async stop(): Promise<void> {\n if (this.container) {\n await this.container.stop();\n this.container = null;\n }\n }\n}\n","export abstract class MockLLMError extends Error {\n abstract readonly code: string;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = this.constructor.name;\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class AdminAPIError extends MockLLMError {\n readonly code = 'ADMIN_API_ERROR' as const;\n constructor(\n public readonly statusCode: number,\n public readonly responseBody: string,\n options?: { cause?: unknown },\n ) {\n super(\n `Admin API returned HTTP ${statusCode}: ${responseBody}`,\n options,\n );\n }\n}\n\nexport class AdminConnectionRefusedError extends MockLLMError {\n readonly code = 'ADMIN_CONNECTION_REFUSED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'Cannot connect to MockLLM admin API. The container may have crashed.',\n options,\n );\n }\n}\n\nexport class AdminTimeoutError extends MockLLMError {\n readonly code = 'ADMIN_TIMEOUT' as const;\n constructor(\n public readonly timeoutMs: number,\n options?: { cause?: unknown },\n ) {\n super(\n `Admin API did not respond within ${timeoutMs}ms.`,\n options,\n );\n }\n}\n","import {\n AdminConnectionRefusedError,\n AdminTimeoutError,\n AdminAPIError,\n} from \"../errors/admin.errors.js\";\nimport type {\n AdminStubDefinition,\n AdminHealthPayload,\n AdminRecordedRequestPayload,\n} from \"./admin.client.types.js\";\n\nconst REQUEST_TIMEOUT_MS = 5_000;\n\nexport class AdminClient {\n private pendingStubs: AdminStubDefinition[] = [];\n\n constructor(private readonly baseUrl: string) {}\n\n enqueueStub(stub: AdminStubDefinition): void {\n this.pendingStubs.push(stub);\n }\n\n async flush(): Promise<void> {\n if (this.pendingStubs.length === 0) return;\n const stubs = this.pendingStubs.splice(0);\n await this.post(\"/_admin/stubs/batch\", { stubs });\n }\n\n async clearStubs(): Promise<void> {\n await this.flush();\n await this.delete(\"/_admin/stubs\");\n }\n\n async getHealth(): Promise<AdminHealthPayload> {\n return this.get<AdminHealthPayload>(\"/_admin/health\");\n }\n\n async getRequests(): Promise<AdminRecordedRequestPayload[\"requests\"]> {\n await this.flush();\n const data = await this.get<AdminRecordedRequestPayload>(\"/_admin/requests\");\n return data.requests;\n }\n\n private async get<T>(path: string): Promise<T> {\n const response = await this.fetch(path, { method: \"GET\" });\n return (await response.json()) as T;\n }\n\n private async post<T>(path: string, body: unknown): Promise<T> {\n const response = await this.fetch(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n return (await response.json()) as T;\n }\n\n private async delete(path: string): Promise<void> {\n await this.fetch(path, { method: \"DELETE\" });\n }\n\n private async fetch(path: string, init: RequestInit): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n let response: Response;\n\n try {\n response = await fetch(url, {\n ...init,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (error: unknown) {\n if (error instanceof TypeError) {\n const cause = error.cause as { code?: string } | undefined;\n if (cause?.code === \"ECONNREFUSED\") {\n throw new AdminConnectionRefusedError({ cause: error });\n }\n }\n if (error instanceof DOMException && error.name === \"TimeoutError\") {\n throw new AdminTimeoutError(REQUEST_TIMEOUT_MS, { cause: error });\n }\n throw error;\n }\n\n if (!response.ok) {\n const body = await response.text();\n throw new AdminAPIError(response.status, body);\n }\n\n return response;\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport type { AdminStubMatcher } from \"../driver/admin.client.types.js\";\n\nexport class ChatCompletionStubBuilder {\n private readonly matcher: AdminStubMatcher = { endpoint: \"chat\" };\n\n constructor(private readonly adminClient: AdminClient) {}\n\n forModel(model: string): this {\n this.matcher.model = model;\n return this;\n }\n\n withMessageContaining(substring: string): this {\n this.matcher.content = substring;\n return this;\n }\n\n willReturn(content: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"chat\", body: content },\n });\n }\n\n willStream(chunks: string[]): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"streaming-chat\", chunks },\n });\n }\n\n willError(statusCode: number, message: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: {\n type: \"error\",\n status: statusCode,\n error: { message, type: \"api_error\", code: null },\n },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport type { AdminStubMatcher } from \"../driver/admin.client.types.js\";\n\nexport class EmbeddingStubBuilder {\n private readonly matcher: AdminStubMatcher = { endpoint: \"embeddings\" };\n\n constructor(private readonly adminClient: AdminClient) {}\n\n forModel(model: string): this {\n this.matcher.model = model;\n return this;\n }\n\n willReturn(vector: number[] | number[][]): void {\n const vectors = Array.isArray(vector[0])\n ? (vector as number[][])\n : [vector as number[]];\n\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"embedding\", vectors },\n });\n }\n\n willError(statusCode: number, message: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: {\n type: \"error\",\n status: statusCode,\n error: { message, type: \"api_error\", code: null },\n },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\n\nexport class ModelsStubBuilder {\n constructor(private readonly adminClient: AdminClient) {}\n\n willReturn(models: Array<{ id: string; ownedBy?: string }>): void {\n this.adminClient.enqueueStub({\n matcher: {},\n response: { type: \"models\", models },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport { ChatCompletionStubBuilder } from \"./chat.builder.js\";\nimport { EmbeddingStubBuilder } from \"./embedding.builder.js\";\nimport { ModelsStubBuilder } from \"./models.builder.js\";\n\nexport class GivenStubs {\n constructor(private readonly adminClient: AdminClient) {}\n\n get chatCompletion(): ChatCompletionStubBuilder {\n return new ChatCompletionStubBuilder(this.adminClient);\n }\n\n get embedding(): EmbeddingStubBuilder {\n return new EmbeddingStubBuilder(this.adminClient);\n }\n\n get models(): ModelsStubBuilder {\n return new ModelsStubBuilder(this.adminClient);\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class ContainerNotStartedError extends MockLLMError {\n readonly code = 'CONTAINER_NOT_STARTED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'MockLLM container is not running. Call `await mock.start()` before configuring stubs.',\n options,\n );\n }\n}\n\nexport class ContainerAlreadyStartedError extends MockLLMError {\n readonly code = 'CONTAINER_ALREADY_STARTED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'MockLLM container is already running. Call `await mock.stop()` first if you need to restart.',\n options,\n );\n }\n}\n","import type { MockLLMOptions } from \"../types/config.js\";\nimport { resolveContainerConfig } from \"./container.config.js\";\nimport { ContainerManager } from \"./container.manager.js\";\nimport { AdminClient } from \"./admin.client.js\";\nimport { GivenStubs } from \"../stubs/given.js\";\nimport { ContainerNotStartedError } from \"../errors/lifecycle.errors.js\";\n\ntype MockLLMState = \"idle\" | \"starting\" | \"running\" | \"stopping\" | \"stopped\";\n\nexport class MockLLM {\n private state: MockLLMState = \"idle\";\n private startPromise: Promise<void> | null = null;\n private containerManager: ContainerManager;\n private adminClient: AdminClient | null = null;\n private _given: GivenStubs | null = null;\n private _baseUrl: string | null = null;\n\n constructor(options?: MockLLMOptions) {\n const config = resolveContainerConfig(options);\n this.containerManager = new ContainerManager(config);\n }\n\n async start(): Promise<void> {\n if (this.state === \"running\") return;\n if (this.state === \"starting\" && this.startPromise) return this.startPromise;\n\n this.state = \"starting\";\n this.startPromise = this.doStart();\n return this.startPromise;\n }\n\n private async doStart(): Promise<void> {\n const { host, port } = await this.containerManager.start();\n this._baseUrl = `http://${host}:${port}`;\n this.adminClient = new AdminClient(this._baseUrl);\n this._given = new GivenStubs(this.adminClient);\n this.state = \"running\";\n }\n\n async stop(): Promise<void> {\n if (this.state === \"stopped\" || this.state === \"idle\") return;\n this.state = \"stopping\";\n try {\n await this.containerManager.stop();\n } finally {\n this.state = \"stopped\";\n this.adminClient = null;\n this._given = null;\n this._baseUrl = null;\n }\n }\n\n get baseUrl(): string {\n this.assertRunning();\n return this._baseUrl!;\n }\n\n get apiBaseUrl(): string {\n return `${this.baseUrl}/v1`;\n }\n\n get given(): GivenStubs {\n this.assertRunning();\n return this._given!;\n }\n\n async clear(): Promise<void> {\n this.assertRunning();\n await this.adminClient!.clearStubs();\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.stop();\n }\n\n private assertRunning(): void {\n if (this.state !== \"running\") {\n throw new ContainerNotStartedError();\n }\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class StubConfigurationError extends MockLLMError {\n readonly code = 'STUB_CONFIGURATION_INVALID' as const;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/driver/container.config.ts","../src/driver/container.manager.ts","../src/errors/base.ts","../src/errors/admin.errors.ts","../src/driver/admin.client.ts","../src/stubs/chat.builder.ts","../src/stubs/embedding.builder.ts","../src/stubs/models.builder.ts","../src/stubs/given.ts","../src/stubs/expect.ts","../src/errors/lifecycle.errors.ts","../src/driver/mock-llm.ts","../src/errors/stub.errors.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { MockLLMOptions, ContainerConfig } from \"../types/config.js\";\n\nconst DEFAULT_IMAGE = \"phantomllm-server:latest\";\nconst DEFAULT_PORT = 8080;\n\nexport function resolveContainerConfig(options?: MockLLMOptions): ContainerConfig {\n return {\n image: options?.image ?? process.env[\"PHANTOMLLM_IMAGE\"] ?? DEFAULT_IMAGE,\n containerPort: options?.containerPort ?? DEFAULT_PORT,\n reuse: options?.reuse ?? true,\n startupTimeout: options?.startupTimeout ?? 30_000,\n };\n}\n","import { GenericContainer, Wait } from \"testcontainers\";\nimport type { StartedTestContainer } from \"testcontainers\";\nimport type { ContainerConfig } from \"../types/config.js\";\n\nexport class ContainerManager {\n private container: StartedTestContainer | null = null;\n\n constructor(private readonly config: ContainerConfig) {}\n\n async start(): Promise<{ host: string; port: number }> {\n const builder = new GenericContainer(this.config.image)\n .withExposedPorts(this.config.containerPort)\n .withWaitStrategy(\n Wait.forHttp(\"/_admin/health\", this.config.containerPort)\n .forStatusCode(200),\n )\n .withStartupTimeout(this.config.startupTimeout)\n .withLabels({ \"com.phantomllm\": \"true\" });\n\n this.container = await builder.start();\n\n return {\n host: this.container.getHost(),\n port: this.container.getMappedPort(this.config.containerPort),\n };\n }\n\n async stop(): Promise<void> {\n if (this.container) {\n await this.container.stop();\n this.container = null;\n }\n }\n}\n","export abstract class MockLLMError extends Error {\n abstract readonly code: string;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = this.constructor.name;\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class AdminAPIError extends MockLLMError {\n readonly code = 'ADMIN_API_ERROR' as const;\n constructor(\n public readonly statusCode: number,\n public readonly responseBody: string,\n options?: { cause?: unknown },\n ) {\n super(\n `Admin API returned HTTP ${statusCode}: ${responseBody}`,\n options,\n );\n }\n}\n\nexport class AdminConnectionRefusedError extends MockLLMError {\n readonly code = 'ADMIN_CONNECTION_REFUSED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'Cannot connect to MockLLM admin API. The container may have crashed.',\n options,\n );\n }\n}\n\nexport class AdminTimeoutError extends MockLLMError {\n readonly code = 'ADMIN_TIMEOUT' as const;\n constructor(\n public readonly timeoutMs: number,\n options?: { cause?: unknown },\n ) {\n super(\n `Admin API did not respond within ${timeoutMs}ms.`,\n options,\n );\n }\n}\n","import {\n AdminConnectionRefusedError,\n AdminTimeoutError,\n AdminAPIError,\n} from \"../errors/admin.errors.js\";\nimport type {\n AdminStubDefinition,\n AdminHealthPayload,\n AdminRecordedRequestPayload,\n} from \"./admin.client.types.js\";\n\nconst REQUEST_TIMEOUT_MS = 5_000;\n\nexport class AdminClient {\n private pendingStubs: AdminStubDefinition[] = [];\n private pendingConfigs: Array<{ apiKey: string }> = [];\n\n constructor(private readonly baseUrl: string) {}\n\n enqueueStub(stub: AdminStubDefinition): void {\n this.pendingStubs.push(stub);\n }\n\n enqueueConfig(config: { apiKey: string }): void {\n this.pendingConfigs.push(config);\n }\n\n async flush(): Promise<void> {\n if (this.pendingConfigs.length > 0) {\n const configs = this.pendingConfigs.splice(0);\n for (const config of configs) {\n await this.post(\"/_admin/config\", config);\n }\n }\n\n if (this.pendingStubs.length > 0) {\n const stubs = this.pendingStubs.splice(0);\n await this.post(\"/_admin/stubs/batch\", { stubs });\n }\n }\n\n async clearStubs(): Promise<void> {\n await this.flush();\n await this.delete(\"/_admin/stubs\");\n }\n\n async getHealth(): Promise<AdminHealthPayload> {\n return this.get<AdminHealthPayload>(\"/_admin/health\");\n }\n\n async getRequests(): Promise<AdminRecordedRequestPayload[\"requests\"]> {\n await this.flush();\n const data = await this.get<AdminRecordedRequestPayload>(\"/_admin/requests\");\n return data.requests;\n }\n\n private async get<T>(path: string): Promise<T> {\n const response = await this.fetch(path, { method: \"GET\" });\n return (await response.json()) as T;\n }\n\n private async post<T>(path: string, body: unknown): Promise<T> {\n const response = await this.fetch(path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n return (await response.json()) as T;\n }\n\n private async delete(path: string): Promise<void> {\n await this.fetch(path, { method: \"DELETE\" });\n }\n\n private async fetch(path: string, init: RequestInit): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n let response: Response;\n\n try {\n response = await fetch(url, {\n ...init,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (error: unknown) {\n if (error instanceof TypeError) {\n const cause = error.cause as { code?: string } | undefined;\n if (cause?.code === \"ECONNREFUSED\") {\n throw new AdminConnectionRefusedError({ cause: error });\n }\n }\n if (error instanceof DOMException && error.name === \"TimeoutError\") {\n throw new AdminTimeoutError(REQUEST_TIMEOUT_MS, { cause: error });\n }\n throw error;\n }\n\n if (!response.ok) {\n const body = await response.text();\n throw new AdminAPIError(response.status, body);\n }\n\n return response;\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport type { AdminStubMatcher } from \"../driver/admin.client.types.js\";\n\nexport class ChatCompletionStubBuilder {\n private readonly matcher: AdminStubMatcher = { endpoint: \"chat\" };\n\n constructor(private readonly adminClient: AdminClient) {}\n\n forModel(model: string): this {\n this.matcher.model = model;\n return this;\n }\n\n withMessageContaining(substring: string): this {\n this.matcher.content = substring;\n return this;\n }\n\n willReturn(content: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"chat\", body: content },\n });\n }\n\n willStream(chunks: string[]): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"streaming-chat\", chunks },\n });\n }\n\n willError(statusCode: number, message: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: {\n type: \"error\",\n status: statusCode,\n error: { message, type: \"api_error\", code: null },\n },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport type { AdminStubMatcher } from \"../driver/admin.client.types.js\";\n\nexport class EmbeddingStubBuilder {\n private readonly matcher: AdminStubMatcher = { endpoint: \"embeddings\" };\n\n constructor(private readonly adminClient: AdminClient) {}\n\n forModel(model: string): this {\n this.matcher.model = model;\n return this;\n }\n\n willReturn(vector: number[] | number[][]): void {\n const vectors = Array.isArray(vector[0])\n ? (vector as number[][])\n : [vector as number[]];\n\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: { type: \"embedding\", vectors },\n });\n }\n\n willError(statusCode: number, message: string): void {\n this.adminClient.enqueueStub({\n matcher: this.matcher,\n response: {\n type: \"error\",\n status: statusCode,\n error: { message, type: \"api_error\", code: null },\n },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\n\nexport class ModelsStubBuilder {\n constructor(private readonly adminClient: AdminClient) {}\n\n willReturn(models: Array<{ id: string; ownedBy?: string }>): void {\n this.adminClient.enqueueStub({\n matcher: {},\n response: { type: \"models\", models },\n });\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\nimport { ChatCompletionStubBuilder } from \"./chat.builder.js\";\nimport { EmbeddingStubBuilder } from \"./embedding.builder.js\";\nimport { ModelsStubBuilder } from \"./models.builder.js\";\n\nexport class GivenStubs {\n constructor(private readonly adminClient: AdminClient) {}\n\n get chatCompletion(): ChatCompletionStubBuilder {\n return new ChatCompletionStubBuilder(this.adminClient);\n }\n\n get embedding(): EmbeddingStubBuilder {\n return new EmbeddingStubBuilder(this.adminClient);\n }\n\n get models(): ModelsStubBuilder {\n return new ModelsStubBuilder(this.adminClient);\n }\n}\n","import type { AdminClient } from \"../driver/admin.client.js\";\n\nexport class ExpectConditions {\n constructor(private readonly adminClient: AdminClient) {}\n\n apiKey(key: string): void {\n this.adminClient.enqueueConfig({ apiKey: key });\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class ContainerNotStartedError extends MockLLMError {\n readonly code = 'CONTAINER_NOT_STARTED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'MockLLM container is not running. Call `await mock.start()` before configuring stubs.',\n options,\n );\n }\n}\n\nexport class ContainerAlreadyStartedError extends MockLLMError {\n readonly code = 'CONTAINER_ALREADY_STARTED' as const;\n constructor(options?: { cause?: unknown }) {\n super(\n 'MockLLM container is already running. Call `await mock.stop()` first if you need to restart.',\n options,\n );\n }\n}\n","import type { MockLLMOptions } from \"../types/config.js\";\nimport { resolveContainerConfig } from \"./container.config.js\";\nimport { ContainerManager } from \"./container.manager.js\";\nimport { AdminClient } from \"./admin.client.js\";\nimport { GivenStubs } from \"../stubs/given.js\";\nimport { ExpectConditions } from \"../stubs/expect.js\";\nimport { ContainerNotStartedError } from \"../errors/lifecycle.errors.js\";\n\ntype MockLLMState = \"idle\" | \"starting\" | \"running\" | \"stopping\" | \"stopped\";\n\nexport class MockLLM {\n private state: MockLLMState = \"idle\";\n private startPromise: Promise<void> | null = null;\n private containerManager: ContainerManager;\n private adminClient: AdminClient | null = null;\n private _given: GivenStubs | null = null;\n private _expect: ExpectConditions | null = null;\n private _baseUrl: string | null = null;\n\n constructor(options?: MockLLMOptions) {\n const config = resolveContainerConfig(options);\n this.containerManager = new ContainerManager(config);\n }\n\n async start(): Promise<void> {\n if (this.state === \"running\") return;\n if (this.state === \"starting\" && this.startPromise) return this.startPromise;\n\n this.state = \"starting\";\n this.startPromise = this.doStart();\n return this.startPromise;\n }\n\n private async doStart(): Promise<void> {\n const { host, port } = await this.containerManager.start();\n this._baseUrl = `http://${host}:${port}`;\n this.adminClient = new AdminClient(this._baseUrl);\n this._given = new GivenStubs(this.adminClient);\n this._expect = new ExpectConditions(this.adminClient);\n this.state = \"running\";\n }\n\n async stop(): Promise<void> {\n if (this.state === \"stopped\" || this.state === \"idle\") return;\n this.state = \"stopping\";\n try {\n await this.containerManager.stop();\n } finally {\n this.state = \"stopped\";\n this.adminClient = null;\n this._given = null;\n this._expect = null;\n this._baseUrl = null;\n }\n }\n\n get baseUrl(): string {\n this.assertRunning();\n return this._baseUrl!;\n }\n\n get apiBaseUrl(): string {\n return `${this.baseUrl}/v1`;\n }\n\n get given(): GivenStubs {\n this.assertRunning();\n return this._given!;\n }\n\n get expect(): ExpectConditions {\n this.assertRunning();\n return this._expect!;\n }\n\n async clear(): Promise<void> {\n this.assertRunning();\n await this.adminClient!.clearStubs();\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.stop();\n }\n\n private assertRunning(): void {\n if (this.state !== \"running\") {\n throw new ContainerNotStartedError();\n }\n }\n}\n","import { MockLLMError } from './base.js';\n\nexport class StubConfigurationError extends MockLLMError {\n readonly code = 'STUB_CONFIGURATION_INVALID' as const;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phantomllm",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Dockerized mock server for OpenAI-compatible APIs. Test your LLM integrations with a real HTTP server.",
5
5
  "license": "MIT",
6
6
  "type": "module",