@codehz/ai 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/index.d.mts +151 -27
- package/dist/index.mjs +1291 -703
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +236 -197
- package/src/adapters/messages.ts +150 -124
- package/src/adapters/mock.ts +44 -11
- package/src/adapters/ollama.ts +219 -191
- package/src/adapters/responses.ts +222 -137
- package/src/core/aggregator.ts +233 -62
- package/src/core/errors.ts +7 -1
- package/src/core/event-factory.ts +24 -14
- package/src/core/merge-auxiliary.ts +22 -0
- package/src/core/normalize.ts +15 -1
- package/src/core/validation.ts +29 -21
- package/src/helpers/adapter-base.ts +23 -25
- package/src/helpers/adapter-security.ts +126 -0
- package/src/helpers/incremental-stream-parser.ts +84 -0
- package/src/helpers/index.ts +19 -0
- package/src/helpers/request-mapper.ts +72 -0
- package/src/helpers/sse-parser.ts +51 -25
- package/src/helpers/synthetic-stream.ts +13 -21
- package/src/helpers/usage-mapping.ts +4 -9
- package/src/types/adapter.ts +12 -1
- package/src/types/events.ts +14 -10
- package/src/types/index.ts +9 -1
- package/src/types/response.ts +0 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { AdapterBase } from "../helpers/adapter-base.js";
|
|
13
|
-
import { AIRequestError } from "../core/errors.js";
|
|
13
|
+
import { AIProviderError, AIRequestError, AIStreamError } from "../core/errors.js";
|
|
14
14
|
import {
|
|
15
15
|
textBlock,
|
|
16
16
|
messageItem,
|
|
@@ -22,9 +22,10 @@ import {
|
|
|
22
22
|
contentBlocksToText,
|
|
23
23
|
} from "../helpers/mapping.js";
|
|
24
24
|
import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
|
|
25
|
+
import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
|
|
25
26
|
import { usageFromOpenAIResponses } from "../helpers/usage-mapping.js";
|
|
26
|
-
|
|
27
|
-
import {
|
|
27
|
+
import { NormalizedRequestMapper, splitSSEFrames, IncrementalStreamParser } from "../helpers/index.js";
|
|
28
|
+
import type { ProviderProfile } from "../helpers/index.js";
|
|
28
29
|
|
|
29
30
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
30
31
|
|
|
@@ -71,54 +72,24 @@ type ResponsesTool = {
|
|
|
71
72
|
input_schema: Record<string, unknown>;
|
|
72
73
|
};
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function ensureResponsesReasoningBlocks(
|
|
93
|
-
blocks: import("../index.js").ContentBlock[],
|
|
94
|
-
field: string,
|
|
95
|
-
): Array<Extract<import("../index.js").ContentBlock, { type: "text" }>> {
|
|
96
|
-
return blocks.map((block, index) => {
|
|
97
|
-
if (block.type !== "text") {
|
|
98
|
-
throw new AIRequestError(
|
|
99
|
-
`responses does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`,
|
|
100
|
-
"UNSUPPORTED_CONTENT_BLOCK",
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return block;
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function instructionsToResponsesText(instructions: string | import("../index.js").InstructionBlock[]): string {
|
|
109
|
-
return typeof instructions === "string"
|
|
110
|
-
? instructions
|
|
111
|
-
: contentBlocksToText(ensureResponsesTextBlocks(instructions, "instructions"));
|
|
112
|
-
}
|
|
75
|
+
// ── ProviderProfile & Mapper ────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const profile: ProviderProfile = {
|
|
78
|
+
kind: "responses",
|
|
79
|
+
instructionsMode: "instructions_field",
|
|
80
|
+
supportedBlockTypes: ["text", "json"] as const,
|
|
81
|
+
reasoningBlockTypes: ["text"] as const,
|
|
82
|
+
capabilities: {
|
|
83
|
+
textStreaming: "native",
|
|
84
|
+
reasoningStreaming: "native",
|
|
85
|
+
toolCallStreaming: "native",
|
|
86
|
+
replay: "opaque",
|
|
87
|
+
usage: "final",
|
|
88
|
+
toolResultOutcomes: ["success"],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
113
91
|
|
|
114
|
-
|
|
115
|
-
if (outcome !== "success") {
|
|
116
|
-
throw new AIRequestError(
|
|
117
|
-
`responses does not preserve tool_result outcome "${outcome}"; only "success" is supported`,
|
|
118
|
-
"UNSUPPORTED_TOOL_RESULT_OUTCOME",
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
92
|
+
const mapper = new NormalizedRequestMapper(profile);
|
|
122
93
|
|
|
123
94
|
// ── SSE 事件类型 ──────────────────────────────────────────────
|
|
124
95
|
|
|
@@ -131,12 +102,43 @@ type ResponsesSSEEvent =
|
|
|
131
102
|
| { type: "response.tool_call.delta"; data: { item_id: string; delta: { arguments?: string } } }
|
|
132
103
|
| { type: "response.tool_call.done"; data: { item_id: string; arguments?: string; name?: string } }
|
|
133
104
|
| { type: "response.completed"; data: { response: ResponsesAPIResponse } }
|
|
134
|
-
| { type: "
|
|
105
|
+
| { type: "response.failed"; data: { response: ResponsesAPIResponse } }
|
|
106
|
+
| { type: "response.incomplete"; data: { response: ResponsesAPIResponse } }
|
|
107
|
+
| { type: "error"; data: { message: string; code?: string } }
|
|
108
|
+
| { type: string; data: Record<string, unknown> };
|
|
109
|
+
|
|
110
|
+
/** 已处理或可安全忽略的 Responses SSE 类型(未知类型会 warning 一次)。 */
|
|
111
|
+
const KNOWN_RESPONSES_SSE_TYPES = new Set([
|
|
112
|
+
"response.output_item.added",
|
|
113
|
+
"response.output_item.done",
|
|
114
|
+
"response.output_text.delta",
|
|
115
|
+
"response.output_text.done",
|
|
116
|
+
"response.reasoning.delta",
|
|
117
|
+
"response.reasoning.done",
|
|
118
|
+
"response.tool_call.delta",
|
|
119
|
+
"response.tool_call.done",
|
|
120
|
+
"response.function_call_arguments.delta",
|
|
121
|
+
"response.function_call_arguments.done",
|
|
122
|
+
"response.content_part.added",
|
|
123
|
+
"response.content_part.done",
|
|
124
|
+
"response.refusal.delta",
|
|
125
|
+
"response.refusal.done",
|
|
126
|
+
"response.in_progress",
|
|
127
|
+
"response.created",
|
|
128
|
+
"response.completed",
|
|
129
|
+
"response.failed",
|
|
130
|
+
"response.incomplete",
|
|
131
|
+
"error",
|
|
132
|
+
]);
|
|
135
133
|
|
|
136
134
|
type ResponsesAPIResponse = {
|
|
137
135
|
id: string;
|
|
138
136
|
model: string;
|
|
139
137
|
output: ResponsesAPIOutputItem[];
|
|
138
|
+
status?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" | string;
|
|
139
|
+
incomplete_details?: { reason?: string | null } | null;
|
|
140
|
+
error?: { message?: string; code?: string } | null;
|
|
141
|
+
failure?: { message?: string; code?: string } | null;
|
|
140
142
|
usage?: {
|
|
141
143
|
input_tokens: number;
|
|
142
144
|
output_tokens: number;
|
|
@@ -156,25 +158,18 @@ type ResponsesAPIOutputItem = {
|
|
|
156
158
|
status?: string;
|
|
157
159
|
};
|
|
158
160
|
|
|
159
|
-
// ── SSE 解析 ──────────────────────────────────────────────────
|
|
160
|
-
|
|
161
|
-
function parseSSE(chunk: string): { events: ResponsesSSEEvent[]; rest: string; malformedEvents: number } {
|
|
162
|
-
const result = parseSSEEvents(chunk);
|
|
163
|
-
return { events: result.events as ResponsesSSEEvent[], rest: result.rest, malformedEvents: result.malformedEvents };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
161
|
function isReplayCanonicalInput(item: ResponsesInputItem): boolean {
|
|
167
162
|
return (
|
|
168
163
|
(item.type === "message" && item.role === "assistant") || item.type === "reasoning" || item.type === "function_call"
|
|
169
164
|
);
|
|
170
165
|
}
|
|
171
166
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
167
|
+
function hasReplayCanonicalInput(input: ResponsesInputItem[]): boolean {
|
|
168
|
+
return input.some(isReplayCanonicalInput);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extractFailureMessage(response: ResponsesAPIResponse): string {
|
|
172
|
+
return response.error?.message ?? response.failure?.message ?? "unknown";
|
|
178
173
|
}
|
|
179
174
|
|
|
180
175
|
// ── Content block 映射 ─────────────────────────────────────────
|
|
@@ -192,7 +187,7 @@ function canonicalToResponsesBlock(b: import("../index.js").ContentBlock): Respo
|
|
|
192
187
|
|
|
193
188
|
export class ResponsesAdapter extends AdapterBase {
|
|
194
189
|
readonly kind = "responses" as const;
|
|
195
|
-
readonly
|
|
190
|
+
readonly capabilities = profile.capabilities;
|
|
196
191
|
|
|
197
192
|
private apiKey: string;
|
|
198
193
|
private baseUrl: string;
|
|
@@ -215,25 +210,25 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
215
210
|
case "message": {
|
|
216
211
|
// Responses API 中只有 assistant 角色支持 content blocks
|
|
217
212
|
if (item.role === "assistant") {
|
|
218
|
-
const blocks =
|
|
219
|
-
|
|
220
|
-
|
|
213
|
+
const blocks = mapper
|
|
214
|
+
.ensureTextBlocks(item.content, `assistant message (${item.role}) content`)
|
|
215
|
+
.map(canonicalToResponsesBlock);
|
|
221
216
|
input.push({ type: "message", role: item.role, content: blocks });
|
|
222
217
|
} else {
|
|
223
218
|
input.push({
|
|
224
219
|
type: "message",
|
|
225
220
|
role: item.role,
|
|
226
221
|
content: contentBlocksToText(
|
|
227
|
-
|
|
222
|
+
mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`),
|
|
228
223
|
),
|
|
229
224
|
});
|
|
230
225
|
}
|
|
231
226
|
break;
|
|
232
227
|
}
|
|
233
228
|
case "reasoning": {
|
|
234
|
-
const blocks =
|
|
235
|
-
(
|
|
236
|
-
|
|
229
|
+
const blocks = mapper
|
|
230
|
+
.ensureReasoningBlocks(item.content, "reasoning content")
|
|
231
|
+
.map((b): ResponsesContentBlock => ({ type: "reasoning", text: b.text }));
|
|
237
232
|
input.push({ type: "reasoning", content: blocks });
|
|
238
233
|
break;
|
|
239
234
|
}
|
|
@@ -247,8 +242,9 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
247
242
|
break;
|
|
248
243
|
}
|
|
249
244
|
case "tool_result": {
|
|
250
|
-
|
|
251
|
-
const output =
|
|
245
|
+
mapper.assertToolResultOutcome(item.outcome);
|
|
246
|
+
const output = mapper
|
|
247
|
+
.ensureTextBlocks(item.content, `tool_result ${item.callId} content`)
|
|
252
248
|
.map(blockToText)
|
|
253
249
|
.join("\n");
|
|
254
250
|
input.push({
|
|
@@ -259,18 +255,20 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
259
255
|
break;
|
|
260
256
|
}
|
|
261
257
|
case "opaque": {
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
258
|
+
// Canonical replay items take priority; item_reference is only a fallback
|
|
259
|
+
// when the consumer kept only the provider continuation id.
|
|
260
|
+
if (item.source !== "responses" || item.purpose !== "replay") break;
|
|
261
|
+
assertOpaqueReplayEnvelope(item.payload);
|
|
262
|
+
const payload = item.payload as Record<string, unknown>;
|
|
263
|
+
if ("id" in payload) {
|
|
264
|
+
if (typeof payload.id !== "string" || payload.id.length === 0 || payload.id.length > 256) {
|
|
265
|
+
throw new AIRequestError(
|
|
266
|
+
"Invalid opaque replay payload: id must be a non-empty string (max 256)",
|
|
267
|
+
"INVALID_OPAQUE_REPLAY",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (!hasReplayCanonicalInput(input)) {
|
|
271
|
+
input.push({ type: "item_reference", id: payload.id });
|
|
274
272
|
}
|
|
275
273
|
}
|
|
276
274
|
break;
|
|
@@ -285,7 +283,7 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
285
283
|
};
|
|
286
284
|
|
|
287
285
|
if (request.instructions) {
|
|
288
|
-
body.instructions =
|
|
286
|
+
body.instructions = mapper.mapInstructions(request.instructions);
|
|
289
287
|
}
|
|
290
288
|
|
|
291
289
|
if (request.tools && request.tools.length > 0) {
|
|
@@ -322,39 +320,66 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
322
320
|
request: NormalizedRequest,
|
|
323
321
|
): AsyncIterable<AIStreamEvent> {
|
|
324
322
|
const auxiliary = this.createAuxiliaryState(request);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
323
|
+
let response: Response;
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
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
|
+
} catch (err) {
|
|
335
|
+
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
336
|
+
}
|
|
333
337
|
|
|
334
338
|
if (!response.ok) {
|
|
335
|
-
const
|
|
336
|
-
throw
|
|
339
|
+
const errorBody = await response.text().catch(() => "");
|
|
340
|
+
throw providerHttpError(response.status, errorBody);
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
const reader = response.body?.getReader();
|
|
340
344
|
if (!reader) {
|
|
341
|
-
throw new
|
|
345
|
+
throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
342
346
|
}
|
|
343
347
|
|
|
344
348
|
// 流式累积状态
|
|
349
|
+
const parser = new IncrementalStreamParser<ResponsesSSEEvent>(splitSSEFrames, (frame: string) => {
|
|
350
|
+
let eventType = "";
|
|
351
|
+
let dataStr = "";
|
|
352
|
+
for (const rawLine of frame.split("\n")) {
|
|
353
|
+
const line = rawLine.trim();
|
|
354
|
+
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
355
|
+
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
356
|
+
}
|
|
357
|
+
if (!eventType) return { status: "ignored" };
|
|
358
|
+
try {
|
|
359
|
+
const data = JSON.parse(dataStr);
|
|
360
|
+
return { status: "parsed", value: { type: eventType, data } as ResponsesSSEEvent };
|
|
361
|
+
} catch {
|
|
362
|
+
return { status: "malformed" };
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
345
366
|
const output: OutputItem[] = [];
|
|
346
|
-
|
|
347
|
-
let buffer = "";
|
|
367
|
+
let streamDone = false;
|
|
348
368
|
let completedResponse: ResponsesAPIResponse | undefined;
|
|
369
|
+
let completedEmitted = false;
|
|
370
|
+
let unknownEventsWarned = false;
|
|
371
|
+
const messageItemsWithDelta = new Set<string>();
|
|
349
372
|
|
|
350
373
|
try {
|
|
351
374
|
while (true) {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
375
|
+
const readResult = await reader.read().catch((err: unknown) => {
|
|
376
|
+
throw new AIStreamError(
|
|
377
|
+
`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`,
|
|
378
|
+
"STREAM_ERROR",
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
const { done, value } = readResult;
|
|
382
|
+
const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value as Uint8Array);
|
|
358
383
|
|
|
359
384
|
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
360
385
|
count: malformedEvents,
|
|
@@ -367,13 +392,14 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
367
392
|
|
|
368
393
|
for (const sseEvent of events) {
|
|
369
394
|
if (sseEvent.type === "error") {
|
|
370
|
-
|
|
395
|
+
const data = sseEvent.data as { message?: string; code?: string };
|
|
396
|
+
yield factory.responseWarning(data.message ?? "Provider error event", data.code);
|
|
371
397
|
continue;
|
|
372
398
|
}
|
|
373
399
|
|
|
374
400
|
// item 级事件
|
|
375
401
|
if (sseEvent.type === "response.output_item.added") {
|
|
376
|
-
const item = sseEvent.data.item;
|
|
402
|
+
const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
|
|
377
403
|
switch (item.type) {
|
|
378
404
|
case "message":
|
|
379
405
|
yield factory.messageStarted(item.id);
|
|
@@ -389,57 +415,96 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
389
415
|
}
|
|
390
416
|
|
|
391
417
|
if (sseEvent.type === "response.output_text.delta") {
|
|
392
|
-
|
|
418
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
419
|
+
yield factory.messageDelta(data.item_id, textBlock(data.delta));
|
|
420
|
+
messageItemsWithDelta.add(data.item_id);
|
|
393
421
|
continue;
|
|
394
422
|
}
|
|
395
423
|
|
|
396
424
|
if (sseEvent.type === "response.output_text.done") {
|
|
397
|
-
|
|
398
|
-
|
|
425
|
+
const data = sseEvent.data as { item_id: string; text: string };
|
|
426
|
+
if (!messageItemsWithDelta.has(data.item_id) && data.text) {
|
|
427
|
+
yield factory.messageDelta(data.item_id, textBlock(data.text));
|
|
428
|
+
}
|
|
429
|
+
yield factory.messageCompleted(data.item_id);
|
|
430
|
+
output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
|
|
399
431
|
continue;
|
|
400
432
|
}
|
|
401
433
|
|
|
402
434
|
if (sseEvent.type === "response.reasoning.delta") {
|
|
403
|
-
|
|
435
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
436
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
404
437
|
continue;
|
|
405
438
|
}
|
|
406
439
|
|
|
407
440
|
if (sseEvent.type === "response.reasoning.done") {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
);
|
|
411
|
-
output.push(reasoningItem([textBlock(sseEvent.data.text)], "full", sseEvent.data.item_id));
|
|
441
|
+
const data = sseEvent.data as { item_id: string; text: string };
|
|
442
|
+
yield factory.reasoningCompleted(data.item_id);
|
|
443
|
+
output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
|
|
412
444
|
continue;
|
|
413
445
|
}
|
|
414
446
|
|
|
415
447
|
if (sseEvent.type === "response.tool_call.delta") {
|
|
416
|
-
|
|
417
|
-
|
|
448
|
+
const data = sseEvent.data as { item_id: string; delta: { arguments?: string } };
|
|
449
|
+
if (data.delta.arguments) {
|
|
450
|
+
yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta.arguments });
|
|
418
451
|
}
|
|
419
452
|
continue;
|
|
420
453
|
}
|
|
421
454
|
|
|
422
455
|
if (sseEvent.type === "response.tool_call.done") {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
sseEvent.data.arguments ?? "",
|
|
427
|
-
);
|
|
428
|
-
yield factory.toolCallCompleted(tcItem);
|
|
456
|
+
const data = sseEvent.data as { item_id: string; arguments?: string; name?: string };
|
|
457
|
+
const tcItem = toolCallItem(data.item_id, data.name ?? "unknown", data.arguments ?? "");
|
|
458
|
+
yield factory.toolCallCompleted(data.item_id);
|
|
429
459
|
output.push(tcItem);
|
|
430
460
|
continue;
|
|
431
461
|
}
|
|
432
462
|
|
|
433
|
-
if (
|
|
434
|
-
|
|
463
|
+
if (
|
|
464
|
+
sseEvent.type === "response.completed" ||
|
|
465
|
+
sseEvent.type === "response.failed" ||
|
|
466
|
+
sseEvent.type === "response.incomplete"
|
|
467
|
+
) {
|
|
468
|
+
const data = sseEvent.data as { response: ResponsesAPIResponse };
|
|
469
|
+
if (completedResponse) {
|
|
470
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
completedResponse = data.response;
|
|
475
|
+
|
|
476
|
+
if (sseEvent.type === "response.failed") {
|
|
477
|
+
yield factory.responseWarning(
|
|
478
|
+
`Response failed: ${extractFailureMessage(data.response)}`,
|
|
479
|
+
"PROVIDER_FAILURE",
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
|
|
486
|
+
unknownEventsWarned = true;
|
|
487
|
+
yield factory.responseWarning(
|
|
488
|
+
`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`,
|
|
489
|
+
"UNKNOWN_PROVIDER_EVENT",
|
|
490
|
+
);
|
|
435
491
|
}
|
|
436
492
|
}
|
|
493
|
+
|
|
494
|
+
if (done) {
|
|
495
|
+
streamDone = true;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
437
498
|
}
|
|
438
499
|
} finally {
|
|
439
|
-
|
|
500
|
+
try {
|
|
501
|
+
if (!streamDone) await reader.cancel().catch(() => undefined);
|
|
502
|
+
} finally {
|
|
503
|
+
reader.releaseLock();
|
|
504
|
+
}
|
|
440
505
|
}
|
|
441
506
|
|
|
442
|
-
if (
|
|
507
|
+
if (parser.getRemaining().trim().length > 0) {
|
|
443
508
|
yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
|
|
444
509
|
}
|
|
445
510
|
|
|
@@ -469,8 +534,9 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
469
534
|
yield event;
|
|
470
535
|
}
|
|
471
536
|
|
|
472
|
-
|
|
473
|
-
|
|
537
|
+
if (!completedEmitted) {
|
|
538
|
+
completedEmitted = true;
|
|
539
|
+
const finalResponse = this.buildResponse(
|
|
474
540
|
request,
|
|
475
541
|
{
|
|
476
542
|
output,
|
|
@@ -484,23 +550,42 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
484
550
|
rawResponseId,
|
|
485
551
|
},
|
|
486
552
|
factory,
|
|
487
|
-
)
|
|
488
|
-
|
|
553
|
+
);
|
|
554
|
+
yield factory.responseCompleted({
|
|
555
|
+
replay: finalResponse.replay,
|
|
556
|
+
stopReason: finalResponse.stopReason,
|
|
557
|
+
trace: finalResponse.backend,
|
|
558
|
+
usage: finalResponse.usage,
|
|
559
|
+
billing: finalResponse.billing,
|
|
560
|
+
auxiliary: finalResponse.auxiliary,
|
|
561
|
+
warnings: finalResponse.warnings,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
489
564
|
}
|
|
490
565
|
|
|
491
566
|
// ── 辅助方法 ──────────────────────────────────────────────
|
|
492
567
|
|
|
493
568
|
private inferStopReason(response: ResponsesAPIResponse): import("../index.js").StopReason {
|
|
569
|
+
if (response.status === "failed") return "error";
|
|
570
|
+
|
|
571
|
+
if (response.status === "incomplete") {
|
|
572
|
+
const reason = response.incomplete_details?.reason;
|
|
573
|
+
if (reason === "content_filter") return "content_filter";
|
|
574
|
+
if (reason === "max_output_tokens") return "max_output_tokens";
|
|
575
|
+
return "max_output_tokens";
|
|
576
|
+
}
|
|
577
|
+
|
|
494
578
|
const output = response.output;
|
|
495
|
-
if (!output || output.length === 0)
|
|
579
|
+
if (!output || output.length === 0) {
|
|
580
|
+
return response.status === "completed" ? "end_turn" : "unknown";
|
|
581
|
+
}
|
|
496
582
|
|
|
497
|
-
// 检查是否有未完成的 function_call
|
|
498
583
|
const hasFunctionCall = output.some((item) => item.type === "function_call");
|
|
499
584
|
if (hasFunctionCall) return "tool_call";
|
|
500
585
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (
|
|
586
|
+
const lastItem = output[output.length - 1];
|
|
587
|
+
if (lastItem?.status === "failed") return "error";
|
|
588
|
+
if (lastItem?.status === "incomplete") return "max_output_tokens";
|
|
504
589
|
|
|
505
590
|
return "end_turn";
|
|
506
591
|
}
|