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
@@ -0,0 +1,315 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ splitText,
4
+ genId,
5
+ toolId,
6
+ shouldEmitText,
7
+ finishReason,
8
+ MS_PER_SECOND,
9
+ DEFAULT_USAGE,
10
+ } from "../../src/formats/serialize-helpers.js";
11
+ import {
12
+ isStreaming,
13
+ buildMockRequest,
14
+ } from "../../src/formats/request-helpers.js";
15
+ import type { ReplyObject } from "../../src/types.js";
16
+
17
+ describe("parse-helpers", () => {
18
+ describe("constants", () => {
19
+ it("MS_PER_SECOND is 1000", () => {
20
+ expect(MS_PER_SECOND).toBe(1000);
21
+ });
22
+
23
+ it("DEFAULT_USAGE has expected shape", () => {
24
+ expect(DEFAULT_USAGE).toEqual({ input: 10, output: 5 });
25
+ });
26
+ });
27
+
28
+ describe("splitText", () => {
29
+ it("returns the full string when chunkSize is 0", () => {
30
+ expect(splitText("hello", 0)).toEqual(["hello"]);
31
+ });
32
+
33
+ it("returns the full string when chunkSize is negative", () => {
34
+ expect(splitText("hello", -1)).toEqual(["hello"]);
35
+ });
36
+
37
+ it("returns the full string when text fits in one chunk", () => {
38
+ expect(splitText("hello", 10)).toEqual(["hello"]);
39
+ });
40
+
41
+ it("returns the full string when chunkSize equals text length", () => {
42
+ expect(splitText("hello", 5)).toEqual(["hello"]);
43
+ });
44
+
45
+ it("splits text into equal chunks", () => {
46
+ expect(splitText("abcdef", 2)).toEqual(["ab", "cd", "ef"]);
47
+ });
48
+
49
+ it("handles a remainder chunk", () => {
50
+ expect(splitText("abcde", 2)).toEqual(["ab", "cd", "e"]);
51
+ });
52
+
53
+ it("splits into single characters with chunkSize 1", () => {
54
+ expect(splitText("abc", 1)).toEqual(["a", "b", "c"]);
55
+ });
56
+
57
+ it("returns single-element array for empty string", () => {
58
+ expect(splitText("", 5)).toEqual([""]);
59
+ });
60
+ });
61
+
62
+ describe("genId", () => {
63
+ it("starts with the given prefix", () => {
64
+ const id = genId("chatcmpl");
65
+ expect(id).toMatch(/^chatcmpl_/);
66
+ });
67
+
68
+ it("generates unique ids", () => {
69
+ const a = genId("msg");
70
+ const b = genId("msg");
71
+ // Could collide if Date.now() returns the same ms, but format is still valid
72
+ expect(a).toMatch(/^msg_[a-z0-9]+$/);
73
+ expect(b).toMatch(/^msg_[a-z0-9]+$/);
74
+ });
75
+ });
76
+
77
+ describe("toolId", () => {
78
+ it("uses the tool's own id when present", () => {
79
+ expect(toolId({ id: "call_abc" }, "call", 0)).toBe("call_abc");
80
+ });
81
+
82
+ it("generates an id when tool has no id", () => {
83
+ const id = toolId({}, "call", 2);
84
+ expect(id).toMatch(/^call_[a-z0-9]+_2$/);
85
+ });
86
+
87
+ it("generates an id when tool id is undefined", () => {
88
+ const id = toolId({ id: undefined }, "call", 0);
89
+ expect(id).toMatch(/^call_[a-z0-9]+_0$/);
90
+ });
91
+ });
92
+
93
+ describe("shouldEmitText", () => {
94
+ it("returns true when reply has text", () => {
95
+ expect(shouldEmitText({ text: "hello" })).toBe(true);
96
+ });
97
+
98
+ it("returns true when reply has no text, no tools, no reasoning", () => {
99
+ expect(shouldEmitText({})).toBe(true);
100
+ });
101
+
102
+ it("returns false when reply has only tools", () => {
103
+ expect(shouldEmitText({ tools: [{ name: "fn", args: {} }] })).toBe(false);
104
+ });
105
+
106
+ it("returns false when reply has only reasoning", () => {
107
+ expect(shouldEmitText({ reasoning: "thinking..." })).toBe(false);
108
+ });
109
+
110
+ it("returns true when reply has text and tools", () => {
111
+ expect(
112
+ shouldEmitText({ text: "hi", tools: [{ name: "fn", args: {} }] }),
113
+ ).toBe(true);
114
+ });
115
+
116
+ it("returns true for empty text with no tools or reasoning", () => {
117
+ expect(shouldEmitText({ text: "" })).toBe(true);
118
+ });
119
+ });
120
+
121
+ describe("finishReason", () => {
122
+ it("returns onTools when tools are present", () => {
123
+ const reply: ReplyObject = { tools: [{ name: "fn", args: {} }] };
124
+ expect(finishReason(reply, "tool_calls", "stop")).toBe("tool_calls");
125
+ });
126
+
127
+ it("returns onStop when no tools", () => {
128
+ expect(finishReason({ text: "hi" }, "tool_calls", "stop")).toBe("stop");
129
+ });
130
+
131
+ it("returns onStop when tools array is empty", () => {
132
+ expect(finishReason({ tools: [] }, "tool_calls", "stop")).toBe("stop");
133
+ });
134
+
135
+ it("returns onStop when tools is undefined", () => {
136
+ expect(finishReason({}, "tool_calls", "stop")).toBe("stop");
137
+ });
138
+ });
139
+
140
+ describe("isStreaming", () => {
141
+ it("returns true when stream is true", () => {
142
+ expect(isStreaming({ stream: true })).toBe(true);
143
+ });
144
+
145
+ it("returns false when stream is false", () => {
146
+ expect(isStreaming({ stream: false })).toBe(false);
147
+ });
148
+
149
+ it("returns true when stream is absent", () => {
150
+ expect(isStreaming({})).toBe(true);
151
+ });
152
+
153
+ it("returns true for null body", () => {
154
+ expect(isStreaming(null)).toBe(true);
155
+ });
156
+
157
+ it("returns true for non-object body", () => {
158
+ expect(isStreaming("not an object")).toBe(true);
159
+ });
160
+
161
+ it("returns true for undefined body", () => {
162
+ expect(isStreaming(undefined)).toBe(true);
163
+ });
164
+ });
165
+
166
+ describe("buildMockRequest", () => {
167
+ it("builds a minimal request with defaults", () => {
168
+ const result = buildMockRequest(
169
+ "openai",
170
+ {},
171
+ [{ role: "user", content: "hello" }],
172
+ undefined,
173
+ "gpt-4",
174
+ {
175
+ messages: [],
176
+ },
177
+ );
178
+
179
+ expect(result.format).toBe("openai");
180
+ expect(result.model).toBe("gpt-4");
181
+ expect(result.streaming).toBe(true);
182
+ expect(result.lastMessage).toBe("hello");
183
+ expect(result.systemMessage).toBe("");
184
+ expect(result.tools).toBeUndefined();
185
+ expect(result.toolNames).toEqual([]);
186
+ expect(result.lastToolCallId).toBeUndefined();
187
+ expect(result.headers).toEqual({});
188
+ expect(result.path).toBe("");
189
+ });
190
+
191
+ it("uses model from body when provided", () => {
192
+ const result = buildMockRequest(
193
+ "anthropic",
194
+ { model: "claude-sonnet" },
195
+ [],
196
+ undefined,
197
+ "default-model",
198
+ {},
199
+ );
200
+ expect(result.model).toBe("claude-sonnet");
201
+ });
202
+
203
+ it("falls back to default model when body model is empty string", () => {
204
+ const result = buildMockRequest(
205
+ "openai",
206
+ { model: "" },
207
+ [],
208
+ undefined,
209
+ "gpt-4",
210
+ {},
211
+ );
212
+ expect(result.model).toBe("gpt-4");
213
+ });
214
+
215
+ it("extracts last user message", () => {
216
+ const messages = [
217
+ { role: "user" as const, content: "first" },
218
+ { role: "assistant" as const, content: "reply" },
219
+ { role: "user" as const, content: "second" },
220
+ ];
221
+ const result = buildMockRequest(
222
+ "openai",
223
+ {},
224
+ messages,
225
+ undefined,
226
+ "m",
227
+ {},
228
+ );
229
+ expect(result.lastMessage).toBe("second");
230
+ });
231
+
232
+ it("extracts system message", () => {
233
+ const messages = [
234
+ { role: "system" as const, content: "be helpful" },
235
+ { role: "user" as const, content: "hi" },
236
+ ];
237
+ const result = buildMockRequest(
238
+ "openai",
239
+ {},
240
+ messages,
241
+ undefined,
242
+ "m",
243
+ {},
244
+ );
245
+ expect(result.systemMessage).toBe("be helpful");
246
+ });
247
+
248
+ it("extracts tool names", () => {
249
+ const tools = [
250
+ { name: "get_weather", parameters: {} },
251
+ { name: "search", parameters: {} },
252
+ ];
253
+ const result = buildMockRequest("openai", {}, [], tools, "m", {});
254
+ expect(result.toolNames).toEqual(["get_weather", "search"]);
255
+ });
256
+
257
+ it("extracts last tool call id", () => {
258
+ const messages = [
259
+ { role: "tool" as const, content: "result1", toolCallId: "call_1" },
260
+ { role: "tool" as const, content: "result2", toolCallId: "call_2" },
261
+ ];
262
+ const result = buildMockRequest(
263
+ "openai",
264
+ {},
265
+ messages,
266
+ undefined,
267
+ "m",
268
+ {},
269
+ );
270
+ expect(result.lastToolCallId).toBe("call_2");
271
+ });
272
+
273
+ it("sets streaming to false when stream is false", () => {
274
+ const result = buildMockRequest(
275
+ "openai",
276
+ { stream: false },
277
+ [],
278
+ undefined,
279
+ "m",
280
+ {},
281
+ );
282
+ expect(result.streaming).toBe(false);
283
+ });
284
+
285
+ it("uses provided meta for headers and path", () => {
286
+ const meta = {
287
+ headers: { authorization: "Bearer sk-test" },
288
+ path: "/v1/chat/completions",
289
+ };
290
+ const result = buildMockRequest(
291
+ "openai",
292
+ {},
293
+ [],
294
+ undefined,
295
+ "m",
296
+ {},
297
+ meta,
298
+ );
299
+ expect(result.headers).toEqual({ authorization: "Bearer sk-test" });
300
+ expect(result.path).toBe("/v1/chat/completions");
301
+ });
302
+
303
+ it("returns empty lastMessage when no user messages", () => {
304
+ const result = buildMockRequest(
305
+ "openai",
306
+ {},
307
+ [{ role: "system" as const, content: "sys" }],
308
+ undefined,
309
+ "m",
310
+ {},
311
+ );
312
+ expect(result.lastMessage).toBe("");
313
+ });
314
+ });
315
+ });
@@ -1,6 +1,10 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { responsesFormat } from "../../src/formats/responses/index.js";
3
- import type { ResponsesEvent, ResponsesComplete, ResponsesError } from "../../src/formats/responses/schema.js";
3
+ import type {
4
+ ResponsesEvent,
5
+ ResponsesComplete,
6
+ ResponsesError,
7
+ } from "../../src/formats/responses/schema.js";
4
8
 
