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,9 +1,17 @@
1
1
  import Fastify from "fastify";
2
2
  import type { FastifyInstance } from "fastify";
3
3
  import type {
4
- Match, PendingRule, Reply, ReplyOptions, Resolver, Rule, RuleHandle, RuleSummary, SequenceEntry,
4
+ Match,
5
+ PendingRule,
6
+ Reply,
7
+ ReplyOptions,
8
+ Resolver,
9
+ Rule,
10
+ RuleHandle,
11
+ RuleSummary,
12
+ SequenceEntry,
5
13
  } from "./types.js";
6
- import { RuleEngine } from "./rule-engine.js";
14
+ import { RuleEngine, createSequenceResolver } from "./rule-engine.js";
7
15
  import { RequestHistory } from "./history.js";
8
16
  import { openaiFormat } from "./formats/openai/index.js";
9
17
  import { anthropicFormat } from "./formats/anthropic/index.js";
@@ -13,7 +21,11 @@ import { Logger } from "./logger.js";
13
21
  import type { LogLevel } from "./logger.js";
14
22
  import { createRouteHandler } from "./route-handler.js";
15
23
 
16
- const formats: readonly Format[] = [openaiFormat, anthropicFormat, responsesFormat];
24
+ const formats: readonly Format[] = [
25
+ openaiFormat,
26
+ anthropicFormat,
27
+ responsesFormat,
28
+ ];
17
29
 
