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.
- package/.claude/skills/desloppify/SKILL.md +308 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +242 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +248 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +248 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +311 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +17 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +255 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +22 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +20 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +20 -0
- package/.desloppify/query.json +284 -0
- package/.desloppify/review_packet_blind.json +1303 -0
- package/.desloppify/review_packets/holistic_packet_20260315_000339.json +1471 -0
- package/.desloppify/state-typescript.json +5114 -0
- package/.desloppify/state-typescript.json.bak +5108 -0
- package/.editorconfig +12 -0
- package/.github/workflows/test.yml +3 -0
- package/.oxfmtrc.json +9 -0
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/formats/anthropic/index.js +1 -1
- package/dist/formats/anthropic/index.js.map +1 -1
- package/dist/formats/anthropic/parse.d.ts +1 -1
- package/dist/formats/anthropic/parse.d.ts.map +1 -1
- package/dist/formats/anthropic/parse.js +1 -1
- package/dist/formats/anthropic/parse.js.map +1 -1
- package/dist/formats/anthropic/serialize.d.ts +2 -2
- package/dist/formats/anthropic/serialize.d.ts.map +1 -1
- package/dist/formats/anthropic/serialize.js +6 -3
- package/dist/formats/anthropic/serialize.js.map +1 -1
- package/dist/formats/openai/index.js +1 -1
- package/dist/formats/openai/index.js.map +1 -1
- package/dist/formats/openai/parse.d.ts +1 -1
- package/dist/formats/openai/parse.d.ts.map +1 -1
- package/dist/formats/openai/parse.js +1 -1
- package/dist/formats/openai/parse.js.map +1 -1
- package/dist/formats/openai/serialize.d.ts +2 -2
- package/dist/formats/openai/serialize.d.ts.map +1 -1
- package/dist/formats/openai/serialize.js +12 -15
- package/dist/formats/openai/serialize.js.map +1 -1
- package/dist/formats/request-helpers.d.ts +13 -0
- package/dist/formats/request-helpers.d.ts.map +1 -0
- package/dist/formats/request-helpers.js +28 -0
- package/dist/formats/request-helpers.js.map +1 -0
- package/dist/formats/responses/index.js +1 -1
- package/dist/formats/responses/index.js.map +1 -1
- package/dist/formats/responses/parse.d.ts +1 -1
- package/dist/formats/responses/parse.d.ts.map +1 -1
- package/dist/formats/responses/parse.js +1 -1
- package/dist/formats/responses/parse.js.map +1 -1
- package/dist/formats/responses/schema.d.ts +1 -20
- package/dist/formats/responses/schema.d.ts.map +1 -1
- package/dist/formats/responses/schema.js.map +1 -1
- package/dist/formats/responses/serialize.d.ts +2 -2
- package/dist/formats/responses/serialize.d.ts.map +1 -1
- package/dist/formats/responses/serialize.js +6 -3
- package/dist/formats/responses/serialize.js.map +1 -1
- package/dist/formats/serialize-helpers.d.ts +14 -0
- package/dist/formats/serialize-helpers.d.ts.map +1 -0
- package/dist/formats/serialize-helpers.js +25 -0
- package/dist/formats/serialize-helpers.js.map +1 -0
- package/dist/formats/types.d.ts +3 -3
- package/dist/formats/types.d.ts.map +1 -1
- package/dist/loader.d.ts +3 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +6 -9
- package/dist/loader.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +17 -23
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.d.ts.map +1 -1
- package/dist/mock-server.js +8 -15
- package/dist/mock-server.js.map +1 -1
- package/dist/route-handler.d.ts +2 -1
- package/dist/route-handler.d.ts.map +1 -1
- package/dist/rule-engine.d.ts +12 -1
- package/dist/rule-engine.d.ts.map +1 -1
- package/dist/rule-engine.js +14 -0
- package/dist/rule-engine.js.map +1 -1
- package/dist/types/reply.d.ts +6 -10
- package/dist/types/reply.d.ts.map +1 -1
- package/dist/types/request.d.ts +7 -11
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/rule.d.ts +3 -10
- package/dist/types/rule.d.ts.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/scorecard.png +0 -0
- package/src/cli-validators.ts +12 -4
- package/src/cli.ts +27 -7
- package/src/formats/anthropic/index.ts +1 -1
- package/src/formats/anthropic/parse.ts +25 -6
- package/src/formats/anthropic/schema.ts +16 -8
- package/src/formats/anthropic/serialize.ts +116 -28
- package/src/formats/openai/index.ts +1 -1
- package/src/formats/openai/parse.ts +13 -3
- package/src/formats/openai/schema.ts +43 -30
- package/src/formats/openai/serialize.ts +84 -30
- package/src/formats/{parse-helpers.ts → request-helpers.ts} +4 -32
- package/src/formats/responses/index.ts +1 -1
- package/src/formats/responses/parse.ts +18 -4
- package/src/formats/responses/schema.ts +34 -22
- package/src/formats/responses/serialize.ts +237 -38
- package/src/formats/serialize-helpers.ts +38 -0
- package/src/formats/types.ts +18 -5
- package/src/index.ts +3 -1
- package/src/loader.ts +43 -20
- package/src/logger.ts +31 -19
- package/src/mock-server.ts +38 -21
- package/src/route-handler.ts +50 -15
- package/src/rule-engine.ts +64 -11
- package/src/types/reply.ts +12 -12
- package/src/types/request.ts +7 -11
- package/src/types/rule.ts +3 -10
- package/src/types.ts +23 -4
- package/test/cli-validators.test.ts +16 -4
- package/test/formats/anthropic.test.ts +84 -23
- package/test/formats/openai.test.ts +85 -24
- package/test/formats/parse-helpers.test.ts +315 -0
- package/test/formats/responses.test.ts +99 -34
- package/test/helpers/make-req.ts +18 -0
- package/test/history.test.ts +361 -0
- package/test/loader.test.ts +44 -45
- package/test/logger.test.ts +344 -0
- package/test/mock-server.test.ts +77 -23
- package/test/rule-engine.test.ts +57 -41
- 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 {
|
|
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({
|
|
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: [
|
|
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: [
|
|
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
|
-
{
|
|
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: [
|
|
73
|
-
{
|
|
74
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
279
|
-
expect(fnCall
|
|
280
|
-
expect(fnCall
|
|
281
|
-
expect(fnCall
|
|
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({
|
|
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({
|
|
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
|
+
}
|