@codehz/ai 0.4.2 → 0.4.4
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/dist/index.d.mts +46 -23
- package/dist/index.mjs +197 -42
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/responses.ts +372 -56
package/package.json
CHANGED
|
@@ -40,7 +40,11 @@ export type ResponsesAdapterOptions = {
|
|
|
40
40
|
fetch?: FetchFn;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
// ── Responses API
|
|
43
|
+
// ── Responses API 请求类型(对齐 OpenAI Responses schema)────
|
|
44
|
+
//
|
|
45
|
+
// input 是 untagged enum ModelInput = string | InputItem[]。
|
|
46
|
+
// 每个 InputItem 也必须命中官方 variant,否则会 422:
|
|
47
|
+
// "data did not match any variant of untagged enum ModelInput"
|
|
44
48
|
|
|
45
49
|
type ResponsesAPIRequest = {
|
|
46
50
|
model: string;
|
|
@@ -51,27 +55,70 @@ type ResponsesAPIRequest = {
|
|
|
51
55
|
metadata?: Record<string, string>;
|
|
52
56
|
temperature?: number;
|
|
53
57
|
max_output_tokens?: number;
|
|
58
|
+
/** 服务端多轮续写;opaque replay 的 response id 映射到此字段,而非 item_reference */
|
|
59
|
+
previous_response_id?: string;
|
|
54
60
|
stream: true;
|
|
55
61
|
};
|
|
56
62
|
|
|
63
|
+
/** EasyInputMessage:content 可为 string,或 input_* content parts */
|
|
64
|
+
type ResponsesEasyMessage = {
|
|
65
|
+
type: "message";
|
|
66
|
+
role: "user" | "assistant" | "system" | "developer";
|
|
67
|
+
content: string | ResponsesInputContentPart[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type ResponsesInputContentPart =
|
|
71
|
+
| { type: "input_text"; text: string }
|
|
72
|
+
| { type: "input_image"; image_url: string; detail?: "auto" | "low" | "high" }
|
|
73
|
+
| { type: "input_file"; file_url?: string; file_id?: string; filename?: string };
|
|
74
|
+
|
|
75
|
+
/** function_call:call_id 必填;id 是可选的 item id */
|
|
76
|
+
type ResponsesFunctionCall = {
|
|
77
|
+
type: "function_call";
|
|
78
|
+
call_id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
arguments: string;
|
|
81
|
+
id?: string;
|
|
82
|
+
status?: "in_progress" | "completed" | "incomplete";
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type ResponsesFunctionCallOutput = {
|
|
86
|
+
type: "function_call_output";
|
|
87
|
+
call_id: string;
|
|
88
|
+
output: string;
|
|
89
|
+
id?: string;
|
|
90
|
+
status?: "in_progress" | "completed" | "incomplete";
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** reasoning:id + summary/content/encrypted_content,不是任意 content blocks */
|
|
94
|
+
type ResponsesReasoningInput = {
|
|
95
|
+
type: "reasoning";
|
|
96
|
+
id: string;
|
|
97
|
+
summary: Array<{ type: "summary_text"; text: string }>;
|
|
98
|
+
content?: Array<{ type: "reasoning_text"; text: string }>;
|
|
99
|
+
encrypted_content?: string | null;
|
|
100
|
+
status?: "in_progress" | "completed" | "incomplete";
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** 引用既有 item(不是 response id) */
|
|
104
|
+
type ResponsesItemReference = {
|
|
105
|
+
type: "item_reference";
|
|
106
|
+
id: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
57
109
|
type ResponsesInputItem =
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
| { type: "item_reference"; id: string };
|
|
64
|
-
|
|
65
|
-
type ResponsesContentBlock =
|
|
66
|
-
| { type: "text"; text: string }
|
|
67
|
-
| { type: "reasoning"; text: string }
|
|
68
|
-
| { type: "refusal"; refusal: string };
|
|
110
|
+
| ResponsesEasyMessage
|
|
111
|
+
| ResponsesFunctionCall
|
|
112
|
+
| ResponsesFunctionCallOutput
|
|
113
|
+
| ResponsesReasoningInput
|
|
114
|
+
| ResponsesItemReference;
|
|
69
115
|
|
|
70
116
|
type ResponsesTool = {
|
|
71
117
|
type: "function";
|
|
72
118
|
name: string;
|
|
73
119
|
description?: string;
|
|
74
120
|
parameters: Record<string, unknown>;
|
|
121
|
+
strict?: boolean | null;
|
|
75
122
|
};
|
|
76
123
|
|
|
77
124
|
const mapper = new NormalizedRequestMapper("responses");
|
|
@@ -80,10 +127,17 @@ const mapper = new NormalizedRequestMapper("responses");
|
|
|
80
127
|
|
|
81
128
|
type ResponsesSSEEvent =
|
|
82
129
|
| { type: "response.output_item.added"; data: { item: { id: string; type: string; [key: string]: unknown } } }
|
|
130
|
+
| { type: "response.output_item.done"; data: { item: { id: string; type: string; [key: string]: unknown } } }
|
|
83
131
|
| { type: "response.output_text.delta"; data: { item_id: string; delta: string } }
|
|
84
132
|
| { type: "response.output_text.done"; data: { item_id: string; text: string } }
|
|
85
133
|
| { type: "response.reasoning.delta"; data: { item_id: string; delta: string } }
|
|
86
134
|
| { type: "response.reasoning.done"; data: { item_id: string; text: string } }
|
|
135
|
+
| { type: "response.reasoning_summary_part.added"; data: { item_id: string; summary_index: number; part?: unknown } }
|
|
136
|
+
| { type: "response.reasoning_summary_part.done"; data: { item_id: string; summary_index: number; part?: unknown } }
|
|
137
|
+
| { type: "response.reasoning_summary_text.delta"; data: { item_id: string; delta: string; summary_index?: number } }
|
|
138
|
+
| { type: "response.reasoning_summary_text.done"; data: { item_id: string; text: string; summary_index?: number } }
|
|
139
|
+
| { type: "response.reasoning_text.delta"; data: { item_id: string; delta: string; content_index?: number } }
|
|
140
|
+
| { type: "response.reasoning_text.done"; data: { item_id: string; text: string; content_index?: number } }
|
|
87
141
|
| { type: "response.function_call_arguments.delta"; data: { item_id: string; delta: string } }
|
|
88
142
|
| { type: "response.function_call_arguments.done"; data: { item_id: string; arguments: string } }
|
|
89
143
|
| { type: "response.completed"; data: { response: ResponsesAPIResponse } }
|
|
@@ -98,8 +152,16 @@ const KNOWN_RESPONSES_SSE_TYPES = new Set([
|
|
|
98
152
|
"response.output_item.done",
|
|
99
153
|
"response.output_text.delta",
|
|
100
154
|
"response.output_text.done",
|
|
155
|
+
// legacy aliases retained for fixtures / older gateways
|
|
101
156
|
"response.reasoning.delta",
|
|
102
157
|
"response.reasoning.done",
|
|
158
|
+
// current OpenAI reasoning summary + full reasoning text events
|
|
159
|
+
"response.reasoning_summary_part.added",
|
|
160
|
+
"response.reasoning_summary_part.done",
|
|
161
|
+
"response.reasoning_summary_text.delta",
|
|
162
|
+
"response.reasoning_summary_text.done",
|
|
163
|
+
"response.reasoning_text.delta",
|
|
164
|
+
"response.reasoning_text.done",
|
|
103
165
|
"response.function_call_arguments.delta",
|
|
104
166
|
"response.function_call_arguments.done",
|
|
105
167
|
"response.content_part.added",
|
|
@@ -108,12 +170,71 @@ const KNOWN_RESPONSES_SSE_TYPES = new Set([
|
|
|
108
170
|
"response.refusal.done",
|
|
109
171
|
"response.in_progress",
|
|
110
172
|
"response.created",
|
|
173
|
+
"response.queued",
|
|
111
174
|
"response.completed",
|
|
112
175
|
"response.failed",
|
|
113
176
|
"response.incomplete",
|
|
114
177
|
"error",
|
|
115
178
|
]);
|
|
116
179
|
|
|
180
|
+
type ReasoningVisibility = import("../index.js").ReasoningItem["visibility"];
|
|
181
|
+
|
|
182
|
+
type ReasoningStreamState = {
|
|
183
|
+
visibility: ReasoningVisibility;
|
|
184
|
+
hasDelta: boolean;
|
|
185
|
+
/** Accumulated final texts from *.done events when deltas were skipped. */
|
|
186
|
+
doneTexts: string[];
|
|
187
|
+
completed: boolean;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
function createReasoningState(visibility: ReasoningVisibility = "summary"): ReasoningStreamState {
|
|
191
|
+
return { visibility, hasDelta: false, doneTexts: [], completed: false };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function extractReasoningFromOutputItem(item: Record<string, unknown>): {
|
|
195
|
+
text: string;
|
|
196
|
+
visibility: ReasoningVisibility;
|
|
197
|
+
} {
|
|
198
|
+
const contentTexts: string[] = [];
|
|
199
|
+
if (Array.isArray(item.content)) {
|
|
200
|
+
for (const part of item.content) {
|
|
201
|
+
if (
|
|
202
|
+
part &&
|
|
203
|
+
typeof part === "object" &&
|
|
204
|
+
(part as { type?: unknown }).type === "reasoning_text" &&
|
|
205
|
+
typeof (part as { text?: unknown }).text === "string"
|
|
206
|
+
) {
|
|
207
|
+
contentTexts.push((part as { text: string }).text);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const summaryTexts: string[] = [];
|
|
213
|
+
if (Array.isArray(item.summary)) {
|
|
214
|
+
for (const part of item.summary) {
|
|
215
|
+
if (
|
|
216
|
+
part &&
|
|
217
|
+
typeof part === "object" &&
|
|
218
|
+
(part as { type?: unknown }).type === "summary_text" &&
|
|
219
|
+
typeof (part as { text?: unknown }).text === "string"
|
|
220
|
+
) {
|
|
221
|
+
summaryTexts.push((part as { text: string }).text);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (contentTexts.length > 0) {
|
|
227
|
+
return { text: contentTexts.join("\n"), visibility: "full" };
|
|
228
|
+
}
|
|
229
|
+
if (summaryTexts.length > 0) {
|
|
230
|
+
return { text: summaryTexts.join("\n"), visibility: "summary" };
|
|
231
|
+
}
|
|
232
|
+
if (typeof item.encrypted_content === "string" && item.encrypted_content.length > 0) {
|
|
233
|
+
return { text: "", visibility: "opaque" };
|
|
234
|
+
}
|
|
235
|
+
return { text: "", visibility: "summary" };
|
|
236
|
+
}
|
|
237
|
+
|
|
117
238
|
type ResponsesAPIResponse = {
|
|
118
239
|
id: string;
|
|
119
240
|
model: string;
|
|
@@ -133,12 +254,16 @@ type ResponsesAPIResponse = {
|
|
|
133
254
|
|
|
134
255
|
type ResponsesAPIOutputItem = {
|
|
135
256
|
id: string;
|
|
136
|
-
type: "message" | "reasoning" | "function_call";
|
|
257
|
+
type: "message" | "reasoning" | "function_call" | string;
|
|
137
258
|
role?: string;
|
|
138
|
-
content?:
|
|
259
|
+
content?: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
260
|
+
summary?: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
261
|
+
encrypted_content?: string | null;
|
|
139
262
|
name?: string;
|
|
140
263
|
arguments?: string;
|
|
264
|
+
call_id?: string;
|
|
141
265
|
status?: string;
|
|
266
|
+
[key: string]: unknown;
|
|
142
267
|
};
|
|
143
268
|
|
|
144
269
|
function isReplayCanonicalInput(item: ResponsesInputItem): boolean {
|
|
@@ -155,15 +280,56 @@ function extractFailureMessage(response: ResponsesAPIResponse): string {
|
|
|
155
280
|
return response.error?.message ?? response.failure?.message ?? "unknown";
|
|
156
281
|
}
|
|
157
282
|
|
|
158
|
-
|
|
283
|
+
function readNonEmptyString(value: unknown, maxLen = 256): string | undefined {
|
|
284
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLen) return undefined;
|
|
285
|
+
return value;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** 将 canonical text/json blocks 压成 EasyInputMessage 的 string content。 */
|
|
289
|
+
function messageContentAsString(
|
|
290
|
+
blocks: import("../index.js").ContentBlock[],
|
|
291
|
+
field: string,
|
|
292
|
+
): string {
|
|
293
|
+
return mapper.textFromBlocks(blocks, field);
|
|
294
|
+
}
|
|
159
295
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
`responses does not support content block type "${b.type}" in canonical mapping`,
|
|
165
|
-
"UNSUPPORTED_CONTENT_BLOCK",
|
|
296
|
+
function mapReasoningInput(item: import("../index.js").ReasoningItem, index: number): ResponsesReasoningInput {
|
|
297
|
+
const text = mapper.textFromBlocks(
|
|
298
|
+
mapper.ensureReasoningBlocks(item.content, "reasoning content"),
|
|
299
|
+
"reasoning content",
|
|
166
300
|
);
|
|
301
|
+
const id = item.id && item.id.length > 0 ? item.id : `reasoning_replay_${index}`;
|
|
302
|
+
|
|
303
|
+
if (item.visibility === "full") {
|
|
304
|
+
return {
|
|
305
|
+
type: "reasoning",
|
|
306
|
+
id,
|
|
307
|
+
summary: [],
|
|
308
|
+
content: text ? [{ type: "reasoning_text", text }] : undefined,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// summary / redacted / opaque:公开可回传的是 summary_text
|
|
313
|
+
return {
|
|
314
|
+
type: "reasoning",
|
|
315
|
+
id,
|
|
316
|
+
summary: text ? [{ type: "summary_text", text }] : [],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function extractOpaqueContinuationId(payload: Record<string, unknown>): {
|
|
321
|
+
previousResponseId?: string;
|
|
322
|
+
itemReferenceId?: string;
|
|
323
|
+
} {
|
|
324
|
+
// 优先显式 previous_response_id;历史 payload 用 id 存 response 续写句柄
|
|
325
|
+
const previousResponseId =
|
|
326
|
+
readNonEmptyString(payload.previous_response_id) ??
|
|
327
|
+
(typeof payload.item_id === "string" ? undefined : readNonEmptyString(payload.id));
|
|
328
|
+
|
|
329
|
+
// 仅在显式给出 item_id 时使用 item_reference(引用的是 item,不是 response)
|
|
330
|
+
const itemReferenceId = readNonEmptyString(payload.item_id);
|
|
331
|
+
|
|
332
|
+
return { previousResponseId, itemReferenceId };
|
|
167
333
|
}
|
|
168
334
|
|
|
169
335
|
// ── Adapter ───────────────────────────────────────────────────
|
|
@@ -187,36 +353,30 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
187
353
|
|
|
188
354
|
protected buildRequest(request: NormalizedRequest): ResponsesAPIRequest {
|
|
189
355
|
const input: ResponsesInputItem[] = [];
|
|
356
|
+
let previousResponseId: string | undefined;
|
|
357
|
+
let reasoningIndex = 0;
|
|
190
358
|
|
|
191
359
|
for (const item of request.input) {
|
|
192
360
|
switch (item.type) {
|
|
193
361
|
case "message": {
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
input.push({
|
|
202
|
-
type: "message",
|
|
203
|
-
role: item.role,
|
|
204
|
-
content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`),
|
|
205
|
-
});
|
|
206
|
-
}
|
|
362
|
+
// EasyInputMessage:string content 对 user/assistant 都合法,且最不易触发 ModelInput 反序列化失败。
|
|
363
|
+
// 切勿发送 { type: "text" } —— 官方 content part 是 input_text / output_text。
|
|
364
|
+
input.push({
|
|
365
|
+
type: "message",
|
|
366
|
+
role: item.role,
|
|
367
|
+
content: messageContentAsString(item.content, `input message (${item.role}) content`),
|
|
368
|
+
});
|
|
207
369
|
break;
|
|
208
370
|
}
|
|
209
371
|
case "reasoning": {
|
|
210
|
-
|
|
211
|
-
.ensureReasoningBlocks(item.content, "reasoning content")
|
|
212
|
-
.map((b): ResponsesContentBlock => ({ type: "reasoning", text: b.text }));
|
|
213
|
-
input.push({ type: "reasoning", content: blocks });
|
|
372
|
+
input.push(mapReasoningInput(item, reasoningIndex++));
|
|
214
373
|
break;
|
|
215
374
|
}
|
|
216
375
|
case "tool_call": {
|
|
376
|
+
// call_id 必填;canonical ToolCallItem.id 即 call_id(流里会优先取 call_id)
|
|
217
377
|
input.push({
|
|
218
378
|
type: "function_call",
|
|
219
|
-
|
|
379
|
+
call_id: item.id,
|
|
220
380
|
name: item.name,
|
|
221
381
|
arguments: item.argumentsText,
|
|
222
382
|
});
|
|
@@ -232,20 +392,28 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
232
392
|
break;
|
|
233
393
|
}
|
|
234
394
|
case "opaque": {
|
|
235
|
-
// Canonical replay
|
|
236
|
-
//
|
|
395
|
+
// Canonical replay 优先;否则用 previous_response_id 做服务端续写。
|
|
396
|
+
// 注意:response id 不能塞进 item_reference(那是 item id)。
|
|
237
397
|
if (item.source !== "responses" || item.purpose !== "replay") break;
|
|
238
398
|
assertOpaqueReplayEnvelope(item.payload);
|
|
239
399
|
const payload = item.payload as Record<string, unknown>;
|
|
240
|
-
|
|
241
|
-
|
|
400
|
+
|
|
401
|
+
// 显式字段校验:id / previous_response_id / item_id 若存在必须是合法 string
|
|
402
|
+
for (const key of ["id", "previous_response_id", "item_id"] as const) {
|
|
403
|
+
if (key in payload && (typeof payload[key] !== "string" || payload[key].length === 0 || payload[key].length > 256)) {
|
|
242
404
|
throw new AIRequestError(
|
|
243
|
-
|
|
405
|
+
`Invalid opaque replay payload: ${key} must be a non-empty string (max 256)`,
|
|
244
406
|
"INVALID_OPAQUE_REPLAY",
|
|
245
407
|
);
|
|
246
408
|
}
|
|
247
|
-
|
|
248
|
-
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const { previousResponseId: prevId, itemReferenceId } = extractOpaqueContinuationId(payload);
|
|
412
|
+
if (!hasReplayCanonicalInput(input)) {
|
|
413
|
+
if (prevId && !previousResponseId) {
|
|
414
|
+
previousResponseId = prevId;
|
|
415
|
+
} else if (itemReferenceId) {
|
|
416
|
+
input.push({ type: "item_reference", id: itemReferenceId });
|
|
249
417
|
}
|
|
250
418
|
}
|
|
251
419
|
break;
|
|
@@ -259,6 +427,10 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
259
427
|
stream: true,
|
|
260
428
|
};
|
|
261
429
|
|
|
430
|
+
if (previousResponseId) {
|
|
431
|
+
body.previous_response_id = previousResponseId;
|
|
432
|
+
}
|
|
433
|
+
|
|
262
434
|
if (request.instructions) {
|
|
263
435
|
body.instructions = mapper.mapInstructions(request.instructions);
|
|
264
436
|
}
|
|
@@ -315,7 +487,35 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
315
487
|
let completedResponse: ResponsesAPIResponse | undefined;
|
|
316
488
|
let unknownEventsWarned = false;
|
|
317
489
|
const messageItemsWithDelta = new Set<string>();
|
|
490
|
+
/** item_id → function name */
|
|
318
491
|
const toolCallNames = new Map<string, string>();
|
|
492
|
+
/** item_id → call_id(canonical ToolCallItem.id / function_call_output.call_id) */
|
|
493
|
+
const toolCallIds = new Map<string, string>();
|
|
494
|
+
const reasoningStates = new Map<string, ReasoningStreamState>();
|
|
495
|
+
|
|
496
|
+
const resolveToolCallId = (itemId: string): string => toolCallIds.get(itemId) ?? itemId;
|
|
497
|
+
|
|
498
|
+
const ensureReasoningState = (itemId: string, visibility: ReasoningVisibility = "summary"): ReasoningStreamState => {
|
|
499
|
+
let state = reasoningStates.get(itemId);
|
|
500
|
+
if (!state) {
|
|
501
|
+
state = createReasoningState(visibility);
|
|
502
|
+
reasoningStates.set(itemId, state);
|
|
503
|
+
}
|
|
504
|
+
return state;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const completeReasoning = function* (
|
|
508
|
+
itemId: string,
|
|
509
|
+
text: string,
|
|
510
|
+
visibility: ReasoningVisibility,
|
|
511
|
+
): Generator<AIStreamEvent, void, undefined> {
|
|
512
|
+
const state = ensureReasoningState(itemId, visibility);
|
|
513
|
+
if (state.completed) return;
|
|
514
|
+
state.completed = true;
|
|
515
|
+
state.visibility = visibility;
|
|
516
|
+
yield factory.reasoningCompleted(itemId);
|
|
517
|
+
output.push(reasoningItem(text ? [textBlock(text)] : [], visibility, itemId));
|
|
518
|
+
};
|
|
319
519
|
|
|
320
520
|
for await (const batch of iterateProviderStreamBatches({
|
|
321
521
|
reader,
|
|
@@ -340,19 +540,40 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
340
540
|
case "message":
|
|
341
541
|
yield factory.messageStarted(item.id);
|
|
342
542
|
break;
|
|
343
|
-
case "reasoning":
|
|
344
|
-
|
|
543
|
+
case "reasoning": {
|
|
544
|
+
// OpenAI 公开流默认是 summary;full/opaque 在后续事件中再收紧。
|
|
545
|
+
const visibility = extractReasoningFromOutputItem(item).visibility;
|
|
546
|
+
ensureReasoningState(item.id, visibility);
|
|
547
|
+
yield factory.reasoningStarted(item.id, visibility);
|
|
345
548
|
break;
|
|
549
|
+
}
|
|
346
550
|
case "function_call": {
|
|
347
551
|
const name = typeof item.name === "string" ? item.name : "unknown";
|
|
552
|
+
// Responses 用 call_id 关联 function_call_output;item.id 是 fc_* item id
|
|
553
|
+
const callId =
|
|
554
|
+
typeof item.call_id === "string" && item.call_id.length > 0 ? item.call_id : item.id;
|
|
348
555
|
toolCallNames.set(item.id, name);
|
|
349
|
-
|
|
556
|
+
toolCallIds.set(item.id, callId);
|
|
557
|
+
yield factory.toolCallStarted(callId, name);
|
|
350
558
|
break;
|
|
351
559
|
}
|
|
352
560
|
}
|
|
353
561
|
continue;
|
|
354
562
|
}
|
|
355
563
|
|
|
564
|
+
if (sseEvent.type === "response.output_item.done") {
|
|
565
|
+
const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
|
|
566
|
+
if (item.type === "reasoning") {
|
|
567
|
+
const extracted = extractReasoningFromOutputItem(item);
|
|
568
|
+
const state = ensureReasoningState(item.id, extracted.visibility);
|
|
569
|
+
const text =
|
|
570
|
+
extracted.text ||
|
|
571
|
+
(state.doneTexts.length > 0 ? state.doneTexts.join("\n") : "");
|
|
572
|
+
yield* completeReasoning(item.id, text, extracted.visibility || state.visibility);
|
|
573
|
+
}
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
|
|
356
577
|
if (sseEvent.type === "response.output_text.delta") {
|
|
357
578
|
const data = sseEvent.data as { item_id: string; delta: string };
|
|
358
579
|
yield factory.messageDelta(data.item_id, textBlock(data.delta));
|
|
@@ -370,29 +591,101 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
370
591
|
continue;
|
|
371
592
|
}
|
|
372
593
|
|
|
594
|
+
// Modern OpenAI reasoning summary text (publicly streamed for o-series / gpt-5)
|
|
595
|
+
if (sseEvent.type === "response.reasoning_summary_text.delta") {
|
|
596
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
597
|
+
const state = ensureReasoningState(data.item_id, "summary");
|
|
598
|
+
if (state.visibility === "opaque") state.visibility = "summary";
|
|
599
|
+
if (data.delta) {
|
|
600
|
+
state.hasDelta = true;
|
|
601
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
602
|
+
}
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (sseEvent.type === "response.reasoning_summary_text.done") {
|
|
607
|
+
const data = sseEvent.data as { item_id: string; text: string };
|
|
608
|
+
const state = ensureReasoningState(data.item_id, "summary");
|
|
609
|
+
if (state.visibility === "opaque") state.visibility = "summary";
|
|
610
|
+
if (!state.hasDelta && data.text) {
|
|
611
|
+
state.doneTexts.push(data.text);
|
|
612
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.text));
|
|
613
|
+
} else if (data.text) {
|
|
614
|
+
state.doneTexts.push(data.text);
|
|
615
|
+
}
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Full reasoning text (opt-in via include); upgrade visibility to full
|
|
620
|
+
if (sseEvent.type === "response.reasoning_text.delta") {
|
|
621
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
622
|
+
const state = ensureReasoningState(data.item_id, "full");
|
|
623
|
+
state.visibility = "full";
|
|
624
|
+
if (data.delta) {
|
|
625
|
+
state.hasDelta = true;
|
|
626
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
627
|
+
}
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (sseEvent.type === "response.reasoning_text.done") {
|
|
632
|
+
const data = sseEvent.data as { item_id: string; text: string };
|
|
633
|
+
const state = ensureReasoningState(data.item_id, "full");
|
|
634
|
+
state.visibility = "full";
|
|
635
|
+
if (!state.hasDelta && data.text) {
|
|
636
|
+
state.doneTexts.push(data.text);
|
|
637
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.text));
|
|
638
|
+
} else if (data.text) {
|
|
639
|
+
state.doneTexts.push(data.text);
|
|
640
|
+
}
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Structural summary part events — known & ignored (text is handled above)
|
|
645
|
+
if (
|
|
646
|
+
sseEvent.type === "response.reasoning_summary_part.added" ||
|
|
647
|
+
sseEvent.type === "response.reasoning_summary_part.done"
|
|
648
|
+
) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Legacy aliases kept for fixtures / older gateways
|
|
373
653
|
if (sseEvent.type === "response.reasoning.delta") {
|
|
374
654
|
const data = sseEvent.data as { item_id: string; delta: string };
|
|
375
|
-
|
|
655
|
+
const state = ensureReasoningState(data.item_id, "full");
|
|
656
|
+
state.visibility = "full";
|
|
657
|
+
if (data.delta) {
|
|
658
|
+
state.hasDelta = true;
|
|
659
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
660
|
+
}
|
|
376
661
|
continue;
|
|
377
662
|
}
|
|
378
663
|
|
|
379
664
|
if (sseEvent.type === "response.reasoning.done") {
|
|
380
665
|
const data = sseEvent.data as { item_id: string; text: string };
|
|
381
|
-
|
|
382
|
-
|
|
666
|
+
const state = ensureReasoningState(data.item_id, "full");
|
|
667
|
+
if (!state.hasDelta && data.text) {
|
|
668
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.text));
|
|
669
|
+
}
|
|
670
|
+
yield* completeReasoning(data.item_id, data.text ?? state.doneTexts.join("\n"), "full");
|
|
383
671
|
continue;
|
|
384
672
|
}
|
|
385
673
|
|
|
386
674
|
if (sseEvent.type === "response.function_call_arguments.delta") {
|
|
387
675
|
const data = sseEvent.data as { item_id: string; delta: string };
|
|
388
|
-
if (data.delta)
|
|
676
|
+
if (data.delta) {
|
|
677
|
+
yield factory.toolCallDelta(resolveToolCallId(data.item_id), { argumentsText: data.delta });
|
|
678
|
+
}
|
|
389
679
|
continue;
|
|
390
680
|
}
|
|
391
681
|
|
|
392
682
|
if (sseEvent.type === "response.function_call_arguments.done") {
|
|
393
683
|
const data = sseEvent.data as { item_id: string; arguments: string };
|
|
394
|
-
const
|
|
395
|
-
|
|
684
|
+
const callId = resolveToolCallId(data.item_id);
|
|
685
|
+
// 若 added 事件缺失,done 时仍尽量从 completed payload 之外兜底 call_id
|
|
686
|
+
if (!toolCallIds.has(data.item_id)) toolCallIds.set(data.item_id, callId);
|
|
687
|
+
const tcItem = toolCallItem(callId, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
|
|
688
|
+
yield factory.toolCallCompleted(callId);
|
|
396
689
|
output.push(tcItem);
|
|
397
690
|
continue;
|
|
398
691
|
}
|
|
@@ -410,6 +703,23 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
410
703
|
|
|
411
704
|
completedResponse = data.response;
|
|
412
705
|
|
|
706
|
+
// Safety net: finalize any still-open reasoning items from the final response payload.
|
|
707
|
+
if (Array.isArray(data.response.output)) {
|
|
708
|
+
for (const item of data.response.output) {
|
|
709
|
+
if (item?.type !== "reasoning" || !item.id) continue;
|
|
710
|
+
const state = reasoningStates.get(item.id);
|
|
711
|
+
if (state?.completed) continue;
|
|
712
|
+
const extracted = extractReasoningFromOutputItem(item);
|
|
713
|
+
const text =
|
|
714
|
+
extracted.text ||
|
|
715
|
+
(state && state.doneTexts.length > 0 ? state.doneTexts.join("\n") : "");
|
|
716
|
+
// Only complete if we already started this item (otherwise aggregator has no active item).
|
|
717
|
+
if (state || reasoningStates.has(item.id)) {
|
|
718
|
+
yield* completeReasoning(item.id, text, extracted.visibility);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
413
723
|
if (sseEvent.type === "response.failed") {
|
|
414
724
|
yield factory.responseWarning(
|
|
415
725
|
`Response failed: ${extractFailureMessage(data.response)}`,
|
|
@@ -439,7 +749,13 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
439
749
|
|
|
440
750
|
const replay = [...replayFromOutput(output)];
|
|
441
751
|
if (completedResponse?.id) {
|
|
442
|
-
|
|
752
|
+
// 同时保留 id(向后兼容)与 previous_response_id(语义明确)
|
|
753
|
+
replay.push(
|
|
754
|
+
opaqueItem("responses", "replay", {
|
|
755
|
+
id: completedResponse.id,
|
|
756
|
+
previous_response_id: completedResponse.id,
|
|
757
|
+
}),
|
|
758
|
+
);
|
|
443
759
|
}
|
|
444
760
|
|
|
445
761
|
const stopReason = completedResponse ? this.inferStopReason(completedResponse) : undefined;
|