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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-mock-server",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A standalone mock LLM server for deterministic testing: OpenAI, Anthropic, and Responses API formats",
5
5
  "type": "module",
6
6
  "engines": {
@@ -20,10 +20,12 @@
20
20
  },
21
21
  "scripts": {
22
22
  "build": "tsc",
23
+ "fmt": "oxfmt --write src/ test/",
24
+ "fmt:check": "oxfmt --check src/ test/",
23
25
  "lint": "oxlint --tsconfig tsconfig.json --import-plugin --vitest-plugin src/ test/",
24
26
  "test": "vitest run",
25
27
  "test:watch": "vitest",
26
- "check": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && npm run lint && npm test",
28
+ "check": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && npm run fmt:check && npm run lint && npm test",
27
29
  "dev": "tsx src/cli.ts",
28
30
  "start": "node dist/cli.js"
29
31
  },
@@ -48,6 +50,7 @@
48
50
  "devDependencies": {
49
51
  "@types/node": "25.5.0",
50
52
  "@vitest/coverage-v8": "4.1.0",
53
+ "oxfmt": "0.40.0",
51
54
  "oxlint": "1.55.0",
52
55
  "tsx": "4.21.0",
53
56
  "typescript": "5.9.3",
package/scorecard.png ADDED
Binary file
@@ -29,7 +29,9 @@ export function parseLogLevel(value: string): LogLevel {
29
29
 
30
30
  export async function parseHost(value: string): Promise<string> {
31
31
  if (!value) {
32
- throw new Error(`Invalid host "${value}". Must be a resolvable hostname or IP address.`);
32
+ throw new Error(
33
+ `Invalid host "${value}". Must be a resolvable hostname or IP address.`,
34
+ );
33
35
  }
34
36
  if (value === "localhost" || isIP(value) !== 0) {
35
37
  return value;
@@ -38,14 +40,18 @@ export async function parseHost(value: string): Promise<string> {
38
40
  await lookup(value);
39
41
  return value;
40
42
  } catch {
41
- throw new Error(`Invalid host "${value}". Must be a resolvable hostname or IP address.`);
43
+ throw new Error(
44
+ `Invalid host "${value}". Must be a resolvable hostname or IP address.`,
45
+ );
42
46
  }
43
47
  }
44
48
 
45
49
  export function parseChunkSize(value: string): number {
46
50
  const size = parseInt(value, 10);
47
51
  if (isNaN(size) || size < 0) {
48
- throw new Error(`Invalid chunk size "${value}". Must be a non-negative integer.`);
52
+ throw new Error(
53
+ `Invalid chunk size "${value}". Must be a non-negative integer.`,
54
+ );
49
55
  }
50
56
  return size;
51
57
  }
@@ -53,7 +59,9 @@ export function parseChunkSize(value: string): number {
53
59
  export function parseLatency(value: string): number {
54
60
  const ms = parseInt(value, 10);
55
61
  if (isNaN(ms) || ms < 0) {
56
- throw new Error(`Invalid latency "${value}". Must be a non-negative integer (ms).`);
62
+ throw new Error(
63
+ `Invalid latency "${value}". Must be a non-negative integer (ms).`,
64
+ );
57
65
  }
58
66
  return ms;
59
67
  }
package/src/cli.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { watch } from "node:fs";
4
+ import { createRequire } from "node:module";
4
5
  import { Command } from "commander";
5
6
  import pc from "picocolors";
6
7
  import { MockServer } from "./mock-server.js";
7
8
  import { Logger } from "./logger.js";
8
- import { parsePort, parseHost, parseLogLevel, parseChunkSize, parseLatency } from "./cli-validators.js";
9
+ import {
10
+ parsePort,
11
+ parseHost,
12
+ parseLogLevel,
13
+ parseChunkSize,
14
+ parseLatency,
15
+ } from "./cli-validators.js";
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const { version } = require("../package.json") as { version: string };
9
19
 
10
20
  const WATCH_DEBOUNCE_MS = 100;
11
21
 
@@ -54,12 +64,18 @@ async function start(options: StartOptions): Promise<void> {
54
64
 
55
65
  if (!quiet) {
56
66
  console.log();
57
- console.log(` ${pc.bold(pc.cyan("llm-mock-server"))} ${pc.dim("v1.0.0")}`);
67
+ console.log(
68
+ ` ${pc.bold(pc.cyan("llm-mock-server"))} ${pc.dim(`v${version}`)}`,
69
+ );
58
70
  console.log();
59
71
  console.log(` ${pc.dim("Port")} ${pc.bold(String(port))}`);
60
- console.log(` ${pc.dim("Rules")} ${pc.bold(String(server.ruleCount))} loaded`);
72
+ console.log(
73
+ ` ${pc.dim("Rules")} ${pc.bold(String(server.ruleCount))} loaded`,
74
+ );
61
75
  if (latency > 0) {
62
- console.log(` ${pc.dim("Latency")} ${pc.bold(`${String(latency)}ms`)} per chunk`);
76
+ console.log(
77
+ ` ${pc.dim("Latency")} ${pc.bold(`${String(latency)}ms`)} per chunk`,
78
+ );
63
79
  }
64
80
  console.log(
65
81
  ` ${pc.dim("Endpoints")} ${pc.green("/v1/chat/completions")}, ${pc.green("/v1/messages")}, ${pc.green("/v1/responses")}`,
@@ -99,14 +115,18 @@ async function start(options: StartOptions): Promise<void> {
99
115
  process.exit(0);
100
116
  };
101
117
 
102
- process.on("SIGINT", () => { shutdown("SIGINT").catch(() => process.exit(1)); });
103
- process.on("SIGTERM", () => { shutdown("SIGTERM").catch(() => process.exit(1)); });
118
+ process.on("SIGINT", () => {
119
+ shutdown("SIGINT").catch(() => process.exit(1));
120
+ });
121
+ process.on("SIGTERM", () => {
122
+ shutdown("SIGTERM").catch(() => process.exit(1));
123
+ });
104
124
  }
105
125
 
106
126
  const program = new Command()
107
127
  .name("llm-mock-server")
108
128
  .description("Mock LLM server for deterministic testing")
109
- .version("1.0.0");
129
+ .version(version);
110
130
 
111
131
  program
112
132
  .command("start", { isDefault: true })
@@ -1,5 +1,5 @@
1
1
  import type { Format } from "../types.js";
2
- import { isStreaming } from "../parse-helpers.js";
2
+ import { isStreaming } from "../request-helpers.js";
3
3
  import { parseRequest } from "./parse.js";
4
4
  import { serialize, serializeComplete, serializeError } from "./serialize.js";
5
5
 
@@ -1,21 +1,30 @@
1
1
  import type { MockRequest, Message, ToolDef } from "../../types.js";
2
- import { buildMockRequest, type RequestMeta } from "../parse-helpers.js";
2
+ import { buildMockRequest, type RequestMeta } from "../request-helpers.js";
3
3
  import { AnthropicRequestSchema, type AnthropicRequest } from "./schema.js";
4
4
 
5
5
  function extractSystem(system: AnthropicRequest["system"]): Message[] {
6
6
  if (system == null) return [];
7
- if (typeof system === "string") return system ? [{ role: "system", content: system }] : [];
7
+ if (typeof system === "string")
8
+ return system ? [{ role: "system", content: system }] : [];
8
9
  const text = system.map((b) => b.text).join("\n");
9
10
  return text ? [{ role: "system", content: text }] : [];
10
11
  }
11
12
 
12
- function extractContent(content: AnthropicRequest["messages"][number]["content"]): { content: string; toolCallId?: string | undefined } {
13
+ function extractContent(
14
+ content: AnthropicRequest["messages"][number]["content"],
15
+ ): {
16
+ content: string;
17
+ toolCallId?: string | undefined;
18
+ } {
13
19
  if (typeof content === "string") return { content };
14
20
  const text = content
15
21
  .filter((b): b is { type: "text"; text: string } => b.type === "text")
16
22
  .map((b) => b.text)
17
23
  .join("\n");
18
- const toolResult = content.find((b): b is { type: "tool_result"; tool_use_id: string } => b.type === "tool_result");
24
+ const toolResult = content.find(
25
+ (b): b is { type: "tool_result"; tool_use_id: string } =>
26
+ b.type === "tool_result",
27
+ );
19
28
  const toolCallId = toolResult?.tool_use_id;
20
29
  return { content: text, toolCallId };
21
30
  }
@@ -27,7 +36,9 @@ function parseMessages(req: AnthropicRequest): readonly Message[] {
27
36
  return {
28
37
  role: m.role,
29
38
  content: extracted.content,
30
- ...(extracted.toolCallId !== undefined && { toolCallId: extracted.toolCallId }),
39
+ ...(extracted.toolCallId !== undefined && {
40
+ toolCallId: extracted.toolCallId,
41
+ }),
31
42
  };
32
43
  });
33
44
  return [...system, ...conversation];
@@ -44,5 +55,13 @@ function parseTools(req: AnthropicRequest): readonly ToolDef[] | undefined {
44
55
 
45
56
  export function parseRequest(body: unknown, meta?: RequestMeta): MockRequest {
46
57
  const req = AnthropicRequestSchema.parse(body);
47
- return buildMockRequest("anthropic", req, parseMessages(req), parseTools(req), "claude-sonnet-4-6", body, meta);
58
+ return buildMockRequest(
59
+ "anthropic",
60
+ req,
61
+ parseMessages(req),
62
+ parseTools(req),
63
+ "claude-sonnet-4-6",
64
+ body,
65
+ meta,
66
+ );
48
67
  }
@@ -1,6 +1,9 @@
1
1
  import { z } from "zod";
2
2
 
3
- export { AnthropicRequestSchema, type AnthropicRequest } from "llm-schemas/anthropic";
3
+ export {
4
+ AnthropicRequestSchema,
5
+ type AnthropicRequest,
6
+ } from "llm-schemas/anthropic";
4
7
 
5
8
  const ResponseContentBlockSchema = z.object({
6
9
  type: z.string(),
@@ -28,18 +31,23 @@ export type AnthropicMessageStart = z.infer<typeof AnthropicMessageStartSchema>;
28
31
  export const AnthropicBlockEventSchema = z.object({
29
32
  index: z.number(),
30
33
  content_block: ResponseContentBlockSchema.optional(),
31
- delta: z.object({
32
- type: z.string(),
33
- text: z.string().optional(),
34
- thinking: z.string().optional(),
35
- partial_json: z.string().optional(),
36
- }).optional(),
34
+ delta: z
35
+ .object({
36
+ type: z.string(),
37
+ text: z.string().optional(),
38
+ thinking: z.string().optional(),
39
+ partial_json: z.string().optional(),
40
+ })
41
+ .optional(),
37
42
  });
38
43
 
39
44
  export type AnthropicBlockEvent = z.infer<typeof AnthropicBlockEventSchema>;
40
45
 
41
46
  export const AnthropicDeltaSchema = z.object({
42
- delta: z.object({ stop_reason: z.string(), stop_sequence: z.string().nullable() }),
47
+ delta: z.object({
48
+ stop_reason: z.string(),
49
+ stop_sequence: z.string().nullable(),
50
+ }),
43
51
  usage: z.object({ output_tokens: z.number() }),
44
52
  });
45
53
 
@@ -1,17 +1,49 @@
1
1
  import type { ReplyObject, ReplyOptions } from "../../types.js";
2
2
  import type { SSEChunk } from "../types.js";
3
- import { splitText, genId, toolId, shouldEmitText, finishReason, DEFAULT_USAGE } from "../parse-helpers.js";
3
+ import {
4
+ splitText,
5
+ genId,
6
+ toolId,
7
+ shouldEmitText,
8
+ finishReason,
9
+ DEFAULT_USAGE,
10
+ } from "../serialize-helpers.js";
4
11
 
5
- function contentBlock(index: number, startBlock: unknown, deltas: SSEChunk[]): SSEChunk[] {
12
+ function buildUsage(usage: { input: number; output: number }) {
13
+ return { input_tokens: usage.input, output_tokens: usage.output };
14
+ }
15
+
16
+ function contentBlock(
17
+ index: number,
18
+ startBlock: unknown,
19
+ deltas: SSEChunk[],
20
+ ): SSEChunk[] {
6
21
  return [
7
- { event: "content_block_start", data: JSON.stringify({ type: "content_block_start", index, content_block: startBlock }) },
22
+ {
23
+ event: "content_block_start",
24
+ data: JSON.stringify({
25
+ type: "content_block_start",
26
+ index,
27
+ content_block: startBlock,
28
+ }),
29
+ },
8
30
  ...deltas,
9
- { event: "content_block_stop", data: JSON.stringify({ type: "content_block_stop", index }) },
31
+ {
32
+ event: "content_block_stop",
33
+ data: JSON.stringify({ type: "content_block_stop", index }),
34
+ },
10
35
  ];
11
36
  }
12
37
 
13
38
  function delta(index: number, payload: Record<string, unknown>): SSEChunk {
14
- return { event: "content_block_delta", data: JSON.stringify({ type: "content_block_delta", index, delta: payload }) };
39
+ return {
40
+ event: "content_block_delta",
41
+ data: JSON.stringify({
42
+ type: "content_block_delta",
43
+ index,
44
+ delta: payload,
45
+ }),
46
+ };
15
47
  }
16
48
 
17
49
  function reasoningBlock(i: number, reasoning: string): SSEChunk[] {
@@ -24,68 +56,124 @@ function textBlock(i: number, text: string, chunkSize: number): SSEChunk[] {
24
56
  return contentBlock(
25
57
  i,
26
58
  { type: "text", text: "" },
27
- splitText(text, chunkSize).map((piece) => delta(i, { type: "text_delta", text: piece })),
59
+ splitText(text, chunkSize).map((piece) =>
60
+ delta(i, { type: "text_delta", text: piece }),
61
+ ),
28
62
  );
29
63
  }
30
64
 
31
- function toolBlocks(startIndex: number, tools: ReplyObject["tools"]): SSEChunk[] {
65
+ function toolBlocks(
66
+ startIndex: number,
67
+ tools: ReplyObject["tools"],
68
+ ): SSEChunk[] {
32
69
  return (tools ?? []).flatMap((tool, i) => {
33
70
  const idx = startIndex + i;
34
71
  const id = toolId(tool, "toolu", idx);
35
72
  return contentBlock(
36
73
  idx,
37
74
  { type: "tool_use", id, name: tool.name, input: {} },
38
- [delta(idx, { type: "input_json_delta", partial_json: JSON.stringify(tool.args) })],
75
+ [
76
+ delta(idx, {
77
+ type: "input_json_delta",
78
+ partial_json: JSON.stringify(tool.args),
79
+ }),
80
+ ],
39
81
  );
40
82
  });
41
83
  }
42
84
 
43
- export function serialize(reply: ReplyObject, model: string, options: ReplyOptions = {}): readonly SSEChunk[] {
85
+ export function serialize(
86
+ reply: ReplyObject,
87
+ model: string,
88
+ options: ReplyOptions = {},
89
+ ): readonly SSEChunk[] {
44
90
  const id = genId("msg");
45
91
  const usage = reply.usage ?? DEFAULT_USAGE;
46
92
  let idx = 0;
47
93
 
48
- const reasoningChunks = reply.reasoning ? reasoningBlock(idx++, reply.reasoning) : [];
49
- const textChunks = shouldEmitText(reply) ? textBlock(idx++, reply.text ?? "", options.chunkSize ?? 0) : [];
94
+ const reasoningChunks = reply.reasoning
95
+ ? reasoningBlock(idx++, reply.reasoning)
96
+ : [];
97
+ const textChunks = shouldEmitText(reply)
98
+ ? textBlock(idx++, reply.text ?? "", options.chunkSize ?? 0)
99
+ : [];
50
100
  const toolChunks = toolBlocks(idx, reply.tools);
51
101
 
52
102
  return [
53
- { event: "message_start", data: JSON.stringify({
54
- type: "message_start",
55
- message: { id, type: "message", role: "assistant", model, content: [], stop_reason: null, usage: { input_tokens: usage.input, output_tokens: 0 } },
56
- })},
103
+ {
104
+ event: "message_start",
105
+ data: JSON.stringify({
106
+ type: "message_start",
107
+ message: {
108
+ id,
109
+ type: "message",
110
+ role: "assistant",
111
+ model,
112
+ content: [],
113
+ stop_reason: null,
114
+ usage: { ...buildUsage(usage), output_tokens: 0 },
115
+ },
116
+ }),
117
+ },
57
118
  ...reasoningChunks,
58
119
  ...textChunks,
59
120
  ...toolChunks,
60
- { event: "message_delta", data: JSON.stringify({
61
- type: "message_delta",
62
- delta: { stop_reason: finishReason(reply, "tool_use", "end_turn"), stop_sequence: null },
63
- usage: { output_tokens: usage.output },
64
- })},
121
+ {
122
+ event: "message_delta",
123
+ data: JSON.stringify({
124
+ type: "message_delta",
125
+ delta: {
126
+ stop_reason: finishReason(reply, "tool_use", "end_turn"),
127
+ stop_sequence: null,
128
+ },
129
+ usage: { output_tokens: usage.output },
130
+ }),
131
+ },
65
132
  { event: "message_stop", data: JSON.stringify({ type: "message_stop" }) },
66
133
  ];
67
134
  }
68
135
 
69
- export function serializeComplete(reply: ReplyObject, model: string): unknown {
136
+ export function serializeComplete(
137
+ reply: ReplyObject,
138
+ model: string,
139
+ ): Record<string, unknown> {
70
140
  const id = genId("msg");
71
141
  const usage = reply.usage ?? DEFAULT_USAGE;
72
142
 
73
143
  const content: unknown[] = [
74
- ...(reply.reasoning ? [{ type: "thinking", thinking: reply.reasoning }] : []),
75
- ...(shouldEmitText(reply) ? [{ type: "text", text: reply.text ?? "" }] : []),
144
+ ...(reply.reasoning
145
+ ? [{ type: "thinking", thinking: reply.reasoning }]
146
+ : []),
147
+ ...(shouldEmitText(reply)
148
+ ? [{ type: "text", text: reply.text ?? "" }]
149
+ : []),
76
150
  ...(reply.tools ?? []).map((tool) => ({
77
- type: "tool_use", id: toolId(tool, "toolu", 0), name: tool.name, input: tool.args,
151
+ type: "tool_use",
152
+ id: toolId(tool, "toolu", 0),
153
+ name: tool.name,
154
+ input: tool.args,
78
155
  })),
79
156
  ];
80
157
 
81
158
  return {
82
- id, type: "message", role: "assistant", model, content,
159
+ id,
160
+ type: "message",
161
+ role: "assistant",
162
+ model,
163
+ content,
83
164
  stop_reason: finishReason(reply, "tool_use", "end_turn"),
84
165
  stop_sequence: null,
85
- usage: { input_tokens: usage.input, output_tokens: usage.output },
166
+ usage: buildUsage(usage),
86
167
  };
87
168
  }
88
169
 
89
- export function serializeError(error: { status: number; message: string; type?: string }): unknown {
90
- return { type: "error", error: { type: error.type ?? "api_error", message: error.message } };
170
+ export function serializeError(error: {
171
+ status: number;
172
+ message: string;
173
+ type?: string;
174
+ }): Record<string, unknown> {
175
+ return {
176
+ type: "error",
177
+ error: { type: error.type ?? "api_error", message: error.message },
178
+ };
91
179
  }
@@ -1,5 +1,5 @@
1
1
  import type { Format } from "../types.js";
2
- import { isStreaming } from "../parse-helpers.js";
2
+ import { isStreaming } from "../request-helpers.js";
3
3
  import { parseRequest } from "./parse.js";
4
4
  import { serialize, serializeComplete, serializeError } from "./serialize.js";
5
5
 
@@ -1,8 +1,10 @@
1
1
  import type { MockRequest, Message, ToolDef } from "../../types.js";
2
- import { buildMockRequest, type RequestMeta } from "../parse-helpers.js";
2
+ import { buildMockRequest, type RequestMeta } from "../request-helpers.js";
3
3
  import { OpenAIRequestSchema, type OpenAIRequest } from "./schema.js";
4
4
 
5
- function extractContent(content: OpenAIRequest["messages"][number]["content"]): string {
5
+ function extractContent(
6
+ content: OpenAIRequest["messages"][number]["content"],
7
+ ): string {
6
8
  if (content == null) return "";
7
9
  if (typeof content === "string") return content;
8
10
  return content
@@ -30,5 +32,13 @@ function parseTools(req: OpenAIRequest): readonly ToolDef[] | undefined {
30
32
 
31
33
  export function parseRequest(body: unknown, meta?: RequestMeta): MockRequest {
32
34
  const req = OpenAIRequestSchema.parse(body);
33
- return buildMockRequest("openai", req, parseMessages(req), parseTools(req), "gpt-5.4", body, meta);
35
+ return buildMockRequest(
36
+ "openai",
37
+ req,
38
+ parseMessages(req),
39
+ parseTools(req),
40
+ "gpt-5.4",
41
+ body,
42
+ meta,
43
+ );
34
44
  }
@@ -1,6 +1,9 @@
1
1
  import { z } from "zod";
2
2
 
3
- export { OpenAIRequestSchema, type OpenAIRequest } from "llm-schemas/openai/chat-completions";
3
+ export {
4
+ OpenAIRequestSchema,
5
+ type OpenAIRequest,
6
+ } from "llm-schemas/openai/chat-completions";
4
7
 
5
8
  const ToolCallResponseSchema = z.object({
6
9
  id: z.string(),
@@ -12,16 +15,20 @@ const UsageSchema = z.object({
12
15
  prompt_tokens: z.number(),
13
16
  completion_tokens: z.number(),
14
17
  total_tokens: z.number(),
15
- prompt_tokens_details: z.object({
16
- cached_tokens: z.number().optional(),
17
- audio_tokens: z.number().optional(),
18
- }).optional(),
19
- completion_tokens_details: z.object({
20
- reasoning_tokens: z.number().optional(),
21
- audio_tokens: z.number().optional(),
22
- accepted_prediction_tokens: z.number().optional(),
23
- rejected_prediction_tokens: z.number().optional(),
24
- }).optional(),
18
+ prompt_tokens_details: z
19
+ .object({
20
+ cached_tokens: z.number().optional(),
21
+ audio_tokens: z.number().optional(),
22
+ })
23
+ .optional(),
24
+ completion_tokens_details: z
25
+ .object({
26
+ reasoning_tokens: z.number().optional(),
27
+ audio_tokens: z.number().optional(),
28
+ accepted_prediction_tokens: z.number().optional(),
29
+ rejected_prediction_tokens: z.number().optional(),
30
+ })
31
+ .optional(),
25
32
  });
26
33
 
27
34
  export const OpenAIChunkSchema = z.object({
@@ -31,16 +38,20 @@ export const OpenAIChunkSchema = z.object({
31
38
  model: z.string(),
32
39
  system_fingerprint: z.string().nullable().optional(),
33
40
  service_tier: z.string().optional(),
34
- choices: z.array(z.object({
35
- index: z.number(),
36
- delta: z.object({
37
- role: z.string(),
38
- content: z.string(),
39
- tool_calls: z.array(ToolCallResponseSchema),
40
- }).partial(),
41
- logprobs: z.unknown().nullable().optional(),
42
- finish_reason: z.string().nullable(),
43
- })),
41
+ choices: z.array(
42
+ z.object({
43
+ index: z.number(),
44
+ delta: z
45
+ .object({
46
+ role: z.string(),
47
+ content: z.string(),
48
+ tool_calls: z.array(ToolCallResponseSchema),
49
+ })
50
+ .partial(),
51
+ logprobs: z.unknown().nullable().optional(),
52
+ finish_reason: z.string().nullable(),
53
+ }),
54
+ ),
44
55
  usage: UsageSchema.nullable().optional(),
45
56
  });
46
57
 
@@ -53,16 +64,18 @@ export const OpenAICompleteSchema = z.object({
53
64
  model: z.string(),
54
65
  system_fingerprint: z.string().nullable().optional(),
55
66
  service_tier: z.string().optional(),
56
- choices: z.array(z.object({
57
- index: z.number(),
58
- message: z.object({
59
- role: z.string(),
60
- content: z.string().nullable(),
61
- tool_calls: z.array(ToolCallResponseSchema).optional(),
67
+ choices: z.array(
68
+ z.object({
69
+ index: z.number(),
70
+ message: z.object({
71
+ role: z.string(),
72
+ content: z.string().nullable(),
73
+ tool_calls: z.array(ToolCallResponseSchema).optional(),
74
+ }),
75
+ logprobs: z.unknown().nullable().optional(),
76
+ finish_reason: z.string(),
62
77
  }),
63
- logprobs: z.unknown().nullable().optional(),
64
- finish_reason: z.string(),
65
- })),
78
+ ),
66
79
  usage: UsageSchema.optional(),
67
80
  });
68
81