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.
@@ -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, AnthropicBlockEvent, AnthropicDelta,
5
- AnthropicComplete, AnthropicError,
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: [{ role: "user", content: [{ type: "text", text: "Hello there" }] }],
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: [{ name: "read_file", description: "Read", input_schema: { type: "object" } }],
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
- { role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_123", content: "result" }] },
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({ text: "Hello" }, "claude-sonnet-4-6");
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({ text: "Hello" }, "claude-sonnet-4-6");
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({ text: "Hello" }, "claude-sonnet-4-6");
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("Let me think");
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) => ({ event: c.event, data: parse<AnthropicBlockEvent>(c) }));
155
- const thinkingStop = events.findIndex((e) => e.event === "content_block_stop" && e.data.index === 0);
156
- const textStart = events.findIndex((e) => e.event === "content_block_start" && e.data.content_block?.type === "text");
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({ stop_reason: "tool_use" });
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({ text: "Hello" }, "claude-sonnet-4-6");
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({ text: "Hello", usage: { input: 20, output: 15 } }, "claude-sonnet-4-6");
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({ text: "Hello" }, "claude-sonnet-4-6") as AnthropicComplete;
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({ text: "Hello, world!" }, "claude-sonnet-4-6") as AnthropicComplete;
210
- expect(result.content[0]).toMatchObject({ type: "text", text: "Hello, world!" });
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({ status: 400, message: "Bad request", type: "invalid_request_error" }) as AnthropicError;
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 { OpenAIChunk, OpenAIComplete, OpenAIError } from "../../src/formats/openai/schema.js";
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({ model: "gpt-5.4", messages: [{ role: "user", content: "hi" }] });
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({ model: "gpt-5.4", messages: [{ role: "user", content: "hi" }], stream: false });
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: [{ type: "function", function: { name: "read_file", description: "Read a file", parameters: {} } }],
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: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
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(() => openaiFormat.parseRequest({
81
- model: "gpt-5.4",
82
- messages: [{ role: "banana", content: "hi" }],
83
- })).toThrow();
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(() => openaiFormat.parseRequest({
88
- messages: [{ role: "user", content: "hi" }],
89
- })).toThrow();
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({ text: "Hello", usage: { input: 10, output: 5 } }, "gpt-5.4");
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(usageChunk.usage?.completion_tokens_details?.reasoning_tokens).toBe(0);
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.tool_calls![0]!;
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({ text: "Hello, world!" }, "gpt-5.4", { chunkSize: 5 });
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.filter((c) => c.data !== "[DONE]").map((c) => parse<OpenAIChunk>(c));
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({ text: "Hello, world!" }, "gpt-5.4") as OpenAIComplete;
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({ text: "Hello, world!" }, "gpt-5.4") as OpenAIComplete;
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({ prompt_tokens: 20, completion_tokens: 15, total_tokens: 35 });
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({ text: "hi" }, "gpt-5.4") as OpenAIComplete;
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({ text: "hi" }, "gpt-5.4") as OpenAIComplete;
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({ status: 429, message: "Rate limited", type: "rate_limit_error" }) as OpenAIError;
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({ status: 500, message: "Internal" }) as OpenAIError;
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(shouldEmitText({ text: "hi", tools: [{ name: "fn", args: {} }] })).toBe(true);
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
- { messages: [] },
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("openai", {}, messages, undefined, "m", {});
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("openai", {}, messages, undefined, "m", {});
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("openai", {}, messages, undefined, "m", {});
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("openai", { stream: false }, [], undefined, "m", {});
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("openai", {}, [], undefined, "m", {}, meta);
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
  });