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
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import type { ReplyObject, ReplyOptions, ToolCall } from "../../types.js";
|
|
2
2
|
import type { SSEChunk } from "../types.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
splitText,
|
|
5
|
+
genId,
|
|
6
|
+
toolId,
|
|
7
|
+
shouldEmitText,
|
|
8
|
+
MS_PER_SECOND,
|
|
9
|
+
DEFAULT_USAGE,
|
|
10
|
+
} from "../serialize-helpers.js";
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
function buildUsage(usage: { input: number; output: number }) {
|
|
13
|
+
return {
|
|
14
|
+
input_tokens: usage.input,
|
|
15
|
+
output_tokens: usage.output,
|
|
16
|
+
total_tokens: usage.input + usage.output,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface StreamBlock {
|
|
21
|
+
chunks: SSEChunk[];
|
|
22
|
+
outputItem: unknown;
|
|
23
|
+
}
|
|
6
24
|
|
|
7
25
|
const NO_ANNOTATIONS: readonly unknown[] = [];
|
|
8
26
|
|
|
@@ -10,43 +28,140 @@ type Chunk = (payload: Record<string, unknown>) => SSEChunk;
|
|
|
10
28
|
|
|
11
29
|
function createChunk(): Chunk {
|
|
12
30
|
let seq = 0;
|
|
13
|
-
return (payload) => ({
|
|
31
|
+
return (payload) => ({
|
|
32
|
+
data: JSON.stringify({ ...payload, sequence_number: seq++ }),
|
|
33
|
+
});
|
|
14
34
|
}
|
|
15
35
|
|
|
16
|
-
function reasoningStreamBlock(
|
|
36
|
+
function reasoningStreamBlock(
|
|
37
|
+
c: Chunk,
|
|
38
|
+
i: number,
|
|
39
|
+
reasoning: string,
|
|
40
|
+
): StreamBlock {
|
|
17
41
|
const itemId = `rs_${genId("rs")}`;
|
|
18
42
|
const summaryPart = { type: "summary_text" as const, text: reasoning };
|
|
19
|
-
const item = {
|
|
43
|
+
const item = {
|
|
44
|
+
type: "reasoning",
|
|
45
|
+
id: itemId,
|
|
46
|
+
status: "completed",
|
|
47
|
+
summary: [summaryPart],
|
|
48
|
+
};
|
|
20
49
|
|
|
21
50
|
return {
|
|
22
51
|
outputItem: item,
|
|
23
52
|
chunks: [
|
|
24
|
-
c({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
53
|
+
c({
|
|
54
|
+
type: "response.output_item.added",
|
|
55
|
+
output_index: i,
|
|
56
|
+
item: {
|
|
57
|
+
type: "reasoning",
|
|
58
|
+
id: itemId,
|
|
59
|
+
status: "in_progress",
|
|
60
|
+
summary: [],
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
c({
|
|
64
|
+
type: "response.reasoning_summary_part.added",
|
|
65
|
+
item_id: itemId,
|
|
66
|
+
output_index: i,
|
|
67
|
+
summary_index: 0,
|
|
68
|
+
part: { type: "summary_text", text: "" },
|
|
69
|
+
}),
|
|
70
|
+
c({
|
|
71
|
+
type: "response.reasoning_summary_text.delta",
|
|
72
|
+
item_id: itemId,
|
|
73
|
+
output_index: i,
|
|
74
|
+
summary_index: 0,
|
|
75
|
+
delta: reasoning,
|
|
76
|
+
}),
|
|
77
|
+
c({
|
|
78
|
+
type: "response.reasoning_summary_text.done",
|
|
79
|
+
item_id: itemId,
|
|
80
|
+
output_index: i,
|
|
81
|
+
summary_index: 0,
|
|
82
|
+
text: reasoning,
|
|
83
|
+
}),
|
|
84
|
+
c({
|
|
85
|
+
type: "response.reasoning_summary_part.done",
|
|
86
|
+
item_id: itemId,
|
|
87
|
+
output_index: i,
|
|
88
|
+
summary_index: 0,
|
|
89
|
+
part: summaryPart,
|
|
90
|
+
}),
|
|
29
91
|
c({ type: "response.output_item.done", output_index: i, item }),
|
|
30
92
|
],
|
|
31
93
|
};
|
|
32
94
|
}
|
|
33
95
|
|
|
34
|
-
function textStreamBlock(
|
|
96
|
+
function textStreamBlock(
|
|
97
|
+
c: Chunk,
|
|
98
|
+
i: number,
|
|
99
|
+
text: string,
|
|
100
|
+
chunkSize: number,
|
|
101
|
+
): StreamBlock {
|
|
35
102
|
const itemId = `msg_${genId("msg")}`;
|
|
36
|
-
const outputText = {
|
|
37
|
-
|
|
103
|
+
const outputText = {
|
|
104
|
+
type: "output_text" as const,
|
|
105
|
+
text,
|
|
106
|
+
annotations: NO_ANNOTATIONS,
|
|
107
|
+
};
|
|
108
|
+
const outputItem = {
|
|
109
|
+
type: "message",
|
|
110
|
+
id: itemId,
|
|
111
|
+
status: "completed",
|
|
112
|
+
role: "assistant",
|
|
113
|
+
content: [outputText],
|
|
114
|
+
};
|
|
38
115
|
|
|
39
116
|
return {
|
|
40
117
|
outputItem,
|
|
41
118
|
chunks: [
|
|
42
|
-
c({
|
|
43
|
-
|
|
119
|
+
c({
|
|
120
|
+
type: "response.output_item.added",
|
|
121
|
+
output_index: i,
|
|
122
|
+
item: {
|
|
123
|
+
type: "message",
|
|
124
|
+
id: itemId,
|
|
125
|
+
status: "in_progress",
|
|
126
|
+
role: "assistant",
|
|
127
|
+
content: [],
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
c({
|
|
131
|
+
type: "response.content_part.added",
|
|
132
|
+
item_id: itemId,
|
|
133
|
+
output_index: i,
|
|
134
|
+
content_index: 0,
|
|
135
|
+
part: { type: "output_text", text: "", annotations: [] },
|
|
136
|
+
}),
|
|
44
137
|
...splitText(text, chunkSize).map((piece) =>
|
|
45
|
-
c({
|
|
138
|
+
c({
|
|
139
|
+
type: "response.output_text.delta",
|
|
140
|
+
item_id: itemId,
|
|
141
|
+
output_index: i,
|
|
142
|
+
content_index: 0,
|
|
143
|
+
delta: piece,
|
|
144
|
+
}),
|
|
46
145
|
),
|
|
47
|
-
c({
|
|
48
|
-
|
|
49
|
-
|
|
146
|
+
c({
|
|
147
|
+
type: "response.output_text.done",
|
|
148
|
+
item_id: itemId,
|
|
149
|
+
output_index: i,
|
|
150
|
+
content_index: 0,
|
|
151
|
+
text,
|
|
152
|
+
}),
|
|
153
|
+
c({
|
|
154
|
+
type: "response.content_part.done",
|
|
155
|
+
item_id: itemId,
|
|
156
|
+
output_index: i,
|
|
157
|
+
content_index: 0,
|
|
158
|
+
part: outputText,
|
|
159
|
+
}),
|
|
160
|
+
c({
|
|
161
|
+
type: "response.output_item.done",
|
|
162
|
+
output_index: i,
|
|
163
|
+
item: outputItem,
|
|
164
|
+
}),
|
|
50
165
|
],
|
|
51
166
|
};
|
|
52
167
|
}
|
|
@@ -54,20 +169,49 @@ function textStreamBlock(c: Chunk, i: number, text: string, chunkSize: number):
|
|
|
54
169
|
function toolStreamBlock(c: Chunk, i: number, tool: ToolCall): StreamBlock {
|
|
55
170
|
const callId = toolId(tool, "call", i);
|
|
56
171
|
const argsJson = JSON.stringify(tool.args);
|
|
57
|
-
const outputItem = {
|
|
172
|
+
const outputItem = {
|
|
173
|
+
type: "function_call",
|
|
174
|
+
id: callId,
|
|
175
|
+
status: "completed",
|
|
176
|
+
name: tool.name,
|
|
177
|
+
call_id: callId,
|
|
178
|
+
arguments: argsJson,
|
|
179
|
+
};
|
|
58
180
|
|
|
59
181
|
return {
|
|
60
182
|
outputItem,
|
|
61
183
|
chunks: [
|
|
62
|
-
c({
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
184
|
+
c({
|
|
185
|
+
type: "response.output_item.added",
|
|
186
|
+
output_index: i,
|
|
187
|
+
item: { ...outputItem, status: "in_progress", arguments: "" },
|
|
188
|
+
}),
|
|
189
|
+
c({
|
|
190
|
+
type: "response.function_call_arguments.delta",
|
|
191
|
+
item_id: callId,
|
|
192
|
+
output_index: i,
|
|
193
|
+
delta: argsJson,
|
|
194
|
+
}),
|
|
195
|
+
c({
|
|
196
|
+
type: "response.function_call_arguments.done",
|
|
197
|
+
item_id: callId,
|
|
198
|
+
output_index: i,
|
|
199
|
+
arguments: argsJson,
|
|
200
|
+
}),
|
|
201
|
+
c({
|
|
202
|
+
type: "response.output_item.done",
|
|
203
|
+
output_index: i,
|
|
204
|
+
item: outputItem,
|
|
205
|
+
}),
|
|
66
206
|
],
|
|
67
207
|
};
|
|
68
208
|
}
|
|
69
209
|
|
|
70
|
-
export function serialize(
|
|
210
|
+
export function serialize(
|
|
211
|
+
reply: ReplyObject,
|
|
212
|
+
model: string,
|
|
213
|
+
options: ReplyOptions = {},
|
|
214
|
+
): readonly SSEChunk[] {
|
|
71
215
|
const id = genId("resp");
|
|
72
216
|
const createdAt = Math.floor(Date.now() / MS_PER_SECOND);
|
|
73
217
|
const usage = reply.usage ?? DEFAULT_USAGE;
|
|
@@ -76,13 +220,21 @@ export function serialize(reply: ReplyObject, model: string, options: ReplyOptio
|
|
|
76
220
|
|
|
77
221
|
const baseResponse = { id, object: "response", created_at: createdAt, model };
|
|
78
222
|
const header = [
|
|
79
|
-
c({
|
|
80
|
-
|
|
223
|
+
c({
|
|
224
|
+
type: "response.created",
|
|
225
|
+
response: { ...baseResponse, status: "in_progress", output: [] },
|
|
226
|
+
}),
|
|
227
|
+
c({
|
|
228
|
+
type: "response.in_progress",
|
|
229
|
+
response: { ...baseResponse, status: "in_progress", output: [] },
|
|
230
|
+
}),
|
|
81
231
|
];
|
|
82
232
|
|
|
83
233
|
const blocks: StreamBlock[] = [
|
|
84
234
|
...(reply.reasoning ? [reasoningStreamBlock(c, i++, reply.reasoning)] : []),
|
|
85
|
-
...(shouldEmitText(reply)
|
|
235
|
+
...(shouldEmitText(reply)
|
|
236
|
+
? [textStreamBlock(c, i++, reply.text ?? "", options.chunkSize ?? 0)]
|
|
237
|
+
: []),
|
|
86
238
|
...(reply.tools ?? []).map((tool) => toolStreamBlock(c, i++, tool)),
|
|
87
239
|
];
|
|
88
240
|
|
|
@@ -94,36 +246,83 @@ export function serialize(reply: ReplyObject, model: string, options: ReplyOptio
|
|
|
94
246
|
...allChunks,
|
|
95
247
|
c({
|
|
96
248
|
type: "response.completed",
|
|
97
|
-
response: {
|
|
98
|
-
|
|
249
|
+
response: {
|
|
250
|
+
...baseResponse,
|
|
251
|
+
status: "completed",
|
|
252
|
+
output,
|
|
253
|
+
usage: buildUsage(usage),
|
|
254
|
+
},
|
|
99
255
|
}),
|
|
100
256
|
];
|
|
101
257
|
}
|
|
102
258
|
|
|
103
|
-
export function serializeComplete(
|
|
259
|
+
export function serializeComplete(
|
|
260
|
+
reply: ReplyObject,
|
|
261
|
+
model: string,
|
|
262
|
+
): Record<string, unknown> {
|
|
104
263
|
const id = genId("resp");
|
|
105
264
|
const createdAt = Math.floor(Date.now() / MS_PER_SECOND);
|
|
106
265
|
const usage = reply.usage ?? DEFAULT_USAGE;
|
|
107
266
|
|
|
108
267
|
const output: unknown[] = [
|
|
109
268
|
...(reply.reasoning
|
|
110
|
-
? [
|
|
269
|
+
? [
|
|
270
|
+
{
|
|
271
|
+
type: "reasoning",
|
|
272
|
+
id: `rs_${genId("rs")}`,
|
|
273
|
+
status: "completed",
|
|
274
|
+
summary: [{ type: "summary_text", text: reply.reasoning }],
|
|
275
|
+
},
|
|
276
|
+
]
|
|
111
277
|
: []),
|
|
112
278
|
...(shouldEmitText(reply)
|
|
113
|
-
? [
|
|
279
|
+
? [
|
|
280
|
+
{
|
|
281
|
+
type: "message",
|
|
282
|
+
id: `msg_${genId("msg")}`,
|
|
283
|
+
status: "completed",
|
|
284
|
+
role: "assistant",
|
|
285
|
+
content: [
|
|
286
|
+
{ type: "output_text", text: reply.text ?? "", annotations: [] },
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
]
|
|
114
290
|
: []),
|
|
115
291
|
...(reply.tools ?? []).map((tool) => {
|
|
116
292
|
const callId = toolId(tool, "call", 0);
|
|
117
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
type: "function_call",
|
|
295
|
+
id: callId,
|
|
296
|
+
status: "completed",
|
|
297
|
+
name: tool.name,
|
|
298
|
+
call_id: callId,
|
|
299
|
+
arguments: JSON.stringify(tool.args),
|
|
300
|
+
};
|
|
118
301
|
}),
|
|
119
302
|
];
|
|
120
303
|
|
|
121
304
|
return {
|
|
122
|
-
id,
|
|
123
|
-
|
|
305
|
+
id,
|
|
306
|
+
object: "response",
|
|
307
|
+
created_at: createdAt,
|
|
308
|
+
status: "completed",
|
|
309
|
+
model,
|
|
310
|
+
output,
|
|
311
|
+
usage: buildUsage(usage),
|
|
124
312
|
};
|
|
125
313
|
}
|
|
126
314
|
|
|
127
|
-
export function serializeError(error: {
|
|
128
|
-
|
|
315
|
+
export function serializeError(error: {
|
|
316
|
+
status: number;
|
|
317
|
+
message: string;
|
|
318
|
+
type?: string;
|
|
319
|
+
}): Record<string, unknown> {
|
|
320
|
+
return {
|
|
321
|
+
type: "error",
|
|
322
|
+
error: {
|
|
323
|
+
message: error.message,
|
|
324
|
+
type: error.type ?? "server_error",
|
|
325
|
+
code: error.type ?? "server_error",
|
|
326
|
+
},
|
|
327
|
+
};
|
|
129
328
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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(
|
|
21
|
+
tool: { id?: string | undefined },
|
|
22
|
+
prefix: string,
|
|
23
|
+
index: number,
|
|
24
|
+
): string {
|
|
25
|
+
return tool.id ?? `${prefix}_${Date.now().toString(BASE_36)}_${index}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function shouldEmitText(reply: ReplyObject): boolean {
|
|
29
|
+
return Boolean(reply.text) || (!reply.tools?.length && !reply.reasoning);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function finishReason(
|
|
33
|
+
reply: ReplyObject,
|
|
34
|
+
onTools: string,
|
|
35
|
+
onStop: string,
|
|
36
|
+
): string {
|
|
37
|
+
return reply.tools?.length ? onTools : onStop;
|
|
38
|
+
}
|
package/src/formats/types.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
FormatName,
|
|
3
|
+
MockRequest,
|
|
4
|
+
ReplyObject,
|
|
5
|
+
ReplyOptions,
|
|
6
|
+
} from "../types.js";
|
|
7
|
+
import type { RequestMeta } from "./request-helpers.js";
|
|
3
8
|
|
|
4
9
|
export interface SSEChunk {
|
|
5
10
|
readonly event?: string | undefined;
|
|
@@ -11,7 +16,15 @@ export interface Format {
|
|
|
11
16
|
readonly route: string;
|
|
12
17
|
parseRequest(body: unknown, meta?: RequestMeta): MockRequest;
|
|
13
18
|
isStreaming(body: unknown): boolean;
|
|
14
|
-
serialize(
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
serialize(
|
|
20
|
+
reply: ReplyObject,
|
|
21
|
+
model: string,
|
|
22
|
+
options?: ReplyOptions,
|
|
23
|
+
): readonly SSEChunk[];
|
|
24
|
+
serializeComplete(reply: ReplyObject, model: string): Record<string, unknown>;
|
|
25
|
+
serializeError(error: {
|
|
26
|
+
status: number;
|
|
27
|
+
message: string;
|
|
28
|
+
type?: string | undefined;
|
|
29
|
+
}): Record<string, unknown>;
|
|
17
30
|
}
|
package/src/index.ts
CHANGED
|
@@ -37,7 +37,9 @@ import type { MockServerOptions } from "./mock-server.js";
|
|
|
37
37
|
* await server.stop();
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
|
-
export async function createMock(
|
|
40
|
+
export async function createMock(
|
|
41
|
+
options: MockServerOptions = {},
|
|
42
|
+
): Promise<MockServer> {
|
|
41
43
|
const server = new MockServer(options);
|
|
42
44
|
await server.start(options.port ?? 0);
|
|
43
45
|
return server;
|
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
|
}
|
|
@@ -25,7 +25,11 @@ const json5ReplySchema = z.union([
|
|
|
25
25
|
z.object({
|
|
26
26
|
text: z.string().optional(),
|
|
27
27
|
reasoning: z.string().optional(),
|
|
28
|
-
tools: z
|
|
28
|
+
tools: z
|
|
29
|
+
.array(
|
|
30
|
+
z.object({ name: z.string(), args: z.record(z.string(), z.unknown()) }),
|
|
31
|
+
)
|
|
32
|
+
.optional(),
|
|
29
33
|
}),
|
|
30
34
|
]);
|
|
31
35
|
|
|
@@ -77,7 +81,9 @@ function compileMatch(when: z.infer<typeof json5MatchSchema>): Match {
|
|
|
77
81
|
return parseRegexString(when);
|
|
78
82
|
}
|
|
79
83
|
const obj: MatchObject = {
|
|
80
|
-
...(when.message !== undefined && {
|
|
84
|
+
...(when.message !== undefined && {
|
|
85
|
+
message: parseRegexString(when.message),
|
|
86
|
+
}),
|
|
81
87
|
...(when.model !== undefined && { model: parseRegexString(when.model) }),
|
|
82
88
|
...(when.system !== undefined && { system: parseRegexString(when.system) }),
|
|
83
89
|
...(when.format !== undefined && { format: when.format }),
|
|
@@ -106,8 +112,7 @@ function addSequenceRule(
|
|
|
106
112
|
templates: Templates,
|
|
107
113
|
filePath: string,
|
|
108
114
|
): void {
|
|
109
|
-
|
|
110
|
-
const resolved = entries.map((entry) => {
|
|
115
|
+
const steps = entries.map((entry) => {
|
|
111
116
|
if (typeof entry === "string" || !("reply" in entry)) {
|
|
112
117
|
return { reply: resolveReplyRef(entry, templates, filePath) };
|
|
113
118
|
}
|
|
@@ -119,23 +124,27 @@ function addSequenceRule(
|
|
|
119
124
|
},
|
|
120
125
|
};
|
|
121
126
|
});
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return step.reply;
|
|
127
|
-
});
|
|
128
|
-
rule.remaining = resolved.length;
|
|
127
|
+
const rule = engine.add(match, "");
|
|
128
|
+
const { resolver, entryCount } = createSequenceResolver(steps, rule);
|
|
129
|
+
rule.resolve = resolver;
|
|
130
|
+
rule.remaining = entryCount;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
async function loadJson5File(
|
|
133
|
+
async function loadJson5File(
|
|
134
|
+
filePath: string,
|
|
135
|
+
ctx: LoadContext,
|
|
136
|
+
): Promise<void> {
|
|
132
137
|
const content = await readFile(filePath, "utf-8");
|
|
133
138
|
const parsed = json5FileSchema.parse(JSON5.parse(content));
|
|
134
139
|
|
|
135
140
|
const rules = Array.isArray(parsed) ? parsed : parsed.rules;
|
|
136
141
|
const templates = Array.isArray(parsed) ? undefined : parsed.templates;
|
|
137
142
|
|
|
138
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
!Array.isArray(parsed) &&
|
|
145
|
+
parsed.fallback !== undefined &&
|
|
146
|
+
ctx.setFallback
|
|
147
|
+
) {
|
|
139
148
|
ctx.setFallback(parsed.fallback);
|
|
140
149
|
}
|
|
141
150
|
|
|
@@ -156,7 +165,9 @@ async function loadJson5File(filePath: string, ctx: LoadContext): Promise<void>
|
|
|
156
165
|
const handlerSchema = z.custom<Handler>((val): val is Handler => {
|
|
157
166
|
if (typeof val !== "object" || val === null) return false;
|
|
158
167
|
const obj = val as Record<string, unknown>;
|
|
159
|
-
return
|
|
168
|
+
return (
|
|
169
|
+
typeof obj["match"] === "function" && typeof obj["respond"] === "function"
|
|
170
|
+
);
|
|
160
171
|
});
|
|
161
172
|
|
|
162
173
|
const handlerExportSchema = z.object({
|
|
@@ -164,7 +175,10 @@ const handlerExportSchema = z.object({
|
|
|
164
175
|
fallback: json5ReplySchema.optional(),
|
|
165
176
|
});
|
|
166
177
|
|
|
167
|
-
async function loadHandlerFile(
|
|
178
|
+
async function loadHandlerFile(
|
|
179
|
+
filePath: string,
|
|
180
|
+
ctx: LoadContext,
|
|
181
|
+
): Promise<void> {
|
|
168
182
|
const mod = await import(filePath);
|
|
169
183
|
const parsed = handlerExportSchema.safeParse(mod);
|
|
170
184
|
if (!parsed.success) {
|
|
@@ -172,14 +186,20 @@ async function loadHandlerFile(filePath: string, ctx: LoadContext): Promise<void
|
|
|
172
186
|
`Invalid handler file ${filePath}. Expected default export with { match: Function, respond: Function }.`,
|
|
173
187
|
);
|
|
174
188
|
}
|
|
175
|
-
const handlers = Array.isArray(parsed.data.default)
|
|
189
|
+
const handlers = Array.isArray(parsed.data.default)
|
|
190
|
+
? parsed.data.default
|
|
191
|
+
: [parsed.data.default];
|
|
176
192
|
|
|
177
193
|
if (parsed.data.fallback !== undefined && ctx.setFallback) {
|
|
178
194
|
ctx.setFallback(parsed.data.fallback);
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
for (const handler of handlers) {
|
|
182
|
-
ctx.engine.addHandler(
|
|
198
|
+
ctx.engine.addHandler(
|
|
199
|
+
handler.match,
|
|
200
|
+
handler.respond,
|
|
201
|
+
`(handler: ${filePath})`,
|
|
202
|
+
);
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
205
|
|
|
@@ -193,7 +213,10 @@ const loaderByExtension: ReadonlyMap<string, FileLoader> = new Map([
|
|
|
193
213
|
[".mjs", loadHandlerFile],
|
|
194
214
|
]);
|
|
195
215
|
|
|
196
|
-
export async function loadRulesFromPath(
|
|
216
|
+
export async function loadRulesFromPath(
|
|
217
|
+
pathOrDir: string,
|
|
218
|
+
ctx: LoadContext,
|
|
219
|
+
): Promise<void> {
|
|
197
220
|
const info = await stat(pathOrDir);
|
|
198
221
|
|
|
199
222
|
if (info.isFile()) {
|
package/src/logger.ts
CHANGED
|
@@ -19,6 +19,18 @@ 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<
|
|
25
|
+
keyof typeof LEVEL_STYLE,
|
|
26
|
+
{ priority: number; method: ConsoleMethod; dim?: boolean }
|
|
27
|
+
> = {
|
|
28
|
+
error: { priority: LEVEL_PRIORITY.error, method: "error" },
|
|
29
|
+
warn: { priority: LEVEL_PRIORITY.warning, method: "warn" },
|
|
30
|
+
info: { priority: LEVEL_PRIORITY.info, method: "log" },
|
|
31
|
+
debug: { priority: LEVEL_PRIORITY.debug, method: "log", dim: true },
|
|
32
|
+
};
|
|
33
|
+
|
|
22
34
|
export class Logger {
|
|
23
35
|
readonly level: LogLevel;
|
|
24
36
|
private threshold: number;
|
|
@@ -28,31 +40,31 @@ export class Logger {
|
|
|
28
40
|
this.threshold = LEVEL_PRIORITY[level];
|
|
29
41
|
}
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
private log(
|
|
44
|
+
key: keyof typeof LEVEL_STYLE,
|
|
45
|
+
msg: string,
|
|
46
|
+
args: unknown[],
|
|
47
|
+
): void {
|
|
48
|
+
const config = LEVEL_CONFIG[key];
|
|
49
|
+
if (this.threshold < config.priority) return;
|
|
50
|
+
const { label, symbol } = LEVEL_STYLE[key];
|
|
51
|
+
const text = config.dim ? pc.dim(msg) : msg;
|
|
52
|
+
console[config.method](
|
|
53
|
+
`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${text}`,
|
|
54
|
+
...args,
|
|
55
|
+
);
|
|
36
56
|
}
|
|
37
57
|
|
|
58
|
+
error(msg: string, ...args: unknown[]): void {
|
|
59
|
+
this.log("error", msg, args);
|
|
60
|
+
}
|
|
38
61
|
warn(msg: string, ...args: unknown[]): void {
|
|
39
|
-
|
|
40
|
-
const { label, symbol } = LEVEL_STYLE.warn;
|
|
41
|
-
console.warn(`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${msg}`, ...args);
|
|
42
|
-
}
|
|
62
|
+
this.log("warn", msg, args);
|
|
43
63
|
}
|
|
44
|
-
|
|
45
64
|
info(msg: string, ...args: unknown[]): void {
|
|
46
|
-
|
|
47
|
-
const { label, symbol } = LEVEL_STYLE.info;
|
|
48
|
-
console.log(`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${msg}`, ...args);
|
|
49
|
-
}
|
|
65
|
+
this.log("info", msg, args);
|
|
50
66
|
}
|
|
51
|
-
|
|
52
67
|
debug(msg: string, ...args: unknown[]): void {
|
|
53
|
-
|
|
54
|
-
const { label, symbol } = LEVEL_STYLE.debug;
|
|
55
|
-
console.log(`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${pc.dim(msg)}`, ...args);
|
|
56
|
-
}
|
|
68
|
+
this.log("debug", msg, args);
|
|
57
69
|
}
|
|
58
70
|
}
|