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,144 +0,0 @@
1
- import type { FastifyReply, FastifyRequest } from "fastify";
2
- import { ZodError } from "zod";
3
- import type { MockRequest } from "./types/request.js";
4
- import type { Reply, ReplyObject, ReplyOptions } from "./types/reply.js";
5
- import type { Rule } from "./types/rule.js";
6
- import type { Format } from "./formats/types.js";
7
- import type { RuleEngine } from "./rule-engine.js";
8
- import type { RequestHistory } from "./history.js";
9
- import type { Logger } from "./logger.js";
10
- import { writeSSE } from "./sse-writer.js";
11
-
12
- const HTTP_BAD_REQUEST = 400;
13
-
14
- function normaliseReply(reply: Reply): ReplyObject {
15
- if (typeof reply === "string") return { text: reply };
16
- return reply;
17
- }
18
-
19
- async function resolveReply(
20
- matched: Rule | undefined,
21
- mockReq: MockRequest,
22
- fallback: Reply,
23
- logger: Logger,
24
- ): Promise<{ reply: ReplyObject; ruleDesc: string | undefined }> {
25
- if (!matched) {
26
- logger.warn(
27
- `No matching rule for "${mockReq.lastMessage}", using fallback`,
28
- );
29
- return { reply: normaliseReply(fallback), ruleDesc: undefined };
30
- }
31
-
32
- try {
33
- const raw =
34
- typeof matched.resolve === "function"
35
- ? await matched.resolve(mockReq)
36
- : matched.resolve;
37
- logger.debug(`Matched rule ${matched.description}`);
38
- return { reply: normaliseReply(raw), ruleDesc: matched.description };
39
- } catch (err) {
40
- logger.error(`Resolver threw for rule ${matched.description}`, err);
41
- return { reply: normaliseReply(fallback), ruleDesc: matched.description };
42
- }
43
- }
44
-
45
- interface RouteHandlerDeps {
46
- engine: RuleEngine;
47
- history: RequestHistory;
48
- logger: Logger;
49
- defaultOptions: ReplyOptions;
50
- getFallback: () => Reply;
51
- }
52
-
53
- export function createRouteHandler(
54
- format: Format,
55
- deps: RouteHandlerDeps,
56
- ): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
57
- const { engine, history, logger, defaultOptions, getFallback } = deps;
58
-
59
- return async (request: FastifyRequest, reply: FastifyReply) => {
60
- const body = request.body;
61
- const headers: Record<string, string | undefined> = {};
62
- for (const [key, val] of Object.entries(request.headers)) {
63
- headers[key] = Array.isArray(val) ? val.join(", ") : val;
64
- }
65
- const meta = { headers, path: request.url };
66
-
67
- let mockReq: MockRequest;
68
- try {
69
- mockReq = format.parseRequest(body, meta);
70
- } catch (err) {
71
- if (err instanceof ZodError) {
72
- logger.warn(
73
- `Invalid ${format.name} request: ${err.issues.map((i) => i.message).join(", ")}`,
74
- );
75
- return reply
76
- .status(HTTP_BAD_REQUEST)
77
- .type("application/json")
78
- .send(
79
- format.serializeError({
80
- status: HTTP_BAD_REQUEST,
81
- message: "Invalid request body",
82
- type: "invalid_request_error",
83
- }),
84
- );
85
- }
86
- throw err;
87
- }
88
- const startTime = Date.now();
89
-
90
- logger.debug(
91
- `${format.name} request: model=${mockReq.model} streaming=${mockReq.streaming} messages=${mockReq.messages.length}`,
92
- );
93
-
94
- const matched = engine.match(mockReq);
95
- const { reply: resolvedReply, ruleDesc } = await resolveReply(
96
- matched,
97
- mockReq,
98
- getFallback(),
99
- logger,
100
- );
101
-
102
- if (resolvedReply.error) {
103
- const { error } = resolvedReply;
104
- logger.info(`Error reply: ${String(error.status)} ${error.message}`);
105
- history.record(mockReq, ruleDesc);
106
- return reply
107
- .status(error.status)
108
- .type("application/json")
109
- .send(format.serializeError(error));
110
- }
111
-
112
- history.record(mockReq, ruleDesc);
113
-
114
- const isStreaming = format.isStreaming(body);
115
- const effectiveOptions = { ...defaultOptions, ...matched?.options };
116
- const elapsed = Date.now() - startTime;
117
- const mode = isStreaming ? "stream" : "json";
118
-
119
- logger.info(
120
- `POST ${format.route} [${mode}] "${mockReq.lastMessage}" -> ${ruleDesc ?? "fallback"} (${elapsed}ms)`,
121
- );
122
- if (resolvedReply.text) {
123
- logger.debug(`Reply text: "${resolvedReply.text}"`);
124
- }
125
- if (resolvedReply.tools?.length) {
126
- logger.debug(
127
- `Reply tool calls: ${resolvedReply.tools.map((t) => t.name).join(", ")}`,
128
- );
129
- }
130
-
131
- if (!isStreaming) {
132
- return reply
133
- .type("application/json")
134
- .send(format.serializeComplete(resolvedReply, mockReq.model));
135
- }
136
-
137
- const chunks = format.serialize(
138
- resolvedReply,
139
- mockReq.model,
140
- effectiveOptions,
141
- );
142
- await writeSSE(reply, chunks, effectiveOptions);
143
- };
144
- }
@@ -1,73 +0,0 @@
1
- import type {
2
- Reply,
3
- ReplyOptions,
4
- Resolver,
5
- SequenceEntry,
6
- } from "./types/reply.js";
7
- import type { Match, PendingRule, Rule, RuleHandle } from "./types/rule.js";
8
- import type { RuleEngine } from "./rule-engine.js";
9
- import { createSequenceResolver } from "./rule-engine.js";
10
-
11
- function makeHandle(engine: RuleEngine, rule: Rule): RuleHandle {
12
- return {
13
- times(n: number): RuleHandle {
14
- rule.remaining = n;
15
- return this;
16
- },
17
- first(): RuleHandle {
18
- engine.moveToFront(rule);
19
- return this;
20
- },
21
- };
22
- }
23
-
24
- /** Builds matching rules against the rule engine. Proxied onto `MockServer`. */
25
- export class RuleBuilder {
26
- constructor(private readonly engine: RuleEngine) {}
27
-
28
- /** Register a matching rule. Call `.reply()` on the result to set the response. */
29
- when(match: Match): PendingRule {
30
- const engine = this.engine;
31
- return {
32
- reply(response: Resolver, options?: ReplyOptions): RuleHandle {
33
- return makeHandle(engine, engine.add(match, response, options));
34
- },
35
- replySequence(entries: readonly SequenceEntry[]): RuleHandle {
36
- const steps = normaliseSequenceEntries(entries);
37
- const rule = engine.add(match, "");
38
- const { resolver, entryCount } = createSequenceResolver(steps, rule);
39
- rule.resolve = resolver;
40
- rule.remaining = entryCount;
41
- return makeHandle(engine, rule);
42
- },
43
- };
44
- }
45
-
46
- /** Shorthand for `when({ toolName })`. */
47
- whenTool(toolName: string): PendingRule {
48
- return this.when({ toolName });
49
- }
50
-
51
- /** Shorthand for `when({ toolCallId })`. */
52
- whenToolResult(toolCallId: string): PendingRule {
53
- return this.when({ toolCallId });
54
- }
55
-
56
- /** Queue a one-shot error for the very next request. Fires once then removes itself. */
57
- nextError(status: number, message: string, type?: string): RuleHandle {
58
- return this.when(() => true)
59
- .reply({ error: { status, message, type } })
60
- .times(1)
61
- .first();
62
- }
63
- }
64
-
65
- export function normaliseSequenceEntries(
66
- entries: readonly SequenceEntry[],
67
- ): { reply: Reply; options?: ReplyOptions | undefined }[] {
68
- return entries.map((entry) =>
69
- typeof entry === "string" || !("reply" in entry)
70
- ? { reply: entry as Reply }
71
- : { reply: entry.reply, options: entry.options },
72
- );
73
- }
@@ -1,165 +0,0 @@
1
- import type { MockRequest } from "./types/request.js";
2
- import type { Reply, ReplyOptions, Resolver } from "./types/reply.js";
3
- import type { Match, MatchObject, Rule, RuleSummary } from "./types/rule.js";
4
-
5
- function safeRegex(re: RegExp): RegExp {
6
- return re.global || re.sticky
7
- ? new RegExp(re.source, re.flags.replace(/[gy]/g, ""))
8
- : re;
9
- }
10
-
11
- function compilePattern(pattern: string | RegExp): (value: string) => boolean {
12
- if (typeof pattern === "string") {
13
- const lower = pattern.toLowerCase();
14
- return (value) => value.toLowerCase().includes(lower);
15
- }
16
- const re = safeRegex(pattern);
17
- return (value) => re.test(value);
18
- }
19
-
20
- function compileMatcher(match: Match): (req: MockRequest) => boolean {
21
- if (typeof match === "string") {
22
- const test = compilePattern(match);
23
- return (req) => test(req.lastMessage);
24
- }
25
- if (match instanceof RegExp) {
26
- const test = compilePattern(match);
27
- return (req) => test(req.lastMessage);
28
- }
29
- if (typeof match === "function") {
30
- return match;
31
- }
32
- const obj = match;
33
- const messageTest =
34
- obj.message !== undefined ? compilePattern(obj.message) : undefined;
35
- const modelTest =
36
- obj.model !== undefined ? compilePattern(obj.model) : undefined;
37
- const systemTest =
38
- obj.system !== undefined ? compilePattern(obj.system) : undefined;
39
- return (req) => {
40
- if (messageTest && !messageTest(req.lastMessage)) return false;
41
- if (modelTest && !modelTest(req.model)) return false;
42
- if (systemTest && !systemTest(req.systemMessage)) return false;
43
- if (obj.format !== undefined && req.format !== obj.format) return false;
44
- if (obj.toolName !== undefined && !req.toolNames.includes(obj.toolName))
45
- return false;
46
- if (obj.toolCallId !== undefined && req.lastToolCallId !== obj.toolCallId)
47
- return false;
48
- if (obj.predicate && !obj.predicate(req)) return false;
49
- return true;
50
- };
51
- }
52
-
53
- function describeMatch(match: Match): string {
54
- if (typeof match === "string") return `"${match}"`;
55
- if (match instanceof RegExp) return match.toString();
56
- if (typeof match === "function") return "(predicate)";
57
- const obj: MatchObject = match;
58
- const parts = Object.entries(obj)
59
- .filter(([, v]) => v !== undefined && typeof v !== "function")
60
- .map(([k, v]) => `${k}=${String(v)}`);
61
- return `{${parts.join(", ")}}`;
62
- }
63
-
64
- function createRule(
65
- match: Match,
66
- resolve: Resolver,
67
- options: ReplyOptions,
68
- description?: string,
69
- ): Rule {
70
- return {
71
- description: description ?? describeMatch(match),
72
- match: compileMatcher(match),
73
- resolve,
74
- options,
75
- remaining: Infinity,
76
- };
77
- }
78
-
79
- interface SequenceStep {
80
- readonly reply: Reply;
81
- readonly options?: ReplyOptions | undefined;
82
- }
83
-
84
- export function createSequenceResolver(
85
- steps: readonly SequenceStep[],
86
- rule: { options: ReplyOptions },
87
- ): { resolver: () => Reply; entryCount: number } {
88
- if (steps.length === 0)
89
- throw new Error("Sequence requires at least one entry.");
90
- let index = 0;
91
- const last = steps[steps.length - 1]!;
92
- return {
93
- resolver: () => {
94
- const step = steps[index++] ?? last;
95
- rule.options = step.options ?? {};
96
- return step.reply;
97
- },
98
- entryCount: steps.length,
99
- };
100
- }
101
-
102
- export class RuleEngine {
103
- private readonly rules: Rule[] = [];
104
-
105
- add(match: Match, resolve: Resolver, options: ReplyOptions = {}): Rule {
106
- const rule = createRule(match, resolve, options);
107
- this.rules.push(rule);
108
- return rule;
109
- }
110
-
111
- moveToFront(rule: Rule): void {
112
- const idx = this.rules.indexOf(rule);
113
- if (idx > 0) {
114
- this.rules.splice(idx, 1);
115
- this.rules.unshift(rule);
116
- }
117
- }
118
-
119
- addHandler(
120
- matchFn: (req: MockRequest) => boolean,
121
- respond: Resolver,
122
- description = "(handler)",
123
- ): Rule {
124
- const rule = createRule(matchFn, respond, {}, description);
125
- this.rules.push(rule);
126
- return rule;
127
- }
128
-
129
- match(req: MockRequest): Rule | undefined {
130
- for (let i = 0; i < this.rules.length; i++) {
131
- const rule = this.rules[i]!;
132
-
133
- if (rule.remaining <= 0) continue;
134
- if (!rule.match(req)) continue;
135
-
136
- rule.remaining--;
137
- if (rule.remaining <= 0) {
138
- this.rules.splice(i, 1);
139
- }
140
- return rule;
141
- }
142
- return undefined;
143
- }
144
-
145
- isDone(): boolean {
146
- return this.rules.every(
147
- (r) => !Number.isFinite(r.remaining) || r.remaining <= 0,
148
- );
149
- }
150
-
151
- get ruleCount(): number {
152
- return this.rules.length;
153
- }
154
-
155
- describe(): readonly RuleSummary[] {
156
- return this.rules.map((r) => ({
157
- description: r.description,
158
- remaining: r.remaining,
159
- }));
160
- }
161
-
162
- clear(): void {
163
- this.rules.length = 0;
164
- }
165
- }
package/src/sse-writer.ts DELETED
@@ -1,35 +0,0 @@
1
- import type { FastifyReply } from "fastify";
2
- import type { SSEChunk } from "./formats/types.js";
3
- import type { ReplyOptions } from "./types/reply.js";
4
-
5
- const HTTP_OK = 200;
6
-
7
- function sleep(ms: number): Promise<void> {
8
- return new Promise((resolve) => setTimeout(resolve, ms));
9
- }
10
-
11
- function formatSSEChunk(chunk: SSEChunk): string {
12
- const eventLine = chunk.event ? `event: ${chunk.event}\n` : "";
13
- return `${eventLine}data: ${chunk.data}\n\n`;
14
- }
15
-
16
- export async function writeSSE(
17
- reply: FastifyReply,
18
- chunks: readonly SSEChunk[],
19
- options: ReplyOptions = {},
20
- ): Promise<void> {
21
- const latency = options.latency ?? 0;
22
-
23
- reply.raw.writeHead(HTTP_OK, {
24
- "Content-Type": "text/event-stream",
25
- "Cache-Control": "no-cache",
26
- Connection: "keep-alive",
27
- });
28
-
29
- for (const chunk of chunks) {
30
- reply.raw.write(formatSSEChunk(chunk));
31
- if (latency > 0) await sleep(latency);
32
- }
33
-
34
- reply.raw.end();
35
- }
@@ -1,92 +0,0 @@
1
- import type { MockRequest } from "./request.js";
2
-
3
- /** A reply is either a plain string (turns into `{ text: "..." }`) or a full reply object. */
4
- export type Reply = string | ReplyObject;
5
-
6
- /**
7
- * A structured reply. Text, reasoning, tool calls, usage, and errors are all optional.
8
- *
9
- * @example
10
- * ```ts
11
- * server.when("hello").reply({ text: "Hi!", reasoning: "Simple greeting." });
12
- * server.when("weather").reply({
13
- * tools: [{ name: "get_weather", args: { city: "London" } }],
14
- * });
15
- * ```
16
- */
17
- export interface ReplyObject {
18
- /** Text content to send back. */
19
- readonly text?: string | undefined;
20
- /** Extended thinking or chain-of-thought. Works with Anthropic and Responses formats. */
21
- readonly reasoning?: string | undefined;
22
- /** Tool calls the model wants to make. */
23
- readonly tools?: readonly ToolCall[] | undefined;
24
- /**
25
- * Token counts to report.
26
- * @defaultValue `{ input: 10, output: 5 }`
27
- */
28
- readonly usage?:
29
- | { readonly input: number; readonly output: number }
30
- | undefined;
31
- /** When set, the server responds with this HTTP error instead of a normal reply. */
32
- readonly error?: ErrorReply | undefined;
33
- }
34
-
35
- /** An HTTP error response. The server returns this status code with a format-appropriate body. */
36
- export interface ErrorReply {
37
- /** HTTP status code, e.g. `429` or `500`. */
38
- readonly status: number;
39
- /** Error message in the response body. */
40
- readonly message: string;
41
- /**
42
- * Error type string in the response body.
43
- * @defaultValue Format-specific (e.g. `"server_error"` for OpenAI, `"api_error"` for Anthropic).
44
- */
45
- readonly type?: string | undefined;
46
- }
47
-
48
- /** A tool call in the mock response. */
49
- export interface ToolCall {
50
- /** Explicit ID for the call. Auto-generated if omitted. */
51
- readonly id?: string | undefined;
52
- /** Tool function name. */
53
- readonly name: string;
54
- /** Arguments to pass to the tool. */
55
- readonly args: Readonly<Record<string, unknown>>;
56
- }
57
-
58
- /**
59
- * A reply value or a function that produces one. Async functions are supported.
60
- *
61
- * @example
62
- * ```ts
63
- * server.when("echo").reply((req) => `You said: ${req.lastMessage}`);
64
- * server.when("slow").reply(async (req) => {
65
- * return { text: "Done thinking." };
66
- * });
67
- * ```
68
- */
69
- export type Resolver = Reply | ((req: MockRequest) => Reply | Promise<Reply>);
70
-
71
- /** Per-rule streaming options. Merged with server-level defaults, with per-rule values winning. */
72
- export interface ReplyOptions {
73
- /** Milliseconds to wait between SSE chunks. */
74
- readonly latency?: number | undefined;
75
- /** Split text into chunks of this many characters for more realistic streaming. */
76
- readonly chunkSize?: number | undefined;
77
- }
78
-
79
- /**
80
- * A single entry in a reply sequence. Can be a plain reply or a reply with per-step options.
81
- *
82
- * @example
83
- * ```ts
84
- * server.when("step").replySequence([
85
- * "Starting.",
86
- * { reply: "Done.", options: { latency: 100 } },
87
- * ]);
88
- * ```
89
- */
90
- export type SequenceEntry =
91
- | Reply
92
- | { readonly reply: Reply; readonly options?: ReplyOptions };
@@ -1,56 +0,0 @@
1
- /** The LLM API wire format that was detected for a request. */
2
- export type FormatName = "openai" | "anthropic" | "responses";
3
-
4
- /**
5
- * A normalised view of an incoming request, regardless of the original wire format.
6
- * This is what rule matchers and resolvers receive.
7
- */
8
- export interface MockRequest {
9
- /** Which API format route the request came in on. */
10
- readonly format: FormatName;
11
- /** The model string from the request, e.g. `"gpt-5.4"` or `"claude-sonnet-4-6"`. */
12
- readonly model: string;
13
- /**
14
- * Whether the client asked for SSE streaming (from the `stream` field).
15
- * @defaultValue `true`
16
- */
17
- readonly streaming: boolean;
18
- /** Full conversation, normalised from whatever format came in. */
19
- readonly messages: readonly Message[];
20
- /** The last user message's text. This is what most matchers check. */
21
- readonly lastMessage: string;
22
- /** System prompt text, or empty string if there wasn't one. */
23
- readonly systemMessage: string;
24
- /** Tool definitions from the request, if any were sent. */
25
- readonly tools?: readonly ToolDef[] | undefined;
26
- /** Tool names pulled out from `tools` for quick lookups via `whenTool()`. */
27
- readonly toolNames: readonly string[];
28
- /** Set when the last message was a tool result. Used by `whenToolResult()`. */
29
- readonly lastToolCallId: string | undefined;
30
- /** The raw request body, for anything we don't extract. */
31
- readonly raw: unknown;
32
- /** HTTP headers from the incoming request. */
33
- readonly headers: Readonly<Record<string, string | undefined>>;
34
- /** The URL path that was hit, e.g. `/v1/chat/completions`. */
35
- readonly path: string;
36
- }
37
-
38
- /** A single conversation message, normalised across all supported formats. */
39
- export interface Message {
40
- /** The role of the message sender. */
41
- readonly role: "system" | "user" | "assistant" | "tool";
42
- /** The text content of the message. */
43
- readonly content: string;
44
- /** Links the result back to its tool call. Only set on `"tool"` messages. */
45
- readonly toolCallId?: string | undefined;
46
- }
47
-
48
- /** A tool definition from the request's `tools` array, normalised across formats. */
49
- export interface ToolDef {
50
- /** The tool function name. */
51
- readonly name: string;
52
- /** A description of what the tool does. */
53
- readonly description?: string | undefined;
54
- /** JSON Schema for the tool's parameters, passed through as-is. */
55
- readonly parameters?: unknown;
56
- }
package/src/types/rule.ts DELETED
@@ -1,125 +0,0 @@
1
- import type { MockRequest, FormatName } from "./request.js";
2
- import type { Resolver, ReplyOptions, Reply, SequenceEntry } from "./reply.js";
3
-
4
- /**
5
- * Determines whether a rule matches an incoming request.
6
- *
7
- * A `string` does a case-insensitive substring match on the last user message.
8
- * A `RegExp` gets tested against the last user message.
9
- * A `MatchObject` checks multiple fields at once with AND logic.
10
- * A function receives the normalised request and returns a boolean.
11
- *
12
- * @example
13
- * ```ts
14
- * server.when("hello").reply("Hi!");
15
- * server.when(/explain (\w+)/i).reply("Here's an explanation.");
16
- * server.when({ model: /claude/, format: "anthropic" }).reply("Bonjour!");
17
- * server.when((req) => req.messages.length > 5).reply("Long conversation!");
18
- * ```
19
- */
20
- export type Match =
21
- | string
22
- | RegExp
23
- | MatchObject
24
- | ((req: MockRequest) => boolean);
25
-
26
- /**
27
- * A structured matcher. Every field you set must match for the rule to fire.
28
- *
29
- * @example
30
- * ```ts
31
- * server.when({
32
- * model: /gpt/,
33
- * format: "openai",
34
- * system: /translator/i,
35
- * predicate: (req) => req.messages.length > 2,
36
- * }).reply("Translated output.");
37
- * ```
38
- */
39
- export interface MatchObject {
40
- /** Substring or regex against the last user message. */
41
- readonly message?: string | RegExp;
42
- /** Substring or regex against the model name. */
43
- readonly model?: string | RegExp;
44
- /** Substring or regex against the system prompt. */
45
- readonly system?: string | RegExp;
46
- /** Only match requests from this API format. */
47
- readonly format?: FormatName;
48
- /** Match when the request includes a tool definition with this name. */
49
- readonly toolName?: string;
50
- /** Match when the last tool-result message has this `tool_call_id`. */
51
- readonly toolCallId?: string;
52
- /** Extra predicate that runs after all other fields pass. */
53
- readonly predicate?: (req: MockRequest) => boolean;
54
- }
55
-
56
- /**
57
- * Returned by `when()`. Call `.reply()` or `.replySequence()` on it to complete the rule.
58
- *
59
- * @example
60
- * ```ts
61
- * server.when("hello").reply("Hi!");
62
- * server.when("step").replySequence(["First.", "Second.", "Done."]);
63
- * ```
64
- */
65
- export interface PendingRule {
66
- /** Set the response for this rule. Accepts a static value, object, or resolver function. */
67
- reply(response: Resolver, options?: ReplyOptions): RuleHandle;
68
- /** Set a sequence of replies. Each match advances through the array. */
69
- replySequence(entries: readonly SequenceEntry[]): RuleHandle;
70
- }
71
-
72
- /**
73
- * A handle to a registered rule. All methods return `this` for chaining.
74
- *
75
- * @example
76
- * ```ts
77
- * server.when("hello").reply("Hi!").times(3);
78
- * server.when("urgent").reply("On it!").first();
79
- * ```
80
- */
81
- export interface RuleHandle {
82
- /** Auto-expire the rule after `n` matches. */
83
- times(n: number): RuleHandle;
84
- /** Move this rule to the front of the list so it matches first. */
85
- first(): RuleHandle;
86
- }
87
-
88
- /**
89
- * The shape of a handler file's default export.
90
- * You can export a single handler or an array of them.
91
- *
92
- * @example
93
- * ```ts
94
- * import type { Handler } from "llm-mock-server";
95
- * export default {
96
- * match: (req) => req.lastMessage.includes("echo"),
97
- * respond: (req) => `Echo: ${req.lastMessage}`,
98
- * } satisfies Handler;
99
- * ```
100
- */
101
- export interface Handler {
102
- /** Return `true` if this handler should respond to the request. */
103
- match: (req: MockRequest) => boolean;
104
- /** Produce the reply for a matched request. Can be async. */
105
- respond: (req: MockRequest) => Reply | Promise<Reply>;
106
- }
107
-
108
- /** A summary of a registered rule, for inspection via `server.rules`. */
109
- export interface RuleSummary {
110
- /** Human-readable description of what the rule matches. */
111
- readonly description: string;
112
- /**
113
- * How many matches are left.
114
- * @defaultValue `Infinity` (unlimited)
115
- */
116
- readonly remaining: number;
117
- }
118
-
119
- export interface Rule {
120
- readonly description: string;
121
- readonly match: (req: MockRequest) => boolean;
122
- resolve: Resolver;
123
- options: ReplyOptions;
124
- remaining: number;
125
- }