llm-mock-server 1.0.1 → 1.0.3

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 (129) hide show
  1. package/.claude/skills/desloppify/SKILL.md +308 -0
  2. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +242 -0
  3. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +248 -0
  4. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +248 -0
  5. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +311 -0
  6. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +17 -0
  7. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +255 -0
  8. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +22 -0
  9. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +20 -0
  10. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +20 -0
  11. package/.desloppify/query.json +284 -0
  12. package/.desloppify/review_packet_blind.json +1303 -0
  13. package/.desloppify/review_packets/holistic_packet_20260315_000339.json +1471 -0
  14. package/.desloppify/state-typescript.json +5114 -0
  15. package/.desloppify/state-typescript.json.bak +5108 -0
  16. package/.editorconfig +12 -0
  17. package/.github/workflows/test.yml +3 -0
  18. package/.oxfmtrc.json +9 -0
  19. package/dist/cli.js +5 -2
  20. package/dist/cli.js.map +1 -1
  21. package/dist/formats/anthropic/index.js +1 -1
  22. package/dist/formats/anthropic/index.js.map +1 -1
  23. package/dist/formats/anthropic/parse.d.ts +1 -1
  24. package/dist/formats/anthropic/parse.d.ts.map +1 -1
  25. package/dist/formats/anthropic/parse.js +1 -1
  26. package/dist/formats/anthropic/parse.js.map +1 -1
  27. package/dist/formats/anthropic/serialize.d.ts +2 -2
  28. package/dist/formats/anthropic/serialize.d.ts.map +1 -1
  29. package/dist/formats/anthropic/serialize.js +6 -3
  30. package/dist/formats/anthropic/serialize.js.map +1 -1
  31. package/dist/formats/openai/index.js +1 -1
  32. package/dist/formats/openai/index.js.map +1 -1
  33. package/dist/formats/openai/parse.d.ts +1 -1
  34. package/dist/formats/openai/parse.d.ts.map +1 -1
  35. package/dist/formats/openai/parse.js +1 -1
  36. package/dist/formats/openai/parse.js.map +1 -1
  37. package/dist/formats/openai/serialize.d.ts +2 -2
  38. package/dist/formats/openai/serialize.d.ts.map +1 -1
  39. package/dist/formats/openai/serialize.js +12 -15
  40. package/dist/formats/openai/serialize.js.map +1 -1
  41. package/dist/formats/request-helpers.d.ts +13 -0
  42. package/dist/formats/request-helpers.d.ts.map +1 -0
  43. package/dist/formats/request-helpers.js +28 -0
  44. package/dist/formats/request-helpers.js.map +1 -0
  45. package/dist/formats/responses/index.js +1 -1
  46. package/dist/formats/responses/index.js.map +1 -1
  47. package/dist/formats/responses/parse.d.ts +1 -1
  48. package/dist/formats/responses/parse.d.ts.map +1 -1
  49. package/dist/formats/responses/parse.js +1 -1
  50. package/dist/formats/responses/parse.js.map +1 -1
  51. package/dist/formats/responses/schema.d.ts +1 -20
  52. package/dist/formats/responses/schema.d.ts.map +1 -1
  53. package/dist/formats/responses/schema.js.map +1 -1
  54. package/dist/formats/responses/serialize.d.ts +2 -2
  55. package/dist/formats/responses/serialize.d.ts.map +1 -1
  56. package/dist/formats/responses/serialize.js +6 -3
  57. package/dist/formats/responses/serialize.js.map +1 -1
  58. package/dist/formats/serialize-helpers.d.ts +14 -0
  59. package/dist/formats/serialize-helpers.d.ts.map +1 -0
  60. package/dist/formats/serialize-helpers.js +25 -0
  61. package/dist/formats/serialize-helpers.js.map +1 -0
  62. package/dist/formats/types.d.ts +3 -3
  63. package/dist/formats/types.d.ts.map +1 -1
  64. package/dist/loader.d.ts +3 -2
  65. package/dist/loader.d.ts.map +1 -1
  66. package/dist/loader.js +6 -9
  67. package/dist/loader.js.map +1 -1
  68. package/dist/logger.d.ts +1 -0
  69. package/dist/logger.d.ts.map +1 -1
  70. package/dist/logger.js +17 -23
  71. package/dist/logger.js.map +1 -1
  72. package/dist/mock-server.d.ts.map +1 -1
  73. package/dist/mock-server.js +8 -15
  74. package/dist/mock-server.js.map +1 -1
  75. package/dist/route-handler.d.ts +2 -1
  76. package/dist/route-handler.d.ts.map +1 -1
  77. package/dist/rule-engine.d.ts +12 -1
  78. package/dist/rule-engine.d.ts.map +1 -1
  79. package/dist/rule-engine.js +14 -0
  80. package/dist/rule-engine.js.map +1 -1
  81. package/dist/types/reply.d.ts +6 -10
  82. package/dist/types/reply.d.ts.map +1 -1
  83. package/dist/types/request.d.ts +7 -11
  84. package/dist/types/request.d.ts.map +1 -1
  85. package/dist/types/rule.d.ts +3 -10
  86. package/dist/types/rule.d.ts.map +1 -1
  87. package/dist/types.d.ts +3 -1
  88. package/dist/types.d.ts.map +1 -1
  89. package/package.json +5 -2
  90. package/scorecard.png +0 -0
  91. package/src/cli-validators.ts +12 -4
  92. package/src/cli.ts +27 -7
  93. package/src/formats/anthropic/index.ts +1 -1
  94. package/src/formats/anthropic/parse.ts +25 -6
  95. package/src/formats/anthropic/schema.ts +16 -8
  96. package/src/formats/anthropic/serialize.ts +116 -28
  97. package/src/formats/openai/index.ts +1 -1
  98. package/src/formats/openai/parse.ts +13 -3
  99. package/src/formats/openai/schema.ts +43 -30
  100. package/src/formats/openai/serialize.ts +84 -30
  101. package/src/formats/{parse-helpers.ts → request-helpers.ts} +4 -32
  102. package/src/formats/responses/index.ts +1 -1
  103. package/src/formats/responses/parse.ts +18 -4
  104. package/src/formats/responses/schema.ts +34 -22
  105. package/src/formats/responses/serialize.ts +237 -38
  106. package/src/formats/serialize-helpers.ts +38 -0
  107. package/src/formats/types.ts +18 -5
  108. package/src/index.ts +3 -1
  109. package/src/loader.ts +43 -20
  110. package/src/logger.ts +31 -19
  111. package/src/mock-server.ts +38 -21
  112. package/src/route-handler.ts +50 -15
  113. package/src/rule-engine.ts +64 -11
  114. package/src/types/reply.ts +12 -12
  115. package/src/types/request.ts +7 -11
  116. package/src/types/rule.ts +3 -10
  117. package/src/types.ts +23 -4
  118. package/test/cli-validators.test.ts +16 -4
  119. package/test/formats/anthropic.test.ts +84 -23
  120. package/test/formats/openai.test.ts +85 -24
  121. package/test/formats/parse-helpers.test.ts +315 -0
  122. package/test/formats/responses.test.ts +99 -34
  123. package/test/helpers/make-req.ts +18 -0
  124. package/test/history.test.ts +361 -0
  125. package/test/loader.test.ts +44 -45
  126. package/test/logger.test.ts +344 -0
  127. package/test/mock-server.test.ts +77 -23
  128. package/test/rule-engine.test.ts +57 -41
  129. package/src/types/index.ts +0 -4