5
9
  function parse<T>(chunk: { data: string }): T {
6
10
  return JSON.parse(chunk.data) as T;
@@ -9,7 +13,10 @@ function parse<T>(chunk: { data: string }): T {
9
13
  describe("Responses Format", () => {
10
14
  describe("parseRequest", () => {
11
15
  it("parses string input", () => {
12
- const req = responsesFormat.parseRequest({ model: "codex-mini", input: "Hello world" });
16
+ const req = responsesFormat.parseRequest({
17
+ model: "codex-mini",
18
+ input: "Hello world",
19
+ });
13
20
  expect(req.format).toBe("responses");
14
21
  expect(req.model).toBe("codex-mini");
15
22
  expect(req.lastMessage).toBe("Hello world");
@@ -40,7 +47,12 @@ describe("Responses Format", () => {
40
47
  it("parses content block arrays", () => {
41
48
  const req = responsesFormat.parseRequest({
42
49
  model: "codex-mini",
43
- input: [{ role: "user", content: [{ type: "input_text", text: "Hello there" }] }],
50
+ input: [
51
+ {
52
+ role: "user",
53
+ content: [{ type: "input_text", text: "Hello there" }],
54
+ },
55
+ ],
44
56
  });
45
57
  expect(req.lastMessage).toBe("Hello there");
46
58
  });
@@ -49,7 +61,14 @@ describe("Responses Format", () => {
49
61
  const req = responsesFormat.parseRequest({
50
62
  model: "codex-mini",
51
63
  input: "read file",
52
- tools: [{ type: "function", name: "read_file", description: "Read", parameters: {} }],
64
+ tools: [
65
+ {
66
+ type: "function",
67
+ name: "read_file",
68
+ description: "Read",
69
+ parameters: {},
70
+ },
71
+ ],
53
72
  });
54
73
  expect(req.tools).toHaveLength(1);
55
74
  expect(req.toolNames).toEqual(["read_file"]);
@@ -60,7 +79,11 @@ describe("Responses Format", () => {
60
79
  model: "codex-mini",
61
80
  input: [
62
81
  { role: "user", content: "hi" },
63
- { type: "function_call_output", call_id: "call_abc", output: "result" },
82
+ {
83
+ type: "function_call_output",
84
+ call_id: "call_abc",
85
+ output: "result",
86
+ },
64
87
  ],
65
88
  });
66
89
  expect(req.lastToolCallId).toBe("call_abc");
@@ -69,10 +92,15 @@ describe("Responses Format", () => {
69
92
  it("handles content blocks with non-text types (image, etc.)", () => {
70
93
  const req = responsesFormat.parseRequest({
71
94
  model: "codex-mini",
72
- input: [{ role: "user", content: [
73
- { type: "image_url", url: "https://example.com/img.png" },
74
- { type: "input_text", text: "describe this" },
75
- ]}],
95
+ input: [
96
+ {
97
+ role: "user",
98
+ content: [
99
+ { type: "image_url", url: "https://example.com/img.png" },
100
+ { type: "input_text", text: "describe this" },
101
+ ],
102
+ },
103
+ ],
76
104
  });
77
105
  expect(req.lastMessage).toBe("describe this");
78
106
  });
@@ -100,10 +128,7 @@ describe("Responses Format", () => {
100
128
  const req = responsesFormat.parseRequest({
101
129
  model: "codex-mini",
102
130
  input: "hi",
103
- tools: [
104
- { type: "function", name: "run_code" },
105
- { type: "web_search" },
106
- ],
131
+ tools: [{ type: "function", name: "run_code" }, { type: "web_search" }],
107
132
  });
108
133
  expect(req.tools).toHaveLength(1);
109
134
  expect(req.tools![0]!.name).toBe("run_code");
@@ -114,17 +139,23 @@ describe("Responses Format", () => {
114
139
  it("starts with response.created and response.in_progress", () => {
115
140
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
116
141
  expect(parse<ResponsesEvent>(chunks[0]!).type).toBe("response.created");
117
- expect(parse<ResponsesEvent>(chunks[1]!).type).toBe("response.in_progress");
142
+ expect(parse<ResponsesEvent>(chunks[1]!).type).toBe(
143
+ "response.in_progress",
144
+ );
118
145
  });
119
146
 
120
147
  it("ends with response.completed", () => {
121
148
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
122
- expect(parse<ResponsesEvent>(chunks.at(-1)!).type).toBe("response.completed");
149
+ expect(parse<ResponsesEvent>(chunks.at(-1)!).type).toBe(
150
+ "response.completed",
151
+ );
123
152
  });
124
153
 
125
154
  it("assigns incrementing sequence_number to every event", () => {
126
155
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
127
- const seqNumbers = chunks.map((c) => parse<ResponsesEvent>(c).sequence_number!);
156
+ const seqNumbers = chunks.map(
157
+ (c) => parse<ResponsesEvent>(c).sequence_number!,
158
+ );
128
159
  for (let i = 1; i < seqNumbers.length; i++) {
129
160
  expect(seqNumbers[i]).toBe(seqNumbers[i - 1]! + 1);
130
161
  }
@@ -135,14 +166,17 @@ describe("Responses Format", () => {
135
166
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
136
167
  const created = parse<ResponsesEvent>(chunks[0]!).response?.created_at;
137
168
  const inProgress = parse<ResponsesEvent>(chunks[1]!).response?.created_at;
138
- const completed = parse<ResponsesEvent>(chunks.at(-1)!).response?.created_at;
169
+ const completed = parse<ResponsesEvent>(chunks.at(-1)!).response
170
+ ?.created_at;
139
171
  expect(created).toBe(inProgress);
140
172
  expect(created).toBe(completed);
141
173
  });
142
174
 
143
175
  it("produces text delta events with item_id", () => {
144
176
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
145
- const delta = chunks.find((c) => parse<ResponsesEvent>(c).type === "response.output_text.delta");
177
+ const delta = chunks.find(
178
+ (c) => parse<ResponsesEvent>(c).type === "response.output_text.delta",
179
+ );
146
180
  expect(delta).toBeDefined();
147
181
  const data = parse<ResponsesEvent>(delta!);
148
182
  expect(data.delta).toBe("Hello");
@@ -153,26 +187,34 @@ describe("Responses Format", () => {
153
187
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
154
188
  const added = chunks.find((c) => {
155
189
  const d = parse<ResponsesEvent>(c);
156
- return d.type === "response.output_item.added" && d.item?.type === "message";
190
+ return (
191
+ d.type === "response.output_item.added" && d.item?.type === "message"
192
+ );
157
193
  });
158
194
  expect(parse<ResponsesEvent>(added!).item?.status).toBe("in_progress");
159
195
 
160
196
  const done = chunks.find((c) => {
161
197
  const d = parse<ResponsesEvent>(c);
162
- return d.type === "response.output_item.done" && d.item?.type === "message";
198
+ return (
199
+ d.type === "response.output_item.done" && d.item?.type === "message"
200
+ );
163
201
  });
164
202
  expect(parse<ResponsesEvent>(done!).item?.status).toBe("completed");
165
203
  });
166
204
 
167
205
  it("includes annotations on output_text parts", () => {
168
206
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
169
- const partAdded = chunks.find((c) => parse<ResponsesEvent>(c).type === "response.content_part.added");
207
+ const partAdded = chunks.find(
208
+ (c) => parse<ResponsesEvent>(c).type === "response.content_part.added",
209
+ );
170
210
  expect(parse<ResponsesEvent>(partAdded!).part?.annotations).toEqual([]);
171
211
  });
172
212
 
173
213
  it("includes content_part.done event with full text", () => {
174
214
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
175
- const partDone = chunks.find((c) => parse<ResponsesEvent>(c).type === "response.content_part.done");
215
+ const partDone = chunks.find(
216
+ (c) => parse<ResponsesEvent>(c).type === "response.content_part.done",
217
+ );
176
218
  expect(partDone).toBeDefined();
177
219
  expect(parse<ResponsesEvent>(partDone!).part?.text).toBe("Hello");
178
220
  });
@@ -189,7 +231,9 @@ describe("Responses Format", () => {
189
231
  expect(types).toContain("response.reasoning_summary_text.done");
190
232
  expect(types).toContain("response.reasoning_summary_part.done");
191
233
 
192
- const reasoningDone = types.indexOf("response.reasoning_summary_text.done");
234
+ const reasoningDone = types.indexOf(
235
+ "response.reasoning_summary_text.done",
236
+ );
193
237
  const textDelta = types.indexOf("response.output_text.delta");
194
238
  expect(reasoningDone).toBeLessThan(textDelta);
195
239
  });
@@ -205,9 +249,14 @@ describe("Responses Format", () => {
205
249
  });
206
250
 
207
251
  it("accumulates text in completed output", () => {
208
- const chunks = responsesFormat.serialize({ text: "hello world" }, "codex-mini");
252
+ const chunks = responsesFormat.serialize(
253
+ { text: "hello world" },
254
+ "codex-mini",
255
+ );
209
256
  const completed = parse<ResponsesEvent>(chunks.at(-1)!);
210
- expect(completed.response?.output[0]!.content?.[0]?.text).toBe("hello world");
257
+ expect(completed.response?.output[0]!.content?.[0]?.text).toBe(
258
+ "hello world",
259
+ );
211
260
  });
212
261
 
213
262
  it("produces function_call events for tool calls", () => {
@@ -217,7 +266,10 @@ describe("Responses Format", () => {
217
266
  );
218
267
  const fnAdded = chunks.find((c) => {
219
268
  const d = parse<ResponsesEvent>(c);
220
- return d.type === "response.output_item.added" && d.item?.type === "function_call";
269
+ return (
270
+ d.type === "response.output_item.added" &&
271
+ d.item?.type === "function_call"
272
+ );
221
273
  });
222
274
  expect(fnAdded).toBeDefined();
223
275
  const item = parse<ResponsesEvent>(fnAdded!).item!;
@@ -242,7 +294,10 @@ describe("Responses Format", () => {
242
294
 
243
295
  describe("serializeComplete (non-streaming)", () => {
244
296
  it("produces correct top-level structure", () => {
245
- const result = responsesFormat.serializeComplete({ text: "Hello" }, "codex-mini") as ResponsesComplete;
297
+ const result = responsesFormat.serializeComplete(
298
+ { text: "Hello" },
299
+ "codex-mini",
300
+ ) as ResponsesComplete;
246
301
  expect(result.object).toBe("response");
247
302
  expect(result.status).toBe("completed");
248
303
  expect(result.model).toBe("codex-mini");
@@ -250,7 +305,10 @@ describe("Responses Format", () => {
250
305
  });
251
306
 
252
307
  it("includes message output item with status and annotations", () => {
253
- const result = responsesFormat.serializeComplete({ text: "Hello, world!" }, "codex-mini") as ResponsesComplete;
308
+ const result = responsesFormat.serializeComplete(
309
+ { text: "Hello, world!" },
310
+ "codex-mini",
311
+ ) as ResponsesComplete;
254
312
  const msg = result.output[0]!;
255
313
  expect(msg.type).toBe("message");
256
314
  expect(msg.status).toBe("completed");
@@ -275,10 +333,10 @@ describe("Responses Format", () => {
275
333
  "codex-mini",
276
334
  ) as ResponsesComplete;
277
335
  const fnCall = result.output.find((o) => o.type === "function_call");
278
- expect(fnCall).toBeDefined();
279
- expect(fnCall!.name).toBe("read_file");
280
- expect(fnCall!.status).toBe("completed");
281
- expect(fnCall!.call_id).toBeTypeOf("string");
336
+ if (!fnCall) throw new Error("expected function_call output");
337
+ expect(fnCall.name).toBe("read_file");
338
+ expect(fnCall.status).toBe("completed");
339
+ expect(fnCall.call_id).toBeTypeOf("string");
282
340
  });
283
341
 
284
342
  it("includes usage tokens", () => {
@@ -286,13 +344,20 @@ describe("Responses Format", () => {
286
344
  { text: "hi", usage: { input: 20, output: 15 } },
287
345
  "codex-mini",
288
346
  ) as ResponsesComplete;
289
- expect(result.usage).toEqual({ input_tokens: 20, output_tokens: 15, total_tokens: 35 });
347
+ expect(result.usage).toEqual({
348
+ input_tokens: 20,
349
+ output_tokens: 15,
350
+ total_tokens: 35,
351
+ });
290
352
  });
291
353
  });
292
354
 
293
355
  describe("serializeError", () => {
294
356
  it("produces Responses error format", () => {
295
- const result = responsesFormat.serializeError({ status: 500, message: "Internal error" }) as ResponsesError;
357
+ const result = responsesFormat.serializeError({
358
+ status: 500,
359
+ message: "Internal error",
360
+ }) as ResponsesError;
296
361
  expect(result.error.message).toBe("Internal error");
297
362
  });
298
363
  });
@@ -0,0 +1,18 @@
1
+ import type { MockRequest } from "../../src/types.js";
2
+
3
+ export function makeReq(overrides: Partial<MockRequest> = {}): MockRequest {
4
+ return {
5
+ format: "openai",
6
+ model: "gpt-5.4",
7
+ streaming: true,
8
+ messages: [{ role: "user", content: "hello" }],
9
+ lastMessage: "hello",
10
+ systemMessage: "",
11
+ toolNames: [],
12
+ lastToolCallId: undefined,
13
+ raw: {},
14
+ headers: {},
15
+ path: "/v1/chat/completions",
16
+ ...overrides,
17
+ };
18
+ }