@codehz/ai 0.1.0
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/.oxfmtrc.json +12 -0
- package/.oxlintrc.json +49 -0
- package/README.md +225 -0
- package/bun.lock +231 -0
- package/dist/index.d.mts +978 -0
- package/dist/index.mjs +2758 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +30 -0
- package/src/adapters/chat-completions.ts +707 -0
- package/src/adapters/index.ts +20 -0
- package/src/adapters/messages.ts +700 -0
- package/src/adapters/ollama.ts +604 -0
- package/src/adapters/responses.ts +516 -0
- package/src/core/aggregator.ts +349 -0
- package/src/core/client.ts +23 -0
- package/src/core/collect-stream.ts +19 -0
- package/src/core/errors.ts +99 -0
- package/src/core/event-factory.ts +141 -0
- package/src/core/index.ts +18 -0
- package/src/core/normalize.ts +51 -0
- package/src/core/validation.ts +341 -0
- package/src/helpers/adapter-auxiliary.ts +181 -0
- package/src/helpers/adapter-base.ts +184 -0
- package/src/helpers/auxiliary-collector.ts +166 -0
- package/src/helpers/index.ts +40 -0
- package/src/helpers/mapping.ts +200 -0
- package/src/helpers/sse-parser.ts +87 -0
- package/src/helpers/synthetic-stream.ts +196 -0
- package/src/index.ts +17 -0
- package/src/types/adapter.ts +109 -0
- package/src/types/content.ts +12 -0
- package/src/types/events.ts +134 -0
- package/src/types/index.ts +59 -0
- package/src/types/items.ts +58 -0
- package/src/types/request.ts +39 -0
- package/src/types/response.ts +65 -0
- package/tsdown.config.ts +10 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responses Adapter
|
|
3
|
+
*
|
|
4
|
+
* 接入 OpenAI Responses API (responses 端点)。
|
|
5
|
+
* 职责分层:
|
|
6
|
+
* 1. buildRequest — 将 NormalizedRequest 转换为 Responses API 请求
|
|
7
|
+
* 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
|
|
8
|
+
*
|
|
9
|
+
* 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { AdapterBase } from "../helpers/adapter-base.js";
|
|
13
|
+
import { AIRequestError } from "../core/errors.js";
|
|
14
|
+
import {
|
|
15
|
+
textBlock,
|
|
16
|
+
messageItem,
|
|
17
|
+
reasoningItem,
|
|
18
|
+
toolCallItem,
|
|
19
|
+
opaqueItem,
|
|
20
|
+
replayFromOutput,
|
|
21
|
+
blockToText,
|
|
22
|
+
contentBlocksToText,
|
|
23
|
+
} from "../helpers/mapping.js";
|
|
24
|
+
import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
|
|
25
|
+
|
|
26
|
+
import { parseSSEEvents } from "../helpers/sse-parser.js";
|
|
27
|
+
|
|
28
|
+
import { CAPABILITY_MATRIX } from "../index.js";
|
|
29
|
+
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
30
|
+
|
|
31
|
+
// ── 类型 ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export type ResponsesAdapterOptions = {
|
|
34
|
+
apiKey: string;
|
|
35
|
+
baseUrl?: string;
|
|
36
|
+
/** 可注入自定义 fetch 实现(用于测试/代理) */
|
|
37
|
+
fetch?: FetchFn;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ── Responses API 请求类型 ────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
type ResponsesAPIRequest = {
|
|
43
|
+
model: string;
|
|
44
|
+
input: ResponsesInputItem[];
|
|
45
|
+
instructions?: string;
|
|
46
|
+
tools?: ResponsesTool[];
|
|
47
|
+
tool_choice?: "auto" | "none" | { type: "function"; name: string };
|
|
48
|
+
metadata?: Record<string, string>;
|
|
49
|
+
temperature?: number;
|
|
50
|
+
max_output_tokens?: number;
|
|
51
|
+
stream: true;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type ResponsesInputItem =
|
|
55
|
+
| { type: "message"; role: "user" | "assistant" | "system" | "developer"; content: string }
|
|
56
|
+
| { type: "message"; role: "assistant"; content: ResponsesContentBlock[] }
|
|
57
|
+
| { type: "function_call"; id: string; name: string; arguments: string; call_id?: string }
|
|
58
|
+
| { type: "function_call_output"; call_id: string; output: string }
|
|
59
|
+
| { type: "reasoning"; content: ResponsesContentBlock[] }
|
|
60
|
+
| { type: "item_reference"; id: string };
|
|
61
|
+
|
|
62
|
+
type ResponsesContentBlock =
|
|
63
|
+
| { type: "text"; text: string }
|
|
64
|
+
| { type: "reasoning"; text: string }
|
|
65
|
+
| { type: "refusal"; refusal: string };
|
|
66
|
+
|
|
67
|
+
type ResponsesTool = {
|
|
68
|
+
type: "function";
|
|
69
|
+
name: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
input_schema: Record<string, unknown>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function ensureResponsesTextBlocks(
|
|
75
|
+
blocks: import("../index.js").ContentBlock[],
|
|
76
|
+
field: string,
|
|
77
|
+
): import("../index.js").ContentBlock[] {
|
|
78
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
79
|
+
const block = blocks[i];
|
|
80
|
+
if (block.type !== "text" && block.type !== "json") {
|
|
81
|
+
throw new AIRequestError(
|
|
82
|
+
`responses does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`,
|
|
83
|
+
"UNSUPPORTED_CONTENT_BLOCK",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return blocks;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ensureResponsesReasoningBlocks(
|
|
92
|
+
blocks: import("../index.js").ContentBlock[],
|
|
93
|
+
field: string,
|
|
94
|
+
): Array<Extract<import("../index.js").ContentBlock, { type: "text" }>> {
|
|
95
|
+
return blocks.map((block, index) => {
|
|
96
|
+
if (block.type !== "text") {
|
|
97
|
+
throw new AIRequestError(
|
|
98
|
+
`responses does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`,
|
|
99
|
+
"UNSUPPORTED_CONTENT_BLOCK",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return block;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function instructionsToResponsesText(instructions: string | import("../index.js").ContentBlock[]): string {
|
|
108
|
+
return typeof instructions === "string"
|
|
109
|
+
? instructions
|
|
110
|
+
: contentBlocksToText(ensureResponsesTextBlocks(instructions, "instructions"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function assertResponsesToolResultOutcome(outcome: import("../index.js").ToolResultItem["outcome"]): void {
|
|
114
|
+
if (outcome !== "success") {
|
|
115
|
+
throw new AIRequestError(
|
|
116
|
+
`responses does not preserve tool_result outcome "${outcome}"; only "success" is supported`,
|
|
117
|
+
"UNSUPPORTED_TOOL_RESULT_OUTCOME",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── SSE 事件类型 ──────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
type ResponsesSSEEvent =
|
|
125
|
+
| { type: "response.output_item.added"; data: { item: { id: string; type: string; [key: string]: unknown } } }
|
|
126
|
+
| { type: "response.output_text.delta"; data: { item_id: string; delta: string } }
|
|
127
|
+
| { type: "response.output_text.done"; data: { item_id: string; text: string } }
|
|
128
|
+
| { type: "response.reasoning.delta"; data: { item_id: string; delta: string } }
|
|
129
|
+
| { type: "response.reasoning.done"; data: { item_id: string; text: string } }
|
|
130
|
+
| { type: "response.tool_call.delta"; data: { item_id: string; delta: { arguments?: string } } }
|
|
131
|
+
| { type: "response.tool_call.done"; data: { item_id: string; arguments?: string; name?: string } }
|
|
132
|
+
| { type: "response.completed"; data: { response: ResponsesAPIResponse } }
|
|
133
|
+
| { type: "error"; data: { message: string; code?: string } };
|
|
134
|
+
|
|
135
|
+
type ResponsesAPIResponse = {
|
|
136
|
+
id: string;
|
|
137
|
+
model: string;
|
|
138
|
+
output: ResponsesAPIOutputItem[];
|
|
139
|
+
usage?: {
|
|
140
|
+
input_tokens: number;
|
|
141
|
+
output_tokens: number;
|
|
142
|
+
total_tokens: number;
|
|
143
|
+
[key: string]: unknown;
|
|
144
|
+
};
|
|
145
|
+
[key: string]: unknown;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
type ResponsesAPIOutputItem = {
|
|
149
|
+
id: string;
|
|
150
|
+
type: "message" | "reasoning" | "function_call";
|
|
151
|
+
role?: string;
|
|
152
|
+
content?: ResponsesContentBlock[];
|
|
153
|
+
name?: string;
|
|
154
|
+
arguments?: string;
|
|
155
|
+
status?: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ── SSE 解析 ──────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
function parseSSE(chunk: string): { events: ResponsesSSEEvent[]; rest: string; malformedEvents: number } {
|
|
161
|
+
const result = parseSSEEvents(chunk);
|
|
162
|
+
return { events: result.events as ResponsesSSEEvent[], rest: result.rest, malformedEvents: result.malformedEvents };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isReplayCanonicalInput(item: ResponsesInputItem): boolean {
|
|
166
|
+
return (
|
|
167
|
+
(item.type === "message" && item.role === "assistant") ||
|
|
168
|
+
item.type === "reasoning" ||
|
|
169
|
+
item.type === "function_call"
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function rollbackTrailingReplayCanonicalItems(input: ResponsesInputItem[]): void {
|
|
174
|
+
while (input.length > 0) {
|
|
175
|
+
const last = input[input.length - 1];
|
|
176
|
+
if (!last || !isReplayCanonicalInput(last)) break;
|
|
177
|
+
input.pop();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Content block 映射 ─────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function canonicalToResponsesBlock(b: import("../index.js").ContentBlock): ResponsesContentBlock {
|
|
184
|
+
if (b.type === "text") return { type: "text", text: b.text };
|
|
185
|
+
if (b.type === "json") return { type: "text", text: JSON.stringify(b.json) };
|
|
186
|
+
throw new AIRequestError(
|
|
187
|
+
`responses does not support content block type "${b.type}" in canonical mapping`,
|
|
188
|
+
"UNSUPPORTED_CONTENT_BLOCK",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Adapter ───────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export class ResponsesAdapter extends AdapterBase {
|
|
195
|
+
readonly kind = "responses" as const;
|
|
196
|
+
readonly capabilities = CAPABILITY_MATRIX.responses;
|
|
197
|
+
|
|
198
|
+
private apiKey: string;
|
|
199
|
+
private baseUrl: string;
|
|
200
|
+
private fetchFn: FetchFn;
|
|
201
|
+
|
|
202
|
+
constructor(options: ResponsesAdapterOptions) {
|
|
203
|
+
super();
|
|
204
|
+
this.apiKey = options.apiKey;
|
|
205
|
+
this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
206
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── buildRequest ──────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
protected buildRequest(request: NormalizedRequest): ResponsesAPIRequest {
|
|
212
|
+
const input: ResponsesInputItem[] = [];
|
|
213
|
+
|
|
214
|
+
for (const item of request.input) {
|
|
215
|
+
switch (item.type) {
|
|
216
|
+
case "message": {
|
|
217
|
+
// Responses API 中只有 assistant 角色支持 content blocks
|
|
218
|
+
if (item.role === "assistant") {
|
|
219
|
+
const blocks = ensureResponsesTextBlocks(item.content, `assistant message (${item.role}) content`).map(
|
|
220
|
+
canonicalToResponsesBlock,
|
|
221
|
+
);
|
|
222
|
+
input.push({ type: "message", role: item.role, content: blocks });
|
|
223
|
+
} else {
|
|
224
|
+
input.push({
|
|
225
|
+
type: "message",
|
|
226
|
+
role: item.role,
|
|
227
|
+
content: contentBlocksToText(
|
|
228
|
+
ensureResponsesTextBlocks(item.content, `input message (${item.role}) content`),
|
|
229
|
+
),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "reasoning": {
|
|
235
|
+
const blocks = ensureResponsesReasoningBlocks(item.content, "reasoning content").map(
|
|
236
|
+
(b): ResponsesContentBlock => ({ type: "reasoning", text: b.text }),
|
|
237
|
+
);
|
|
238
|
+
input.push({ type: "reasoning", content: blocks });
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "tool_call": {
|
|
242
|
+
input.push({
|
|
243
|
+
type: "function_call",
|
|
244
|
+
id: item.id,
|
|
245
|
+
name: item.name,
|
|
246
|
+
arguments: item.argumentsText,
|
|
247
|
+
});
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case "tool_result": {
|
|
251
|
+
assertResponsesToolResultOutcome(item.outcome);
|
|
252
|
+
const output = ensureResponsesTextBlocks(item.content, `tool_result ${item.callId} content`)
|
|
253
|
+
.map(blockToText)
|
|
254
|
+
.join("\n");
|
|
255
|
+
input.push({
|
|
256
|
+
type: "function_call_output",
|
|
257
|
+
call_id: item.callId,
|
|
258
|
+
output,
|
|
259
|
+
});
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "opaque": {
|
|
263
|
+
// opaque items with item_reference purpose can be passed through
|
|
264
|
+
if (
|
|
265
|
+
item.source === "responses" &&
|
|
266
|
+
item.purpose === "replay" &&
|
|
267
|
+
typeof item.payload === "object" &&
|
|
268
|
+
item.payload !== null &&
|
|
269
|
+
"id" in (item.payload as Record<string, unknown>)
|
|
270
|
+
) {
|
|
271
|
+
const { id } = item.payload as Record<string, unknown>;
|
|
272
|
+
if (typeof id === "string") {
|
|
273
|
+
rollbackTrailingReplayCanonicalItems(input);
|
|
274
|
+
input.push({ type: "item_reference", id });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const body: ResponsesAPIRequest = {
|
|
283
|
+
model: request.model,
|
|
284
|
+
input,
|
|
285
|
+
stream: true,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if (request.instructions) {
|
|
289
|
+
body.instructions = instructionsToResponsesText(request.instructions);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (request.tools && request.tools.length > 0) {
|
|
293
|
+
body.tools = request.tools.map(
|
|
294
|
+
(t): ResponsesTool => ({
|
|
295
|
+
type: "function",
|
|
296
|
+
name: t.name,
|
|
297
|
+
description: t.description,
|
|
298
|
+
input_schema: t.inputSchema,
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (request.toolChoice) {
|
|
304
|
+
if (request.toolChoice === "auto") body.tool_choice = "auto";
|
|
305
|
+
else if (request.toolChoice === "none") body.tool_choice = "none";
|
|
306
|
+
else if (request.toolChoice.type === "tool") {
|
|
307
|
+
body.tool_choice = { type: "function", name: request.toolChoice.name };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (request.temperature !== undefined) body.temperature = request.temperature;
|
|
312
|
+
if (request.maxOutputTokens !== undefined) body.max_output_tokens = request.maxOutputTokens;
|
|
313
|
+
if (request.metadata) body.metadata = request.metadata;
|
|
314
|
+
|
|
315
|
+
return body;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── runStream ─────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
protected async *runStream(
|
|
321
|
+
providerRequest: ResponsesAPIRequest,
|
|
322
|
+
factory: EventFactory,
|
|
323
|
+
request: NormalizedRequest,
|
|
324
|
+
): AsyncIterable<AIStreamEvent> {
|
|
325
|
+
const auxiliary = this.createAuxiliaryState(request);
|
|
326
|
+
const response = await this.fetchFn(`${this.baseUrl}/responses`, {
|
|
327
|
+
method: "POST",
|
|
328
|
+
headers: {
|
|
329
|
+
"Content-Type": "application/json",
|
|
330
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
331
|
+
},
|
|
332
|
+
body: JSON.stringify(providerRequest),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (!response.ok) {
|
|
336
|
+
const errorText = await response.text().catch(() => "unknown error");
|
|
337
|
+
throw new Error(`Responses API error ${response.status}: ${errorText}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const reader = response.body?.getReader();
|
|
341
|
+
if (!reader) {
|
|
342
|
+
throw new Error("Response body is not readable");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 流式累积状态
|
|
346
|
+
const output: OutputItem[] = [];
|
|
347
|
+
const decoder = new TextDecoder();
|
|
348
|
+
let buffer = "";
|
|
349
|
+
let completedResponse: ResponsesAPIResponse | undefined;
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
while (true) {
|
|
353
|
+
const { done, value } = await reader.read();
|
|
354
|
+
if (done) break;
|
|
355
|
+
|
|
356
|
+
buffer += decoder.decode(value, { stream: true });
|
|
357
|
+
const { events, rest, malformedEvents } = parseSSE(buffer);
|
|
358
|
+
buffer = rest;
|
|
359
|
+
|
|
360
|
+
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
361
|
+
count: malformedEvents,
|
|
362
|
+
providerLabel: "Responses",
|
|
363
|
+
transportLabel: "SSE event(s)",
|
|
364
|
+
});
|
|
365
|
+
if (malformedWarning) {
|
|
366
|
+
yield malformedWarning;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const sseEvent of events) {
|
|
370
|
+
if (sseEvent.type === "error") {
|
|
371
|
+
yield factory.responseWarning(sseEvent.data.message, sseEvent.data.code);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// item 级事件
|
|
376
|
+
if (sseEvent.type === "response.output_item.added") {
|
|
377
|
+
const item = sseEvent.data.item;
|
|
378
|
+
switch (item.type) {
|
|
379
|
+
case "message":
|
|
380
|
+
yield factory.messageStarted(item.id);
|
|
381
|
+
break;
|
|
382
|
+
case "reasoning":
|
|
383
|
+
yield factory.reasoningStarted(item.id, "full");
|
|
384
|
+
break;
|
|
385
|
+
case "function_call":
|
|
386
|
+
yield factory.toolCallStarted(item.id, ((item as Record<string, unknown>).name as string) ?? "unknown");
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (sseEvent.type === "response.output_text.delta") {
|
|
393
|
+
yield factory.messageDelta(sseEvent.data.item_id, sseEvent.data.delta);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (sseEvent.type === "response.output_text.done") {
|
|
398
|
+
yield factory.messageCompleted(messageItem([textBlock(sseEvent.data.text)], { id: sseEvent.data.item_id }));
|
|
399
|
+
output.push(messageItem([textBlock(sseEvent.data.text)], { id: sseEvent.data.item_id }));
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (sseEvent.type === "response.reasoning.delta") {
|
|
404
|
+
yield factory.reasoningDelta(sseEvent.data.item_id, textBlock(sseEvent.data.delta));
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (sseEvent.type === "response.reasoning.done") {
|
|
409
|
+
yield factory.reasoningCompleted(
|
|
410
|
+
reasoningItem([textBlock(sseEvent.data.text)], "full", sseEvent.data.item_id),
|
|
411
|
+
);
|
|
412
|
+
output.push(reasoningItem([textBlock(sseEvent.data.text)], "full", sseEvent.data.item_id));
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (sseEvent.type === "response.tool_call.delta") {
|
|
417
|
+
if (sseEvent.data.delta.arguments) {
|
|
418
|
+
yield factory.toolCallDelta(sseEvent.data.item_id, { argumentsText: sseEvent.data.delta.arguments });
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (sseEvent.type === "response.tool_call.done") {
|
|
424
|
+
const tcItem = toolCallItem(
|
|
425
|
+
sseEvent.data.item_id,
|
|
426
|
+
sseEvent.data.name ?? "unknown",
|
|
427
|
+
sseEvent.data.arguments ?? "",
|
|
428
|
+
);
|
|
429
|
+
yield factory.toolCallCompleted(tcItem);
|
|
430
|
+
output.push(tcItem);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (sseEvent.type === "response.completed") {
|
|
435
|
+
completedResponse = sseEvent.data.response;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} finally {
|
|
440
|
+
reader.releaseLock();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (buffer.trim().length > 0) {
|
|
444
|
+
yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 解析完成响应中的 usage 和 replay
|
|
448
|
+
let rawResponseId: string | undefined;
|
|
449
|
+
|
|
450
|
+
if (completedResponse) {
|
|
451
|
+
rawResponseId = completedResponse.id;
|
|
452
|
+
if (completedResponse.usage) {
|
|
453
|
+
auxiliary.recordUsage(
|
|
454
|
+
{
|
|
455
|
+
inputTokens: completedResponse.usage.input_tokens,
|
|
456
|
+
outputTokens: completedResponse.usage.output_tokens,
|
|
457
|
+
totalTokens: completedResponse.usage.total_tokens,
|
|
458
|
+
},
|
|
459
|
+
"final",
|
|
460
|
+
completedResponse.usage,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 构造 replay:在 output 基础上追加 opaque continuation
|
|
466
|
+
const replay = [...replayFromOutput(output)];
|
|
467
|
+
|
|
468
|
+
// 如果有 provider continuation id,附加 opaque replay item
|
|
469
|
+
if (completedResponse?.id) {
|
|
470
|
+
replay.push(opaqueItem("responses", "replay", { id: completedResponse.id }));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 从 completedResponse 推断 stop reason
|
|
474
|
+
const stopReason = completedResponse ? this.inferStopReason(completedResponse) : undefined;
|
|
475
|
+
|
|
476
|
+
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
477
|
+
for (const event of auxiliaryResult.events) {
|
|
478
|
+
yield event;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
yield factory.responseCompleted(
|
|
482
|
+
this.buildResponse(
|
|
483
|
+
request,
|
|
484
|
+
{
|
|
485
|
+
output,
|
|
486
|
+
replay,
|
|
487
|
+
stopReason,
|
|
488
|
+
usage: auxiliaryResult.usage,
|
|
489
|
+
billing: auxiliaryResult.billing,
|
|
490
|
+
auxiliary: auxiliaryResult.auxiliary,
|
|
491
|
+
warnings: auxiliaryResult.warnings,
|
|
492
|
+
metadataSources: auxiliaryResult.metadataSources,
|
|
493
|
+
rawResponseId,
|
|
494
|
+
},
|
|
495
|
+
factory,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ── 辅助方法 ──────────────────────────────────────────────
|
|
501
|
+
|
|
502
|
+
private inferStopReason(response: ResponsesAPIResponse): import("../index.js").StopReason {
|
|
503
|
+
const output = response.output;
|
|
504
|
+
if (!output || output.length === 0) return "unknown";
|
|
505
|
+
|
|
506
|
+
// 检查是否有未完成的 function_call
|
|
507
|
+
const hasFunctionCall = output.some((item) => item.type === "function_call");
|
|
508
|
+
if (hasFunctionCall) return "tool_call";
|
|
509
|
+
|
|
510
|
+
// 检查最后一条 message 的 status
|
|
511
|
+
const lastMsg = output[output.length - 1];
|
|
512
|
+
if (lastMsg?.status === "incomplete") return "max_output_tokens";
|
|
513
|
+
|
|
514
|
+
return "end_turn";
|
|
515
|
+
}
|
|
516
|
+
}
|