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