@@ -1,8 +1,26 @@
1
1
  import type { ReplyObject, ReplyOptions, ToolCall } from "../../types.js";
2
2
  import type { SSEChunk } from "../types.js";
3
- import { splitText, genId, toolId, shouldEmitText, MS_PER_SECOND, DEFAULT_USAGE } from "../parse-helpers.js";
3
+ import {
4
+ splitText,
5
+ genId,
6
+ toolId,
7
+ shouldEmitText,
8
+ MS_PER_SECOND,
9
+ DEFAULT_USAGE,
10
+ } from "../serialize-helpers.js";
4
11
 
5
- interface StreamBlock { chunks: SSEChunk[]; outputItem: unknown }
12
+ function buildUsage(usage: { input: number; output: number }) {
13
+ return {
14
+ input_tokens: usage.input,
15
+ output_tokens: usage.output,
16
+ total_tokens: usage.input + usage.output,
17
+ };
18
+ }
19
+
20
+ interface StreamBlock {
21
+ chunks: SSEChunk[];
22
+ outputItem: unknown;
23
+ }
6
24
 
7
25
  const NO_ANNOTATIONS: readonly unknown[] = [];
8
26
 
@@ -10,43 +28,140 @@ type Chunk = (payload: Record<string, unknown>) => SSEChunk;
10
28
 
