llm-mock-server 1.0.2 → 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/.editorconfig +12 -0
- package/.github/workflows/test.yml +3 -0
- package/.oxfmtrc.json +9 -0
- package/package.json +5 -2
- package/src/cli-validators.ts +12 -4
- package/src/cli.ts +22 -6
- package/src/formats/anthropic/parse.ts +24 -5
- package/src/formats/anthropic/schema.ts +16 -8
- package/src/formats/anthropic/serialize.ts +111 -27
- package/src/formats/openai/parse.ts +12 -2
- package/src/formats/openai/schema.ts +43 -30
- package/src/formats/openai/serialize.ts +73 -17
- package/src/formats/request-helpers.ts +2 -1
- package/src/formats/responses/parse.ts +17 -3
- package/src/formats/responses/schema.ts +34 -20
- package/src/formats/responses/serialize.ts +233 -38
- package/src/formats/serialize-helpers.ts +10 -2
- package/src/formats/types.ts +16 -3
- package/src/index.ts +3 -1
- package/src/loader.ts +36 -9
- package/src/logger.ts +25 -7
- package/src/mock-server.ts +28 -7
- package/src/route-handler.ts +49 -14
- package/src/rule-engine.ts +43 -12
- package/src/types/reply.ts +6 -2
- package/src/types.ts +24 -3
- package/test/cli-validators.test.ts +16 -4
- package/test/formats/anthropic.test.ts +80 -19
- package/test/formats/openai.test.ts +85 -24
- package/test/formats/parse-helpers.test.ts +47 -7
- package/test/formats/responses.test.ts +95 -30
- package/test/history.test.ts +18 -5
- package/test/loader.test.ts +33 -18
- package/test/logger.test.ts +59 -9
- package/test/mock-server.test.ts +76 -22
- package/test/rule-engine.test.ts +49 -19
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { anthropicFormat } from "../../src/formats/anthropic/index.js";
|
|
3
3
|
import type {
|
|
4
|
-
AnthropicMessageStart,
|
|
5
|
-
|
|
4
|
+
AnthropicMessageStart,
|
|
5
|
+
AnthropicBlockEvent,
|
|
6
|
+
AnthropicDelta,
|
|
7
|
+
AnthropicComplete,
|
|
8
|
+
AnthropicError,
|
|
6
9
|
} from "../../src/formats/anthropic/schema.js";
|
|
7
10
|
|
|
8
11
|
function parse<T>(chunk: { data: string }): T {
|
|
@@ -40,7 +43,9 @@ describe("Anthropic Format", () => {
|
|
|
40
43
|
const req = anthropicFormat.parseRequest({
|
|
41
44
|
model: "claude-sonnet-4-6",
|
|
42
45
|
max_tokens: 1024,
|
|
43
|
-
messages: [
|
|
46
|
+
messages: [
|
|
47
|
+
{ role: "user", content: [{ type: "text", text: "Hello there" }] },
|
|
48
|
+
],
|
|
44
49
|
});
|
|
45
50
|
expect(req.lastMessage).toBe("Hello there");
|
|
46
51
|
});
|
|
@@ -50,7 +55,13 @@ describe("Anthropic Format", () => {
|
|
|
50
55
|
model: "claude-sonnet-4-6",
|
|
51
56
|
max_tokens: 1024,
|
|
52
57
|
messages: [{ role: "user", content: "read file" }],
|
|
53
|
-
tools: [
|
|
58
|
+
tools: [
|
|
59
|
+
{
|
|
60
|
+
name: "read_file",
|
|
61
|
+
description: "Read",
|
|
62
|
+
input_schema: { type: "object" },
|
|
63
|
+
},
|
|
64
|
+
],
|
|
54
65
|
});
|
|
55
66
|
expect(req.tools).toHaveLength(1);
|
|
56
67
|
expect(req.tools![0]!.name).toBe("read_file");
|
|
@@ -75,7 +86,16 @@ describe("Anthropic Format", () => {
|
|
|
75
86
|
max_tokens: 1024,
|
|
76
87
|
messages: [
|
|
77
88
|
{ role: "user", content: "hi" },
|
|
78
|
-
{
|
|
89
|
+
{
|
|
90
|
+
role: "user",
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: "tool_result",
|
|
94
|
+
tool_use_id: "toolu_123",
|
|
95
|
+
content: "result",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
79
99
|
],
|
|
80
100
|
});
|
|
81
101
|
expect(req.lastToolCallId).toBe("toolu_123");
|
|
@@ -84,7 +104,10 @@ describe("Anthropic Format", () => {
|
|
|
84
104
|
|
|
85
105
|
describe("serialize (streaming)", () => {
|
|
86
106
|
it("produces correct event sequence for text", () => {
|
|
87
|
-
const chunks = anthropicFormat.serialize(
|
|
107
|
+
const chunks = anthropicFormat.serialize(
|
|
108
|
+
{ text: "Hello" },
|
|
109
|
+
"claude-sonnet-4-6",
|
|
110
|
+
);
|
|
88
111
|
const events = chunks.map((c) => c.event);
|
|
89
112
|
expect(events).toEqual([
|
|
90
113
|
"message_start",
|
|
@@ -97,7 +120,10 @@ describe("Anthropic Format", () => {
|
|
|
97
120
|
});
|
|
98
121
|
|
|
99
122
|
it("message_start contains correct structure", () => {
|
|
100
|
-
const chunks = anthropicFormat.serialize(
|
|
123
|
+
const chunks = anthropicFormat.serialize(
|
|
124
|
+
{ text: "Hello" },
|
|
125
|
+
"claude-sonnet-4-6",
|
|
126
|
+
);
|
|
101
127
|
const msg = parse<AnthropicMessageStart>(chunks[0]!);
|
|
102
128
|
expect(msg.message).toMatchObject({
|
|
103
129
|
type: "message",
|
|
@@ -111,7 +137,10 @@ describe("Anthropic Format", () => {
|
|
|
111
137
|
});
|
|
112
138
|
|
|
113
139
|
it("text block uses index 0 when no reasoning", () => {
|
|
114
|
-
const chunks = anthropicFormat.serialize(
|
|
140
|
+
const chunks = anthropicFormat.serialize(
|
|
141
|
+
{ text: "Hello" },
|
|
142
|
+
"claude-sonnet-4-6",
|
|
143
|
+
);
|
|
115
144
|
const blockStart = chunks.find((c) => c.event === "content_block_start");
|
|
116
145
|
const data = parse<AnthropicBlockEvent>(blockStart!);
|
|
117
146
|
expect(data.index).toBe(0);
|
|
@@ -143,7 +172,9 @@ describe("Anthropic Format", () => {
|
|
|
143
172
|
return parse<AnthropicBlockEvent>(c).delta?.type === "thinking_delta";
|
|
144
173
|
});
|
|
145
174
|
expect(thinkingDelta).toBeDefined();
|
|
146
|
-
expect(parse<AnthropicBlockEvent>(thinkingDelta!).delta?.thinking).toBe(
|
|
175
|
+
expect(parse<AnthropicBlockEvent>(thinkingDelta!).delta?.thinking).toBe(
|
|
176
|
+
"Let me think",
|
|
177
|
+
);
|
|
147
178
|
});
|
|
148
179
|
|
|
149
180
|
it("closes thinking block before text block starts", () => {
|
|
@@ -151,9 +182,18 @@ describe("Anthropic Format", () => {
|
|
|
151
182
|
{ text: "answer", reasoning: "think" },
|
|
152
183
|
"claude-sonnet-4-6",
|
|
153
184
|
);
|
|
154
|
-
const events = chunks.map((c) => ({
|
|
155
|
-
|
|
156
|
-
|
|
185
|
+
const events = chunks.map((c) => ({
|
|
186
|
+
event: c.event,
|
|
187
|
+
data: parse<AnthropicBlockEvent>(c),
|
|
188
|
+
}));
|
|
189
|
+
const thinkingStop = events.findIndex(
|
|
190
|
+
(e) => e.event === "content_block_stop" && e.data.index === 0,
|
|
191
|
+
);
|
|
192
|
+
const textStart = events.findIndex(
|
|
193
|
+
(e) =>
|
|
194
|
+
e.event === "content_block_start" &&
|
|
195
|
+
e.data.content_block?.type === "text",
|
|
196
|
+
);
|
|
157
197
|
expect(thinkingStop).toBeLessThan(textStart);
|
|
158
198
|
});
|
|
159
199
|
|
|
@@ -179,17 +219,25 @@ describe("Anthropic Format", () => {
|
|
|
179
219
|
"claude-sonnet-4-6",
|
|
180
220
|
);
|
|
181
221
|
const delta = chunks.find((c) => c.event === "message_delta");
|
|
182
|
-
expect(parse<AnthropicDelta>(delta!).delta).toMatchObject({
|
|
222
|
+
expect(parse<AnthropicDelta>(delta!).delta).toMatchObject({
|
|
223
|
+
stop_reason: "tool_use",
|
|
224
|
+
});
|
|
183
225
|
});
|
|
184
226
|
|
|
185
227
|
it("includes stop_sequence: null in message_delta", () => {
|
|
186
|
-
const chunks = anthropicFormat.serialize(
|
|
228
|
+
const chunks = anthropicFormat.serialize(
|
|
229
|
+
{ text: "Hello" },
|
|
230
|
+
"claude-sonnet-4-6",
|
|
231
|
+
);
|
|
187
232
|
const delta = chunks.find((c) => c.event === "message_delta");
|
|
188
233
|
expect(parse<AnthropicDelta>(delta!).delta.stop_sequence).toBeNull();
|
|
189
234
|
});
|
|
190
235
|
|
|
191
236
|
it("message_delta includes output_tokens in usage", () => {
|
|
192
|
-
const chunks = anthropicFormat.serialize(
|
|
237
|
+
const chunks = anthropicFormat.serialize(
|
|
238
|
+
{ text: "Hello", usage: { input: 20, output: 15 } },
|
|
239
|
+
"claude-sonnet-4-6",
|
|
240
|
+
);
|
|
193
241
|
const delta = chunks.find((c) => c.event === "message_delta");
|
|
194
242
|
expect(parse<AnthropicDelta>(delta!).usage.output_tokens).toBe(15);
|
|
195
243
|
});
|
|
@@ -197,7 +245,10 @@ describe("Anthropic Format", () => {
|
|
|
197
245
|
|
|
198
246
|
describe("serializeComplete (non-streaming)", () => {
|
|
199
247
|
it("produces correct top-level structure", () => {
|
|
200
|
-
const result = anthropicFormat.serializeComplete(
|
|
248
|
+
const result = anthropicFormat.serializeComplete(
|
|
249
|
+
{ text: "Hello" },
|
|
250
|
+
"claude-sonnet-4-6",
|
|
251
|
+
) as AnthropicComplete;
|
|
201
252
|
expect(result.type).toBe("message");
|
|
202
253
|
expect(result.role).toBe("assistant");
|
|
203
254
|
expect(result.model).toBe("claude-sonnet-4-6");
|
|
@@ -206,8 +257,14 @@ describe("Anthropic Format", () => {
|
|
|
206
257
|
});
|
|
207
258
|
|
|
208
259
|
it("includes text content block", () => {
|
|
209
|
-
const result = anthropicFormat.serializeComplete(
|
|
210
|
-
|
|
260
|
+
const result = anthropicFormat.serializeComplete(
|
|
261
|
+
{ text: "Hello, world!" },
|
|
262
|
+
"claude-sonnet-4-6",
|
|
263
|
+
) as AnthropicComplete;
|
|
264
|
+
expect(result.content[0]).toMatchObject({
|
|
265
|
+
type: "text",
|
|
266
|
+
text: "Hello, world!",
|
|
267
|
+
});
|
|
211
268
|
});
|
|
212
269
|
|
|
213
270
|
it("includes thinking before text when reasoning provided", () => {
|
|
@@ -251,7 +308,11 @@ describe("Anthropic Format", () => {
|
|
|
251
308
|
|
|
252
309
|
describe("serializeError", () => {
|
|
253
310
|
it("produces Anthropic error format", () => {
|
|
254
|
-
const result = anthropicFormat.serializeError({
|
|
311
|
+
const result = anthropicFormat.serializeError({
|
|
312
|
+
status: 400,
|
|
313
|
+
message: "Bad request",
|
|
314
|
+
type: "invalid_request_error",
|
|
315
|
+
}) as AnthropicError;
|
|
255
316
|
expect(result.type).toBe("error");
|
|
256
317
|
expect(result.error.type).toBe("invalid_request_error");
|
|
257
318
|
expect(result.error.message).toBe("Bad request");
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { openaiFormat } from "../../src/formats/openai/index.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
OpenAIChunk,
|
|
5
|
+
OpenAIComplete,
|
|
6
|
+
OpenAIError,
|
|
7
|
+
} from "../../src/formats/openai/schema.js";
|
|
4
8
|
|
|
5
9
|
function parse<T>(chunk: { data: string }): T {
|
|
6
10
|
return JSON.parse(chunk.data) as T;
|
|
@@ -26,12 +30,19 @@ describe("OpenAI Format", () => {
|
|
|
26
30
|
});
|
|
27
31
|
|
|
28
32
|
it("defaults stream to true", () => {
|
|
29
|
-
const req = openaiFormat.parseRequest({
|
|
33
|
+
const req = openaiFormat.parseRequest({
|
|
34
|
+
model: "gpt-5.4",
|
|
35
|
+
messages: [{ role: "user", content: "hi" }],
|
|
36
|
+
});
|
|
30
37
|
expect(req.streaming).toBe(true);
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
it("detects stream: false", () => {
|
|
34
|
-
const req = openaiFormat.parseRequest({
|
|
41
|
+
const req = openaiFormat.parseRequest({
|
|
42
|
+
model: "gpt-5.4",
|
|
43
|
+
messages: [{ role: "user", content: "hi" }],
|
|
44
|
+
stream: false,
|
|
45
|
+
});
|
|
35
46
|
expect(req.streaming).toBe(false);
|
|
36
47
|
});
|
|
37
48
|
|
|
@@ -39,7 +50,16 @@ describe("OpenAI Format", () => {
|
|
|
39
50
|
const req = openaiFormat.parseRequest({
|
|
40
51
|
model: "gpt-5.4",
|
|
41
52
|
messages: [{ role: "user", content: "read file" }],
|
|
42
|
-
tools: [
|
|
53
|
+
tools: [
|
|
54
|
+
{
|
|
55
|
+
type: "function",
|
|
56
|
+
function: {
|
|
57
|
+
name: "read_file",
|
|
58
|
+
description: "Read a file",
|
|
59
|
+
parameters: {},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
43
63
|
});
|
|
44
64
|
expect(req.tools).toHaveLength(1);
|
|
45
65
|
expect(req.tools![0]!.name).toBe("read_file");
|
|
@@ -71,22 +91,28 @@ describe("OpenAI Format", () => {
|
|
|
71
91
|
it("handles non-string content (array of content parts)", () => {
|
|
72
92
|
const req = openaiFormat.parseRequest({
|
|
73
93
|
model: "gpt-5.4",
|
|
74
|
-
messages: [
|
|
94
|
+
messages: [
|
|
95
|
+
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
96
|
+
],
|
|
75
97
|
});
|
|
76
98
|
expect(req.lastMessage).toContain("Hello");
|
|
77
99
|
});
|
|
78
100
|
|
|
79
101
|
it("rejects requests with invalid role values", () => {
|
|
80
|
-
expect(() =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
expect(() =>
|
|
103
|
+
openaiFormat.parseRequest({
|
|
104
|
+
model: "gpt-5.4",
|
|
105
|
+
messages: [{ role: "banana", content: "hi" }],
|
|
106
|
+
}),
|
|
107
|
+
).toThrow();
|
|
84
108
|
});
|
|
85
109
|
|
|
86
110
|
it("rejects requests missing model", () => {
|
|
87
|
-
expect(() =>
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
expect(() =>
|
|
112
|
+
openaiFormat.parseRequest({
|
|
113
|
+
messages: [{ role: "user", content: "hi" }],
|
|
114
|
+
}),
|
|
115
|
+
).toThrow();
|
|
90
116
|
});
|
|
91
117
|
});
|
|
92
118
|
|
|
@@ -123,14 +149,19 @@ describe("OpenAI Format", () => {
|
|
|
123
149
|
});
|
|
124
150
|
|
|
125
151
|
it("includes usage chunk before [DONE]", () => {
|
|
126
|
-
const chunks = openaiFormat.serialize(
|
|
152
|
+
const chunks = openaiFormat.serialize(
|
|
153
|
+
{ text: "Hello", usage: { input: 10, output: 5 } },
|
|
154
|
+
"gpt-5.4",
|
|
155
|
+
);
|
|
127
156
|
const usageChunk = parse<OpenAIChunk>(chunks.at(-2)!);
|
|
128
157
|
expect(usageChunk.usage).toMatchObject({
|
|
129
158
|
prompt_tokens: 10,
|
|
130
159
|
completion_tokens: 5,
|
|
131
160
|
total_tokens: 15,
|
|
132
161
|
});
|
|
133
|
-
expect(
|
|
162
|
+
expect(
|
|
163
|
+
usageChunk.usage?.completion_tokens_details?.reasoning_tokens,
|
|
164
|
+
).toBe(0);
|
|
134
165
|
expect(usageChunk.usage?.prompt_tokens_details?.cached_tokens).toBe(0);
|
|
135
166
|
});
|
|
136
167
|
|
|
@@ -144,7 +175,8 @@ describe("OpenAI Format", () => {
|
|
|
144
175
|
return parse<OpenAIChunk>(c).choices[0]?.delta.tool_calls !== undefined;
|
|
145
176
|
});
|
|
146
177
|
expect(toolChunk).toBeDefined();
|
|
147
|
-
const tc = parse<OpenAIChunk>(toolChunk!).choices[0]!.delta
|
|
178
|
+
const tc = parse<OpenAIChunk>(toolChunk!).choices[0]!.delta
|
|
179
|
+
.tool_calls![0]!;
|
|
148
180
|
expect(tc.type).toBe("function");
|
|
149
181
|
expect(tc.id).toBeTypeOf("string");
|
|
150
182
|
expect(tc.function.name).toBe("read_file");
|
|
@@ -158,7 +190,11 @@ describe("OpenAI Format", () => {
|
|
|
158
190
|
});
|
|
159
191
|
|
|
160
192
|
it("splits text into multiple delta chunks with chunkSize", () => {
|
|
161
|
-
const chunks = openaiFormat.serialize(
|
|
193
|
+
const chunks = openaiFormat.serialize(
|
|
194
|
+
{ text: "Hello, world!" },
|
|
195
|
+
"gpt-5.4",
|
|
196
|
+
{ chunkSize: 5 },
|
|
197
|
+
);
|
|
162
198
|
const contentDeltas = chunks
|
|
163
199
|
.filter((c) => c.data !== "[DONE]")
|
|
164
200
|
.map((c) => parse<OpenAIChunk>(c))
|
|
@@ -169,7 +205,9 @@ describe("OpenAI Format", () => {
|
|
|
169
205
|
|
|
170
206
|
it("all chunks share same id and created timestamp", () => {
|
|
171
207
|
const chunks = openaiFormat.serialize({ text: "Hello" }, "gpt-5.4");
|
|
172
|
-
const dataChunks = chunks
|
|
208
|
+
const dataChunks = chunks
|
|
209
|
+
.filter((c) => c.data !== "[DONE]")
|
|
210
|
+
.map((c) => parse<OpenAIChunk>(c));
|
|
173
211
|
const ids = dataChunks.map((c) => c.id);
|
|
174
212
|
const created = dataChunks.map((c) => c.created);
|
|
175
213
|
expect(new Set(ids).size).toBe(1);
|
|
@@ -179,7 +217,10 @@ describe("OpenAI Format", () => {
|
|
|
179
217
|
|
|
180
218
|
describe("serializeComplete (non-streaming)", () => {
|
|
181
219
|
it("produces correct top-level structure", () => {
|
|
182
|
-
const result = openaiFormat.serializeComplete(
|
|
220
|
+
const result = openaiFormat.serializeComplete(
|
|
221
|
+
{ text: "Hello, world!" },
|
|
222
|
+
"gpt-5.4",
|
|
223
|
+
) as OpenAIComplete;
|
|
183
224
|
expect(result.object).toBe("chat.completion");
|
|
184
225
|
expect(result.model).toBe("gpt-5.4");
|
|
185
226
|
expect(result.id).toBeTypeOf("string");
|
|
@@ -187,7 +228,10 @@ describe("OpenAI Format", () => {
|
|
|
187
228
|
});
|
|
188
229
|
|
|
189
230
|
it("message has correct content and finish_reason", () => {
|
|
190
|
-
const result = openaiFormat.serializeComplete(
|
|
231
|
+
const result = openaiFormat.serializeComplete(
|
|
232
|
+
{ text: "Hello, world!" },
|
|
233
|
+
"gpt-5.4",
|
|
234
|
+
) as OpenAIComplete;
|
|
191
235
|
expect(result.choices[0]!.message.role).toBe("assistant");
|
|
192
236
|
expect(result.choices[0]!.message.content).toBe("Hello, world!");
|
|
193
237
|
expect(result.choices[0]!.finish_reason).toBe("stop");
|
|
@@ -210,33 +254,50 @@ describe("OpenAI Format", () => {
|
|
|
210
254
|
{ text: "hi", usage: { input: 20, output: 15 } },
|
|
211
255
|
"gpt-5.4",
|
|
212
256
|
) as OpenAIComplete;
|
|
213
|
-
expect(result.usage).toMatchObject({
|
|
257
|
+
expect(result.usage).toMatchObject({
|
|
258
|
+
prompt_tokens: 20,
|
|
259
|
+
completion_tokens: 15,
|
|
260
|
+
total_tokens: 35,
|
|
261
|
+
});
|
|
214
262
|
expect(result.usage?.completion_tokens_details?.reasoning_tokens).toBe(0);
|
|
215
263
|
expect(result.usage?.prompt_tokens_details?.cached_tokens).toBe(0);
|
|
216
264
|
});
|
|
217
265
|
|
|
218
266
|
it("includes service_tier and system_fingerprint", () => {
|
|
219
|
-
const result = openaiFormat.serializeComplete(
|
|
267
|
+
const result = openaiFormat.serializeComplete(
|
|
268
|
+
{ text: "hi" },
|
|
269
|
+
"gpt-5.4",
|
|
270
|
+
) as OpenAIComplete;
|
|
220
271
|
expect(result.service_tier).toBe("default");
|
|
221
272
|
expect(result.system_fingerprint).toBeNull();
|
|
222
273
|
});
|
|
223
274
|
|
|
224
275
|
it("includes logprobs: null on choices", () => {
|
|
225
|
-
const result = openaiFormat.serializeComplete(
|
|
276
|
+
const result = openaiFormat.serializeComplete(
|
|
277
|
+
{ text: "hi" },
|
|
278
|
+
"gpt-5.4",
|
|
279
|
+
) as OpenAIComplete;
|
|
226
280
|
expect(result.choices[0]!.logprobs).toBeNull();
|
|
227
281
|
});
|
|
228
282
|
});
|
|
229
283
|
|
|
230
284
|
describe("serializeError", () => {
|
|
231
285
|
it("produces OpenAI error format", () => {
|
|
232
|
-
const result = openaiFormat.serializeError({
|
|
286
|
+
const result = openaiFormat.serializeError({
|
|
287
|
+
status: 429,
|
|
288
|
+
message: "Rate limited",
|
|
289
|
+
type: "rate_limit_error",
|
|
290
|
+
}) as OpenAIError;
|
|
233
291
|
expect(result.error.message).toBe("Rate limited");
|
|
234
292
|
expect(result.error.type).toBe("rate_limit_error");
|
|
235
293
|
expect(result.error.code).toBeNull();
|
|
236
294
|
});
|
|
237
295
|
|
|
238
296
|
it("defaults type to server_error", () => {
|
|
239
|
-
const result = openaiFormat.serializeError({
|
|
297
|
+
const result = openaiFormat.serializeError({
|
|
298
|
+
status: 500,
|
|
299
|
+
message: "Internal",
|
|
300
|
+
}) as OpenAIError;
|
|
240
301
|
expect(result.error.type).toBe("server_error");
|
|
241
302
|
});
|
|
242
303
|
});
|
|
@@ -108,7 +108,9 @@ describe("parse-helpers", () => {
|
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
it("returns true when reply has text and tools", () => {
|
|
111
|
-
expect(
|
|
111
|
+
expect(
|
|
112
|
+
shouldEmitText({ text: "hi", tools: [{ name: "fn", args: {} }] }),
|
|
113
|
+
).toBe(true);
|
|
112
114
|
});
|
|
113
115
|
|
|
114
116
|
it("returns true for empty text with no tools or reasoning", () => {
|
|
@@ -169,7 +171,9 @@ describe("parse-helpers", () => {
|
|
|
169
171
|
[{ role: "user", content: "hello" }],
|
|
170
172
|
undefined,
|
|
171
173
|
"gpt-4",
|
|
172
|
-
{
|
|
174
|
+
{
|
|
175
|
+
messages: [],
|
|
176
|
+
},
|
|
173
177
|
);
|
|
174
178
|
|
|
175
179
|
expect(result.format).toBe("openai");
|
|
@@ -214,7 +218,14 @@ describe("parse-helpers", () => {
|
|
|
214
218
|
{ role: "assistant" as const, content: "reply" },
|
|
215
219
|
{ role: "user" as const, content: "second" },
|
|
216
220
|
];
|
|
217
|
-
const result = buildMockRequest(
|
|
221
|
+
const result = buildMockRequest(
|
|
222
|
+
"openai",
|
|
223
|
+
{},
|
|
224
|
+
messages,
|
|
225
|
+
undefined,
|
|
226
|
+
"m",
|
|
227
|
+
{},
|
|
228
|
+
);
|
|
218
229
|
expect(result.lastMessage).toBe("second");
|
|
219
230
|
});
|
|
220
231
|
|
|
@@ -223,7 +234,14 @@ describe("parse-helpers", () => {
|
|
|
223
234
|
{ role: "system" as const, content: "be helpful" },
|
|
224
235
|
{ role: "user" as const, content: "hi" },
|
|
225
236
|
];
|
|
226
|
-
const result = buildMockRequest(
|
|
237
|
+
const result = buildMockRequest(
|
|
238
|
+
"openai",
|
|
239
|
+
{},
|
|
240
|
+
messages,
|
|
241
|
+
undefined,
|
|
242
|
+
"m",
|
|
243
|
+
{},
|
|
244
|
+
);
|
|
227
245
|
expect(result.systemMessage).toBe("be helpful");
|
|
228
246
|
});
|
|
229
247
|
|
|
@@ -241,12 +259,26 @@ describe("parse-helpers", () => {
|
|
|
241
259
|
{ role: "tool" as const, content: "result1", toolCallId: "call_1" },
|
|
242
260
|
{ role: "tool" as const, content: "result2", toolCallId: "call_2" },
|
|
243
261
|
];
|
|
244
|
-
const result = buildMockRequest(
|
|
262
|
+
const result = buildMockRequest(
|
|
263
|
+
"openai",
|
|
264
|
+
{},
|
|
265
|
+
messages,
|
|
266
|
+
undefined,
|
|
267
|
+
"m",
|
|
268
|
+
{},
|
|
269
|
+
);
|
|
245
270
|
expect(result.lastToolCallId).toBe("call_2");
|
|
246
271
|
});
|
|
247
272
|
|
|
248
273
|
it("sets streaming to false when stream is false", () => {
|
|
249
|
-
const result = buildMockRequest(
|
|
274
|
+
const result = buildMockRequest(
|
|
275
|
+
"openai",
|
|
276
|
+
{ stream: false },
|
|
277
|
+
[],
|
|
278
|
+
undefined,
|
|
279
|
+
"m",
|
|
280
|
+
{},
|
|
281
|
+
);
|
|
250
282
|
expect(result.streaming).toBe(false);
|
|
251
283
|
});
|
|
252
284
|
|
|
@@ -255,7 +287,15 @@ describe("parse-helpers", () => {
|
|
|
255
287
|
headers: { authorization: "Bearer sk-test" },
|
|
256
288
|
path: "/v1/chat/completions",
|
|
257
289
|
};
|
|
258
|
-
const result = buildMockRequest(
|
|
290
|
+
const result = buildMockRequest(
|
|
291
|
+
"openai",
|
|
292
|
+
{},
|
|
293
|
+
[],
|
|
294
|
+
undefined,
|
|
295
|
+
"m",
|
|
296
|
+
{},
|
|
297
|
+
meta,
|
|
298
|
+
);
|
|
259
299
|
expect(result.headers).toEqual({ authorization: "Bearer sk-test" });
|
|
260
300
|
expect(result.path).toBe("/v1/chat/completions");
|
|
261
301
|
});
|