llm-mock-server 1.0.6 → 1.0.7

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.
Files changed (250) hide show
  1. package/dist/cli/cli.d.ts +3 -0
  2. package/dist/cli/cli.d.ts.map +1 -0
  3. package/dist/cli/cli.js +103 -0
  4. package/dist/cli/cli.js.map +1 -0
  5. package/dist/cli/validators.d.ts +7 -0
  6. package/dist/cli/validators.d.ts.map +1 -0
  7. package/dist/cli/validators.js +53 -0
  8. package/dist/cli/validators.js.map +1 -0
  9. package/dist/formats/anthropic/index.d.ts +1 -1
  10. package/dist/formats/anthropic/index.d.ts.map +1 -1
  11. package/dist/formats/anthropic/index.js +1 -1
  12. package/dist/formats/anthropic/index.js.map +1 -1
  13. package/dist/formats/anthropic/parse.d.ts +2 -2
  14. package/dist/formats/anthropic/parse.d.ts.map +1 -1
  15. package/dist/formats/anthropic/parse.js +4 -2
  16. package/dist/formats/anthropic/parse.js.map +1 -1
  17. package/dist/formats/anthropic/schema.d.ts +1 -1
  18. package/dist/formats/anthropic/schema.d.ts.map +1 -1
  19. package/dist/formats/anthropic/schema.js +9 -4
  20. package/dist/formats/anthropic/schema.js.map +1 -1
  21. package/dist/formats/anthropic/serialize.d.ts +2 -2
  22. package/dist/formats/anthropic/serialize.d.ts.map +1 -1
  23. package/dist/formats/anthropic/serialize.js +76 -19
  24. package/dist/formats/anthropic/serialize.js.map +1 -1
  25. package/dist/formats/openai/chat-completions/index.d.ts +3 -0
  26. package/dist/formats/openai/chat-completions/index.d.ts.map +1 -0
  27. package/dist/formats/openai/chat-completions/index.js +13 -0
  28. package/dist/formats/openai/chat-completions/index.js.map +1 -0
  29. package/dist/formats/openai/chat-completions/parse.d.ts +4 -0
  30. package/dist/formats/openai/chat-completions/parse.d.ts.map +1 -0
  31. package/dist/formats/openai/chat-completions/parse.js +33 -0
  32. package/dist/formats/openai/chat-completions/parse.js.map +1 -0
  33. package/dist/formats/openai/chat-completions/schema.d.ts +93 -0
  34. package/dist/formats/openai/chat-completions/schema.d.ts.map +1 -0
  35. package/dist/formats/openai/chat-completions/schema.js +74 -0
  36. package/dist/formats/openai/chat-completions/schema.js.map +1 -0
  37. package/dist/formats/openai/chat-completions/serialize.d.ts +10 -0
  38. package/dist/formats/openai/chat-completions/serialize.d.ts.map +1 -0
  39. package/dist/formats/openai/chat-completions/serialize.js +99 -0
  40. package/dist/formats/openai/chat-completions/serialize.js.map +1 -0
  41. package/dist/formats/openai/responses/index.d.ts +3 -0
  42. package/dist/formats/openai/responses/index.d.ts.map +1 -0
  43. package/dist/formats/openai/responses/index.js +13 -0
  44. package/dist/formats/openai/responses/index.js.map +1 -0
  45. package/dist/formats/openai/responses/parse.d.ts +4 -0
  46. package/dist/formats/openai/responses/parse.d.ts.map +1 -0
  47. package/dist/formats/openai/responses/parse.js +51 -0
  48. package/dist/formats/openai/responses/parse.js.map +1 -0
  49. package/dist/formats/openai/responses/schema.d.ts +103 -0
  50. package/dist/formats/openai/responses/schema.d.ts.map +1 -0
  51. package/dist/formats/openai/responses/schema.js +71 -0
  52. package/dist/formats/openai/responses/schema.js.map +1 -0
  53. package/dist/formats/openai/responses/serialize.d.ts +10 -0
  54. package/dist/formats/openai/responses/serialize.d.ts.map +1 -0
  55. package/dist/formats/openai/responses/serialize.js +273 -0
  56. package/dist/formats/openai/responses/serialize.js.map +1 -0
  57. package/dist/formats/request-helpers.d.ts +1 -1
  58. package/dist/formats/request-helpers.d.ts.map +1 -1
  59. package/dist/formats/request-helpers.js.map +1 -1
  60. package/dist/formats/serialize-helpers.d.ts +1 -1
  61. package/dist/formats/serialize-helpers.d.ts.map +1 -1
  62. package/dist/formats/serialize-helpers.js +6 -3
  63. package/dist/formats/serialize-helpers.js.map +1 -1
  64. package/dist/formats/types.d.ts +2 -1
  65. package/dist/formats/types.d.ts.map +1 -1
  66. package/dist/history.d.ts +6 -2
  67. package/dist/history.d.ts.map +1 -1
  68. package/dist/history.js +2 -0
  69. package/dist/history.js.map +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/loader.d.ts +1 -1
  73. package/dist/loader.d.ts.map +1 -1
  74. package/dist/loader.js +26 -9
  75. package/dist/loader.js.map +1 -1
  76. package/dist/logger.d.ts.map +1 -1
  77. package/dist/logger.js +12 -4
  78. package/dist/logger.js.map +1 -1
  79. package/dist/mock-server.d.ts +44 -48
  80. package/dist/mock-server.d.ts.map +1 -1
  81. package/dist/mock-server.js +37 -85
  82. package/dist/mock-server.js.map +1 -1
  83. package/dist/route-handler.d.ts +1 -1
  84. package/dist/route-handler.d.ts.map +1 -1
  85. package/dist/route-handler.js +19 -7
  86. package/dist/route-handler.js.map +1 -1
  87. package/dist/rule-builder.d.ts +21 -0
  88. package/dist/rule-builder.d.ts.map +1 -0
  89. package/dist/rule-builder.js +58 -0
  90. package/dist/rule-builder.js.map +1 -0
  91. package/dist/rule-engine.d.ts +3 -1
  92. package/dist/rule-engine.d.ts.map +1 -1
  93. package/dist/rule-engine.js +7 -2
  94. package/dist/rule-engine.js.map +1 -1
  95. package/dist/sse-writer.d.ts +1 -1
  96. package/dist/sse-writer.d.ts.map +1 -1
  97. package/dist/types/reply.d.ts +51 -8
  98. package/dist/types/reply.d.ts.map +1 -1
  99. package/dist/types/request.d.ts +21 -6
  100. package/dist/types/request.d.ts.map +1 -1
  101. package/dist/types/rule.d.ts +65 -7
  102. package/dist/types/rule.d.ts.map +1 -1
  103. package/dist/types.d.ts +3 -3
  104. package/dist/types.d.ts.map +1 -1
  105. package/package.json +15 -9
  106. package/.claude/skills/desloppify/SKILL.md +0 -308
  107. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +0 -242
  108. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +0 -248
  109. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +0 -248
  110. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +0 -311
  111. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +0 -17
  112. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +0 -255
  113. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +0 -22
  114. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +0 -20
  115. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +0 -20
  116. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050000.json +0 -286
  117. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050028.json +0 -303
  118. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/claude_launch_prompt.md +0 -17
  119. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.json +0 -297
  120. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.template.json +0 -22
  121. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/reviewer_instructions.md +0 -20
  122. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/session.json +0 -20
  123. package/.desloppify/query.json +0 -1312
  124. package/.desloppify/review_packet_blind.json +0 -1249
  125. package/.desloppify/review_packets/holistic_packet_20260315_000339.json +0 -1471
  126. package/.desloppify/review_packets/holistic_packet_20260315_045546.json +0 -1480
  127. package/.desloppify/review_packets/holistic_packet_20260315_185401.json +0 -1407
  128. package/.desloppify/review_packets/holistic_packet_20260315_185613.json +0 -1407
  129. package/.desloppify/state-typescript.json +0 -8438
  130. package/.desloppify/state-typescript.json.bak +0 -8432
  131. package/.desloppify/subagents/runs/20260315_185401/logs/batch-1.log +0 -384
  132. package/.desloppify/subagents/runs/20260315_185401/logs/batch-10.log +0 -484
  133. package/.desloppify/subagents/runs/20260315_185401/logs/batch-2.log +0 -408
  134. package/.desloppify/subagents/runs/20260315_185401/logs/batch-3.log +0 -416
  135. package/.desloppify/subagents/runs/20260315_185401/logs/batch-4.log +0 -360
  136. package/.desloppify/subagents/runs/20260315_185401/logs/batch-5.log +0 -360
  137. package/.desloppify/subagents/runs/20260315_185401/logs/batch-6.log +0 -364
  138. package/.desloppify/subagents/runs/20260315_185401/logs/batch-7.log +0 -428
  139. package/.desloppify/subagents/runs/20260315_185401/logs/batch-8.log +0 -388
  140. package/.desloppify/subagents/runs/20260315_185401/logs/batch-9.log +0 -500
  141. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-1.md +0 -83
  142. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-10.md +0 -108
  143. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-2.md +0 -89
  144. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-3.md +0 -91
  145. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-4.md +0 -77
  146. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-5.md +0 -77
  147. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-6.md +0 -78
  148. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-7.md +0 -94
  149. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-8.md +0 -84
  150. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-9.md +0 -112
  151. package/.desloppify/subagents/runs/20260315_185401/results/batch-1.raw.txt +0 -0
  152. package/.desloppify/subagents/runs/20260315_185401/results/batch-10.raw.txt +0 -0
  153. package/.desloppify/subagents/runs/20260315_185401/results/batch-2.raw.txt +0 -0
  154. package/.desloppify/subagents/runs/20260315_185401/results/batch-3.raw.txt +0 -0
  155. package/.desloppify/subagents/runs/20260315_185401/results/batch-4.raw.txt +0 -0
  156. package/.desloppify/subagents/runs/20260315_185401/results/batch-5.raw.txt +0 -0
  157. package/.desloppify/subagents/runs/20260315_185401/results/batch-6.raw.txt +0 -0
  158. package/.desloppify/subagents/runs/20260315_185401/results/batch-7.raw.txt +0 -0
  159. package/.desloppify/subagents/runs/20260315_185401/results/batch-8.raw.txt +0 -0
  160. package/.desloppify/subagents/runs/20260315_185401/results/batch-9.raw.txt +0 -0
  161. package/.desloppify/subagents/runs/20260315_185401/run.log +0 -36
  162. package/.desloppify/subagents/runs/20260315_185401/run_summary.json +0 -156
  163. package/.desloppify/subagents/runs/20260315_185613/holistic_findings_merged.json +0 -741
  164. package/.desloppify/subagents/runs/20260315_185613/logs/batch-1.log +0 -579
  165. package/.desloppify/subagents/runs/20260315_185613/logs/batch-10.log +0 -1537
  166. package/.desloppify/subagents/runs/20260315_185613/logs/batch-2.log +0 -829
  167. package/.desloppify/subagents/runs/20260315_185613/logs/batch-3.log +0 -927
  168. package/.desloppify/subagents/runs/20260315_185613/logs/batch-4.log +0 -429
  169. package/.desloppify/subagents/runs/20260315_185613/logs/batch-5.log +0 -276
  170. package/.desloppify/subagents/runs/20260315_185613/logs/batch-6.log +0 -450
  171. package/.desloppify/subagents/runs/20260315_185613/logs/batch-7.log +0 -730
  172. package/.desloppify/subagents/runs/20260315_185613/logs/batch-8.log +0 -698
  173. package/.desloppify/subagents/runs/20260315_185613/logs/batch-9.log +0 -938
  174. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-1.md +0 -83
  175. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-10.md +0 -108
  176. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-2.md +0 -89
  177. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-3.md +0 -91
  178. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-4.md +0 -77
  179. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-5.md +0 -77
  180. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-6.md +0 -78
  181. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-7.md +0 -94
  182. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-8.md +0 -84
  183. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-9.md +0 -112
  184. package/.desloppify/subagents/runs/20260315_185613/results/batch-1.raw.txt +0 -78
  185. package/.desloppify/subagents/runs/20260315_185613/results/batch-10.raw.txt +0 -242
  186. package/.desloppify/subagents/runs/20260315_185613/results/batch-2.raw.txt +0 -102
  187. package/.desloppify/subagents/runs/20260315_185613/results/batch-3.raw.txt +0 -94
  188. package/.desloppify/subagents/runs/20260315_185613/results/batch-4.raw.txt +0 -86
  189. package/.desloppify/subagents/runs/20260315_185613/results/batch-5.raw.txt +0 -1
  190. package/.desloppify/subagents/runs/20260315_185613/results/batch-6.raw.txt +0 -87
  191. package/.desloppify/subagents/runs/20260315_185613/results/batch-7.raw.txt +0 -1
  192. package/.desloppify/subagents/runs/20260315_185613/results/batch-8.raw.txt +0 -107
  193. package/.desloppify/subagents/runs/20260315_185613/results/batch-9.raw.txt +0 -67
  194. package/.desloppify/subagents/runs/20260315_185613/run.log +0 -96
  195. package/.desloppify/subagents/runs/20260315_185613/run_summary.json +0 -156
  196. package/.editorconfig +0 -12
  197. package/.github/dependabot.yml +0 -11
  198. package/.github/workflows/docs.yml +0 -46
  199. package/.github/workflows/test.yml +0 -40
  200. package/.markdownlint.jsonc +0 -11
  201. package/.node-version +0 -1
  202. package/.oxfmtrc.json +0 -9
  203. package/.oxlintrc.json +0 -35
  204. package/docs/ARCHITECTURE.md +0 -125
  205. package/scorecard.png +0 -0
  206. package/src/cli/cli.ts +0 -141
  207. package/src/cli/validators.ts +0 -68
  208. package/src/formats/anthropic/index.ts +0 -14
  209. package/src/formats/anthropic/parse.ts +0 -70
  210. package/src/formats/anthropic/schema.ts +0 -74
  211. package/src/formats/anthropic/serialize.ts +0 -179
  212. package/src/formats/openai/chat-completions/index.ts +0 -14
  213. package/src/formats/openai/chat-completions/parse.ts +0 -47
  214. package/src/formats/openai/chat-completions/schema.ts +0 -92
  215. package/src/formats/openai/chat-completions/serialize.ts +0 -146
  216. package/src/formats/openai/responses/index.ts +0 -14
  217. package/src/formats/openai/responses/parse.ts +0 -73
  218. package/src/formats/openai/responses/schema.ts +0 -86
  219. package/src/formats/openai/responses/serialize.ts +0 -328
  220. package/src/formats/request-helpers.ts +0 -56
  221. package/src/formats/serialize-helpers.ts +0 -43
  222. package/src/formats/types.ts +0 -26
  223. package/src/history.ts +0 -70
  224. package/src/index.ts +0 -46
  225. package/src/loader.ts +0 -246
  226. package/src/logger.ts +0 -70
  227. package/src/mock-server.ts +0 -203
  228. package/src/route-handler.ts +0 -144
  229. package/src/rule-builder.ts +0 -73
  230. package/src/rule-engine.ts +0 -165
  231. package/src/sse-writer.ts +0 -35
  232. package/src/types/reply.ts +0 -92
  233. package/src/types/request.ts +0 -56
  234. package/src/types/rule.ts +0 -125
  235. package/src/types.ts +0 -24
  236. package/test/cli-validators.test.ts +0 -151
  237. package/test/formats/anthropic.test.ts +0 -336
  238. package/test/formats/openai.test.ts +0 -316
  239. package/test/formats/parse-helpers.test.ts +0 -315
  240. package/test/formats/responses.test.ts +0 -380
  241. package/test/helpers/make-req.ts +0 -18
  242. package/test/history.test.ts +0 -361
  243. package/test/loader.test.ts +0 -333
  244. package/test/logger.test.ts +0 -344
  245. package/test/mock-server.test.ts +0 -619
  246. package/test/rule-engine.test.ts +0 -229
  247. package/tsconfig.json +0 -24
  248. package/tsconfig.test.json +0 -11
  249. package/typedoc.json +0 -9
  250. package/vitest.config.ts +0 -18
