llm-mock-server 1.0.0 → 1.0.2
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/dist/cli-validators.d.ts +7 -0
- package/dist/cli-validators.d.ts.map +1 -0
- package/dist/cli-validators.js +51 -0
- package/dist/cli-validators.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +106 -0
- package/dist/cli.js.map +1 -0
- package/dist/formats/anthropic/index.d.ts +3 -0
- package/dist/formats/anthropic/index.d.ts.map +1 -0
- package/dist/formats/anthropic/index.js +13 -0
- package/dist/formats/anthropic/index.js.map +1 -0
- package/dist/formats/anthropic/parse.d.ts +4 -0
- package/dist/formats/anthropic/parse.d.ts.map +1 -0
- package/dist/formats/anthropic/parse.js +47 -0
- package/dist/formats/anthropic/parse.js.map +1 -0
- package/dist/formats/anthropic/schema.d.ts +75 -0
- package/dist/formats/anthropic/schema.d.ts.map +1 -0
- package/dist/formats/anthropic/schema.js +50 -0
- package/dist/formats/anthropic/schema.js.map +1 -0
- package/dist/formats/anthropic/serialize.d.ts +10 -0
- package/dist/formats/anthropic/serialize.d.ts.map +1 -0
- package/dist/formats/anthropic/serialize.js +73 -0
- package/dist/formats/anthropic/serialize.js.map +1 -0
- package/dist/formats/openai/index.d.ts +3 -0
- package/dist/formats/openai/index.d.ts.map +1 -0
- package/dist/formats/openai/index.js +13 -0
- package/dist/formats/openai/index.js.map +1 -0
- package/dist/formats/openai/parse.d.ts +4 -0
- package/dist/formats/openai/parse.d.ts.map +1 -0
- package/dist/formats/openai/parse.js +33 -0
- package/dist/formats/openai/parse.js.map +1 -0
- package/dist/formats/openai/schema.d.ts +93 -0
- package/dist/formats/openai/schema.d.ts.map +1 -0
- package/dist/formats/openai/schema.js +68 -0
- package/dist/formats/openai/schema.js.map +1 -0
- package/dist/formats/openai/serialize.d.ts +10 -0
- package/dist/formats/openai/serialize.d.ts.map +1 -0
- package/dist/formats/openai/serialize.js +70 -0
- package/dist/formats/openai/serialize.js.map +1 -0
- package/dist/formats/parse-helpers.d.ts +24 -0
- package/dist/formats/parse-helpers.d.ts.map +1 -0
- package/dist/formats/parse-helpers.js +52 -0
- package/dist/formats/parse-helpers.js.map +1 -0
- 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.d.ts +3 -0
- package/dist/formats/responses/index.d.ts.map +1 -0
- package/dist/formats/responses/index.js +13 -0
- package/dist/formats/responses/index.js.map +1 -0
- package/dist/formats/responses/parse.d.ts +4 -0
- package/dist/formats/responses/parse.d.ts.map +1 -0
- package/dist/formats/responses/parse.js +51 -0
- package/dist/formats/responses/parse.js.map +1 -0
- package/dist/formats/responses/schema.d.ts +103 -0
- package/dist/formats/responses/schema.d.ts.map +1 -0
- package/dist/formats/responses/schema.js +61 -0
- package/dist/formats/responses/schema.js.map +1 -0
- package/dist/formats/responses/serialize.d.ts +10 -0
- package/dist/formats/responses/serialize.d.ts.map +1 -0
- package/dist/formats/responses/serialize.js +108 -0
- package/dist/formats/responses/serialize.js.map +1 -0
- 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 +20 -0
- package/dist/formats/types.d.ts.map +1 -0
- package/dist/formats/types.js +2 -0
- package/dist/formats/types.js.map +1 -0
- package/dist/history.d.ts +38 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +48 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +9 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +169 -0
- package/dist/loader.js.map +1 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +42 -0
- package/dist/logger.js.map +1 -0
- package/dist/mock-server.d.ts +102 -0
- package/dist/mock-server.d.ts.map +1 -0
- package/dist/mock-server.js +195 -0
- package/dist/mock-server.js.map +1 -0
- package/dist/route-handler.d.ts +16 -0
- package/dist/route-handler.d.ts.map +1 -0
- package/dist/route-handler.js +75 -0
- package/dist/route-handler.js.map +1 -0
- package/dist/rule-engine.d.ts +24 -0
- package/dist/rule-engine.d.ts.map +1 -0
- package/dist/rule-engine.js +129 -0
- package/dist/rule-engine.js.map +1 -0
- package/dist/sse-writer.d.ts +5 -0
- package/dist/sse-writer.d.ts.map +1 -0
- package/dist/sse-writer.js +23 -0
- package/dist/sse-writer.js.map +1 -0
- package/{src/types/index.ts → dist/types/index.d.ts} +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/reply.d.ts +45 -0
- package/dist/types/reply.d.ts.map +1 -0
- package/dist/types/reply.js +2 -0
- package/dist/types/reply.js.map +1 -0
- package/dist/types/request.d.ts +39 -0
- package/dist/types/request.d.ts.map +1 -0
- package/dist/types/request.js +2 -0
- package/dist/types/request.js.map +1 -0
- package/dist/types/rule.d.ts +57 -0
- package/dist/types/rule.d.ts.map +1 -0
- package/dist/types/rule.js +2 -0
- package/dist/types/rule.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +2 -1
- package/scorecard.png +0 -0
- package/src/cli-validators.ts +3 -0
- package/src/cli.ts +6 -2
- package/src/formats/anthropic/index.ts +1 -1
- package/src/formats/anthropic/parse.ts +4 -4
- package/src/formats/anthropic/schema.ts +1 -68
- package/src/formats/anthropic/serialize.ts +9 -5
- package/src/formats/openai/index.ts +1 -1
- package/src/formats/openai/parse.ts +1 -1
- package/src/formats/openai/schema.ts +1 -69
- package/src/formats/openai/serialize.ts +15 -17
- package/src/formats/{parse-helpers.ts → request-helpers.ts} +2 -31
- package/src/formats/responses/index.ts +1 -1
- package/src/formats/responses/parse.ts +1 -1
- package/src/formats/responses/schema.ts +1 -72
- package/src/formats/responses/serialize.ts +9 -5
- package/src/formats/serialize-helpers.ts +30 -0
- package/src/formats/types.ts +3 -3
- package/src/loader.ts +7 -11
- package/src/logger.ts +19 -25
- package/src/mock-server.ts +10 -14
- package/src/route-handler.ts +1 -1
- package/src/rule-engine.ts +23 -1
- package/src/types/reply.ts +6 -10
- package/src/types/request.ts +7 -11
- package/src/types/rule.ts +3 -10
- package/src/types.ts +3 -5
- package/test/formats/anthropic.test.ts +4 -4
- package/test/formats/parse-helpers.test.ts +275 -0
- package/test/formats/responses.test.ts +4 -4
- package/test/helpers/make-req.ts +18 -0
- package/test/history.test.ts +348 -0
- package/test/loader.test.ts +11 -27
- package/test/logger.test.ts +294 -0
- package/test/mock-server.test.ts +1 -1
- package/test/rule-engine.test.ts +8 -22
- package/test/formats/anthropic-schema.test.ts +0 -192
- package/test/formats/openai-schema.test.ts +0 -105
- package/test/formats/responses-schema.test.ts +0 -114
|
@@ -1,39 +1,10 @@
|
|
|
1
|
-
import type { FormatName, Message, MockRequest,
|
|
2
|
-
|
|
3
|
-
export const MS_PER_SECOND = 1000;
|
|
4
|
-
const BASE_36 = 36;
|
|
5
|
-
export const DEFAULT_USAGE = { input: 10, output: 5 } as const;
|
|
1
|
+
import type { FormatName, Message, MockRequest, ToolDef } from "../types.js";
|
|
6
2
|
|
|
7
3
|
function asRecord(body: unknown): Record<string, unknown> {
|
|
8
4
|
if (typeof body === "object" && body !== null) return body as Record<string, unknown>;
|
|
9
5
|
return {};
|
|
10
6
|
}
|
|
11
7
|
|
|
12
|
-
export function splitText(text: string, chunkSize: number): string[] {
|
|
13
|
-
if (chunkSize <= 0 || text.length <= chunkSize) return [text];
|
|
14
|
-
const chunks: string[] = [];
|
|
15
|
-
for (let i = 0; i < text.length; i += chunkSize) {
|
|
16
|
-
chunks.push(text.slice(i, i + chunkSize));
|
|
17
|
-
}
|
|
18
|
-
return chunks;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function genId(prefix: string): string {
|
|
22
|
-
return `${prefix}_${Date.now().toString(BASE_36)}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function toolId(tool: { id?: string | undefined }, prefix: string, index: number): string {
|
|
26
|
-
return tool.id ?? `${prefix}_${Date.now().toString(BASE_36)}_${index}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function shouldEmitText(reply: ReplyObject): boolean {
|
|
30
|
-
return Boolean(reply.text) || (!reply.tools?.length && !reply.reasoning);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function finishReason(reply: ReplyObject, onTools: string, onStop: string): string {
|
|
34
|
-
return reply.tools?.length ? onTools : onStop;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
8
|
export function isStreaming(body: unknown): boolean {
|
|
38
9
|
return asRecord(body)["stream"] !== false;
|
|
39
10
|
}
|
|
@@ -45,7 +16,7 @@ export interface RequestMeta {
|
|
|
45
16
|
|
|
46
17
|
const EMPTY_META: RequestMeta = { headers: {}, path: "" };
|
|
47
18
|
|
|
48
|
-
|
|
19
|
+
interface ParsedBody {
|
|
49
20
|
readonly model?: string | undefined;
|
|
50
21
|
readonly stream?: boolean | undefined;
|
|
51
22
|
}
|
|
@@ -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,5 +1,5 @@
|
|
|
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 { ResponsesRequestSchema, FunctionToolSchema, type ResponsesRequest } from "./schema.js";
|
|
4
4
|
|
|
5
5
|
function extractInputContent(content: string | Record<string, unknown>[]): string {
|
|
@@ -1,75 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
type: z.literal("message").optional(),
|
|
5
|
-
role: z.enum(["user", "assistant", "system", "developer"]),
|
|
6
|
-
content: z.union([z.string(), z.array(z.record(z.string(), z.unknown()))]),
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
const FunctionCallInputSchema = z.object({
|
|
10
|
-
type: z.literal("function_call"),
|
|
11
|
-
id: z.string().optional(),
|
|
12
|
-
call_id: z.string(),
|
|
13
|
-
name: z.string(),
|
|
14
|
-
arguments: z.string(),
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const FunctionCallOutputSchema = z.object({
|
|
18
|
-
type: z.literal("function_call_output"),
|
|
19
|
-
call_id: z.string(),
|
|
20
|
-
output: z.string(),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const InputItemSchema = z.union([
|
|
24
|
-
InputMessageSchema,
|
|
25
|
-
FunctionCallInputSchema,
|
|
26
|
-
FunctionCallOutputSchema,
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
const RawToolSchema = z.record(z.string(), z.unknown());
|
|
30
|
-
|
|
31
|
-
export const FunctionToolSchema = z.object({
|
|
32
|
-
type: z.literal("function"),
|
|
33
|
-
name: z.string(),
|
|
34
|
-
description: z.string().optional(),
|
|
35
|
-
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
export type FunctionTool = z.infer<typeof FunctionToolSchema>;
|
|
39
|
-
|
|
40
|
-
export const ResponsesRequestSchema = z.looseObject({
|
|
41
|
-
model: z.string().min(1).optional(),
|
|
42
|
-
input: z.union([z.string(), z.array(InputItemSchema)]).optional(),
|
|
43
|
-
instructions: z.string().optional(),
|
|
44
|
-
tools: z.array(RawToolSchema).optional(),
|
|
45
|
-
stream: z.boolean().optional(),
|
|
46
|
-
temperature: z.number().optional(),
|
|
47
|
-
previous_response_id: z.string().optional(),
|
|
48
|
-
background: z.boolean().optional(),
|
|
49
|
-
context_management: z.unknown().optional(),
|
|
50
|
-
conversation: z.unknown().optional(),
|
|
51
|
-
include: z.array(z.string()).optional(),
|
|
52
|
-
max_output_tokens: z.number().optional(),
|
|
53
|
-
max_tool_calls: z.number().optional(),
|
|
54
|
-
metadata: z.record(z.string(), z.string()).optional(),
|
|
55
|
-
parallel_tool_calls: z.boolean().optional(),
|
|
56
|
-
prompt: z.unknown().optional(),
|
|
57
|
-
prompt_cache_key: z.string().optional(),
|
|
58
|
-
prompt_cache_retention: z.string().optional(),
|
|
59
|
-
reasoning: z.unknown().optional(),
|
|
60
|
-
safety_identifier: z.string().optional(),
|
|
61
|
-
service_tier: z.string().optional(),
|
|
62
|
-
store: z.boolean().optional(),
|
|
63
|
-
stream_options: z.unknown().optional(),
|
|
64
|
-
text: z.unknown().optional(),
|
|
65
|
-
tool_choice: z.unknown().optional(),
|
|
66
|
-
top_logprobs: z.number().optional(),
|
|
67
|
-
top_p: z.number().optional(),
|
|
68
|
-
truncation: z.string().optional(),
|
|
69
|
-
user: z.string().optional(),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
export type ResponsesRequest = z.infer<typeof ResponsesRequestSchema>;
|
|
3
|
+
export { ResponsesRequestSchema, FunctionToolSchema, type ResponsesRequest } from "llm-schemas/openai/responses";
|
|
73
4
|
|
|
74
5
|
const OutputContentSchema = z.object({
|
|
75
6
|
type: z.string(),
|
|
@@ -89,8 +20,6 @@ const OutputItemSchema = z.object({
|
|
|
89
20
|
summary: z.array(z.object({ type: z.string(), text: z.string() })).optional(),
|
|
90
21
|
});
|
|
91
22
|
|
|
92
|
-
export type ResponsesOutputItem = z.infer<typeof OutputItemSchema>;
|
|
93
|
-
|
|
94
23
|
export const ResponsesEventSchema = z.object({
|
|
95
24
|
type: z.string(),
|
|
96
25
|
sequence_number: z.number().optional(),
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ReplyObject, ReplyOptions, ToolCall } from "../../types.js";
|
|
2
2
|
import type { SSEChunk } from "../types.js";
|
|
3
|
-
import { splitText, genId, toolId, shouldEmitText, MS_PER_SECOND, DEFAULT_USAGE } from "../
|
|
3
|
+
import { splitText, genId, toolId, shouldEmitText, MS_PER_SECOND, DEFAULT_USAGE } from "../serialize-helpers.js";
|
|
4
|
+
|
|
5
|
+
function buildUsage(usage: { input: number; output: number }) {
|
|
6
|
+
return { input_tokens: usage.input, output_tokens: usage.output, total_tokens: usage.input + usage.output };
|
|
7
|
+
}
|
|
4
8
|
|
|
5
9
|
interface StreamBlock { chunks: SSEChunk[]; outputItem: unknown }
|
|
6
10
|
|
|
@@ -95,12 +99,12 @@ export function serialize(reply: ReplyObject, model: string, options: ReplyOptio
|
|
|
95
99
|
c({
|
|
96
100
|
type: "response.completed",
|
|
97
101
|
response: { ...baseResponse, status: "completed", output,
|
|
98
|
-
usage:
|
|
102
|
+
usage: buildUsage(usage) },
|
|
99
103
|
}),
|
|
100
104
|
];
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
export function serializeComplete(reply: ReplyObject, model: string): unknown {
|
|
107
|
+
export function serializeComplete(reply: ReplyObject, model: string): Record<string, unknown> {
|
|
104
108
|
const id = genId("resp");
|
|
105
109
|
const createdAt = Math.floor(Date.now() / MS_PER_SECOND);
|
|
106
110
|
const usage = reply.usage ?? DEFAULT_USAGE;
|
|
@@ -120,10 +124,10 @@ export function serializeComplete(reply: ReplyObject, model: string): unknown {
|
|
|
120
124
|
|
|
121
125
|
return {
|
|
122
126
|
id, object: "response", created_at: createdAt, status: "completed", model, output,
|
|
123
|
-
usage:
|
|
127
|
+
usage: buildUsage(usage),
|
|
124
128
|
};
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
export function serializeError(error: { status: number; message: string; type?: string }): unknown {
|
|
131
|
+
export function serializeError(error: { status: number; message: string; type?: string }): Record<string, unknown> {
|
|
128
132
|
return { type: "error", error: { message: error.message, type: error.type ?? "server_error", code: error.type ?? "server_error" } };
|
|
129
133
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReplyObject } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const MS_PER_SECOND = 1000;
|
|
4
|
+
const BASE_36 = 36;
|
|
5
|
+
export const DEFAULT_USAGE = { input: 10, output: 5 } as const;
|
|
6
|
+
|
|
7
|
+
export function splitText(text: string, chunkSize: number): string[] {
|
|
8
|
+
if (chunkSize <= 0 || text.length <= chunkSize) return [text];
|
|
9
|
+
const chunks: string[] = [];
|
|
10
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
11
|
+
chunks.push(text.slice(i, i + chunkSize));
|
|
12
|
+
}
|
|
13
|
+
return chunks;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function genId(prefix: string): string {
|
|
17
|
+
return `${prefix}_${Date.now().toString(BASE_36)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function toolId(tool: { id?: string | undefined }, prefix: string, index: number): string {
|
|
21
|
+
return tool.id ?? `${prefix}_${Date.now().toString(BASE_36)}_${index}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shouldEmitText(reply: ReplyObject): boolean {
|
|
25
|
+
return Boolean(reply.text) || (!reply.tools?.length && !reply.reasoning);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function finishReason(reply: ReplyObject, onTools: string, onStop: string): string {
|
|
29
|
+
return reply.tools?.length ? onTools : onStop;
|
|
30
|
+
}
|
package/src/formats/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FormatName, MockRequest, ReplyObject, ReplyOptions } from "../types.js";
|
|
2
|
-
import type { RequestMeta } from "./
|
|
2
|
+
import type { RequestMeta } from "./request-helpers.js";
|
|
3
3
|
|
|
4
4
|
export interface SSEChunk {
|
|
5
5
|
readonly event?: string | undefined;
|
|
@@ -12,6 +12,6 @@ export interface Format {
|
|
|
12
12
|
parseRequest(body: unknown, meta?: RequestMeta): MockRequest;
|
|
13
13
|
isStreaming(body: unknown): boolean;
|
|
14
14
|
serialize(reply: ReplyObject, model: string, options?: ReplyOptions): readonly SSEChunk[];
|
|
15
|
-
serializeComplete(reply: ReplyObject, model: string): unknown
|
|
16
|
-
serializeError(error: { status: number; message: string; type?: string | undefined }): unknown
|
|
15
|
+
serializeComplete(reply: ReplyObject, model: string): Record<string, unknown>;
|
|
16
|
+
serializeError(error: { status: number; message: string; type?: string | undefined }): Record<string, unknown>;
|
|
17
17
|
}
|
package/src/loader.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { join, extname } from "node:path";
|
|
|
3
3
|
import JSON5 from "json5";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import type { Handler, Match, MatchObject, Reply } from "./types.js";
|
|
6
|
-
import type
|
|
6
|
+
import { type RuleEngine, createSequenceResolver } from "./rule-engine.js";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface LoadContext {
|
|
9
9
|
engine: RuleEngine;
|
|
10
10
|
setFallback?: (reply: Reply) => void;
|
|
11
11
|
}
|
|
@@ -106,8 +106,7 @@ function addSequenceRule(
|
|
|
106
106
|
templates: Templates,
|
|
107
107
|
filePath: string,
|
|
108
108
|
): void {
|
|
109
|
-
|
|
110
|
-
const resolved = entries.map((entry) => {
|
|
109
|
+
const steps = entries.map((entry) => {
|
|
111
110
|
if (typeof entry === "string" || !("reply" in entry)) {
|
|
112
111
|
return { reply: resolveReplyRef(entry, templates, filePath) };
|
|
113
112
|
}
|
|
@@ -119,13 +118,10 @@ function addSequenceRule(
|
|
|
119
118
|
},
|
|
120
119
|
};
|
|
121
120
|
});
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return step.reply;
|
|
127
|
-
});
|
|
128
|
-
rule.remaining = resolved.length;
|
|
121
|
+
const rule = engine.add(match, "");
|
|
122
|
+
const { resolver, entryCount } = createSequenceResolver(steps, rule);
|
|
123
|
+
rule.resolve = resolver;
|
|
124
|
+
rule.remaining = entryCount;
|
|
129
125
|
}
|
|
130
126
|
|
|
131
127
|
async function loadJson5File(filePath: string, ctx: LoadContext): Promise<void> {
|
package/src/logger.ts
CHANGED
|
@@ -19,6 +19,15 @@ const LEVEL_STYLE = {
|
|
|
19
19
|
debug: { label: pc.dim("DEBUG"), symbol: pc.dim("·") },
|
|
20
20
|
} as const;
|
|
21
21
|
|
|
22
|
+
type ConsoleMethod = "error" | "warn" | "log";
|
|
23
|
+
|
|
24
|
+
const LEVEL_CONFIG: Record<keyof typeof LEVEL_STYLE, { priority: number; method: ConsoleMethod; dim?: boolean }> = {
|
|
25
|
+
error: { priority: LEVEL_PRIORITY.error, method: "error" },
|
|
26
|
+
warn: { priority: LEVEL_PRIORITY.warning, method: "warn" },
|
|
27
|
+
info: { priority: LEVEL_PRIORITY.info, method: "log" },
|
|
28
|
+
debug: { priority: LEVEL_PRIORITY.debug, method: "log", dim: true },
|
|
29
|
+
};
|
|
30
|
+
|
|
22
31
|
export class Logger {
|
|
23
32
|
readonly level: LogLevel;
|
|
24
33
|
private threshold: number;
|
|
@@ -28,31 +37,16 @@ export class Logger {
|
|
|
28
37
|
this.threshold = LEVEL_PRIORITY[level];
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
warn(msg: string, ...args: unknown[]): void {
|
|
39
|
-
if (this.threshold >= LEVEL_PRIORITY.warning) {
|
|
40
|
-
const { label, symbol } = LEVEL_STYLE.warn;
|
|
41
|
-
console.warn(`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${msg}`, ...args);
|
|
42
|
-
}
|
|
40
|
+
private log(key: keyof typeof LEVEL_STYLE, msg: string, args: unknown[]): void {
|
|
41
|
+
const config = LEVEL_CONFIG[key];
|
|
42
|
+
if (this.threshold < config.priority) return;
|
|
43
|
+
const { label, symbol } = LEVEL_STYLE[key];
|
|
44
|
+
const text = config.dim ? pc.dim(msg) : msg;
|
|
45
|
+
console[config.method](`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${text}`, ...args);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
debug(msg: string, ...args: unknown[]): void {
|
|
53
|
-
if (this.threshold >= LEVEL_PRIORITY.debug) {
|
|
54
|
-
const { label, symbol } = LEVEL_STYLE.debug;
|
|
55
|
-
console.log(`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${pc.dim(msg)}`, ...args);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
48
|
+
error(msg: string, ...args: unknown[]): void { this.log("error", msg, args); }
|
|
49
|
+
warn(msg: string, ...args: unknown[]): void { this.log("warn", msg, args); }
|
|
50
|
+
info(msg: string, ...args: unknown[]): void { this.log("info", msg, args); }
|
|
51
|
+
debug(msg: string, ...args: unknown[]): void { this.log("debug", msg, args); }
|
|
58
52
|
}
|
package/src/mock-server.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { FastifyInstance } from "fastify";
|
|
|
3
3
|
import type {
|
|
4
4
|
Match, PendingRule, Reply, ReplyOptions, Resolver, Rule, RuleHandle, RuleSummary, SequenceEntry,
|
|
5
5
|
} from "./types.js";
|
|
6
|
-
import { RuleEngine } from "./rule-engine.js";
|
|
6
|
+
import { RuleEngine, createSequenceResolver } from "./rule-engine.js";
|
|
7
7
|
import { RequestHistory } from "./history.js";
|
|
8
8
|
import { openaiFormat } from "./formats/openai/index.js";
|
|
9
9
|
import { anthropicFormat } from "./formats/anthropic/index.js";
|
|
@@ -103,19 +103,15 @@ export class MockServer {
|
|
|
103
103
|
return makeHandle(engine.add(match, response, options));
|
|
104
104
|
},
|
|
105
105
|
replySequence(entries: readonly SequenceEntry[]): RuleHandle {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
rule.options = entry.options ?? {};
|
|
116
|
-
return entry.reply;
|
|
117
|
-
});
|
|
118
|
-
rule.remaining = entries.length;
|
|
106
|
+
const steps = entries.map((entry) =>
|
|
107
|
+
typeof entry === "string" || !("reply" in entry)
|
|
108
|
+
? { reply: entry as Reply }
|
|
109
|
+
: { reply: entry.reply, options: entry.options },
|
|
110
|
+
);
|
|
111
|
+
const rule = engine.add(match, "");
|
|
112
|
+
const { resolver, entryCount } = createSequenceResolver(steps, rule);
|
|
113
|
+
rule.resolve = resolver;
|
|
114
|
+
rule.remaining = entryCount;
|
|
119
115
|
return makeHandle(rule);
|
|
120
116
|
},
|
|
121
117
|
};
|
package/src/route-handler.ts
CHANGED
package/src/rule-engine.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Match, MatchObject, MockRequest, Resolver, ReplyOptions, Rule, RuleSummary } from "./types.js";
|
|
1
|
+
import type { Match, MatchObject, MockRequest, Resolver, Reply, ReplyOptions, Rule, RuleSummary } from "./types.js";
|
|
2
2
|
|
|
3
3
|
function safeRegex(re: RegExp): RegExp {
|
|
4
4
|
return (re.global || re.sticky) ? new RegExp(re.source, re.flags.replace(/[gy]/g, "")) : re;
|
|
@@ -62,6 +62,28 @@ function createRule(match: Match, resolve: Resolver, options: ReplyOptions, desc
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
interface SequenceStep {
|
|
66
|
+
readonly reply: Reply;
|
|
67
|
+
readonly options?: ReplyOptions | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createSequenceResolver(
|
|
71
|
+
steps: readonly SequenceStep[],
|
|
72
|
+
rule: { options: ReplyOptions },
|
|
73
|
+
): { resolver: () => Reply; entryCount: number } {
|
|
74
|
+
if (steps.length === 0) throw new Error("Sequence requires at least one entry.");
|
|
75
|
+
let index = 0;
|
|
76
|
+
const last = steps[steps.length - 1]!;
|
|
77
|
+
return {
|
|
78
|
+
resolver: () => {
|
|
79
|
+
const step = steps[index++] ?? last;
|
|
80
|
+
rule.options = step.options ?? {};
|
|
81
|
+
return step.reply;
|
|
82
|
+
},
|
|
83
|
+
entryCount: steps.length,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
export class RuleEngine {
|
|
66
88
|
private readonly rules: Rule[] = [];
|
|
67
89
|
|
package/src/types/reply.ts
CHANGED
|
@@ -5,13 +5,11 @@ export type Reply = string | ReplyObject;
|
|
|
5
5
|
|
|
6
6
|
/** A structured reply. Text, reasoning, tool calls, usage, and errors are all optional. */
|
|
7
7
|
export interface ReplyObject {
|
|
8
|
-
/** Text content to send back. */
|
|
9
8
|
readonly text?: string | undefined;
|
|
10
9
|
/** Extended thinking or chain-of-thought. Works with Anthropic and Responses formats. */
|
|
11
10
|
readonly reasoning?: string | undefined;
|
|
12
|
-
/** Tool calls the "model" wants to make. */
|
|
13
11
|
readonly tools?: readonly ToolCall[] | undefined;
|
|
14
|
-
/**
|
|
12
|
+
/** Falls back to `{ input: 10, output: 5 }` if omitted. */
|
|
15
13
|
readonly usage?: { readonly input: number; readonly output: number } | undefined;
|
|
16
14
|
/** When set, the server responds with this HTTP error instead of a normal reply. */
|
|
17
15
|
readonly error?: ErrorReply | undefined;
|
|
@@ -19,16 +17,14 @@ export interface ReplyObject {
|
|
|
19
17
|
|
|
20
18
|
/** An HTTP error response. The server returns this status code with a format-appropriate body. */
|
|
21
19
|
export interface ErrorReply {
|
|
22
|
-
/** HTTP status code, like 429 or 500. */
|
|
23
20
|
readonly status: number;
|
|
24
21
|
readonly message: string;
|
|
25
|
-
/**
|
|
22
|
+
/** Each format has its own default if omitted. */
|
|
26
23
|
readonly type?: string | undefined;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
/** A tool call in the mock response. */
|
|
30
26
|
export interface ToolCall {
|
|
31
|
-
/**
|
|
27
|
+
/** Auto-generated if omitted. */
|
|
32
28
|
readonly id?: string | undefined;
|
|
33
29
|
readonly name: string;
|
|
34
30
|
readonly args: Readonly<Record<string, unknown>>;
|
|
@@ -37,11 +33,11 @@ export interface ToolCall {
|
|
|
37
33
|
/** A reply value or a function that produces one. Async functions are supported. */
|
|
38
34
|
export type Resolver = Reply | ((req: MockRequest) => Reply | Promise<Reply>);
|
|
39
35
|
|
|
40
|
-
/**
|
|
36
|
+
/** Per-rule streaming options. Merged with server-level defaults, with per-rule values winning. */
|
|
41
37
|
export interface ReplyOptions {
|
|
42
|
-
/** Milliseconds
|
|
38
|
+
/** Milliseconds between SSE chunks. */
|
|
43
39
|
readonly latency?: number | undefined;
|
|
44
|
-
/**
|
|
40
|
+
/** Characters per SSE chunk for more realistic streaming. */
|
|
45
41
|
readonly chunkSize?: number | undefined;
|
|
46
42
|
}
|
|
47
43
|
|
package/src/types/request.ts
CHANGED
|
@@ -3,28 +3,24 @@ export type FormatName = "openai" | "anthropic" | "responses";
|
|
|
3
3
|
|
|
4
4
|
/** A normalised view of an incoming request, regardless of the original wire format. */
|
|
5
5
|
export interface MockRequest {
|
|
6
|
-
/** Which format route the request hit. */
|
|
7
6
|
readonly format: FormatName;
|
|
8
7
|
readonly model: string;
|
|
9
|
-
/** Whether the client asked for SSE streaming. */
|
|
10
8
|
readonly streaming: boolean;
|
|
11
9
|
/** Full conversation, normalised from whatever format came in. */
|
|
12
10
|
readonly messages: readonly Message[];
|
|
13
11
|
/** The last user message's text. This is what most matchers check. */
|
|
14
12
|
readonly lastMessage: string;
|
|
15
|
-
/**
|
|
13
|
+
/** Empty string if there wasn't one. */
|
|
16
14
|
readonly systemMessage: string;
|
|
17
|
-
/** Tool definitions from the request, if any were sent. */
|
|
18
15
|
readonly tools?: readonly ToolDef[] | undefined;
|
|
19
|
-
/**
|
|
16
|
+
/** Pulled out from `tools` for quick lookups. */
|
|
20
17
|
readonly toolNames: readonly string[];
|
|
21
|
-
/**
|
|
18
|
+
/** Set when the last message was a tool result. */
|
|
22
19
|
readonly lastToolCallId: string | undefined;
|
|
23
|
-
/** The raw request body,
|
|
20
|
+
/** The raw request body, for anything we don't extract. */
|
|
24
21
|
readonly raw: unknown;
|
|
25
|
-
/** HTTP headers from the incoming request. */
|
|
26
22
|
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
27
|
-
/**
|
|
23
|
+
/** e.g. `/v1/chat/completions` */
|
|
28
24
|
readonly path: string;
|
|
29
25
|
}
|
|
30
26
|
|
|
@@ -32,7 +28,7 @@ export interface MockRequest {
|
|
|
32
28
|
export interface Message {
|
|
33
29
|
readonly role: "system" | "user" | "assistant" | "tool";
|
|
34
30
|
readonly content: string;
|
|
35
|
-
/**
|
|
31
|
+
/** Links the result back to its tool call. Only set on `"tool"` messages. */
|
|
36
32
|
readonly toolCallId?: string | undefined;
|
|
37
33
|
}
|
|
38
34
|
|
|
@@ -40,6 +36,6 @@ export interface Message {
|
|
|
40
36
|
export interface ToolDef {
|
|
41
37
|
readonly name: string;
|
|
42
38
|
readonly description?: string | undefined;
|
|
43
|
-
/** JSON Schema
|
|
39
|
+
/** JSON Schema, passed through as-is. */
|
|
44
40
|
readonly parameters?: unknown;
|
|
45
41
|
}
|
package/src/types/rule.ts
CHANGED
|
@@ -17,13 +17,9 @@ export type Match =
|
|
|
17
17
|
|
|
18
18
|
/** A structured matcher. Every field you set must match for the rule to fire. */
|
|
19
19
|
export interface MatchObject {
|
|
20
|
-
/** Substring or regex against the last user message. */
|
|
21
20
|
readonly message?: string | RegExp;
|
|
22
|
-
/** Substring or regex against the model name. */
|
|
23
21
|
readonly model?: string | RegExp;
|
|
24
|
-
/** Substring or regex against the system prompt. */
|
|
25
22
|
readonly system?: string | RegExp;
|
|
26
|
-
/** Only match requests from this API format. */
|
|
27
23
|
readonly format?: FormatName;
|
|
28
24
|
/** Match when the request includes a tool definition with this name. */
|
|
29
25
|
readonly toolName?: string;
|
|
@@ -36,15 +32,13 @@ export interface MatchObject {
|
|
|
36
32
|
/** Returned by `when()`. Call `.reply()` or `.replySequence()` on it to complete the rule. */
|
|
37
33
|
export interface PendingRule {
|
|
38
34
|
reply(response: Resolver, options?: ReplyOptions): RuleHandle;
|
|
39
|
-
/** Each match advances through the array. The last entry repeats once
|
|
35
|
+
/** Each match advances through the array. The last entry repeats once exhausted. */
|
|
40
36
|
replySequence(entries: readonly SequenceEntry[]): RuleHandle;
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
/** A handle to a registered rule. All methods return `this` for chaining. */
|
|
44
40
|
export interface RuleHandle {
|
|
45
|
-
/** Auto-expire the rule after `n` matches. */
|
|
46
41
|
times(n: number): RuleHandle;
|
|
47
|
-
/** Move this rule to the front of the list so it matches first. */
|
|
48
42
|
first(): RuleHandle;
|
|
49
43
|
}
|
|
50
44
|
|
|
@@ -59,16 +53,15 @@ export interface Handler {
|
|
|
59
53
|
|
|
60
54
|
/** A summary of a registered rule, for inspection. */
|
|
61
55
|
export interface RuleSummary {
|
|
62
|
-
/** Human-readable description of what the rule matches. */
|
|
63
56
|
readonly description: string;
|
|
64
|
-
/**
|
|
57
|
+
/** `Infinity` means unlimited. */
|
|
65
58
|
readonly remaining: number;
|
|
66
59
|
}
|
|
67
60
|
|
|
68
61
|
export interface Rule {
|
|
69
62
|
readonly description: string;
|
|
70
63
|
readonly match: (req: MockRequest) => boolean;
|
|
71
|
-
|
|
64
|
+
resolve: Resolver;
|
|
72
65
|
options: ReplyOptions;
|
|
73
66
|
remaining: number;
|
|
74
67
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Match, MatchObject, PendingRule, RuleHandle, RuleSummary, Handler, RecordedRequest, Rule,
|
|
5
|
-
} from "./types/index.js";
|
|
1
|
+
export type { FormatName, MockRequest, Message, ToolDef } from "./types/request.js";
|
|
2
|
+
export type { Reply, ReplyObject, ErrorReply, ToolCall, Resolver, ReplyOptions, SequenceEntry } from "./types/reply.js";
|
|
3
|
+
export type { Match, MatchObject, PendingRule, RuleHandle, RuleSummary, Handler, Rule } from "./types/rule.js";
|
|
@@ -226,10 +226,10 @@ describe("Anthropic Format", () => {
|
|
|
226
226
|
"claude-sonnet-4-6",
|
|
227
227
|
) as AnthropicComplete;
|
|
228
228
|
const tool = result.content.find((c) => c.type === "tool_use");
|
|
229
|
-
|
|
230
|
-
expect(tool
|
|
231
|
-
expect(tool
|
|
232
|
-
expect(tool
|
|
229
|
+
if (!tool) throw new Error("expected tool_use content block");
|
|
230
|
+
expect(tool.name).toBe("read_file");
|
|
231
|
+
expect(tool.input).toEqual({ path: "/tmp" });
|
|
232
|
+
expect(tool.id).toBeTypeOf("string");
|
|
233
233
|
});
|
|
234
234
|
|
|
235
235
|
it("sets stop_reason to tool_use when tools present", () => {
|