phantomllm 0.1.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.
@@ -0,0 +1,230 @@
1
+ interface MockLLMOptions {
2
+ image?: string;
3
+ containerPort?: number;
4
+ reuse?: boolean;
5
+ startupTimeout?: number;
6
+ }
7
+
8
+ interface AdminStubMatcher {
9
+ model?: string;
10
+ content?: string;
11
+ input?: string;
12
+ endpoint?: 'chat' | 'embeddings';
13
+ }
14
+ interface AdminChatResponseConfig {
15
+ type: 'chat';
16
+ body: string;
17
+ finishReason?: string;
18
+ }
19
+ interface AdminStreamingChatResponseConfig {
20
+ type: 'streaming-chat';
21
+ chunks: string[];
22
+ finishReason?: string;
23
+ }
24
+ interface AdminEmbeddingResponseConfig {
25
+ type: 'embedding';
26
+ vectors: number[][];
27
+ }
28
+ interface AdminErrorResponseConfig {
29
+ type: 'error';
30
+ status: number;
31
+ error: {
32
+ message: string;
33
+ type: string;
34
+ code: string | null;
35
+ };
36
+ }
37
+ interface AdminModelsResponseConfig {
38
+ type: 'models';
39
+ models: Array<{
40
+ id: string;
41
+ ownedBy?: string;
42
+ }>;
43
+ }
44
+ type AdminStubResponseConfig = AdminChatResponseConfig | AdminStreamingChatResponseConfig | AdminEmbeddingResponseConfig | AdminErrorResponseConfig | AdminModelsResponseConfig;
45
+ interface AdminStubDefinition {
46
+ matcher: AdminStubMatcher;
47
+ response: AdminStubResponseConfig;
48
+ delay?: number;
49
+ }
50
+ interface AdminHealthPayload {
51
+ status: string;
52
+ stubCount: number;
53
+ uptime: number;
54
+ }
55
+ interface AdminRecordedRequestPayload {
56
+ requests: Array<{
57
+ timestamp: number;
58
+ method: string;
59
+ path: string;
60
+ body: unknown;
61
+ }>;
62
+ }
63
+
64
+ declare class AdminClient {
65
+ private readonly baseUrl;
66
+ private pendingStubs;
67
+ constructor(baseUrl: string);
68
+ enqueueStub(stub: AdminStubDefinition): void;
69
+ flush(): Promise<void>;
70
+ clearStubs(): Promise<void>;
71
+ getHealth(): Promise<AdminHealthPayload>;
72
+ getRequests(): Promise<AdminRecordedRequestPayload["requests"]>;
73
+ private get;
74
+ private post;
75
+ private delete;
76
+ private fetch;
77
+ }
78
+
79
+ declare class ChatCompletionStubBuilder {
80
+ private readonly adminClient;
81
+ private readonly matcher;
82
+ constructor(adminClient: AdminClient);
83
+ forModel(model: string): this;
84
+ withMessageContaining(substring: string): this;
85
+ willReturn(content: string): void;
86
+ willStream(chunks: string[]): void;
87
+ willError(statusCode: number, message: string): void;
88
+ }
89
+
90
+ declare class EmbeddingStubBuilder {
91
+ private readonly adminClient;
92
+ private readonly matcher;
93
+ constructor(adminClient: AdminClient);
94
+ forModel(model: string): this;
95
+ willReturn(vector: number[] | number[][]): void;
96
+ willError(statusCode: number, message: string): void;
97
+ }
98
+
99
+ declare class ModelsStubBuilder {
100
+ private readonly adminClient;
101
+ constructor(adminClient: AdminClient);
102
+ willReturn(models: Array<{
103
+ id: string;
104
+ ownedBy?: string;
105
+ }>): void;
106
+ }
107
+
108
+ declare class GivenStubs {
109
+ private readonly adminClient;
110
+ constructor(adminClient: AdminClient);
111
+ get chatCompletion(): ChatCompletionStubBuilder;
112
+ get embedding(): EmbeddingStubBuilder;
113
+ get models(): ModelsStubBuilder;
114
+ }
115
+
116
+ declare class MockLLM {
117
+ private state;
118
+ private startPromise;
119
+ private containerManager;
120
+ private adminClient;
121
+ private _given;
122
+ private _baseUrl;
123
+ constructor(options?: MockLLMOptions);
124
+ start(): Promise<void>;
125
+ private doStart;
126
+ stop(): Promise<void>;
127
+ get baseUrl(): string;
128
+ get apiBaseUrl(): string;
129
+ get given(): GivenStubs;
130
+ clear(): Promise<void>;
131
+ [Symbol.asyncDispose](): Promise<void>;
132
+ private assertRunning;
133
+ }
134
+
135
+ declare abstract class MockLLMError extends Error {
136
+ abstract readonly code: string;
137
+ constructor(message: string, options?: {
138
+ cause?: unknown;
139
+ });
140
+ }
141
+
142
+ declare class ContainerNotStartedError extends MockLLMError {
143
+ readonly code: "CONTAINER_NOT_STARTED";
144
+ constructor(options?: {
145
+ cause?: unknown;
146
+ });
147
+ }
148
+
149
+ declare class StubConfigurationError extends MockLLMError {
150
+ readonly code: "STUB_CONFIGURATION_INVALID";
151
+ constructor(message: string, options?: {
152
+ cause?: unknown;
153
+ });
154
+ }
155
+
156
+ interface ChatMessage {
157
+ role: 'system' | 'user' | 'assistant';
158
+ content: string | null;
159
+ }
160
+ interface ChatCompletionRequest {
161
+ model: string;
162
+ messages: ChatMessage[];
163
+ stream?: boolean;
164
+ temperature?: number;
165
+ max_tokens?: number;
166
+ stream_options?: {
167
+ include_usage?: boolean;
168
+ };
169
+ }
170
+ interface ChatCompletionChoice {
171
+ index: number;
172
+ message: ChatMessage;
173
+ finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | null;
174
+ logprobs: null;
175
+ }
176
+ interface UsageInfo {
177
+ prompt_tokens: number;
178
+ completion_tokens: number;
179
+ total_tokens: number;
180
+ }
181
+ interface ChatCompletionResponse {
182
+ id: string;
183
+ object: 'chat.completion';
184
+ created: number;
185
+ model: string;
186
+ system_fingerprint: string;
187
+ choices: ChatCompletionChoice[];
188
+ usage: UsageInfo;
189
+ }
190
+ interface ChatCompletionChunkDelta {
191
+ role?: 'assistant';
192
+ content?: string;
193
+ }
194
+ interface ChatCompletionChunkChoice {
195
+ index: number;
196
+ delta: ChatCompletionChunkDelta;
197
+ finish_reason: 'stop' | null;
198
+ logprobs: null;
199
+ }
200
+ interface ChatCompletionChunk {
201
+ id: string;
202
+ object: 'chat.completion.chunk';
203
+ created: number;
204
+ model: string;
205
+ system_fingerprint: string;
206
+ choices: ChatCompletionChunkChoice[];
207
+ usage?: UsageInfo | null;
208
+ }
209
+ interface EmbeddingRequest {
210
+ model: string;
211
+ input: string | string[];
212
+ encoding_format?: 'float' | 'base64';
213
+ dimensions?: number;
214
+ }
215
+ interface EmbeddingData {
216
+ object: 'embedding';
217
+ index: number;
218
+ embedding: number[];
219
+ }
220
+ interface EmbeddingResponse {
221
+ object: 'list';
222
+ data: EmbeddingData[];
223
+ model: string;
224
+ usage: {
225
+ prompt_tokens: number;
226
+ total_tokens: number;
227
+ };
228
+ }
229
+
230
+ export { type ChatCompletionChunk, type ChatCompletionRequest, type ChatCompletionResponse, ContainerNotStartedError, type EmbeddingRequest, type EmbeddingResponse, MockLLM, MockLLMError, type MockLLMOptions, StubConfigurationError };
package/dist/index.js ADDED
@@ -0,0 +1,327 @@
1
+ import { GenericContainer, Wait } from 'testcontainers';
2
+
3
+ // src/driver/container.config.ts
4
+ var DEFAULT_IMAGE = "phantomllm-server:latest";
5
+ var DEFAULT_PORT = 8080;
6
+ function resolveContainerConfig(options) {
7
+ return {
8
+ image: options?.image ?? process.env["PHANTOMLLM_IMAGE"] ?? DEFAULT_IMAGE,
9
+ containerPort: options?.containerPort ?? DEFAULT_PORT,
10
+ reuse: options?.reuse ?? true,
11
+ startupTimeout: options?.startupTimeout ?? 3e4
12
+ };
13
+ }
14
+ var ContainerManager = class {
15
+ constructor(config) {
16
+ this.config = config;
17
+ }
18
+ container = null;
19
+ async start() {
20
+ const container = new GenericContainer(this.config.image).withExposedPorts(this.config.containerPort).withWaitStrategy(
21
+ Wait.forHttp("/_admin/health", this.config.containerPort).forStatusCode(200)
22
+ ).withStartupTimeout(this.config.startupTimeout).withLabels({ "com.phantomllm": "true" });
23
+ this.container = await container.start();
24
+ return {
25
+ host: this.container.getHost(),
26
+ port: this.container.getMappedPort(this.config.containerPort)
27
+ };
28
+ }
29
+ async stop() {
30
+ if (this.container) {
31
+ await this.container.stop();
32
+ this.container = null;
33
+ }
34
+ }
35
+ };
36
+
37
+ // src/errors/base.ts
38
+ var MockLLMError = class extends Error {
39
+ constructor(message, options) {
40
+ super(message, options);
41
+ this.name = this.constructor.name;
42
+ }
43
+ };
44
+
45
+ // src/errors/admin.errors.ts
46
+ var AdminAPIError = class extends MockLLMError {
47
+ constructor(statusCode, responseBody, options) {
48
+ super(
49
+ `Admin API returned HTTP ${statusCode}: ${responseBody}`,
50
+ options
51
+ );
52
+ this.statusCode = statusCode;
53
+ this.responseBody = responseBody;
54
+ }
55
+ code = "ADMIN_API_ERROR";
56
+ };
57
+ var AdminConnectionRefusedError = class extends MockLLMError {
58
+ code = "ADMIN_CONNECTION_REFUSED";
59
+ constructor(options) {
60
+ super(
61
+ "Cannot connect to MockLLM admin API. The container may have crashed.",
62
+ options
63
+ );
64
+ }
65
+ };
66
+ var AdminTimeoutError = class extends MockLLMError {
67
+ constructor(timeoutMs, options) {
68
+ super(
69
+ `Admin API did not respond within ${timeoutMs}ms.`,
70
+ options
71
+ );
72
+ this.timeoutMs = timeoutMs;
73
+ }
74
+ code = "ADMIN_TIMEOUT";
75
+ };
76
+
77
+ // src/driver/admin.client.ts
78
+ var REQUEST_TIMEOUT_MS = 5e3;
79
+ var AdminClient = class {
80
+ constructor(baseUrl) {
81
+ this.baseUrl = baseUrl;
82
+ }
83
+ pendingStubs = [];
84
+ enqueueStub(stub) {
85
+ this.pendingStubs.push(stub);
86
+ }
87
+ async flush() {
88
+ if (this.pendingStubs.length === 0) return;
89
+ const stubs = this.pendingStubs.splice(0);
90
+ await this.post("/_admin/stubs/batch", { stubs });
91
+ }
92
+ async clearStubs() {
93
+ await this.flush();
94
+ await this.delete("/_admin/stubs");
95
+ }
96
+ async getHealth() {
97
+ return this.get("/_admin/health");
98
+ }
99
+ async getRequests() {
100
+ await this.flush();
101
+ const data = await this.get("/_admin/requests");
102
+ return data.requests;
103
+ }
104
+ async get(path) {
105
+ const response = await this.fetch(path, { method: "GET" });
106
+ return await response.json();
107
+ }
108
+ async post(path, body) {
109
+ const response = await this.fetch(path, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify(body)
113
+ });
114
+ return await response.json();
115
+ }
116
+ async delete(path) {
117
+ await this.fetch(path, { method: "DELETE" });
118
+ }
119
+ async fetch(path, init) {
120
+ const url = `${this.baseUrl}${path}`;
121
+ let response;
122
+ try {
123
+ response = await fetch(url, {
124
+ ...init,
125
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
126
+ });
127
+ } catch (error) {
128
+ if (error instanceof TypeError) {
129
+ const cause = error.cause;
130
+ if (cause?.code === "ECONNREFUSED") {
131
+ throw new AdminConnectionRefusedError({ cause: error });
132
+ }
133
+ }
134
+ if (error instanceof DOMException && error.name === "TimeoutError") {
135
+ throw new AdminTimeoutError(REQUEST_TIMEOUT_MS, { cause: error });
136
+ }
137
+ throw error;
138
+ }
139
+ if (!response.ok) {
140
+ const body = await response.text();
141
+ throw new AdminAPIError(response.status, body);
142
+ }
143
+ return response;
144
+ }
145
+ };
146
+
147
+ // src/stubs/chat.builder.ts
148
+ var ChatCompletionStubBuilder = class {
149
+ constructor(adminClient) {
150
+ this.adminClient = adminClient;
151
+ }
152
+ matcher = { endpoint: "chat" };
153
+ forModel(model) {
154
+ this.matcher.model = model;
155
+ return this;
156
+ }
157
+ withMessageContaining(substring) {
158
+ this.matcher.content = substring;
159
+ return this;
160
+ }
161
+ willReturn(content) {
162
+ this.adminClient.enqueueStub({
163
+ matcher: this.matcher,
164
+ response: { type: "chat", body: content }
165
+ });
166
+ }
167
+ willStream(chunks) {
168
+ this.adminClient.enqueueStub({
169
+ matcher: this.matcher,
170
+ response: { type: "streaming-chat", chunks }
171
+ });
172
+ }
173
+ willError(statusCode, message) {
174
+ this.adminClient.enqueueStub({
175
+ matcher: this.matcher,
176
+ response: {
177
+ type: "error",
178
+ status: statusCode,
179
+ error: { message, type: "api_error", code: null }
180
+ }
181
+ });
182
+ }
183
+ };
184
+
185
+ // src/stubs/embedding.builder.ts
186
+ var EmbeddingStubBuilder = class {
187
+ constructor(adminClient) {
188
+ this.adminClient = adminClient;
189
+ }
190
+ matcher = { endpoint: "embeddings" };
191
+ forModel(model) {
192
+ this.matcher.model = model;
193
+ return this;
194
+ }
195
+ willReturn(vector) {
196
+ const vectors = Array.isArray(vector[0]) ? vector : [vector];
197
+ this.adminClient.enqueueStub({
198
+ matcher: this.matcher,
199
+ response: { type: "embedding", vectors }
200
+ });
201
+ }
202
+ willError(statusCode, message) {
203
+ this.adminClient.enqueueStub({
204
+ matcher: this.matcher,
205
+ response: {
206
+ type: "error",
207
+ status: statusCode,
208
+ error: { message, type: "api_error", code: null }
209
+ }
210
+ });
211
+ }
212
+ };
213
+
214
+ // src/stubs/models.builder.ts
215
+ var ModelsStubBuilder = class {
216
+ constructor(adminClient) {
217
+ this.adminClient = adminClient;
218
+ }
219
+ willReturn(models) {
220
+ this.adminClient.enqueueStub({
221
+ matcher: {},
222
+ response: { type: "models", models }
223
+ });
224
+ }
225
+ };
226
+
227
+ // src/stubs/given.ts
228
+ var GivenStubs = class {
229
+ constructor(adminClient) {
230
+ this.adminClient = adminClient;
231
+ }
232
+ get chatCompletion() {
233
+ return new ChatCompletionStubBuilder(this.adminClient);
234
+ }
235
+ get embedding() {
236
+ return new EmbeddingStubBuilder(this.adminClient);
237
+ }
238
+ get models() {
239
+ return new ModelsStubBuilder(this.adminClient);
240
+ }
241
+ };
242
+
243
+ // src/errors/lifecycle.errors.ts
244
+ var ContainerNotStartedError = class extends MockLLMError {
245
+ code = "CONTAINER_NOT_STARTED";
246
+ constructor(options) {
247
+ super(
248
+ "MockLLM container is not running. Call `await mock.start()` before configuring stubs.",
249
+ options
250
+ );
251
+ }
252
+ };
253
+
254
+ // src/driver/mock-llm.ts
255
+ var MockLLM = class {
256
+ state = "idle";
257
+ startPromise = null;
258
+ containerManager;
259
+ adminClient = null;
260
+ _given = null;
261
+ _baseUrl = null;
262
+ constructor(options) {
263
+ const config = resolveContainerConfig(options);
264
+ this.containerManager = new ContainerManager(config);
265
+ }
266
+ async start() {
267
+ if (this.state === "running") return;
268
+ if (this.state === "starting" && this.startPromise) return this.startPromise;
269
+ this.state = "starting";
270
+ this.startPromise = this.doStart();
271
+ return this.startPromise;
272
+ }
273
+ async doStart() {
274
+ const { host, port } = await this.containerManager.start();
275
+ this._baseUrl = `http://${host}:${port}`;
276
+ this.adminClient = new AdminClient(this._baseUrl);
277
+ this._given = new GivenStubs(this.adminClient);
278
+ this.state = "running";
279
+ }
280
+ async stop() {
281
+ if (this.state === "stopped" || this.state === "idle") return;
282
+ this.state = "stopping";
283
+ try {
284
+ await this.containerManager.stop();
285
+ } finally {
286
+ this.state = "stopped";
287
+ this.adminClient = null;
288
+ this._given = null;
289
+ this._baseUrl = null;
290
+ }
291
+ }
292
+ get baseUrl() {
293
+ this.assertRunning();
294
+ return this._baseUrl;
295
+ }
296
+ get apiBaseUrl() {
297
+ return `${this.baseUrl}/v1`;
298
+ }
299
+ get given() {
300
+ this.assertRunning();
301
+ return this._given;
302
+ }
303
+ async clear() {
304
+ this.assertRunning();
305
+ await this.adminClient.clearStubs();
306
+ }
307
+ async [Symbol.asyncDispose]() {
308
+ await this.stop();
309
+ }
310
+ assertRunning() {
311
+ if (this.state !== "running") {
312
+ throw new ContainerNotStartedError();
313
+ }
314
+ }
315
+ };
316
+
317
+ // src/errors/stub.errors.ts
318
+ var StubConfigurationError = class extends MockLLMError {
319
+ code = "STUB_CONFIGURATION_INVALID";
320
+ constructor(message, options) {
321
+ super(message, options);
322
+ }
323
+ };
324
+
325
+ export { ContainerNotStartedError, MockLLM, MockLLMError, StubConfigurationError };
326
+ //# sourceMappingURL=index.js.map
327
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "phantomllm",
3
+ "version": "0.1.0",
4
+ "description": "Dockerized mock server for OpenAI-compatible APIs. Test your LLM integrations with a real HTTP server.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": {
10
+ "types": "./dist/index.d.mts",
11
+ "default": "./dist/index.mjs"
12
+ },
13
+ "require": {
14
+ "types": "./dist/index.d.cts",
15
+ "default": "./dist/index.cjs"
16
+ }
17
+ }
18
+ },
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.cts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "build:server": "tsc --project tsconfig.server.json",
31
+ "typecheck": "tsc --noEmit",
32
+ "test": "vitest run",
33
+ "test:unit": "vitest run --project unit",
34
+ "test:integration": "vitest run --project integration",
35
+ "test:sdk": "vitest run --project sdk",
36
+ "test:bench": "vitest run --project bench",
37
+ "docker:build": "npm run build:server && docker build -t phantomllm-server:latest .",
38
+ "prepublishOnly": "npm run build",
39
+ "clean": "rm -rf dist"
40
+ },
41
+ "dependencies": {
42
+ "testcontainers": "^11.13.0"
43
+ },
44
+ "devDependencies": {
45
+ "@ai-sdk/openai": "^3.0.47",
46
+ "@types/node": "^22.15.0",
47
+ "ai": "^6.0.134",
48
+ "fastify": "^5.8.2",
49
+ "fastify-plugin": "^5.1.0",
50
+ "openai": "^6.32.0",
51
+ "tsup": "^8.5.1",
52
+ "typescript": "^5.9.3",
53
+ "vitest": "^4.1.0"
54
+ },
55
+ "keywords": [
56
+ "openai",
57
+ "mock",
58
+ "testing",
59
+ "testcontainers",
60
+ "docker",
61
+ "llm",
62
+ "ai-sdk"
63
+ ]
64
+ }