11
29
  function createChunk(): Chunk {
12
30
  let seq = 0;
13
- return (payload) => ({ data: JSON.stringify({ ...payload, sequence_number: seq++ }) });
31
+ return (payload) => ({
32
+ data: JSON.stringify({ ...payload, sequence_number: seq++ }),
33
+ });
14
34
  }
15
35
 
16
- function reasoningStreamBlock(c: Chunk, i: number, reasoning: string): StreamBlock {
36
+ function reasoningStreamBlock(
37
+ c: Chunk,
38
+ i: number,
39
+ reasoning: string,
40
+ ): StreamBlock {
17
41
  const itemId = `rs_${genId("rs")}`;
18
42
  const summaryPart = { type: "summary_text" as const, text: reasoning };
19
- const item = { type: "reasoning", id: itemId, status: "completed", summary: [summaryPart] };
43
+ const item = {
44
+ type: "reasoning",
45
+ id: itemId,
46
+ status: "completed",
47
+ summary: [summaryPart],
48
+ };
20
49
 
21
50
  return {
22
51
  outputItem: item,
23
52
  chunks: [
24
- c({ type: "response.output_item.added", output_index: i, item: { type: "reasoning", id: itemId, status: "in_progress", summary: [] } }),
25
- c({ type: "response.reasoning_summary_part.added", item_id: itemId, output_index: i, summary_index: 0, part: { type: "summary_text", text: "" } }),
26
- c({ type: "response.reasoning_summary_text.delta", item_id: itemId, output_index: i, summary_index: 0, delta: reasoning }),
27
- c({ type: "response.reasoning_summary_text.done", item_id: itemId, output_index: i, summary_index: 0, text: reasoning }),
28
- c({ type: "response.reasoning_summary_part.done", item_id: itemId, output_index: i, summary_index: 0, part: summaryPart }),
53
+ c({
54
+ type: "response.output_item.added",
55
+ output_index: i,
56
+ item: {
57
+ type: "reasoning",
58
+ id: itemId,
59
+ status: "in_progress",
60
+ summary: [],
61
+ },
62
+ }),
63
+ c({
64
+ type: "response.reasoning_summary_part.added",
65
+ item_id: itemId,
66
+ output_index: i,
67
+ summary_index: 0,
68
+ part: { type: "summary_text", text: "" },
69
+ }),
70
+ c({
71
+ type: "response.reasoning_summary_text.delta",
72
+ item_id: itemId,
73
+ output_index: i,
74
+ summary_index: 0,
75
+ delta: reasoning,
76
+ }),
77
+ c({
78
+ type: "response.reasoning_summary_text.done",
79
+ item_id: itemId,
80
+ output_index: i,
81
+ summary_index: 0,
82
+ text: reasoning,
83
+ }),
84
+ c({
85
+ type: "response.reasoning_summary_part.done",
86
+ item_id: itemId,
87
+ output_index: i,
88
+ summary_index: 0,
89
+ part: summaryPart,
90
+ }),
29
91
  c({ type: "response.output_item.done", output_index: i, item }),
30
92
  ],
31
93
  };
32
94
  }
33
95
 
34
- function textStreamBlock(c: Chunk, i: number, text: string, chunkSize: number): StreamBlock {
96
+ function textStreamBlock(
97
+ c: Chunk,
98
+ i: number,
99
+ text: string,
100
+ chunkSize: number,
101
+ ): StreamBlock {
35
102
  const itemId = `msg_${genId("msg")}`;
36
- const outputText = { type: "output_text" as const, text, annotations: NO_ANNOTATIONS };
37
- const outputItem = { type: "message", id: itemId, status: "completed", role: "assistant", content: [outputText] };
103
+ const outputText = {
104
+ type: "output_text" as const,
105
+ text,
106
+ annotations: NO_ANNOTATIONS,
107
+ };
108
+ const outputItem = {
109
+ type: "message",
110
+ id: itemId,
111
+ status: "completed",
112
+ role: "assistant",
113
+ content: [outputText],
114
+ };
38
115
 
39
116
  return {
40
117
  outputItem,
41
118
  chunks: [
42
- c({ type: "response.output_item.added", output_index: i, item: { type: "message", id: itemId, status: "in_progress", role: "assistant", content: [] } }),
43
- c({ type: "response.content_part.added", item_id: itemId, output_index: i, content_index: 0, part: { type: "output_text", text: "", annotations: [] } }),
119
+ c({
120
+ type: "response.output_item.added",
121
+ output_index: i,
122
+ item: {
123
+ type: "message",
124
+ id: itemId,
125
+ status: "in_progress",
126
+ role: "assistant",
127
+ content: [],
128
+ },
129
+ }),
130
+ c({
131
+ type: "response.content_part.added",
132
+ item_id: itemId,
133
+ output_index: i,
134
+ content_index: 0,
135
+ part: { type: "output_text", text: "", annotations: [] },
136
+ }),
44
137
  ...splitText(text, chunkSize).map((piece) =>
45
- c({ type: "response.output_text.delta", item_id: itemId, output_index: i, content_index: 0, delta: piece }),
138
+ c({
139
+ type: "response.output_text.delta",
140
+ item_id: itemId,
141
+ output_index: i,
142
+ content_index: 0,
143
+ delta: piece,
144
+ }),
46
145
  ),
47
- c({ type: "response.output_text.done", item_id: itemId, output_index: i, content_index: 0, text }),
48
- c({ type: "response.content_part.done", item_id: itemId, output_index: i, content_index: 0, part: outputText }),
49
- c({ type: "response.output_item.done", output_index: i, item: outputItem }),
146
+ c({
147
+ type: "response.output_text.done",
148
+ item_id: itemId,
149
+ output_index: i,
150
+ content_index: 0,
151
+ text,
152
+ }),
153
+ c({
154
+ type: "response.content_part.done",
155
+ item_id: itemId,
156
+ output_index: i,
157
+ content_index: 0,
158
+ part: outputText,
159
+ }),
160
+ c({
161
+ type: "response.output_item.done",
162
+ output_index: i,
163
+ item: outputItem,
164
+ }),
50
165
  ],
51
166
  };
52
167
  }
@@ -54,20 +169,49 @@ function textStreamBlock(c: Chunk, i: number, text: string, chunkSize: number):
54
169
  function toolStreamBlock(c: Chunk, i: number, tool: ToolCall): StreamBlock {
55
170
  const callId = toolId(tool, "call", i);
56
171
  const argsJson = JSON.stringify(tool.args);
57
- const outputItem = { type: "function_call", id: callId, status: "completed", name: tool.name, call_id: callId, arguments: argsJson };
172
+ const outputItem = {
173
+ type: "function_call",
174
+ id: callId,
175
+ status: "completed",
176
+ name: tool.name,
177
+ call_id: callId,
178
+ arguments: argsJson,
179
+ };
58
180
 
59
181
  return {
60
182
  outputItem,
61
183
  chunks: [
62
- c({ type: "response.output_item.added", output_index: i, item: { ...outputItem, status: "in_progress", arguments: "" } }),
63
- c({ type: "response.function_call_arguments.delta", item_id: callId, output_index: i, delta: argsJson }),
64
- c({ type: "response.function_call_arguments.done", item_id: callId, output_index: i, arguments: argsJson }),
65
- c({ type: "response.output_item.done", output_index: i, item: outputItem }),
184
+ c({
185
+ type: "response.output_item.added",
186
+ output_index: i,
187
+ item: { ...outputItem, status: "in_progress", arguments: "" },
188
+ }),
189
+ c({
190
+ type: "response.function_call_arguments.delta",
191
+ item_id: callId,
192
+ output_index: i,
193
+ delta: argsJson,
194
+ }),
195
+ c({
196
+ type: "response.function_call_arguments.done",
197
+ item_id: callId,
198
+ output_index: i,
199
+ arguments: argsJson,
200
+ }),
201
+ c({
202
+ type: "response.output_item.done",
203
+ output_index: i,
204
+ item: outputItem,
205
+ }),
66
206
  ],
67
207
  };
68
208
  }
69
209
 
70
- export function serialize(reply: ReplyObject, model: string, options: ReplyOptions = {}): readonly SSEChunk[] {
210
+ export function serialize(
211
+ reply: ReplyObject,
212
+ model: string,
213
+ options: ReplyOptions = {},
214
+ ): readonly SSEChunk[] {
71
215
  const id = genId("resp");
72
216
  const createdAt = Math.floor(Date.now() / MS_PER_SECOND);
73
217
  const usage = reply.usage ?? DEFAULT_USAGE;
@@ -76,13 +220,21 @@ export function serialize(reply: ReplyObject, model: string, options: ReplyOptio
76
220
 
77
221
  const baseResponse = { id, object: "response", created_at: createdAt, model };
78
222
  const header = [
79
- c({ type: "response.created", response: { ...baseResponse, status: "in_progress", output: [] } }),
80
- c({ type: "response.in_progress", response: { ...baseResponse, status: "in_progress", output: [] } }),
223
+ c({
224
+ type: "response.created",
225
+ response: { ...baseResponse, status: "in_progress", output: [] },
226
+ }),
227
+ c({
228
+ type: "response.in_progress",
229
+ response: { ...baseResponse, status: "in_progress", output: [] },
230
+ }),
81
231
  ];
82
232
 
83
233
  const blocks: StreamBlock[] = [
84
234
  ...(reply.reasoning ? [reasoningStreamBlock(c, i++, reply.reasoning)] : []),
85
- ...(shouldEmitText(reply) ? [textStreamBlock(c, i++, reply.text ?? "", options.chunkSize ?? 0)] : []),
235
+ ...(shouldEmitText(reply)
236
+ ? [textStreamBlock(c, i++, reply.text ?? "", options.chunkSize ?? 0)]
237
+ : []),
86
238
  ...(reply.tools ?? []).map((tool) => toolStreamBlock(c, i++, tool)),
87
239
  ];
88
240
 
@@ -94,36 +246,83 @@ export function serialize(reply: ReplyObject, model: string, options: ReplyOptio
94
246
  ...allChunks,
95
247
  c({
96
248
  type: "response.completed",
97
- response: { ...baseResponse, status: "completed", output,
98
- usage: { input_tokens: usage.input, output_tokens: usage.output, total_tokens: usage.input + usage.output } },
249
+ response: {
250
+ ...baseResponse,
251
+ status: "completed",
252
+ output,
253
+ usage: buildUsage(usage),
254
+ },
99
255
  }),
100
256
  ];
101
257
  }
102
258
 
103
- export function serializeComplete(reply: ReplyObject, model: string): unknown {
259
+ export function serializeComplete(
260
+ reply: ReplyObject,
261
+ model: string,
262
+ ): Record<string, unknown> {
104
263
  const id = genId("resp");
105
264
  const createdAt = Math.floor(Date.now() / MS_PER_SECOND);
106
265
  const usage = reply.usage ?? DEFAULT_USAGE;
107
266
 
108
267
  const output: unknown[] = [
109
268
  ...(reply.reasoning
110
- ? [{ type: "reasoning", id: `rs_${genId("rs")}`, status: "completed", summary: [{ type: "summary_text", text: reply.reasoning }] }]
269
+ ? [
270
+ {
271
+ type: "reasoning",
272
+ id: `rs_${genId("rs")}`,
273
+ status: "completed",
274
+ summary: [{ type: "summary_text", text: reply.reasoning }],
275
+ },
276
+ ]
111
277
  : []),
112
278
  ...(shouldEmitText(reply)
113
- ? [{ type: "message", id: `msg_${genId("msg")}`, status: "completed", role: "assistant", content: [{ type: "output_text", text: reply.text ?? "", annotations: [] }] }]
279
+ ? [
280
+ {
281
+ type: "message",
282
+ id: `msg_${genId("msg")}`,
283
+ status: "completed",
284
+ role: "assistant",
285
+ content: [
286
+ { type: "output_text", text: reply.text ?? "", annotations: [] },
287
+ ],
288
+ },
289
+ ]
114
290
  : []),
115
291
  ...(reply.tools ?? []).map((tool) => {
116
292
  const callId = toolId(tool, "call", 0);
117
- return { type: "function_call", id: callId, status: "completed", name: tool.name, call_id: callId, arguments: JSON.stringify(tool.args) };
293
+ return {
294
+ type: "function_call",
295
+ id: callId,
296
+ status: "completed",
297
+ name: tool.name,
298
+ call_id: callId,
299
+ arguments: JSON.stringify(tool.args),
300
+ };
118
301
  }),
119
302
  ];
120
303
 
121
304
  return {
122
- id, object: "response", created_at: createdAt, status: "completed", model, output,
123
- usage: { input_tokens: usage.input, output_tokens: usage.output, total_tokens: usage.input + usage.output },
305
+ id,
306
+ object: "response",
307
+ created_at: createdAt,
308
+ status: "completed",
309
+ model,
310
+ output,
311
+ usage: buildUsage(usage),
124
312
  };
125
313
  }
126
314
 
127
- export function serializeError(error: { status: number; message: string; type?: string }): unknown {
128
- return { type: "error", error: { message: error.message, type: error.type ?? "server_error", code: error.type ?? "server_error" } };
315
+ export function serializeError(error: {
316
+ status: number;
317
+ message: string;
318
+ type?: string;
319
+ }): Record<string, unknown> {
320
+ return {
321
+ type: "error",
322
+ error: {
323
+ message: error.message,
324
+ type: error.type ?? "server_error",
325
+ code: error.type ?? "server_error",
326
+ },
327
+ };
129
328
  }
@@ -0,0 +1,38 @@
1
+ import type { ReplyObject } from "../types.js";
2
+
3
+ export const MS_PER_SECOND = 1000;
4
+ const BASE_36 = 36;
5
+ export const DEFAULT_USAGE = { input: 10, output: 5 } as const;
6
+
7
+ export function splitText(text: string, chunkSize: number): string[] {
8
+ if (chunkSize <= 0 || text.length <= chunkSize) return [text];
9
+ const chunks: string[] = [];
10
+ for (let i = 0; i < text.length; i += chunkSize) {
11
+ chunks.push(text.slice(i, i + chunkSize));
12
+ }
13
+ return chunks;
14
+ }
15
+
16
+ export function genId(prefix: string): string {
17
+ return `${prefix}_${Date.now().toString(BASE_36)}`;
18
+ }
19
+
20
+ export function toolId(
21
+ tool: { id?: string | undefined },
22
+ prefix: string,
23
+ index: number,
24
+ ): string {
25
+ return tool.id ?? `${prefix}_${Date.now().toString(BASE_36)}_${index}`;
26
+ }
27
+
28
+ export function shouldEmitText(reply: ReplyObject): boolean {
29
+ return Boolean(reply.text) || (!reply.tools?.length && !reply.reasoning);
30
+ }
31
+
32
+ export function finishReason(
33
+ reply: ReplyObject,
34
+ onTools: string,
35
+ onStop: string,
36
+ ): string {
37
+ return reply.tools?.length ? onTools : onStop;
38
+ }
@@ -1,5 +1,10 @@
1
- import type { FormatName, MockRequest, ReplyObject, ReplyOptions } from "../types.js";
2
- import type { RequestMeta } from "./parse-helpers.js";
1
+ import type {
2
+ FormatName,
3
+ MockRequest,
4
+ ReplyObject,
5
+ ReplyOptions,
6
+ } from "../types.js";
7
+ import type { RequestMeta } from "./request-helpers.js";
3
8
 
4
9
  export interface SSEChunk {
5
10
  readonly event?: string | undefined;
@@ -11,7 +16,15 @@ export interface Format {
11
16
  readonly route: string;
12
17
  parseRequest(body: unknown, meta?: RequestMeta): MockRequest;
13
18
  isStreaming(body: unknown): boolean;
14
- serialize(reply: ReplyObject, model: string, options?: ReplyOptions): readonly SSEChunk[];
15
- serializeComplete(reply: ReplyObject, model: string): unknown;
16
- serializeError(error: { status: number; message: string; type?: string | undefined }): unknown;
19
+ serialize(
20
+ reply: ReplyObject,
21
+ model: string,
22
+ options?: ReplyOptions,
23
+ ): readonly SSEChunk[];
24
+ serializeComplete(reply: ReplyObject, model: string): Record<string, unknown>;
25
+ serializeError(error: {
26
+ status: number;
27
+ message: string;
28
+ type?: string | undefined;
29
+ }): Record<string, unknown>;
17
30
  }
package/src/index.ts CHANGED
@@ -37,7 +37,9 @@ import type { MockServerOptions } from "./mock-server.js";
37
37
  * await server.stop();
38
38
  * ```
39
39
  */
40
- export async function createMock(options: MockServerOptions = {}): Promise<MockServer> {
40
+ export async function createMock(
41
+ options: MockServerOptions = {},
42
+ ): Promise<MockServer> {
41
43
  const server = new MockServer(options);
42
44
  await server.start(options.port ?? 0);
43
45
  return server;
package/src/loader.ts CHANGED
@@ -3,9 +3,9 @@ import { join, extname } from "node:path";
3
3
  import JSON5 from "json5";
4
4
  import { z } from "zod";
5
5
  import type { Handler, Match, MatchObject, Reply } from "./types.js";
6
- import type { RuleEngine } from "./rule-engine.js";
6
+ import { type RuleEngine, createSequenceResolver } from "./rule-engine.js";
7
7
 
8
- export interface LoadContext {
8
+ interface LoadContext {
9
9
  engine: RuleEngine;
10
10
  setFallback?: (reply: Reply) => void;
11
11
  }
@@ -25,7 +25,11 @@ const json5ReplySchema = z.union([
25
25
  z.object({
26
26
  text: z.string().optional(),
27
27
  reasoning: z.string().optional(),
28
- tools: z.array(z.object({ name: z.string(), args: z.record(z.string(), z.unknown()) })).optional(),
28
+ tools: z
29
+ .array(
30
+ z.object({ name: z.string(), args: z.record(z.string(), z.unknown()) }),
31
+ )
32
+ .optional(),
29
33
  }),
30
34
  ]);
31
35
 
@@ -77,7 +81,9 @@ function compileMatch(when: z.infer<typeof json5MatchSchema>): Match {
77
81
  return parseRegexString(when);
78
82
  }
79
83
  const obj: MatchObject = {
80
- ...(when.message !== undefined && { message: parseRegexString(when.message) }),
84
+ ...(when.message !== undefined && {
85
+ message: parseRegexString(when.message),
86
+ }),
81
87
  ...(when.model !== undefined && { model: parseRegexString(when.model) }),
82
88
  ...(when.system !== undefined && { system: parseRegexString(when.system) }),
83
89
  ...(when.format !== undefined && { format: when.format }),
@@ -106,8 +112,7 @@ function addSequenceRule(
106
112
  templates: Templates,
107
113
  filePath: string,
108
114
  ): void {
109
- let index = 0;
110
- const resolved = entries.map((entry) => {
115
+ const steps = entries.map((entry) => {
111
116
  if (typeof entry === "string" || !("reply" in entry)) {
112
117
  return { reply: resolveReplyRef(entry, templates, filePath) };
113
118
  }
@@ -119,23 +124,27 @@ function addSequenceRule(
119
124
  },
120
125
  };
121
126
  });
122
- const lastStep = resolved[resolved.length - 1]!;
123
- const rule = engine.add(match, () => {
124
- const step = resolved[index++] ?? lastStep;
125
- rule.options = step.options ?? {};
126
- return step.reply;
127
- });
128
- rule.remaining = resolved.length;
127
+ const rule = engine.add(match, "");
128
+ const { resolver, entryCount } = createSequenceResolver(steps, rule);
129
+ rule.resolve = resolver;
130
+ rule.remaining = entryCount;
129
131
  }
130
132
 
131
- async function loadJson5File(filePath: string, ctx: LoadContext): Promise<void> {
133
+ async function loadJson5File(
134
+ filePath: string,
135
+ ctx: LoadContext,
136
+ ): Promise<void> {
132
137
  const content = await readFile(filePath, "utf-8");
133
138
  const parsed = json5FileSchema.parse(JSON5.parse(content));
134
139
 
135
140
  const rules = Array.isArray(parsed) ? parsed : parsed.rules;
136
141
  const templates = Array.isArray(parsed) ? undefined : parsed.templates;
137
142
 
138
- if (!Array.isArray(parsed) && parsed.fallback !== undefined && ctx.setFallback) {
143
+ if (
144
+ !Array.isArray(parsed) &&
145
+ parsed.fallback !== undefined &&
146
+ ctx.setFallback
147
+ ) {
139
148
  ctx.setFallback(parsed.fallback);
140
149
  }
141
150
 
@@ -156,7 +165,9 @@ async function loadJson5File(filePath: string, ctx: LoadContext): Promise<void>
156
165
  const handlerSchema = z.custom<Handler>((val): val is Handler => {
157
166
  if (typeof val !== "object" || val === null) return false;
158
167
  const obj = val as Record<string, unknown>;
159
- return typeof obj["match"] === "function" && typeof obj["respond"] === "function";
168
+ return (
169
+ typeof obj["match"] === "function" && typeof obj["respond"] === "function"
170
+ );
160
171
  });
161
172
 
162
173
  const handlerExportSchema = z.object({
@@ -164,7 +175,10 @@ const handlerExportSchema = z.object({
164
175
  fallback: json5ReplySchema.optional(),
165
176
  });
166
177
 
167
- async function loadHandlerFile(filePath: string, ctx: LoadContext): Promise<void> {
178
+ async function loadHandlerFile(
179
+ filePath: string,
180
+ ctx: LoadContext,
181
+ ): Promise<void> {
168
182
  const mod = await import(filePath);
169
183
  const parsed = handlerExportSchema.safeParse(mod);
170
184
  if (!parsed.success) {
@@ -172,14 +186,20 @@ async function loadHandlerFile(filePath: string, ctx: LoadContext): Promise<void
172
186
  `Invalid handler file ${filePath}. Expected default export with { match: Function, respond: Function }.`,
173
187
  );
174
188
  }
175
- const handlers = Array.isArray(parsed.data.default) ? parsed.data.default : [parsed.data.default];
189
+ const handlers = Array.isArray(parsed.data.default)
190
+ ? parsed.data.default
191
+ : [parsed.data.default];
176
192
 
177
193
  if (parsed.data.fallback !== undefined && ctx.setFallback) {
178
194
  ctx.setFallback(parsed.data.fallback);
179
195
  }
180
196
 
181
197
  for (const handler of handlers) {
182
- ctx.engine.addHandler(handler.match, handler.respond, `(handler: ${filePath})`);
198
+ ctx.engine.addHandler(
199
+ handler.match,
200
+ handler.respond,
201
+ `(handler: ${filePath})`,
202
+ );
183
203
  }
184
204
  }
185
205
 
@@ -193,7 +213,10 @@ const loaderByExtension: ReadonlyMap<string, FileLoader> = new Map([
193
213
  [".mjs", loadHandlerFile],
194
214
  ]);
195
215
 
196
- export async function loadRulesFromPath(pathOrDir: string, ctx: LoadContext): Promise<void> {
216
+ export async function loadRulesFromPath(
217
+ pathOrDir: string,
218
+ ctx: LoadContext,
219
+ ): Promise<void> {
197
220
  const info = await stat(pathOrDir);
198
221
 
199
222
  if (info.isFile()) {
package/src/logger.ts CHANGED
@@ -19,6 +19,18 @@ const LEVEL_STYLE = {
19
19
  debug: { label: pc.dim("DEBUG"), symbol: pc.dim("·") },
20
20
  } as const;
21
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
+
22
34
  export class Logger {
23
35
  readonly level: LogLevel;
24
36
  private threshold: number;
@@ -28,31 +40,31 @@ export class Logger {
28
40
  this.threshold = LEVEL_PRIORITY[level];
29
41
  }
30
42
 
31
- error(msg: string, ...args: unknown[]): void {
32
- if (this.threshold >= LEVEL_PRIORITY.error) {
33
- const { label, symbol } = LEVEL_STYLE.error;
34
- console.error(`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${msg}`, ...args);
35
- }
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
+ );
36
56
  }
37
57
 
58
+ error(msg: string, ...args: unknown[]): void {
59
+ this.log("error", msg, args);
60
+ }
38
61
  warn(msg: string, ...args: unknown[]): void {
39
- if (this.threshold >= LEVEL_PRIORITY.warning) {
40
- const { label, symbol } = LEVEL_STYLE.warn;
41
- console.warn(`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${msg}`, ...args);
42
- }
62
+ this.log("warn", msg, args);
43
63
  }
44
-
45
64
  info(msg: string, ...args: unknown[]): void {
46
- if (this.threshold >= LEVEL_PRIORITY.info) {
47
- const { label, symbol } = LEVEL_STYLE.info;
48
- console.log(`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${msg}`, ...args);
49
- }
65
+ this.log("info", msg, args);
50
66
  }
51
-
52
67
  debug(msg: string, ...args: unknown[]): void {
53
- if (this.threshold >= LEVEL_PRIORITY.debug) {
54
- const { label, symbol } = LEVEL_STYLE.debug;
55
- console.log(`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${pc.dim(msg)}`, ...args);
56
- }
68
+ this.log("debug", msg, args);
57
69
  }
58
70
  }