@@ -1,43 +0,0 @@
1
- import type { ReplyObject } from "#/types/reply.js";
2
-
3
- export const MS_PER_SECOND = 1000;
4
- export const DEFAULT_USAGE = { input: 10, output: 5 } as const;
5
-
6
- export function splitText(text: string, chunkSize: number): string[] {
7
- if (chunkSize <= 0 || text.length <= chunkSize) return [text];
8
- const chunks: string[] = [];
9
- for (let i = 0; i < text.length; i += chunkSize) {
10
- chunks.push(text.slice(i, i + chunkSize));
11
- }
12
- return chunks;
13
- }
14
-
15
- const ID_SUFFIX_LENGTH = 12;
16
-
17
- function randomSuffix(): string {
18
- return crypto.randomUUID().replaceAll("-", "").slice(0, ID_SUFFIX_LENGTH);
19
- }
20
-
21
- export function genId(prefix: string): string {
22
- return `${prefix}_${randomSuffix()}`;
23
- }
24
-
25
- export function toolId(
26
- tool: { id?: string | undefined },
27
- prefix: string,
28
- index: number,
29
- ): string {
30
- return tool.id ?? `${prefix}_${randomSuffix()}_${index}`;
31
- }
32
-
33
- export function shouldEmitText(reply: ReplyObject): boolean {
34
- return Boolean(reply.text) || (!reply.tools?.length && !reply.reasoning);
35
- }
36
-
37
- export function finishReason(
38
- reply: ReplyObject,
39
- onTools: string,
40
- onStop: string,
41
- ): string {
42
- return reply.tools?.length ? onTools : onStop;
43
- }
@@ -1,26 +0,0 @@
1
- import type { FormatName, MockRequest } from "#/types/request.js";
2
- import type { ReplyObject, ReplyOptions } from "#/types/reply.js";
3
- import type { RequestMeta } from "./request-helpers.js";
4
-
5
- export interface SSEChunk {
6
- readonly event?: string | undefined;
7
- readonly data: string;
8
- }
9
-
10
- export interface Format {
11
- readonly name: FormatName;
12
- readonly route: string;
13
- parseRequest(body: unknown, meta?: RequestMeta): MockRequest;
14
- isStreaming(body: unknown): boolean;
15
- serialize(
16
- reply: ReplyObject,
17
- model: string,
18
- options?: ReplyOptions,
19
- ): readonly SSEChunk[];
20
- serializeComplete(reply: ReplyObject, model: string): Record<string, unknown>;
21
- serializeError(error: {
22
- status: number;
23
- message: string;
24
- type?: string | undefined;
25
- }): Record<string, unknown>;
26
- }
package/src/history.ts DELETED
@@ -1,70 +0,0 @@
1
- import type { MockRequest } from "./types/request.js";
2
-
3
- /** A recorded request with the rule that matched and when it happened. */
4
- export interface RecordedRequest {
5
- /** The normalised request that was received. */
6
- readonly request: MockRequest;
7
- /** Description of the rule that matched, or `undefined` if the fallback was used. */
8
- readonly rule: string | undefined;
9
- /** When the request was recorded (`Date.now()` value). */
10
- readonly timestamp: number;
11
- }
12
-
13
- /**
14
- * Records every request the server handles.
15
- * Iterable and has fluent query methods for test assertions.
16
- *
17
- * @example
18
- * ```ts
19
- * expect(server.history.count()).toBe(3);
20
- * expect(server.history.last()?.request.lastMessage).toBe("hello");
21
- * const matched = server.history.where(r => r.rule !== undefined);
22
- * ```
23
- */
24
- export class RequestHistory {
25
- private readonly entries: RecordedRequest[] = [];
26
-
27
- record(request: MockRequest, rule: string | undefined): void {
28
- this.entries.push({ request, rule, timestamp: Date.now() });
29
- }
30
-
31
- /** Number of recorded requests. */
32
- count(): number {
33
- return this.entries.length;
34
- }
35
-
36
- /** First recorded request, or `undefined` if empty. */
37
- first(): RecordedRequest | undefined {
38
- return this.entries[0];
39
- }
40
-
41
- /** Most recent recorded request, or `undefined` if empty. */
42
- last(): RecordedRequest | undefined {
43
- return this.entries.at(-1);
44
- }
45
-
46
- /** Get the entry at a specific index. Supports negative indices. */
47
- at(index: number): RecordedRequest | undefined {
48
- return this.entries.at(index);
49
- }
50
-
51
- /** Filter entries by a predicate. */
52
- where(predicate: (entry: RecordedRequest) => boolean): RecordedRequest[] {
53
- return this.entries.filter(predicate);
54
- }
55
-
56
- /** All entries as a readonly array. */
57
- get all(): readonly RecordedRequest[] {
58
- return this.entries;
59
- }
60
-
61
- /** Remove all recorded entries. */
62
- clear(): void {
63
- this.entries.length = 0;
64
- }
65
-
66
- /** Enables `for...of` iteration over recorded entries. */
67
- [Symbol.iterator](): Iterator<RecordedRequest> {
68
- return this.entries[Symbol.iterator]();
69
- }
70
- }
package/src/index.ts DELETED
@@ -1,46 +0,0 @@
1
- export { MockServer } from "./mock-server.js";
2
- export type { MockServerOptions } from "./mock-server.js";
3
- export type { LogLevel } from "./logger.js";
4
- export { RequestHistory } from "./history.js";
5
- export type { RecordedRequest } from "./history.js";
6
- export type {
7
- FormatName,
8
- MockRequest,
9
- Message,
10
- ToolDef,
11
- Reply,
12
- ReplyObject,
13
- ToolCall,
14
- Resolver,
15
- Match,
16
- MatchObject,
17
- ReplyOptions,
18
- ErrorReply,
19
- PendingRule,
20
- RuleHandle,
21
- RuleSummary,
22
- SequenceEntry,
23
- Handler,
24
- } from "./types.js";
25
-
26
- import { MockServer } from "./mock-server.js";
27
- import type { MockServerOptions } from "./mock-server.js";
28
-
29
- /**
30
- * Create a server and start it in one go.
31
- *
32
- * @example
33
- * ```ts
34
- * const server = await createMock({ port: 0, logLevel: "info" });
35
- * server.when("hello").reply("Hi!");
36
- * // run your tests
37
- * await server.stop();
38
- * ```
39
- */
40
- export async function createMock(
41
- options: MockServerOptions = {},
42
- ): Promise<MockServer> {
43
- const server = new MockServer(options);
44
- await server.start(options.port ?? 0);
45
- return server;
46
- }
package/src/loader.ts DELETED
@@ -1,246 +0,0 @@
1
- import { readFile, readdir, stat } from "node:fs/promises";
2
- import { join, extname } from "node:path";
3
- import JSON5 from "json5";
4
- import { z } from "zod";
5
- import type { Reply } from "./types/reply.js";
6
- import type { Handler, Match, MatchObject } from "./types/rule.js";
7
- import { type RuleEngine, createSequenceResolver } from "./rule-engine.js";
8
-
9
- interface LoadContext {
10
- engine: RuleEngine;
11
- setFallback?: (reply: Reply) => void;
12
- }
13
-
14
- const json5MatchSchema = z.union([
15
- z.string(),
16
- z.object({
17
- message: z.string().optional(),
18
- model: z.string().optional(),
19
- system: z.string().optional(),
20
- format: z.enum(["openai", "anthropic", "responses"]).optional(),
21
- }),
22
- ]);
23
-
24
- const json5ReplySchema = z.union([
25
- z.string(),
26
- z.object({
27
- text: z.string().optional(),
28
- reasoning: z.string().optional(),
29
- tools: z
30
- .array(
31
- z.object({ name: z.string(), args: z.record(z.string(), z.unknown()) }),
32
- )
33
- .optional(),
34
- }),
35
- ]);
36
-
37
- const json5ReplyRef = z.union([json5ReplySchema, z.string().startsWith("$")]);
38
-
39
- const json5SequenceEntrySchema = z.union([
40
- json5ReplyRef,
41
- z.object({
42
- reply: json5ReplyRef,
43
- latency: z.int().nonnegative().optional(),
44
- chunkSize: z.int().nonnegative().optional(),
45
- }),
46
- ]);
47
-
48
- const json5RuleSchema = z.union([
49
- z.object({
50
- when: json5MatchSchema,
51
- reply: json5ReplyRef,
52
- times: z.int().positive().optional(),
53
- }),
54
- z.object({
55
- when: json5MatchSchema,
56
- replies: z.array(json5SequenceEntrySchema).min(1),
57
- }),
58
- ]);
59
-
60
- const json5FileSchema = z.union([
61
- z.array(json5RuleSchema),
62
- z.object({
63
- templates: z.record(z.string(), json5ReplySchema).optional(),
64
- fallback: json5ReplySchema.optional(),
65
- rules: z.array(json5RuleSchema),
66
- }),
67
- ]);
68
-
69
- function parseRegexString(s: string): RegExp | string {
70
- const match = /^\/(.+)\/([dgimsuyv]*)$/.exec(s);
71
- if (match) {
72
- return new RegExp(match[1]!, match[2]);
73
- }
74
- return s;
75
- }
76
-
77
- type Json5ReplyRef = z.infer<typeof json5ReplyRef>;
78
- type Templates = Record<string, z.infer<typeof json5ReplySchema>> | undefined;
79
-
80
- function compileMatch(when: z.infer<typeof json5MatchSchema>): Match {
81
- if (typeof when === "string") {
82
- return parseRegexString(when);
83
- }
84
- const obj: MatchObject = {
85
- ...(when.message !== undefined && {
86
- message: parseRegexString(when.message),
87
- }),
88
- ...(when.model !== undefined && { model: parseRegexString(when.model) }),
89
- ...(when.system !== undefined && { system: parseRegexString(when.system) }),
90
- ...(when.format !== undefined && { format: when.format }),
91
- };
92
- return obj;
93
- }
94
-
95
- function resolveReplyRef(
96
- ref: Json5ReplyRef,
97
- templates: Templates,
98
- filePath: string,
99
- ): z.infer<typeof json5ReplySchema> {
100
- if (typeof ref === "string" && ref.startsWith("$")) {
101
- const name = ref.slice(1);
102
- const resolved = templates?.[name];
103
- if (!resolved) throw new Error(`Unknown template "${name}" in ${filePath}`);
104
- return resolved;
105
- }
106
- return ref;
107
- }
108
-
109
- function addSequenceRule(
110
- engine: RuleEngine,
111
- match: Match,
112
- entries: z.infer<typeof json5SequenceEntrySchema>[],
113
- templates: Templates,
114
- filePath: string,
115
- ): void {
116
- const steps = entries.map((entry) => {
117
- if (typeof entry === "string" || !("reply" in entry)) {
118
- return { reply: resolveReplyRef(entry, templates, filePath) };
119
- }
120
- return {
121
- reply: resolveReplyRef(entry.reply, templates, filePath),
122
- options: {
123
- ...(entry.latency !== undefined && { latency: entry.latency }),
124
- ...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),
125
- },
126
- };
127
- });
128
- const rule = engine.add(match, "");
129
- const { resolver, entryCount } = createSequenceResolver(steps, rule);
130
- rule.resolve = resolver;
131
- rule.remaining = entryCount;
132
- }
133
-
134
- async function loadJson5File(
135
- filePath: string,
136
- ctx: LoadContext,
137
- ): Promise<void> {
138
- const content = await readFile(filePath, "utf-8");
139
- const parsed = json5FileSchema.parse(JSON5.parse(content));
140
-
141
- const rules = Array.isArray(parsed) ? parsed : parsed.rules;
142
- const templates = Array.isArray(parsed) ? undefined : parsed.templates;
143
-
144
- if (
145
- !Array.isArray(parsed) &&
146
- parsed.fallback !== undefined &&
147
- ctx.setFallback
148
- ) {
149
- ctx.setFallback(parsed.fallback);
150
- }
151
-
152
- for (const r of rules) {
153
- const match = compileMatch(r.when);
154
- if ("replies" in r) {
155
- addSequenceRule(ctx.engine, match, r.replies, templates, filePath);
156
- } else {
157
- const reply = resolveReplyRef(r.reply, templates, filePath);
158
- const rule = ctx.engine.add(match, reply);
159
- if (r.times !== undefined) {
160
- rule.remaining = r.times;
161
- }
162
- }
163
- }
164
- }
165
-
166
- const handlerSchema = z.custom<Handler>((val): val is Handler => {
167
- if (typeof val !== "object" || val === null) return false;
168
- const obj = val as Record<string, unknown>;
169
- return (
170
- typeof obj["match"] === "function" && typeof obj["respond"] === "function"
171
- );
172
- });
173
-
174
- const handlerExportSchema = z.object({
175
- default: z.union([handlerSchema, z.array(handlerSchema)]),
176
- fallback: json5ReplySchema.optional(),
177
- });
178
-
179
- async function loadHandlerFile(
180
- filePath: string,
181
- ctx: LoadContext,
182
- ): Promise<void> {
183
- const mod = await import(filePath);
184
- const parsed = handlerExportSchema.safeParse(mod);
185
- if (!parsed.success) {
186
- throw new Error(
187
- `Invalid handler file ${filePath}. Expected default export with { match: Function, respond: Function }.`,
188
- );
189
- }
190
- const handlers = Array.isArray(parsed.data.default)
191
- ? parsed.data.default
192
- : [parsed.data.default];
193
-
194
- if (parsed.data.fallback !== undefined && ctx.setFallback) {
195
- ctx.setFallback(parsed.data.fallback);
196
- }
197
-
198
- for (const handler of handlers) {
199
- ctx.engine.addHandler(
200
- handler.match,
201
- handler.respond,
202
- `(handler: ${filePath})`,
203
- );
204
- }
205
- }
206
-
207
- type FileLoader = (filePath: string, ctx: LoadContext) => Promise<void>;
208
-
209
- const loaderByExtension: ReadonlyMap<string, FileLoader> = new Map([
210
- [".json5", loadJson5File],
211
- [".json", loadJson5File],
212
- [".ts", loadHandlerFile],
213
- [".js", loadHandlerFile],
214
- [".mjs", loadHandlerFile],
215
- ]);
216
-
217
- export async function loadRulesFromPath(
218
- pathOrDir: string,
219
- ctx: LoadContext,
220
- ): Promise<void> {
221
- const info = await stat(pathOrDir);
222
-
223
- if (info.isFile()) {
224
- const ext = extname(pathOrDir);
225
- const loader = loaderByExtension.get(ext);
226
- if (!loader) {
227
- throw new Error(`Unsupported file extension "${ext}" for ${pathOrDir}`);
228
- }
229
- await loader(pathOrDir, ctx);
230
- return;
231
- }
232
-
233
- if (!info.isDirectory()) return;
234
-
235
- const entries = (await readdir(pathOrDir)).toSorted();
236
- for (const entry of entries) {
237
- const fullPath = join(pathOrDir, entry);
238
- const entryStat = await stat(fullPath);
239
- if (entryStat.isDirectory()) {
240
- await loadRulesFromPath(fullPath, ctx);
241
- } else if (entryStat.isFile()) {
242
- const loader = loaderByExtension.get(extname(fullPath));
243
- if (loader) await loader(fullPath, ctx);
244
- }
245
- }
246
- }
package/src/logger.ts DELETED
@@ -1,70 +0,0 @@
1
- import pc from "picocolors";
2
-
3
- export const LEVEL_PRIORITY = {
4
- none: 0,
5
- error: 1,
6
- warning: 2,
7
- info: 3,
8
- debug: 4,
9
- all: 5,
10
- } as const satisfies Record<string, number>;
11
-
12
- /** Log verbosity, from `"none"` (silent) through to `"all"` (everything). */
13
- export type LogLevel = keyof typeof LEVEL_PRIORITY;
14
-
15
- const LEVEL_STYLE = {
16
- error: { label: pc.red(pc.bold("ERROR")), symbol: pc.red("✗") },
17
- warn: { label: pc.yellow(pc.bold("WARN")), symbol: pc.yellow("!") },
18
- info: { label: pc.cyan("INFO"), symbol: pc.cyan("●") },
19
- debug: { label: pc.dim("DEBUG"), symbol: pc.dim("·") },
20
- } as const;
21
-
22
- type ConsoleMethod = "error" | "warn" | "log";
23
-
24
- const LEVEL_CONFIG: Record<
25
- keyof typeof LEVEL_STYLE,
26
- { priority: number; method: ConsoleMethod; dim?: boolean }
27
- > = {
28
- error: { priority: LEVEL_PRIORITY.error, method: "error" },
29
- warn: { priority: LEVEL_PRIORITY.warning, method: "warn" },
30
- info: { priority: LEVEL_PRIORITY.info, method: "log" },
31
- debug: { priority: LEVEL_PRIORITY.debug, method: "log", dim: true },
32
- };
33
-
34
- export class Logger {
35
- readonly level: LogLevel;
36
- private threshold: number;
37
-
38
- constructor(level: LogLevel = "info") {
39
- this.level = level;
40
- this.threshold = LEVEL_PRIORITY[level];
41
- }
42
-
43
- private log(
44
- key: keyof typeof LEVEL_STYLE,
45
- msg: string,
46
- args: unknown[],
47
- ): void {
48
- const config = LEVEL_CONFIG[key];
49
- if (this.threshold < config.priority) return;
50
- const { label, symbol } = LEVEL_STYLE[key];
51
- const text = config.dim ? pc.dim(msg) : msg;
52
- console[config.method](
53
- `${pc.dim(new Date().toISOString())} ${symbol} ${label} ${text}`,
54
- ...args,
55
- );
56
- }
57
-
58
- error(msg: string, ...args: unknown[]): void {
59
- this.log("error", msg, args);
60
- }
61
- warn(msg: string, ...args: unknown[]): void {
62
- this.log("warn", msg, args);
63
- }
64
- info(msg: string, ...args: unknown[]): void {
65
- this.log("info", msg, args);
66
- }
67
- debug(msg: string, ...args: unknown[]): void {
68
- this.log("debug", msg, args);
69
- }
70
- }
@@ -1,203 +0,0 @@
1
- import Fastify from "fastify";
2
- import type { FastifyInstance } from "fastify";
3
- import type { Reply, ReplyOptions } from "./types/reply.js";
4
- import type { RuleSummary } from "./types/rule.js";
5
- import { RuleEngine } from "./rule-engine.js";
6
- import { RuleBuilder } from "./rule-builder.js";
7
- import { RequestHistory } from "./history.js";
8
- import { chatCompletionsFormat } from "./formats/openai/chat-completions/index.js";
9
- import { anthropicFormat } from "./formats/anthropic/index.js";
10
- import { responsesFormat } from "./formats/openai/responses/index.js";
11
- import type { Format } from "./formats/types.js";
12
- import { Logger } from "./logger.js";
13
- import type { LogLevel } from "./logger.js";
14
- import { createRouteHandler } from "./route-handler.js";
15
-
16
- const formats: readonly Format[] = [
17
- chatCompletionsFormat,
18
- anthropicFormat,
19
- responsesFormat,
20
- ];
21
-
22
- /** Options for constructing a `MockServer` or calling `createMock()`. */
23
- export interface MockServerOptions {
24
- /**
25
- * Port to listen on. Pass `0` for a random port (useful in tests).
26
- * @defaultValue `0`
27
- */
28
- readonly port?: number;
29
- /**
30
- * Host to bind to. Set to `"0.0.0.0"` to listen on all interfaces.
31
- * @defaultValue `"127.0.0.1"`
32
- */
33
- readonly host?: string;
34
- /**
35
- * Log verbosity.
36
- * @defaultValue `"none"`
37
- */
38
- readonly logLevel?: LogLevel;
39
- /**
40
- * Default ms delay between SSE chunks. Individual rules can override this.
41
- * @defaultValue `0`
42
- */
43
- readonly defaultLatency?: number;
44
- /**
45
- * Default characters per SSE text chunk. Individual rules can override this.
46
- * @defaultValue `0`
47
- */
48
- readonly defaultChunkSize?: number;
49
- }
50
-
51
- type RuleAPI = Pick<
52
- RuleBuilder,
53
- "when" | "whenTool" | "whenToolResult" | "nextError"
54
- >;
55
-
56
- /**
57
- * Mock LLM server that handles OpenAI Chat Completions, Anthropic Messages, and OpenAI Responses API formats.
58
- * Register rules with `when()`, point your SDK at `url`, and go.
59
- *
60
- * Supports `await using` for automatic cleanup.
61
- *
62
- * @example
63
- * ```ts
64
- * const server = new MockServer({ logLevel: "info" });
65
- * server.when("hello").reply("Hi there!");
66
- * await server.start();
67
- * // Point your client at server.url
68
- * await server.stop();
69
- * ```
70
- */
71
- export class MockServer implements RuleAPI {
72
- private readonly app: FastifyInstance;
73
- private readonly engine = new RuleEngine();
74
- private readonly rules_ = new RuleBuilder(this.engine);
75
- private readonly history_ = new RequestHistory();
76
- private readonly logger: Logger;
77
- private readonly host: string;
78
- private readonly defaultOptions: ReplyOptions;
79
- private fallbackReply: Reply = "Mock server: no matching rule.";
80
- private listening = false;
81
-
82
- /** Register a matching rule. Call `.reply()` on the result to set the response. */
83
- when = this.rules_.when.bind(this.rules_);
84
- /** Shorthand for `when({ toolName })`. */
85
- whenTool = this.rules_.whenTool.bind(this.rules_);
86
- /** Shorthand for `when({ toolCallId })`. */
87
- whenToolResult = this.rules_.whenToolResult.bind(this.rules_);
88
- /** Queue a one-shot error for the very next request. Fires once then removes itself. */
89
- nextError = this.rules_.nextError.bind(this.rules_);
90
-
91
- constructor(options: MockServerOptions = {}) {
92
- this.host = options.host ?? "127.0.0.1";
93
- this.logger = new Logger(options.logLevel ?? "none");
94
- this.defaultOptions = {
95
- ...(options.defaultLatency !== undefined && {
96
- latency: options.defaultLatency,
97
- }),
98
- ...(options.defaultChunkSize !== undefined && {
99
- chunkSize: options.defaultChunkSize,
100
- }),
101
- };
102
- this.app = Fastify({ logger: false });
103
-
104
- const deps = {
105
- engine: this.engine,
106
- history: this.history_,
107
- logger: this.logger,
108
- defaultOptions: this.defaultOptions,
109
- getFallback: () => this.fallbackReply,
110
- };
111
-
112
- for (const format of formats) {
113
- this.app.post(format.route, createRouteHandler(format, deps));
114
- }
115
- }
116
-
117
- /**
118
- * Set the reply used when no rule matches.
119
- * @defaultValue `"Mock server: no matching rule."`
120
- */
121
- fallback(reply: Reply): void {
122
- this.fallbackReply = reply;
123
- }
124
-
125
- /** Load rules from a `.json5` file, a `.ts`/`.js` handler file, or a directory containing them. */
126
- async load(pathOrDir: string): Promise<void> {
127
- const before = this.engine.ruleCount;
128
- const { loadRulesFromPath } = await import("./loader.js");
129
- await loadRulesFromPath(pathOrDir, {
130
- engine: this.engine,
131
- setFallback: (reply) => {
132
- this.fallbackReply = reply;
133
- },
134
- });
135
- const loaded = this.engine.ruleCount - before;
136
- this.logger.info(
137
- `Loaded ${loaded} rule${loaded !== 1 ? "s" : ""} from ${pathOrDir}`,
138
- );
139
- }
140
-
141
- /** Every request the server has handled. */
142
- get history(): RequestHistory {
143
- return this.history_;
144
- }
145
-
146
- /** Returns `true` when all rules with a `.times()` limit have been consumed. */
147
- isDone(): boolean {
148
- return this.engine.isDone();
149
- }
150
-
151
- /** Clear all rules, request history, and reset the fallback to its default. */
152
- reset(): void {
153
- this.engine.clear();
154
- this.history_.clear();
155
- this.fallbackReply = "Mock server: no matching rule.";
156
- this.logger.info("Server reset: rules and history cleared");
157
- }
158
-
159
- /** The base URL the server is listening on, e.g. `http://127.0.0.1:12345`. Throws if the server hasn't started. */
160
- get url(): string {
161
- if (!this.listening)
162
- throw new Error("Server is not running. Call start() first.");
163
- const addr = this.app.server.address();
164
- const port = addr !== null && typeof addr === "object" ? addr.port : 0;
165
- return `http://${this.host}:${port}`;
166
- }
167
-
168
- /** The API routes registered on this server, e.g. `["/v1/chat/completions", ...]`. */
169
- get routes(): readonly string[] {
170
- return formats.map((f) => f.route);
171
- }
172
-
173
- /** Number of currently registered rules. */
174
- get ruleCount(): number {
175
- return this.engine.ruleCount;
176
- }
177
-
178
- /** A snapshot of all registered rules with their descriptions and remaining match counts. */
179
- get rules(): readonly RuleSummary[] {
180
- return this.engine.describe();
181
- }
182
-
183
- /** Start listening. Pass `0` (the default) for a random port. */
184
- async start(port = 0): Promise<void> {
185
- if (this.listening) throw new Error("Server is already running.");
186
- await this.app.listen({ port, host: this.host });
187
- this.listening = true;
188
- this.logger.info(`Listening on ${this.url}`);
189
- }
190
-
191
- /** Stop the server. Safe to call multiple times. */
192
- async stop(): Promise<void> {
193
- if (!this.listening) return;
194
- await this.app.close();
195
- this.listening = false;
196
- this.logger.info("Server stopped");
197
- }
198
-
199
- /** Calls `stop()`. Enables `await using server = ...` for automatic cleanup. */
200
- async [Symbol.asyncDispose](): Promise<void> {
201
- await this.stop();
202
- }
203
- }