18
30
  export interface MockServerOptions {
19
31
  readonly port?: number;
@@ -56,8 +68,12 @@ export class MockServer {
56
68
  this.host = options.host ?? "127.0.0.1";
57
69
  this.logger = new Logger(options.logLevel ?? "none");
58
70
  this.defaultOptions = {
59
- ...(options.defaultLatency !== undefined && { latency: options.defaultLatency }),
60
- ...(options.defaultChunkSize !== undefined && { chunkSize: options.defaultChunkSize }),
71
+ ...(options.defaultLatency !== undefined && {
72
+ latency: options.defaultLatency,
73
+ }),
74
+ ...(options.defaultChunkSize !== undefined && {
75
+ chunkSize: options.defaultChunkSize,
76
+ }),
61
77
  };
62
78
  this.app = Fastify({ logger: false });
63
79
 
@@ -103,19 +119,15 @@ export class MockServer {
103
119
  return makeHandle(engine.add(match, response, options));
104
120
  },
105
121
  replySequence(entries: readonly SequenceEntry[]): RuleHandle {
106
- if (entries.length === 0) throw new Error("replySequence requires at least one entry.");
107
- let index = 0;
108
- const last = entries[entries.length - 1]!;
109
- const rule = engine.add(match, () => {
110
- const entry = entries[index++] ?? last;
111
- if (typeof entry === "string" || !("reply" in entry)) {
112
- rule.options = {};
113
- return entry;
114
- }
115
- rule.options = entry.options ?? {};
116
- return entry.reply;
117
- });
118
- rule.remaining = entries.length;
122
+ const steps = entries.map((entry) =>
123
+ typeof entry === "string" || !("reply" in entry)
124
+ ? { reply: entry as Reply }
125
+ : { reply: entry.reply, options: entry.options },
126
+ );
127
+ const rule = engine.add(match, "");
128
+ const { resolver, entryCount } = createSequenceResolver(steps, rule);
129
+ rule.resolve = resolver;
130
+ rule.remaining = entryCount;
119
131
  return makeHandle(rule);
120
132
  },
121
133
  };
@@ -175,10 +187,14 @@ export class MockServer {
175
187
  const { loadRulesFromPath } = await import("./loader.js");
176
188
  await loadRulesFromPath(pathOrDir, {
177
189
  engine: this.engine,
178
- setFallback: (reply) => { this.fallbackReply = reply; },
190
+ setFallback: (reply) => {
191
+ this.fallbackReply = reply;
192
+ },
179
193
  });
180
194
  const loaded = this.engine.ruleCount - before;
181
- this.logger.info(`Loaded ${loaded} rule${loaded !== 1 ? "s" : ""} from ${pathOrDir}`);
195
+ this.logger.info(
196
+ `Loaded ${loaded} rule${loaded !== 1 ? "s" : ""} from ${pathOrDir}`,
197
+ );
182
198
  }
183
199
 
184
200
  /** Every request the server has handled. */
@@ -201,7 +217,8 @@ export class MockServer {
201
217
 
202
218
  /** The base URL the server is listening on, e.g. `http://127.0.0.1:12345`. Throws if the server hasn't started. */
203
219
  get url(): string {
204
- if (!this.listening) throw new Error("Server is not running. Call start() first.");
220
+ if (!this.listening)
221
+ throw new Error("Server is not running. Call start() first.");
205
222
  const addr = this.app.server.address();
206
223
  const port = addr !== null && typeof addr === "object" ? addr.port : 0;
207
224
  return `http://${this.host}:${port}`;
@@ -1,6 +1,12 @@
1
1
  import type { FastifyReply, FastifyRequest } from "fastify";
2
2
  import { ZodError } from "zod";
3
- import type { Reply, ReplyObject, ReplyOptions, MockRequest, Rule } from "./types.js";
3
+ import type {
4
+ Reply,
5
+ ReplyObject,
6
+ ReplyOptions,
7
+ MockRequest,
8
+ Rule,
9
+ } from "./types.js";
4
10
  import type { Format } from "./formats/types.js";
5
11
  import type { RuleEngine } from "./rule-engine.js";
6
12
  import type { RequestHistory } from "./history.js";
@@ -21,14 +27,17 @@ async function resolveReply(
21
27
  logger: Logger,
22
28
  ): Promise<{ reply: ReplyObject; ruleDesc: string | undefined }> {
23
29
  if (!matched) {
24
- logger.warn(`No matching rule for "${mockReq.lastMessage}", using fallback`);
30
+ logger.warn(
31
+ `No matching rule for "${mockReq.lastMessage}", using fallback`,
32
+ );
25
33
  return { reply: normalizeReply(fallback), ruleDesc: undefined };
26
34
  }
27
35
 
28
36
  try {
29
- const raw = typeof matched.resolve === "function"
30
- ? await matched.resolve(mockReq)
31
- : matched.resolve;
37
+ const raw =
38
+ typeof matched.resolve === "function"
39
+ ? await matched.resolve(mockReq)
40
+ : matched.resolve;
32
41
  logger.debug(`Matched rule ${matched.description}`);
33
42
  return { reply: normalizeReply(raw), ruleDesc: matched.description };
34
43
  } catch (err) {
@@ -37,7 +46,7 @@ async function resolveReply(
37
46
  }
38
47
  }
39
48
 
40
- export interface RouteHandlerDeps {
49
+ interface RouteHandlerDeps {
41
50
  engine: RuleEngine;
42
51
  history: RequestHistory;
43
52
  logger: Logger;
@@ -45,7 +54,10 @@ export interface RouteHandlerDeps {
45
54
  getFallback: () => Reply;
46
55
  }
47
56
 
48
- export function createRouteHandler(format: Format, deps: RouteHandlerDeps): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
57
+ export function createRouteHandler(
58
+ format: Format,
59
+ deps: RouteHandlerDeps,
60
+ ): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
49
61
  const { engine, history, logger, defaultOptions, getFallback } = deps;
50
62
 
51
63
  return async (request: FastifyRequest, reply: FastifyReply) => {
@@ -61,10 +73,19 @@ export function createRouteHandler(format: Format, deps: RouteHandlerDeps): (req
61
73
  mockReq = format.parseRequest(body, meta);
62
74
  } catch (err) {
63
75
  if (err instanceof ZodError) {
64
- logger.warn(`Invalid ${format.name} request: ${err.issues.map((i) => i.message).join(", ")}`);
65
- return reply.status(HTTP_BAD_REQUEST).type("application/json").send(
66
- format.serializeError({ status: HTTP_BAD_REQUEST, message: "Invalid request body", type: "invalid_request_error" }),
76
+ logger.warn(
77
+ `Invalid ${format.name} request: ${err.issues.map((i) => i.message).join(", ")}`,
67
78
  );
79
+ return reply
80
+ .status(HTTP_BAD_REQUEST)
81
+ .type("application/json")
82
+ .send(
83
+ format.serializeError({
84
+ status: HTTP_BAD_REQUEST,
85
+ message: "Invalid request body",
86
+ type: "invalid_request_error",
87
+ }),
88
+ );
68
89
  }
69
90
  throw err;
70
91
  }
@@ -76,14 +97,20 @@ export function createRouteHandler(format: Format, deps: RouteHandlerDeps): (req
76
97
 
77
98
  const matched = engine.match(mockReq);
78
99
  const { reply: resolvedReply, ruleDesc } = await resolveReply(
79
- matched, mockReq, getFallback(), logger,
100
+ matched,
101
+ mockReq,
102
+ getFallback(),
103
+ logger,
80
104
  );
81
105
 
82
106
  if (resolvedReply.error) {
83
107
  const { error } = resolvedReply;
84
108
  logger.info(`Error reply: ${String(error.status)} ${error.message}`);
85
109
  history.record(mockReq, ruleDesc);
86
- return reply.status(error.status).type("application/json").send(format.serializeError(error));
110
+ return reply
111
+ .status(error.status)
112
+ .type("application/json")
113
+ .send(format.serializeError(error));
87
114
  }
88
115
 
89
116
  history.record(mockReq, ruleDesc);
@@ -100,14 +127,22 @@ export function createRouteHandler(format: Format, deps: RouteHandlerDeps): (req
100
127
  logger.debug(`Reply text: "${resolvedReply.text}"`);
101
128
  }
102
129
  if (resolvedReply.tools?.length) {
103
- logger.debug(`Reply tool calls: ${resolvedReply.tools.map((t) => t.name).join(", ")}`);
130
+ logger.debug(
131
+ `Reply tool calls: ${resolvedReply.tools.map((t) => t.name).join(", ")}`,
132
+ );
104
133
  }
105
134
 
106
135
  if (!isStreaming) {
107
- return reply.type("application/json").send(format.serializeComplete(resolvedReply, mockReq.model));
136
+ return reply
137
+ .type("application/json")
138
+ .send(format.serializeComplete(resolvedReply, mockReq.model));
108
139
  }
109
140
 
110
- const chunks = format.serialize(resolvedReply, mockReq.model, effectiveOptions);
141
+ const chunks = format.serialize(
142
+ resolvedReply,
143
+ mockReq.model,
144
+ effectiveOptions,
145
+ );
111
146
  await writeSSE(reply, chunks, effectiveOptions);
112
147
  };
113
148
  }
@@ -1,7 +1,18 @@
1
- import type { Match, MatchObject, MockRequest, Resolver, ReplyOptions, Rule, RuleSummary } from "./types.js";
1
+ import type {
2
+ Match,
3
+ MatchObject,
4
+ MockRequest,
5
+ Resolver,
6
+ Reply,
7
+ ReplyOptions,
8
+ Rule,
9
+ RuleSummary,
10
+ } from "./types.js";
2
11
 
3
12
  function safeRegex(re: RegExp): RegExp {
4
- return (re.global || re.sticky) ? new RegExp(re.source, re.flags.replace(/[gy]/g, "")) : re;
13
+ return re.global || re.sticky
14
+ ? new RegExp(re.source, re.flags.replace(/[gy]/g, ""))
15
+ : re;
5
16
  }
6
17
 
7
18
  function compilePattern(pattern: string | RegExp): (value: string) => boolean {
@@ -26,16 +37,21 @@ function compileMatcher(match: Match): (req: MockRequest) => boolean {
26
37
  return match;
27
38
  }
28
39
  const obj = match;
29
- const messageTest = obj.message !== undefined ? compilePattern(obj.message) : undefined;
30
- const modelTest = obj.model !== undefined ? compilePattern(obj.model) : undefined;
31
- const systemTest = obj.system !== undefined ? compilePattern(obj.system) : undefined;
40
+ const messageTest =
41
+ obj.message !== undefined ? compilePattern(obj.message) : undefined;
42
+ const modelTest =
43
+ obj.model !== undefined ? compilePattern(obj.model) : undefined;
44
+ const systemTest =
45
+ obj.system !== undefined ? compilePattern(obj.system) : undefined;
32
46
  return (req) => {
33
47
  if (messageTest && !messageTest(req.lastMessage)) return false;
34
48
  if (modelTest && !modelTest(req.model)) return false;
35
49
  if (systemTest && !systemTest(req.systemMessage)) return false;
36
50
  if (obj.format !== undefined && req.format !== obj.format) return false;
37
- if (obj.toolName !== undefined && !req.toolNames.includes(obj.toolName)) return false;
38
- if (obj.toolCallId !== undefined && req.lastToolCallId !== obj.toolCallId) return false;
51
+ if (obj.toolName !== undefined && !req.toolNames.includes(obj.toolName))
52
+ return false;
53
+ if (obj.toolCallId !== undefined && req.lastToolCallId !== obj.toolCallId)
54
+ return false;
39
55
  if (obj.predicate && !obj.predicate(req)) return false;
40
56
  return true;
41
57
  };
@@ -52,7 +68,12 @@ function describeMatch(match: Match): string {
52
68
  return `{${parts.join(", ")}}`;
53
69
  }
54
70
 
55
- function createRule(match: Match, resolve: Resolver, options: ReplyOptions, description?: string): Rule {
71
+ function createRule(
72
+ match: Match,
73
+ resolve: Resolver,
74
+ options: ReplyOptions,
75
+ description?: string,
76
+ ): Rule {
56
77
  return {
57
78
  description: description ?? describeMatch(match),
58
79
  match: compileMatcher(match),
@@ -62,6 +83,29 @@ function createRule(match: Match, resolve: Resolver, options: ReplyOptions, desc
62
83
  };
63
84
  }
64
85
 
86
+ interface SequenceStep {
87
+ readonly reply: Reply;
88
+ readonly options?: ReplyOptions | undefined;
89
+ }
90
+
91
+ export function createSequenceResolver(
92
+ steps: readonly SequenceStep[],
93
+ rule: { options: ReplyOptions },
94
+ ): { resolver: () => Reply; entryCount: number } {
95
+ if (steps.length === 0)
96
+ throw new Error("Sequence requires at least one entry.");
97
+ let index = 0;
98
+ const last = steps[steps.length - 1]!;
99
+ return {
100
+ resolver: () => {
101
+ const step = steps[index++] ?? last;
102
+ rule.options = step.options ?? {};
103
+ return step.reply;
104
+ },
105
+ entryCount: steps.length,
106
+ };
107
+ }
108
+
65
109
  export class RuleEngine {
66
110
  private readonly rules: Rule[] = [];
67
111
 
@@ -79,7 +123,11 @@ export class RuleEngine {
79
123
  }
80
124
  }
81
125
 
82
- addHandler(matchFn: (req: MockRequest) => boolean, respond: Resolver, description = "(handler)"): Rule {
126
+ addHandler(
127
+ matchFn: (req: MockRequest) => boolean,
128
+ respond: Resolver,
129
+ description = "(handler)",
130
+ ): Rule {
83
131
  const rule = createRule(matchFn, respond, {}, description);
84
132
  this.rules.push(rule);
85
133
  return rule;
@@ -102,7 +150,9 @@ export class RuleEngine {
102
150
  }
103
151
 
104
152
  isDone(): boolean {
105
- return this.rules.every((r) => !Number.isFinite(r.remaining) || r.remaining <= 0);
153
+ return this.rules.every(
154
+ (r) => !Number.isFinite(r.remaining) || r.remaining <= 0,
155
+ );
106
156
  }
107
157
 
108
158
  get ruleCount(): number {
@@ -110,7 +160,10 @@ export class RuleEngine {
110
160
  }
111
161
 
112
162
  describe(): readonly RuleSummary[] {
113
- return this.rules.map((r) => ({ description: r.description, remaining: r.remaining }));
163
+ return this.rules.map((r) => ({
164
+ description: r.description,
165
+ remaining: r.remaining,
166
+ }));
114
167
  }
115
168
 
116
169
  clear(): void {
@@ -5,30 +5,28 @@ export type Reply = string | ReplyObject;
5
5
 
6
6
  /** A structured reply. Text, reasoning, tool calls, usage, and errors are all optional. */
7
7
  export interface ReplyObject {
8
- /** Text content to send back. */
9
8
  readonly text?: string | undefined;
10
9
  /** Extended thinking or chain-of-thought. Works with Anthropic and Responses formats. */
11
10
  readonly reasoning?: string | undefined;
12
- /** Tool calls the "model" wants to make. */
13
11
  readonly tools?: readonly ToolCall[] | undefined;
14
- /** Token counts to report. Falls back to `{ input: 10, output: 5 }` if omitted. */
15
- readonly usage?: { readonly input: number; readonly output: number } | undefined;
12
+ /** Falls back to `{ input: 10, output: 5 }` if omitted. */
13
+ readonly usage?:
14
+ | { readonly input: number; readonly output: number }
15
+ | undefined;
16
16
  /** When set, the server responds with this HTTP error instead of a normal reply. */
17
17
  readonly error?: ErrorReply | undefined;
18
18
  }
19
19
 
20
20
  /** An HTTP error response. The server returns this status code with a format-appropriate body. */
21
21
  export interface ErrorReply {
22
- /** HTTP status code, like 429 or 500. */
23
22
  readonly status: number;
24
23
  readonly message: string;
25
- /** Error type string in the response body. Each format has its own default if omitted. */
24
+ /** Each format has its own default if omitted. */
26
25
  readonly type?: string | undefined;
27
26
  }
28
27
 
29
- /** A tool call in the mock response. */
30
28
  export interface ToolCall {
31
- /** Explicit ID for the call. Auto-generated if omitted. */
29
+ /** Auto-generated if omitted. */
32
30
  readonly id?: string | undefined;
33
31
  readonly name: string;
34
32
  readonly args: Readonly<Record<string, unknown>>;
@@ -37,13 +35,15 @@ export interface ToolCall {
37
35
  /** A reply value or a function that produces one. Async functions are supported. */
38
36
  export type Resolver = Reply | ((req: MockRequest) => Reply | Promise<Reply>);
39
37
 
40
- /** Streaming options for a rule. Merged with server-level defaults, with per-rule values winning. */
38
+ /** Per-rule streaming options. Merged with server-level defaults, with per-rule values winning. */
41
39
  export interface ReplyOptions {
42
- /** Milliseconds to wait between SSE chunks. */
40
+ /** Milliseconds between SSE chunks. */
43
41
  readonly latency?: number | undefined;
44
- /** Split text into chunks of this many characters for more realistic streaming. */
42
+ /** Characters per SSE chunk for more realistic streaming. */
45
43
  readonly chunkSize?: number | undefined;
46
44
  }
47
45
 
48
46
  /** A single entry in a reply sequence. Can be a plain reply or a reply with per-step options. */
49
- export type SequenceEntry = Reply | { readonly reply: Reply; readonly options?: ReplyOptions };
47
+ export type SequenceEntry =
48
+ | Reply
49
+ | { readonly reply: Reply; readonly options?: ReplyOptions };
@@ -3,28 +3,24 @@ export type FormatName = "openai" | "anthropic" | "responses";
3
3
 
4
4
  /** A normalised view of an incoming request, regardless of the original wire format. */
5
5
  export interface MockRequest {
6
- /** Which format route the request hit. */
7
6
  readonly format: FormatName;
8
7
  readonly model: string;
9
- /** Whether the client asked for SSE streaming. */
10
8
  readonly streaming: boolean;
11
9
  /** Full conversation, normalised from whatever format came in. */
12
10
  readonly messages: readonly Message[];
13
11
  /** The last user message's text. This is what most matchers check. */
14
12
  readonly lastMessage: string;
15
- /** System prompt text, or `""` if there wasn't one. */
13
+ /** Empty string if there wasn't one. */
16
14
  readonly systemMessage: string;
17
- /** Tool definitions from the request, if any were sent. */
18
15
  readonly tools?: readonly ToolDef[] | undefined;
19
- /** The names from `tools`, pulled out for quick lookups. */
16
+ /** Pulled out from `tools` for quick lookups. */
20
17
  readonly toolNames: readonly string[];
21
- /** If the last message was a tool result, this is its `tool_call_id`. */
18
+ /** Set when the last message was a tool result. */
22
19
  readonly lastToolCallId: string | undefined;
23
- /** The raw request body, in case you need something we don't extract. */
20
+ /** The raw request body, for anything we don't extract. */
24
21
  readonly raw: unknown;
25
- /** HTTP headers from the incoming request. */
26
22
  readonly headers: Readonly<Record<string, string | undefined>>;
27
- /** The URL path that was hit, e.g. `/v1/chat/completions`. */
23
+ /** e.g. `/v1/chat/completions` */
28
24
  readonly path: string;
29
25
  }
30
26
 
@@ -32,7 +28,7 @@ export interface MockRequest {
32
28
  export interface Message {
33
29
  readonly role: "system" | "user" | "assistant" | "tool";
34
30
  readonly content: string;
35
- /** Only set on `"tool"` messages. Links the result back to its tool call. */
31
+ /** Links the result back to its tool call. Only set on `"tool"` messages. */
36
32
  readonly toolCallId?: string | undefined;
37
33
  }
38
34
 
@@ -40,6 +36,6 @@ export interface Message {
40
36
  export interface ToolDef {
41
37
  readonly name: string;
42
38
  readonly description?: string | undefined;
43
- /** JSON Schema for the tool's parameters, passed through as-is. */
39
+ /** JSON Schema, passed through as-is. */
44
40
  readonly parameters?: unknown;
45
41
  }
package/src/types/rule.ts CHANGED
@@ -17,13 +17,9 @@ export type Match =
17
17
 
18
18
  /** A structured matcher. Every field you set must match for the rule to fire. */
19
19
  export interface MatchObject {
20
- /** Substring or regex against the last user message. */
21
20
  readonly message?: string | RegExp;
22
- /** Substring or regex against the model name. */
23
21
  readonly model?: string | RegExp;
24
- /** Substring or regex against the system prompt. */
25
22
  readonly system?: string | RegExp;
26
- /** Only match requests from this API format. */
27
23
  readonly format?: FormatName;
28
24
  /** Match when the request includes a tool definition with this name. */
29
25
  readonly toolName?: string;
@@ -36,15 +32,13 @@ export interface MatchObject {
36
32
  /** Returned by `when()`. Call `.reply()` or `.replySequence()` on it to complete the rule. */
37
33
  export interface PendingRule {
38
34
  reply(response: Resolver, options?: ReplyOptions): RuleHandle;
39
- /** Each match advances through the array. The last entry repeats once the sequence is exhausted. */
35
+ /** Each match advances through the array. The last entry repeats once exhausted. */
40
36
  replySequence(entries: readonly SequenceEntry[]): RuleHandle;
41
37
  }
42
38
 
43
39
  /** A handle to a registered rule. All methods return `this` for chaining. */
44
40
  export interface RuleHandle {
45
- /** Auto-expire the rule after `n` matches. */
46
41
  times(n: number): RuleHandle;
47
- /** Move this rule to the front of the list so it matches first. */
48
42
  first(): RuleHandle;
49
43
  }
50
44
 
@@ -59,16 +53,15 @@ export interface Handler {
59
53
 
60
54
  /** A summary of a registered rule, for inspection. */
61
55
  export interface RuleSummary {
62
- /** Human-readable description of what the rule matches. */
63
56
  readonly description: string;
64
- /** How many matches are left. `Infinity` means unlimited. */
57
+ /** `Infinity` means unlimited. */
65
58
  readonly remaining: number;
66
59
  }
67
60
 
68
61
  export interface Rule {
69
62
  readonly description: string;
70
63
  readonly match: (req: MockRequest) => boolean;
71
- readonly resolve: Resolver;
64
+ resolve: Resolver;
72
65
  options: ReplyOptions;
73
66
  remaining: number;
74
67
  }
package/src/types.ts CHANGED
@@ -1,5 +1,24 @@
1
1
  export type {
2
- FormatName, MockRequest, Message, ToolDef,
3
- Reply, ReplyObject, ErrorReply, ToolCall, Resolver, ReplyOptions, SequenceEntry,
4
- Match, MatchObject, PendingRule, RuleHandle, RuleSummary, Handler, RecordedRequest, Rule,
5
- } from "./types/index.js";
2
+ FormatName,
3
+ MockRequest,
4
+ Message,
5
+ ToolDef,
6
+ } from "./types/request.js";
7
+ export type {
8
+ Reply,
9
+ ReplyObject,
10
+ ErrorReply,
11
+ ToolCall,
12
+ Resolver,
13
+ ReplyOptions,
14
+ SequenceEntry,
15
+ } from "./types/reply.js";
16
+ export type {
17
+ Match,
18
+ MatchObject,
19
+ PendingRule,
20
+ RuleHandle,
21
+ RuleSummary,
22
+ Handler,
23
+ Rule,
24
+ } from "./types/rule.js";
@@ -1,5 +1,11 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { parsePort, parseHost, parseChunkSize, parseLogLevel, parseLatency } from "../src/cli-validators.js";
2
+ import {
3
+ parsePort,
4
+ parseHost,
5
+ parseChunkSize,
6
+ parseLogLevel,
7
+ parseLatency,
8
+ } from "../src/cli-validators.js";
3
9
 
4
10
  describe("parsePort", () => {
5
11
  it("parses a valid port", () => {
@@ -48,7 +54,9 @@ describe("parseLogLevel", () => {
48
54
  );
49
55
 
50
56
  it("throws on invalid level", () => {
51
- expect(() => parseLogLevel("verbose")).toThrow('Invalid log level "verbose"');
57
+ expect(() => parseLogLevel("verbose")).toThrow(
58
+ 'Invalid log level "verbose"',
59
+ );
52
60
  });
53
61
 
54
62
  it("throws on empty string", () => {
@@ -78,11 +86,15 @@ describe("parseHost", () => {
78
86
  });
79
87
 
80
88
  it("rejects an unresolvable hostname", async () => {
81
- await expect(parseHost("not.a.real.host.invalid")).rejects.toThrow("Invalid host");
89
+ await expect(parseHost("not.a.real.host.invalid")).rejects.toThrow(
90
+ "Invalid host",
91
+ );
82
92
  });
83
93
 
84
94
  it("rejects a string with spaces", async () => {
85
- await expect(parseHost("local host")).rejects.toThrow('Invalid host "local host"');
95
+ await expect(parseHost("local host")).rejects.toThrow(
96
+ 'Invalid host "local host"',
97
+ );
86
98
  });
87
99
  });
88
100