llm-mock-server 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +12 -0
- package/.github/workflows/test.yml +3 -0
- package/.oxfmtrc.json +9 -0
- package/package.json +5 -2
- package/src/cli-validators.ts +12 -4
- package/src/cli.ts +22 -6
- package/src/formats/anthropic/parse.ts +24 -5
- package/src/formats/anthropic/schema.ts +16 -8
- package/src/formats/anthropic/serialize.ts +111 -27
- package/src/formats/openai/parse.ts +12 -2
- package/src/formats/openai/schema.ts +43 -30
- package/src/formats/openai/serialize.ts +73 -17
- package/src/formats/request-helpers.ts +2 -1
- package/src/formats/responses/parse.ts +17 -3
- package/src/formats/responses/schema.ts +34 -20
- package/src/formats/responses/serialize.ts +233 -38
- package/src/formats/serialize-helpers.ts +10 -2
- package/src/formats/types.ts +16 -3
- package/src/index.ts +3 -1
- package/src/loader.ts +36 -9
- package/src/logger.ts +25 -7
- package/src/mock-server.ts +28 -7
- package/src/route-handler.ts +49 -14
- package/src/rule-engine.ts +43 -12
- package/src/types/reply.ts +6 -2
- package/src/types.ts +24 -3
- package/test/cli-validators.test.ts +16 -4
- package/test/formats/anthropic.test.ts +80 -19
- package/test/formats/openai.test.ts +85 -24
- package/test/formats/parse-helpers.test.ts +47 -7
- package/test/formats/responses.test.ts +95 -30
- package/test/history.test.ts +18 -5
- package/test/loader.test.ts +33 -18
- package/test/logger.test.ts +59 -9
- package/test/mock-server.test.ts +76 -22
- package/test/rule-engine.test.ts +49 -19
|
@@ -1,6 +1,13 @@
|
|
|
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
|
+
finishReason,
|
|
8
|
+
MS_PER_SECOND,
|
|
9
|
+
DEFAULT_USAGE,
|
|
10
|
+
} from "../serialize-helpers.js";
|
|
4
11
|
|
|
5
12
|
function buildUsage(usage: { input: number; output: number }) {
|
|
6
13
|
return {
|
|
@@ -8,18 +15,29 @@ function buildUsage(usage: { input: number; output: number }) {
|
|
|
8
15
|
completion_tokens: usage.output,
|
|
9
16
|
total_tokens: usage.input + usage.output,
|
|
10
17
|
prompt_tokens_details: { cached_tokens: 0, audio_tokens: 0 },
|
|
11
|
-
completion_tokens_details: {
|
|
18
|
+
completion_tokens_details: {
|
|
19
|
+
reasoning_tokens: 0,
|
|
20
|
+
audio_tokens: 0,
|
|
21
|
+
accepted_prediction_tokens: 0,
|
|
22
|
+
rejected_prediction_tokens: 0,
|
|
23
|
+
},
|
|
12
24
|
};
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
function chunkEnvelope(
|
|
16
|
-
id: string,
|
|
17
|
-
|
|
28
|
+
id: string,
|
|
29
|
+
created: number,
|
|
30
|
+
model: string,
|
|
31
|
+
delta: Record<string, unknown>,
|
|
32
|
+
finish_reason: string | null = null,
|
|
18
33
|
usage: Record<string, unknown> | null = null,
|
|
19
34
|
): SSEChunk {
|
|
20
35
|
return {
|
|
21
36
|
data: JSON.stringify({
|
|
22
|
-
id,
|
|
37
|
+
id,
|
|
38
|
+
object: "chat.completion.chunk",
|
|
39
|
+
created,
|
|
40
|
+
model,
|
|
23
41
|
system_fingerprint: null,
|
|
24
42
|
service_tier: "default",
|
|
25
43
|
choices: [{ index: 0, delta, logprobs: null, finish_reason }],
|
|
@@ -28,7 +46,11 @@ function chunkEnvelope(
|
|
|
28
46
|
};
|
|
29
47
|
}
|
|
30
48
|
|
|
31
|
-
export function serialize(
|
|
49
|
+
export function serialize(
|
|
50
|
+
reply: ReplyObject,
|
|
51
|
+
model: string,
|
|
52
|
+
options: ReplyOptions = {},
|
|
53
|
+
): readonly SSEChunk[] {
|
|
32
54
|
const id = genId("chatcmpl");
|
|
33
55
|
const created = Math.floor(Date.now() / MS_PER_SECOND);
|
|
34
56
|
const usage = reply.usage ?? DEFAULT_USAGE;
|
|
@@ -41,10 +63,14 @@ export function serialize(reply: ReplyObject, model: string, options: ReplyOptio
|
|
|
41
63
|
|
|
42
64
|
const toolChunks = (reply.tools ?? []).map((tool, i) =>
|
|
43
65
|
chunkEnvelope(id, created, model, {
|
|
44
|
-
tool_calls: [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
tool_calls: [
|
|
67
|
+
{
|
|
68
|
+
index: i,
|
|
69
|
+
id: toolId(tool, "call", i),
|
|
70
|
+
type: "function",
|
|
71
|
+
function: { name: tool.name, arguments: JSON.stringify(tool.args) },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
48
74
|
}),
|
|
49
75
|
);
|
|
50
76
|
|
|
@@ -54,13 +80,22 @@ export function serialize(reply: ReplyObject, model: string, options: ReplyOptio
|
|
|
54
80
|
chunkEnvelope(id, created, model, { role: "assistant" }),
|
|
55
81
|
...textChunks,
|
|
56
82
|
...toolChunks,
|
|
57
|
-
chunkEnvelope(
|
|
83
|
+
chunkEnvelope(
|
|
84
|
+
id,
|
|
85
|
+
created,
|
|
86
|
+
model,
|
|
87
|
+
{},
|
|
88
|
+
finishReason(reply, "tool_calls", "stop"),
|
|
89
|
+
),
|
|
58
90
|
chunkEnvelope(id, created, model, {}, null, usageChunk),
|
|
59
91
|
{ data: "[DONE]" },
|
|
60
92
|
];
|
|
61
93
|
}
|
|
62
94
|
|
|
63
|
-
export function serializeComplete(
|
|
95
|
+
export function serializeComplete(
|
|
96
|
+
reply: ReplyObject,
|
|
97
|
+
model: string,
|
|
98
|
+
): Record<string, unknown> {
|
|
64
99
|
const id = genId("chatcmpl");
|
|
65
100
|
const created = Math.floor(Date.now() / MS_PER_SECOND);
|
|
66
101
|
const usage = reply.usage ?? DEFAULT_USAGE;
|
|
@@ -70,21 +105,42 @@ export function serializeComplete(reply: ReplyObject, model: string): Record<str
|
|
|
70
105
|
content: reply.text ?? null,
|
|
71
106
|
...(reply.tools?.length && {
|
|
72
107
|
tool_calls: reply.tools.map((tool, i) => ({
|
|
73
|
-
id: toolId(tool, "call", i),
|
|
108
|
+
id: toolId(tool, "call", i),
|
|
109
|
+
type: "function",
|
|
74
110
|
function: { name: tool.name, arguments: JSON.stringify(tool.args) },
|
|
75
111
|
})),
|
|
76
112
|
}),
|
|
77
113
|
};
|
|
78
114
|
|
|
79
115
|
return {
|
|
80
|
-
id,
|
|
116
|
+
id,
|
|
117
|
+
object: "chat.completion",
|
|
118
|
+
created,
|
|
119
|
+
model,
|
|
81
120
|
system_fingerprint: null,
|
|
82
121
|
service_tier: "default",
|
|
83
|
-
choices: [
|
|
122
|
+
choices: [
|
|
123
|
+
{
|
|
124
|
+
index: 0,
|
|
125
|
+
message,
|
|
126
|
+
logprobs: null,
|
|
127
|
+
finish_reason: finishReason(reply, "tool_calls", "stop"),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
84
130
|
usage: buildUsage(usage),
|
|
85
131
|
};
|
|
86
132
|
}
|
|
87
133
|
|
|
88
|
-
export function serializeError(error: {
|
|
89
|
-
|
|
134
|
+
export function serializeError(error: {
|
|
135
|
+
status: number;
|
|
136
|
+
message: string;
|
|
137
|
+
type?: string;
|
|
138
|
+
}): Record<string, unknown> {
|
|
139
|
+
return {
|
|
140
|
+
error: {
|
|
141
|
+
message: error.message,
|
|
142
|
+
type: error.type ?? "server_error",
|
|
143
|
+
code: null,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
90
146
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { FormatName, Message, MockRequest, ToolDef } from "../types.js";
|
|
2
2
|
|
|
3
3
|
function asRecord(body: unknown): Record<string, unknown> {
|
|
4
|
-
if (typeof body === "object" && body !== null)
|
|
4
|
+
if (typeof body === "object" && body !== null)
|
|
5
|
+
return body as Record<string, unknown>;
|
|
5
6
|
return {};
|
|
6
7
|
}
|
|
7
8
|
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { MockRequest, Message, ToolDef } from "../../types.js";
|
|
2
2
|
import { buildMockRequest, type RequestMeta } from "../request-helpers.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ResponsesRequestSchema,
|
|
5
|
+
FunctionToolSchema,
|
|
6
|
+
type ResponsesRequest,
|
|
7
|
+
} from "./schema.js";
|
|
4
8
|
|
|
5
|
-
function extractInputContent(
|
|
9
|
+
function extractInputContent(
|
|
10
|
+
content: string | Record<string, unknown>[],
|
|
11
|
+
): string {
|
|
6
12
|
if (typeof content === "string") return content;
|
|
7
13
|
return content
|
|
8
14
|
.filter((b) => b["type"] === "input_text" || b["type"] === "text")
|
|
@@ -52,5 +58,13 @@ function parseTools(req: ResponsesRequest): readonly ToolDef[] | undefined {
|
|
|
52
58
|
|
|
53
59
|
export function parseRequest(body: unknown, meta?: RequestMeta): MockRequest {
|
|
54
60
|
const req = ResponsesRequestSchema.parse(body);
|
|
55
|
-
return buildMockRequest(
|
|
61
|
+
return buildMockRequest(
|
|
62
|
+
"responses",
|
|
63
|
+
req,
|
|
64
|
+
parseInput(req),
|
|
65
|
+
parseTools(req),
|
|
66
|
+
"codex-mini",
|
|
67
|
+
body,
|
|
68
|
+
meta,
|
|
69
|
+
);
|
|
56
70
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
ResponsesRequestSchema,
|
|
5
|
+
FunctionToolSchema,
|
|
6
|
+
type ResponsesRequest,
|
|
7
|
+
} from "llm-schemas/openai/responses";
|
|
4
8
|
|
|
5
9
|
const OutputContentSchema = z.object({
|
|
6
10
|
type: z.string(),
|
|
@@ -23,25 +27,31 @@ const OutputItemSchema = z.object({
|
|
|
23
27
|
export const ResponsesEventSchema = z.object({
|
|
24
28
|
type: z.string(),
|
|
25
29
|
sequence_number: z.number().optional(),
|
|
26
|
-
response: z
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
response: z
|
|
31
|
+
.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
object: z.string(),
|
|
34
|
+
created_at: z.number(),
|
|
35
|
+
model: z.string(),
|
|
36
|
+
status: z.string(),
|
|
37
|
+
output: z.array(OutputItemSchema),
|
|
38
|
+
usage: z
|
|
39
|
+
.object({
|
|
40
|
+
input_tokens: z.number(),
|
|
41
|
+
output_tokens: z.number(),
|
|
42
|
+
total_tokens: z.number(),
|
|
43
|
+
})
|
|
44
|
+
.optional(),
|
|
45
|
+
})
|
|
46
|
+
.optional(),
|
|
39
47
|
item: OutputItemSchema.optional(),
|
|
40
|
-
part: z
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
part: z
|
|
49
|
+
.object({
|
|
50
|
+
type: z.string(),
|
|
51
|
+
text: z.string().optional(),
|
|
52
|
+
annotations: z.array(z.unknown()).optional(),
|
|
53
|
+
})
|
|
54
|
+
.optional(),
|
|
45
55
|
delta: z.string().optional(),
|
|
46
56
|
item_id: z.string().optional(),
|
|
47
57
|
});
|
|
@@ -66,7 +76,11 @@ export type ResponsesComplete = z.infer<typeof ResponsesCompleteSchema>;
|
|
|
66
76
|
|
|
67
77
|
export const ResponsesErrorSchema = z.object({
|
|
68
78
|
type: z.literal("error"),
|
|
69
|
-
error: z.object({
|
|
79
|
+
error: z.object({
|
|
80
|
+
message: z.string(),
|
|
81
|
+
type: z.string().optional(),
|
|
82
|
+
code: z.string().optional(),
|
|
83
|
+
}),
|
|
70
84
|
});
|
|
71
85
|
|
|
72
86
|
export type ResponsesError = z.infer<typeof ResponsesErrorSchema>;
|
|
@@ -1,12 +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 }) {
|
|
6
|
-
return {
|
|
13
|
+
return {
|
|
14
|
+
input_tokens: usage.input,
|
|
15
|
+
output_tokens: usage.output,
|
|
16
|
+
total_tokens: usage.input + usage.output,
|
|
17
|
+
};
|
|
7
18
|
}
|
|
8
19
|
|
|
9
|
-
interface StreamBlock {
|
|
20
|
+
interface StreamBlock {
|
|
21
|
+
chunks: SSEChunk[];
|
|
22
|
+
outputItem: unknown;
|
|
23
|
+
}
|
|
10
24
|
|
|
11
25
|
const NO_ANNOTATIONS: readonly unknown[] = [];
|
|
12
26
|
|
|
@@ -14,43 +28,140 @@ type Chunk = (payload: Record<string, unknown>) => SSEChunk;
|
|
|
14
28
|
|
|
15
29
|
function createChunk(): Chunk {
|
|
16
30
|
let seq = 0;
|
|
17
|
-
return (payload) => ({
|
|
31
|
+
return (payload) => ({
|
|
32
|
+
data: JSON.stringify({ ...payload, sequence_number: seq++ }),
|
|
33
|
+
});
|
|
18
34
|
}
|
|
19
35
|
|
|
20
|
-
function reasoningStreamBlock(
|
|
36
|
+
function reasoningStreamBlock(
|
|
37
|
+
c: Chunk,
|
|
38
|
+
i: number,
|
|
39
|
+
reasoning: string,
|
|
40
|
+
): StreamBlock {
|
|
21
41
|
const itemId = `rs_${genId("rs")}`;
|
|
22
42
|
const summaryPart = { type: "summary_text" as const, text: reasoning };
|
|
23
|
-
const item = {
|
|
43
|
+
const item = {
|
|
44
|
+
type: "reasoning",
|
|
45
|
+
id: itemId,
|
|
46
|
+
status: "completed",
|
|
47
|
+
summary: [summaryPart],
|
|
48
|
+
};
|
|
24
49
|
|
|
25
50
|
return {
|
|
26
51
|
outputItem: item,
|
|
27
52
|
chunks: [
|
|
28
|
-
c({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}),
|
|
33
91
|
c({ type: "response.output_item.done", output_index: i, item }),
|
|
34
92
|
],
|
|
35
93
|
};
|
|
36
94
|
}
|
|
37
95
|
|
|
38
|
-
function textStreamBlock(
|
|
96
|
+
function textStreamBlock(
|
|
97
|
+
c: Chunk,
|
|
98
|
+
i: number,
|
|
99
|
+
text: string,
|
|
100
|
+
chunkSize: number,
|
|
101
|
+
): StreamBlock {
|
|
39
102
|
const itemId = `msg_${genId("msg")}`;
|
|
40
|
-
const outputText = {
|
|
41
|
-
|
|
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
|
+
};
|
|
42
115
|
|
|
43
116
|
return {
|
|
44
117
|
outputItem,
|
|
45
118
|
chunks: [
|
|
46
|
-
c({
|
|
47
|
-
|
|
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
|
+
}),
|
|
48
137
|
...splitText(text, chunkSize).map((piece) =>
|
|
49
|
-
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
|
+
}),
|
|
50
145
|
),
|
|
51
|
-
c({
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}),
|
|
54
165
|
],
|
|
55
166
|
};
|
|
56
167
|
}
|
|
@@ -58,20 +169,49 @@ function textStreamBlock(c: Chunk, i: number, text: string, chunkSize: number):
|
|
|
58
169
|
function toolStreamBlock(c: Chunk, i: number, tool: ToolCall): StreamBlock {
|
|
59
170
|
const callId = toolId(tool, "call", i);
|
|
60
171
|
const argsJson = JSON.stringify(tool.args);
|
|
61
|
-
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
|
+
};
|
|
62
180
|
|
|
63
181
|
return {
|
|
64
182
|
outputItem,
|
|
65
183
|
chunks: [
|
|
66
|
-
c({
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}),
|
|
70
206
|
],
|
|
71
207
|
};
|
|
72
208
|
}
|
|
73
209
|
|
|
74
|
-
export function serialize(
|
|
210
|
+
export function serialize(
|
|
211
|
+
reply: ReplyObject,
|
|
212
|
+
model: string,
|
|
213
|
+
options: ReplyOptions = {},
|
|
214
|
+
): readonly SSEChunk[] {
|
|
75
215
|
const id = genId("resp");
|
|
76
216
|
const createdAt = Math.floor(Date.now() / MS_PER_SECOND);
|
|
77
217
|
const usage = reply.usage ?? DEFAULT_USAGE;
|
|
@@ -80,13 +220,21 @@ export function serialize(reply: ReplyObject, model: string, options: ReplyOptio
|
|
|
80
220
|
|
|
81
221
|
const baseResponse = { id, object: "response", created_at: createdAt, model };
|
|
82
222
|
const header = [
|
|
83
|
-
c({
|
|
84
|
-
|
|
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
|
+
}),
|
|
85
231
|
];
|
|
86
232
|
|
|
87
233
|
const blocks: StreamBlock[] = [
|
|
88
234
|
...(reply.reasoning ? [reasoningStreamBlock(c, i++, reply.reasoning)] : []),
|
|
89
|
-
...(shouldEmitText(reply)
|
|
235
|
+
...(shouldEmitText(reply)
|
|
236
|
+
? [textStreamBlock(c, i++, reply.text ?? "", options.chunkSize ?? 0)]
|
|
237
|
+
: []),
|
|
90
238
|
...(reply.tools ?? []).map((tool) => toolStreamBlock(c, i++, tool)),
|
|
91
239
|
];
|
|
92
240
|
|
|
@@ -98,36 +246,83 @@ export function serialize(reply: ReplyObject, model: string, options: ReplyOptio
|
|
|
98
246
|
...allChunks,
|
|
99
247
|
c({
|
|
100
248
|
type: "response.completed",
|
|
101
|
-
response: {
|
|
102
|
-
|
|
249
|
+
response: {
|
|
250
|
+
...baseResponse,
|
|
251
|
+
status: "completed",
|
|
252
|
+
output,
|
|
253
|
+
usage: buildUsage(usage),
|
|
254
|
+
},
|
|
103
255
|
}),
|
|
104
256
|
];
|
|
105
257
|
}
|
|
106
258
|
|
|
107
|
-
export function serializeComplete(
|
|
259
|
+
export function serializeComplete(
|
|
260
|
+
reply: ReplyObject,
|
|
261
|
+
model: string,
|
|
262
|
+
): Record<string, unknown> {
|
|
108
263
|
const id = genId("resp");
|
|
109
264
|
const createdAt = Math.floor(Date.now() / MS_PER_SECOND);
|
|
110
265
|
const usage = reply.usage ?? DEFAULT_USAGE;
|
|
111
266
|
|
|
112
267
|
const output: unknown[] = [
|
|
113
268
|
...(reply.reasoning
|
|
114
|
-
? [
|
|
269
|
+
? [
|
|
270
|
+
{
|
|
271
|
+
type: "reasoning",
|
|
272
|
+
id: `rs_${genId("rs")}`,
|
|
273
|
+
status: "completed",
|
|
274
|
+
summary: [{ type: "summary_text", text: reply.reasoning }],
|
|
275
|
+
},
|
|
276
|
+
]
|
|
115
277
|
: []),
|
|
116
278
|
...(shouldEmitText(reply)
|
|
117
|
-
? [
|
|
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
|
+
]
|
|
118
290
|
: []),
|
|
119
291
|
...(reply.tools ?? []).map((tool) => {
|
|
120
292
|
const callId = toolId(tool, "call", 0);
|
|
121
|
-
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
|
+
};
|
|
122
301
|
}),
|
|
123
302
|
];
|
|
124
303
|
|
|
125
304
|
return {
|
|
126
|
-
id,
|
|
305
|
+
id,
|
|
306
|
+
object: "response",
|
|
307
|
+
created_at: createdAt,
|
|
308
|
+
status: "completed",
|
|
309
|
+
model,
|
|
310
|
+
output,
|
|
127
311
|
usage: buildUsage(usage),
|
|
128
312
|
};
|
|
129
313
|
}
|
|
130
314
|
|
|
131
|
-
export function serializeError(error: {
|
|
132
|
-
|
|
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
|
+
};
|
|
133
328
|
}
|
|
@@ -17,7 +17,11 @@ export function genId(prefix: string): string {
|
|
|
17
17
|
return `${prefix}_${Date.now().toString(BASE_36)}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function toolId(
|
|
20
|
+
export function toolId(
|
|
21
|
+
tool: { id?: string | undefined },
|
|
22
|
+
prefix: string,
|
|
23
|
+
index: number,
|
|
24
|
+
): string {
|
|
21
25
|
return tool.id ?? `${prefix}_${Date.now().toString(BASE_36)}_${index}`;
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -25,6 +29,10 @@ export function shouldEmitText(reply: ReplyObject): boolean {
|
|
|
25
29
|
return Boolean(reply.text) || (!reply.tools?.length && !reply.reasoning);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
export function finishReason(
|
|
32
|
+
export function finishReason(
|
|
33
|
+
reply: ReplyObject,
|
|
34
|
+
onTools: string,
|
|
35
|
+
onStop: string,
|
|
36
|
+
): string {
|
|
29
37
|
return reply.tools?.length ? onTools : onStop;
|
|
30
38
|
}
|
package/src/formats/types.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
FormatName,
|
|
3
|
+
MockRequest,
|
|
4
|
+
ReplyObject,
|
|
5
|
+
ReplyOptions,
|
|
6
|
+
} from "../types.js";
|
|
2
7
|
import type { RequestMeta } from "./request-helpers.js";
|
|
3
8
|
|
|
4
9
|
export interface SSEChunk {
|
|
@@ -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(
|
|
19
|
+
serialize(
|
|
20
|
+
reply: ReplyObject,
|
|
21
|
+
model: string,
|
|
22
|
+
options?: ReplyOptions,
|
|
23
|
+
): readonly SSEChunk[];
|
|
15
24
|
serializeComplete(reply: ReplyObject, model: string): Record<string, unknown>;
|
|
16
|
-
serializeError(error: {
|
|
25
|
+
serializeError(error: {
|
|
26
|
+
status: number;
|
|
27
|
+
message: string;
|
|
28
|
+
type?: string | undefined;
|
|
29
|
+
}): Record<string, unknown>;
|
|
17
30
|
}
|