@codehz/ai 0.4.5 → 0.7.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/README.md +223 -77
- package/dist/index.d.mts +664 -525
- package/dist/index.mjs +3632 -2205
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -8
- package/.github/workflows/publish.yml +0 -56
- package/.oxfmtrc.json +0 -12
- package/.oxlintrc.json +0 -34
- package/AGENTS.md +0 -37
- package/src/adapters/chat-completions.ts +0 -624
- package/src/adapters/index.ts +0 -44
- package/src/adapters/messages.ts +0 -635
- package/src/adapters/mock.ts +0 -934
- package/src/adapters/ollama.ts +0 -526
- package/src/adapters/responses.ts +0 -818
- package/src/core/aggregator.ts +0 -428
- package/src/core/client.ts +0 -36
- package/src/core/collect-stream.ts +0 -19
- package/src/core/errors.ts +0 -105
- package/src/core/event-factory.ts +0 -151
- package/src/core/index.ts +0 -18
- package/src/core/merge-auxiliary.ts +0 -22
- package/src/core/normalize.ts +0 -65
- package/src/core/validation.ts +0 -404
- package/src/helpers/adapter-auxiliary.ts +0 -155
- package/src/helpers/adapter-base.ts +0 -218
- package/src/helpers/adapter-security.ts +0 -126
- package/src/helpers/auxiliary-collector.ts +0 -166
- package/src/helpers/incremental-stream-parser.ts +0 -142
- package/src/helpers/index.ts +0 -87
- package/src/helpers/mapping.ts +0 -192
- package/src/helpers/provider-request-options.ts +0 -25
- package/src/helpers/provider-stream.ts +0 -147
- package/src/helpers/reasoning-level.ts +0 -85
- package/src/helpers/request-mapper.ts +0 -94
- package/src/helpers/synthetic-stream.ts +0 -188
- package/src/helpers/usage-mapping.ts +0 -110
- package/src/index.ts +0 -17
- package/src/types/adapter.ts +0 -42
- package/src/types/content.ts +0 -15
- package/src/types/events.ts +0 -138
- package/src/types/index.ts +0 -49
- package/src/types/items.ts +0 -57
- package/src/types/request.ts +0 -52
- package/src/types/response.ts +0 -68
- package/tsdown.config.ts +0 -10
|
@@ -1,818 +0,0 @@
|
|
|
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
|
-
} from "../helpers/mapping.js";
|
|
22
|
-
import { assertOpaqueReplayEnvelope } from "../helpers/adapter-security.js";
|
|
23
|
-
import { usageFromOpenAIResponses } from "../helpers/usage-mapping.js";
|
|
24
|
-
import {
|
|
25
|
-
NormalizedRequestMapper,
|
|
26
|
-
createSseJsonParser,
|
|
27
|
-
openProviderJsonStream,
|
|
28
|
-
iterateProviderStreamBatches,
|
|
29
|
-
createCompletionGate,
|
|
30
|
-
mergeProviderHeaders,
|
|
31
|
-
applyExtraBody,
|
|
32
|
-
mapResponsesReasoning,
|
|
33
|
-
} from "../helpers/index.js";
|
|
34
|
-
|
|
35
|
-
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
36
|
-
|
|
37
|
-
// ── 类型 ──────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
export type ResponsesAdapterOptions = {
|
|
40
|
-
apiKey: string;
|
|
41
|
-
baseUrl?: string;
|
|
42
|
-
/** 可注入自定义 fetch 实现(用于测试/代理) */
|
|
43
|
-
fetch?: FetchFn;
|
|
44
|
-
/** 额外请求头;后写覆盖内置 Authorization / Content-Type */
|
|
45
|
-
headers?: Record<string, string>;
|
|
46
|
-
/** 额外 body 顶层字段;浅层合并,同名键可覆盖 */
|
|
47
|
-
extraBody?: Record<string, unknown>;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// ── Responses API 请求类型(对齐 OpenAI Responses schema)────
|
|
51
|
-
//
|
|
52
|
-
// input 是 untagged enum ModelInput = string | InputItem[]。
|
|
53
|
-
// 每个 InputItem 也必须命中官方 variant,否则会 422:
|
|
54
|
-
// "data did not match any variant of untagged enum ModelInput"
|
|
55
|
-
|
|
56
|
-
type ResponsesAPIRequest = {
|
|
57
|
-
model: string;
|
|
58
|
-
input: ResponsesInputItem[];
|
|
59
|
-
instructions?: string;
|
|
60
|
-
tools?: ResponsesTool[];
|
|
61
|
-
tool_choice?: "auto" | "none" | { type: "function"; name: string };
|
|
62
|
-
metadata?: Record<string, string>;
|
|
63
|
-
temperature?: number;
|
|
64
|
-
max_output_tokens?: number;
|
|
65
|
-
/** Portable reasoningLevel → effort;summary 等特化字段不在此层 */
|
|
66
|
-
reasoning?: { effort: string };
|
|
67
|
-
/** 服务端多轮续写;opaque replay 的 response id 映射到此字段,而非 item_reference */
|
|
68
|
-
previous_response_id?: string;
|
|
69
|
-
stream: true;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
/** EasyInputMessage:content 可为 string,或 input_* content parts */
|
|
73
|
-
type ResponsesEasyMessage = {
|
|
74
|
-
type: "message";
|
|
75
|
-
role: "user" | "assistant" | "system" | "developer";
|
|
76
|
-
content: string | ResponsesInputContentPart[];
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
type ResponsesInputContentPart =
|
|
80
|
-
| { type: "input_text"; text: string }
|
|
81
|
-
| { type: "input_image"; image_url: string; detail?: "auto" | "low" | "high" }
|
|
82
|
-
| { type: "input_file"; file_url?: string; file_id?: string; filename?: string };
|
|
83
|
-
|
|
84
|
-
/** function_call:call_id 必填;id 是可选的 item id */
|
|
85
|
-
type ResponsesFunctionCall = {
|
|
86
|
-
type: "function_call";
|
|
87
|
-
call_id: string;
|
|
88
|
-
name: string;
|
|
89
|
-
arguments: string;
|
|
90
|
-
id?: string;
|
|
91
|
-
status?: "in_progress" | "completed" | "incomplete";
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
type ResponsesFunctionCallOutput = {
|
|
95
|
-
type: "function_call_output";
|
|
96
|
-
call_id: string;
|
|
97
|
-
output: string;
|
|
98
|
-
id?: string;
|
|
99
|
-
status?: "in_progress" | "completed" | "incomplete";
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/** reasoning:id + summary/content/encrypted_content,不是任意 content blocks */
|
|
103
|
-
type ResponsesReasoningInput = {
|
|
104
|
-
type: "reasoning";
|
|
105
|
-
id: string;
|
|
106
|
-
summary: Array<{ type: "summary_text"; text: string }>;
|
|
107
|
-
content?: Array<{ type: "reasoning_text"; text: string }>;
|
|
108
|
-
encrypted_content?: string | null;
|
|
109
|
-
status?: "in_progress" | "completed" | "incomplete";
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
/** 引用既有 item(不是 response id) */
|
|
113
|
-
type ResponsesItemReference = {
|
|
114
|
-
type: "item_reference";
|
|
115
|
-
id: string;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
type ResponsesInputItem =
|
|
119
|
-
| ResponsesEasyMessage
|
|
120
|
-
| ResponsesFunctionCall
|
|
121
|
-
| ResponsesFunctionCallOutput
|
|
122
|
-
| ResponsesReasoningInput
|
|
123
|
-
| ResponsesItemReference;
|
|
124
|
-
|
|
125
|
-
type ResponsesTool = {
|
|
126
|
-
type: "function";
|
|
127
|
-
name: string;
|
|
128
|
-
description?: string;
|
|
129
|
-
parameters: Record<string, unknown>;
|
|
130
|
-
strict?: boolean | null;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const mapper = new NormalizedRequestMapper("responses");
|
|
134
|
-
|
|
135
|
-
// ── SSE 事件类型 ──────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
type ResponsesSSEEvent =
|
|
138
|
-
| { type: "response.output_item.added"; data: { item: { id: string; type: string; [key: string]: unknown } } }
|
|
139
|
-
| { type: "response.output_item.done"; data: { item: { id: string; type: string; [key: string]: unknown } } }
|
|
140
|
-
| { type: "response.output_text.delta"; data: { item_id: string; delta: string } }
|
|
141
|
-
| { type: "response.output_text.done"; data: { item_id: string; text: string } }
|
|
142
|
-
| { type: "response.reasoning.delta"; data: { item_id: string; delta: string } }
|
|
143
|
-
| { type: "response.reasoning.done"; data: { item_id: string; text: string } }
|
|
144
|
-
| { type: "response.reasoning_summary_part.added"; data: { item_id: string; summary_index: number; part?: unknown } }
|
|
145
|
-
| { type: "response.reasoning_summary_part.done"; data: { item_id: string; summary_index: number; part?: unknown } }
|
|
146
|
-
| { type: "response.reasoning_summary_text.delta"; data: { item_id: string; delta: string; summary_index?: number } }
|
|
147
|
-
| { type: "response.reasoning_summary_text.done"; data: { item_id: string; text: string; summary_index?: number } }
|
|
148
|
-
| { type: "response.reasoning_text.delta"; data: { item_id: string; delta: string; content_index?: number } }
|
|
149
|
-
| { type: "response.reasoning_text.done"; data: { item_id: string; text: string; content_index?: number } }
|
|
150
|
-
| { type: "response.function_call_arguments.delta"; data: { item_id: string; delta: string } }
|
|
151
|
-
| { type: "response.function_call_arguments.done"; data: { item_id: string; arguments: string } }
|
|
152
|
-
| { type: "response.completed"; data: { response: ResponsesAPIResponse } }
|
|
153
|
-
| { type: "response.failed"; data: { response: ResponsesAPIResponse } }
|
|
154
|
-
| { type: "response.incomplete"; data: { response: ResponsesAPIResponse } }
|
|
155
|
-
| { type: "error"; data: { message: string; code?: string } }
|
|
156
|
-
| { type: string; data: Record<string, unknown> };
|
|
157
|
-
|
|
158
|
-
/** 已处理或可安全忽略的 Responses SSE 类型(未知类型会 warning 一次)。 */
|
|
159
|
-
const KNOWN_RESPONSES_SSE_TYPES = new Set([
|
|
160
|
-
"response.output_item.added",
|
|
161
|
-
"response.output_item.done",
|
|
162
|
-
"response.output_text.delta",
|
|
163
|
-
"response.output_text.done",
|
|
164
|
-
// legacy aliases retained for fixtures / older gateways
|
|
165
|
-
"response.reasoning.delta",
|
|
166
|
-
"response.reasoning.done",
|
|
167
|
-
// current OpenAI reasoning summary + full reasoning text events
|
|
168
|
-
"response.reasoning_summary_part.added",
|
|
169
|
-
"response.reasoning_summary_part.done",
|
|
170
|
-
"response.reasoning_summary_text.delta",
|
|
171
|
-
"response.reasoning_summary_text.done",
|
|
172
|
-
"response.reasoning_text.delta",
|
|
173
|
-
"response.reasoning_text.done",
|
|
174
|
-
"response.function_call_arguments.delta",
|
|
175
|
-
"response.function_call_arguments.done",
|
|
176
|
-
"response.content_part.added",
|
|
177
|
-
"response.content_part.done",
|
|
178
|
-
"response.refusal.delta",
|
|
179
|
-
"response.refusal.done",
|
|
180
|
-
"response.in_progress",
|
|
181
|
-
"response.created",
|
|
182
|
-
"response.queued",
|
|
183
|
-
"response.completed",
|
|
184
|
-
"response.failed",
|
|
185
|
-
"response.incomplete",
|
|
186
|
-
"error",
|
|
187
|
-
]);
|
|
188
|
-
|
|
189
|
-
type ReasoningVisibility = import("../index.js").ReasoningItem["visibility"];
|
|
190
|
-
|
|
191
|
-
type ReasoningStreamState = {
|
|
192
|
-
visibility: ReasoningVisibility;
|
|
193
|
-
hasDelta: boolean;
|
|
194
|
-
/** Accumulated final texts from *.done events when deltas were skipped. */
|
|
195
|
-
doneTexts: string[];
|
|
196
|
-
completed: boolean;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
function createReasoningState(visibility: ReasoningVisibility = "summary"): ReasoningStreamState {
|
|
200
|
-
return { visibility, hasDelta: false, doneTexts: [], completed: false };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function extractReasoningFromOutputItem(item: Record<string, unknown>): {
|
|
204
|
-
text: string;
|
|
205
|
-
visibility: ReasoningVisibility;
|
|
206
|
-
} {
|
|
207
|
-
const contentTexts: string[] = [];
|
|
208
|
-
if (Array.isArray(item.content)) {
|
|
209
|
-
for (const part of item.content) {
|
|
210
|
-
if (
|
|
211
|
-
part &&
|
|
212
|
-
typeof part === "object" &&
|
|
213
|
-
(part as { type?: unknown }).type === "reasoning_text" &&
|
|
214
|
-
typeof (part as { text?: unknown }).text === "string"
|
|
215
|
-
) {
|
|
216
|
-
contentTexts.push((part as { text: string }).text);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const summaryTexts: string[] = [];
|
|
222
|
-
if (Array.isArray(item.summary)) {
|
|
223
|
-
for (const part of item.summary) {
|
|
224
|
-
if (
|
|
225
|
-
part &&
|
|
226
|
-
typeof part === "object" &&
|
|
227
|
-
(part as { type?: unknown }).type === "summary_text" &&
|
|
228
|
-
typeof (part as { text?: unknown }).text === "string"
|
|
229
|
-
) {
|
|
230
|
-
summaryTexts.push((part as { text: string }).text);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (contentTexts.length > 0) {
|
|
236
|
-
return { text: contentTexts.join("\n"), visibility: "full" };
|
|
237
|
-
}
|
|
238
|
-
if (summaryTexts.length > 0) {
|
|
239
|
-
return { text: summaryTexts.join("\n"), visibility: "summary" };
|
|
240
|
-
}
|
|
241
|
-
if (typeof item.encrypted_content === "string" && item.encrypted_content.length > 0) {
|
|
242
|
-
return { text: "", visibility: "opaque" };
|
|
243
|
-
}
|
|
244
|
-
return { text: "", visibility: "summary" };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
type ResponsesAPIResponse = {
|
|
248
|
-
id: string;
|
|
249
|
-
model: string;
|
|
250
|
-
output: ResponsesAPIOutputItem[];
|
|
251
|
-
status?: "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete" | string;
|
|
252
|
-
incomplete_details?: { reason?: string | null } | null;
|
|
253
|
-
error?: { message?: string; code?: string } | null;
|
|
254
|
-
failure?: { message?: string; code?: string } | null;
|
|
255
|
-
usage?: {
|
|
256
|
-
input_tokens: number;
|
|
257
|
-
output_tokens: number;
|
|
258
|
-
total_tokens: number;
|
|
259
|
-
[key: string]: unknown;
|
|
260
|
-
};
|
|
261
|
-
[key: string]: unknown;
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
type ResponsesAPIOutputItem = {
|
|
265
|
-
id: string;
|
|
266
|
-
type: "message" | "reasoning" | "function_call" | string;
|
|
267
|
-
role?: string;
|
|
268
|
-
content?: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
269
|
-
summary?: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
270
|
-
encrypted_content?: string | null;
|
|
271
|
-
name?: string;
|
|
272
|
-
arguments?: string;
|
|
273
|
-
call_id?: string;
|
|
274
|
-
status?: string;
|
|
275
|
-
[key: string]: unknown;
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
function isReplayCanonicalInput(item: ResponsesInputItem): boolean {
|
|
279
|
-
return (
|
|
280
|
-
(item.type === "message" && item.role === "assistant") || item.type === "reasoning" || item.type === "function_call"
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function hasReplayCanonicalInput(input: ResponsesInputItem[]): boolean {
|
|
285
|
-
return input.some(isReplayCanonicalInput);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function extractFailureMessage(response: ResponsesAPIResponse): string {
|
|
289
|
-
return response.error?.message ?? response.failure?.message ?? "unknown";
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function readNonEmptyString(value: unknown, maxLen = 256): string | undefined {
|
|
293
|
-
if (typeof value !== "string" || value.length === 0 || value.length > maxLen) return undefined;
|
|
294
|
-
return value;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** 将 canonical text/json blocks 压成 EasyInputMessage 的 string content。 */
|
|
298
|
-
function messageContentAsString(
|
|
299
|
-
blocks: import("../index.js").ContentBlock[],
|
|
300
|
-
field: string,
|
|
301
|
-
): string {
|
|
302
|
-
return mapper.textFromBlocks(blocks, field);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function mapReasoningInput(item: import("../index.js").ReasoningItem, index: number): ResponsesReasoningInput {
|
|
306
|
-
const text = mapper.textFromBlocks(
|
|
307
|
-
mapper.ensureReasoningBlocks(item.content, "reasoning content"),
|
|
308
|
-
"reasoning content",
|
|
309
|
-
);
|
|
310
|
-
const id = item.id && item.id.length > 0 ? item.id : `reasoning_replay_${index}`;
|
|
311
|
-
|
|
312
|
-
if (item.visibility === "full") {
|
|
313
|
-
return {
|
|
314
|
-
type: "reasoning",
|
|
315
|
-
id,
|
|
316
|
-
summary: [],
|
|
317
|
-
content: text ? [{ type: "reasoning_text", text }] : undefined,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// summary / redacted / opaque:公开可回传的是 summary_text
|
|
322
|
-
return {
|
|
323
|
-
type: "reasoning",
|
|
324
|
-
id,
|
|
325
|
-
summary: text ? [{ type: "summary_text", text }] : [],
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function extractOpaqueContinuationId(payload: Record<string, unknown>): {
|
|
330
|
-
previousResponseId?: string;
|
|
331
|
-
itemReferenceId?: string;
|
|
332
|
-
} {
|
|
333
|
-
// 优先显式 previous_response_id;历史 payload 用 id 存 response 续写句柄
|
|
334
|
-
const previousResponseId =
|
|
335
|
-
readNonEmptyString(payload.previous_response_id) ??
|
|
336
|
-
(typeof payload.item_id === "string" ? undefined : readNonEmptyString(payload.id));
|
|
337
|
-
|
|
338
|
-
// 仅在显式给出 item_id 时使用 item_reference(引用的是 item,不是 response)
|
|
339
|
-
const itemReferenceId = readNonEmptyString(payload.item_id);
|
|
340
|
-
|
|
341
|
-
return { previousResponseId, itemReferenceId };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ── Adapter ───────────────────────────────────────────────────
|
|
345
|
-
|
|
346
|
-
export class ResponsesAdapter extends AdapterBase {
|
|
347
|
-
readonly kind = "responses" as const;
|
|
348
|
-
readonly isSyntheticStream = false;
|
|
349
|
-
|
|
350
|
-
private apiKey: string;
|
|
351
|
-
private baseUrl: string;
|
|
352
|
-
private fetchFn: FetchFn;
|
|
353
|
-
private headers: Record<string, string> | undefined;
|
|
354
|
-
private extraBody: Record<string, unknown> | undefined;
|
|
355
|
-
|
|
356
|
-
constructor(options: ResponsesAdapterOptions) {
|
|
357
|
-
super();
|
|
358
|
-
this.apiKey = options.apiKey;
|
|
359
|
-
this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
360
|
-
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
361
|
-
this.headers = options.headers;
|
|
362
|
-
this.extraBody = options.extraBody;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// ── buildRequest ──────────────────────────────────────────
|
|
366
|
-
|
|
367
|
-
protected buildRequest(request: NormalizedRequest): ResponsesAPIRequest {
|
|
368
|
-
const input: ResponsesInputItem[] = [];
|
|
369
|
-
let previousResponseId: string | undefined;
|
|
370
|
-
let reasoningIndex = 0;
|
|
371
|
-
|
|
372
|
-
for (const item of request.input) {
|
|
373
|
-
switch (item.type) {
|
|
374
|
-
case "message": {
|
|
375
|
-
// EasyInputMessage:string content 对 user/assistant 都合法,且最不易触发 ModelInput 反序列化失败。
|
|
376
|
-
// 切勿发送 { type: "text" } —— 官方 content part 是 input_text / output_text。
|
|
377
|
-
input.push({
|
|
378
|
-
type: "message",
|
|
379
|
-
role: item.role,
|
|
380
|
-
content: messageContentAsString(item.content, `input message (${item.role}) content`),
|
|
381
|
-
});
|
|
382
|
-
break;
|
|
383
|
-
}
|
|
384
|
-
case "reasoning": {
|
|
385
|
-
input.push(mapReasoningInput(item, reasoningIndex++));
|
|
386
|
-
break;
|
|
387
|
-
}
|
|
388
|
-
case "tool_call": {
|
|
389
|
-
// call_id 必填;canonical ToolCallItem.id 即 call_id(流里会优先取 call_id)
|
|
390
|
-
input.push({
|
|
391
|
-
type: "function_call",
|
|
392
|
-
call_id: item.id,
|
|
393
|
-
name: item.name,
|
|
394
|
-
arguments: item.argumentsText,
|
|
395
|
-
});
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
case "tool_result": {
|
|
399
|
-
const output = mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`);
|
|
400
|
-
input.push({
|
|
401
|
-
type: "function_call_output",
|
|
402
|
-
call_id: item.callId,
|
|
403
|
-
output,
|
|
404
|
-
});
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
case "opaque": {
|
|
408
|
-
// Canonical replay 优先;否则用 previous_response_id 做服务端续写。
|
|
409
|
-
// 注意:response id 不能塞进 item_reference(那是 item id)。
|
|
410
|
-
if (item.source !== "responses" || item.purpose !== "replay") break;
|
|
411
|
-
assertOpaqueReplayEnvelope(item.payload);
|
|
412
|
-
const payload = item.payload as Record<string, unknown>;
|
|
413
|
-
|
|
414
|
-
// 显式字段校验:id / previous_response_id / item_id 若存在必须是合法 string
|
|
415
|
-
for (const key of ["id", "previous_response_id", "item_id"] as const) {
|
|
416
|
-
if (key in payload && (typeof payload[key] !== "string" || payload[key].length === 0 || payload[key].length > 256)) {
|
|
417
|
-
throw new AIRequestError(
|
|
418
|
-
`Invalid opaque replay payload: ${key} must be a non-empty string (max 256)`,
|
|
419
|
-
"INVALID_OPAQUE_REPLAY",
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const { previousResponseId: prevId, itemReferenceId } = extractOpaqueContinuationId(payload);
|
|
425
|
-
if (!hasReplayCanonicalInput(input)) {
|
|
426
|
-
if (prevId && !previousResponseId) {
|
|
427
|
-
previousResponseId = prevId;
|
|
428
|
-
} else if (itemReferenceId) {
|
|
429
|
-
input.push({ type: "item_reference", id: itemReferenceId });
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
break;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const body: ResponsesAPIRequest = {
|
|
438
|
-
model: request.model,
|
|
439
|
-
input,
|
|
440
|
-
stream: true,
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
if (previousResponseId) {
|
|
444
|
-
body.previous_response_id = previousResponseId;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (request.instructions) {
|
|
448
|
-
body.instructions = mapper.mapInstructions(request.instructions);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
body.tools = mapper.mapToolsIfPresent(
|
|
452
|
-
request.tools,
|
|
453
|
-
(t): ResponsesTool => ({
|
|
454
|
-
type: "function",
|
|
455
|
-
name: t.name,
|
|
456
|
-
description: t.description,
|
|
457
|
-
parameters: t.inputSchema,
|
|
458
|
-
}),
|
|
459
|
-
);
|
|
460
|
-
|
|
461
|
-
body.tool_choice = mapper.mapToolChoice<Exclude<ResponsesAPIRequest["tool_choice"], undefined>>(
|
|
462
|
-
request.toolChoice,
|
|
463
|
-
{
|
|
464
|
-
auto: "auto",
|
|
465
|
-
none: "none",
|
|
466
|
-
tool: (name) => ({ type: "function" as const, name }),
|
|
467
|
-
},
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
if (request.temperature !== undefined) body.temperature = request.temperature;
|
|
471
|
-
if (request.maxOutputTokens !== undefined) body.max_output_tokens = request.maxOutputTokens;
|
|
472
|
-
if (request.metadata) body.metadata = request.metadata;
|
|
473
|
-
if (request.reasoningLevel !== undefined) {
|
|
474
|
-
body.reasoning = mapResponsesReasoning(request.reasoningLevel);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return applyExtraBody(body, this.extraBody);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// ── runStream ─────────────────────────────────────────────
|
|
481
|
-
|
|
482
|
-
protected async *runStream(
|
|
483
|
-
providerRequest: ResponsesAPIRequest,
|
|
484
|
-
factory: EventFactory,
|
|
485
|
-
request: NormalizedRequest,
|
|
486
|
-
): AsyncIterable<AIStreamEvent> {
|
|
487
|
-
const auxiliary = this.createAuxiliaryState(request);
|
|
488
|
-
const gate = createCompletionGate();
|
|
489
|
-
|
|
490
|
-
const { reader } = await openProviderJsonStream({
|
|
491
|
-
fetchFn: this.fetchFn,
|
|
492
|
-
url: `${this.baseUrl}/responses`,
|
|
493
|
-
headers: mergeProviderHeaders(
|
|
494
|
-
{
|
|
495
|
-
"Content-Type": "application/json",
|
|
496
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
497
|
-
},
|
|
498
|
-
this.headers,
|
|
499
|
-
),
|
|
500
|
-
body: providerRequest,
|
|
501
|
-
signal: request.signal,
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
const parser = createSseJsonParser<ResponsesSSEEvent>();
|
|
505
|
-
const output: OutputItem[] = [];
|
|
506
|
-
let completedResponse: ResponsesAPIResponse | undefined;
|
|
507
|
-
let unknownEventsWarned = false;
|
|
508
|
-
const messageItemsWithDelta = new Set<string>();
|
|
509
|
-
/** item_id → function name */
|
|
510
|
-
const toolCallNames = new Map<string, string>();
|
|
511
|
-
/** item_id → call_id(canonical ToolCallItem.id / function_call_output.call_id) */
|
|
512
|
-
const toolCallIds = new Map<string, string>();
|
|
513
|
-
const reasoningStates = new Map<string, ReasoningStreamState>();
|
|
514
|
-
|
|
515
|
-
const resolveToolCallId = (itemId: string): string => toolCallIds.get(itemId) ?? itemId;
|
|
516
|
-
|
|
517
|
-
const ensureReasoningState = (itemId: string, visibility: ReasoningVisibility = "summary"): ReasoningStreamState => {
|
|
518
|
-
let state = reasoningStates.get(itemId);
|
|
519
|
-
if (!state) {
|
|
520
|
-
state = createReasoningState(visibility);
|
|
521
|
-
reasoningStates.set(itemId, state);
|
|
522
|
-
}
|
|
523
|
-
return state;
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
const completeReasoning = function* (
|
|
527
|
-
itemId: string,
|
|
528
|
-
text: string,
|
|
529
|
-
visibility: ReasoningVisibility,
|
|
530
|
-
): Generator<AIStreamEvent, void, undefined> {
|
|
531
|
-
const state = ensureReasoningState(itemId, visibility);
|
|
532
|
-
if (state.completed) return;
|
|
533
|
-
state.completed = true;
|
|
534
|
-
state.visibility = visibility;
|
|
535
|
-
yield factory.reasoningCompleted(itemId);
|
|
536
|
-
output.push(reasoningItem(text ? [textBlock(text)] : [], visibility, itemId));
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
for await (const batch of iterateProviderStreamBatches({
|
|
540
|
-
reader,
|
|
541
|
-
parser,
|
|
542
|
-
factory,
|
|
543
|
-
providerLabel: "Responses",
|
|
544
|
-
transportLabel: "SSE event(s)",
|
|
545
|
-
incompleteMessage: "Stream ended with an incomplete Responses SSE frame",
|
|
546
|
-
})) {
|
|
547
|
-
for (const warning of batch.warnings) yield warning;
|
|
548
|
-
|
|
549
|
-
for (const sseEvent of batch.items) {
|
|
550
|
-
if (sseEvent.type === "error") {
|
|
551
|
-
const data = sseEvent.data as { message?: string; code?: string };
|
|
552
|
-
yield factory.responseWarning(data.message ?? "Provider error event", data.code);
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (sseEvent.type === "response.output_item.added") {
|
|
557
|
-
const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
|
|
558
|
-
switch (item.type) {
|
|
559
|
-
case "message":
|
|
560
|
-
yield factory.messageStarted(item.id);
|
|
561
|
-
break;
|
|
562
|
-
case "reasoning": {
|
|
563
|
-
// OpenAI 公开流默认是 summary;full/opaque 在后续事件中再收紧。
|
|
564
|
-
const visibility = extractReasoningFromOutputItem(item).visibility;
|
|
565
|
-
ensureReasoningState(item.id, visibility);
|
|
566
|
-
yield factory.reasoningStarted(item.id, visibility);
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
case "function_call": {
|
|
570
|
-
const name = typeof item.name === "string" ? item.name : "unknown";
|
|
571
|
-
// Responses 用 call_id 关联 function_call_output;item.id 是 fc_* item id
|
|
572
|
-
const callId =
|
|
573
|
-
typeof item.call_id === "string" && item.call_id.length > 0 ? item.call_id : item.id;
|
|
574
|
-
toolCallNames.set(item.id, name);
|
|
575
|
-
toolCallIds.set(item.id, callId);
|
|
576
|
-
yield factory.toolCallStarted(callId, name);
|
|
577
|
-
break;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
if (sseEvent.type === "response.output_item.done") {
|
|
584
|
-
const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
|
|
585
|
-
if (item.type === "reasoning") {
|
|
586
|
-
const extracted = extractReasoningFromOutputItem(item);
|
|
587
|
-
const state = ensureReasoningState(item.id, extracted.visibility);
|
|
588
|
-
const text =
|
|
589
|
-
extracted.text ||
|
|
590
|
-
(state.doneTexts.length > 0 ? state.doneTexts.join("\n") : "");
|
|
591
|
-
yield* completeReasoning(item.id, text, extracted.visibility || state.visibility);
|
|
592
|
-
}
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (sseEvent.type === "response.output_text.delta") {
|
|
597
|
-
const data = sseEvent.data as { item_id: string; delta: string };
|
|
598
|
-
yield factory.messageDelta(data.item_id, textBlock(data.delta));
|
|
599
|
-
messageItemsWithDelta.add(data.item_id);
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (sseEvent.type === "response.output_text.done") {
|
|
604
|
-
const data = sseEvent.data as { item_id: string; text: string };
|
|
605
|
-
if (!messageItemsWithDelta.has(data.item_id) && data.text) {
|
|
606
|
-
yield factory.messageDelta(data.item_id, textBlock(data.text));
|
|
607
|
-
}
|
|
608
|
-
yield factory.messageCompleted(data.item_id);
|
|
609
|
-
output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Modern OpenAI reasoning summary text (publicly streamed for o-series / gpt-5)
|
|
614
|
-
if (sseEvent.type === "response.reasoning_summary_text.delta") {
|
|
615
|
-
const data = sseEvent.data as { item_id: string; delta: string };
|
|
616
|
-
const state = ensureReasoningState(data.item_id, "summary");
|
|
617
|
-
if (state.visibility === "opaque") state.visibility = "summary";
|
|
618
|
-
if (data.delta) {
|
|
619
|
-
state.hasDelta = true;
|
|
620
|
-
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
621
|
-
}
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (sseEvent.type === "response.reasoning_summary_text.done") {
|
|
626
|
-
const data = sseEvent.data as { item_id: string; text: string };
|
|
627
|
-
const state = ensureReasoningState(data.item_id, "summary");
|
|
628
|
-
if (state.visibility === "opaque") state.visibility = "summary";
|
|
629
|
-
if (!state.hasDelta && data.text) {
|
|
630
|
-
state.doneTexts.push(data.text);
|
|
631
|
-
yield factory.reasoningDelta(data.item_id, textBlock(data.text));
|
|
632
|
-
} else if (data.text) {
|
|
633
|
-
state.doneTexts.push(data.text);
|
|
634
|
-
}
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Full reasoning text (opt-in via include); upgrade visibility to full
|
|
639
|
-
if (sseEvent.type === "response.reasoning_text.delta") {
|
|
640
|
-
const data = sseEvent.data as { item_id: string; delta: string };
|
|
641
|
-
const state = ensureReasoningState(data.item_id, "full");
|
|
642
|
-
state.visibility = "full";
|
|
643
|
-
if (data.delta) {
|
|
644
|
-
state.hasDelta = true;
|
|
645
|
-
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
646
|
-
}
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (sseEvent.type === "response.reasoning_text.done") {
|
|
651
|
-
const data = sseEvent.data as { item_id: string; text: string };
|
|
652
|
-
const state = ensureReasoningState(data.item_id, "full");
|
|
653
|
-
state.visibility = "full";
|
|
654
|
-
if (!state.hasDelta && data.text) {
|
|
655
|
-
state.doneTexts.push(data.text);
|
|
656
|
-
yield factory.reasoningDelta(data.item_id, textBlock(data.text));
|
|
657
|
-
} else if (data.text) {
|
|
658
|
-
state.doneTexts.push(data.text);
|
|
659
|
-
}
|
|
660
|
-
continue;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Structural summary part events — known & ignored (text is handled above)
|
|
664
|
-
if (
|
|
665
|
-
sseEvent.type === "response.reasoning_summary_part.added" ||
|
|
666
|
-
sseEvent.type === "response.reasoning_summary_part.done"
|
|
667
|
-
) {
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Legacy aliases kept for fixtures / older gateways
|
|
672
|
-
if (sseEvent.type === "response.reasoning.delta") {
|
|
673
|
-
const data = sseEvent.data as { item_id: string; delta: string };
|
|
674
|
-
const state = ensureReasoningState(data.item_id, "full");
|
|
675
|
-
state.visibility = "full";
|
|
676
|
-
if (data.delta) {
|
|
677
|
-
state.hasDelta = true;
|
|
678
|
-
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
679
|
-
}
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
if (sseEvent.type === "response.reasoning.done") {
|
|
684
|
-
const data = sseEvent.data as { item_id: string; text: string };
|
|
685
|
-
const state = ensureReasoningState(data.item_id, "full");
|
|
686
|
-
if (!state.hasDelta && data.text) {
|
|
687
|
-
yield factory.reasoningDelta(data.item_id, textBlock(data.text));
|
|
688
|
-
}
|
|
689
|
-
yield* completeReasoning(data.item_id, data.text ?? state.doneTexts.join("\n"), "full");
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if (sseEvent.type === "response.function_call_arguments.delta") {
|
|
694
|
-
const data = sseEvent.data as { item_id: string; delta: string };
|
|
695
|
-
if (data.delta) {
|
|
696
|
-
yield factory.toolCallDelta(resolveToolCallId(data.item_id), { argumentsText: data.delta });
|
|
697
|
-
}
|
|
698
|
-
continue;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (sseEvent.type === "response.function_call_arguments.done") {
|
|
702
|
-
const data = sseEvent.data as { item_id: string; arguments: string };
|
|
703
|
-
const callId = resolveToolCallId(data.item_id);
|
|
704
|
-
// 若 added 事件缺失,done 时仍尽量从 completed payload 之外兜底 call_id
|
|
705
|
-
if (!toolCallIds.has(data.item_id)) toolCallIds.set(data.item_id, callId);
|
|
706
|
-
const tcItem = toolCallItem(callId, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
|
|
707
|
-
yield factory.toolCallCompleted(callId);
|
|
708
|
-
output.push(tcItem);
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
if (
|
|
713
|
-
sseEvent.type === "response.completed" ||
|
|
714
|
-
sseEvent.type === "response.failed" ||
|
|
715
|
-
sseEvent.type === "response.incomplete"
|
|
716
|
-
) {
|
|
717
|
-
const data = sseEvent.data as { response: ResponsesAPIResponse };
|
|
718
|
-
if (completedResponse) {
|
|
719
|
-
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
720
|
-
continue;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
completedResponse = data.response;
|
|
724
|
-
|
|
725
|
-
// Safety net: finalize any still-open reasoning items from the final response payload.
|
|
726
|
-
if (Array.isArray(data.response.output)) {
|
|
727
|
-
for (const item of data.response.output) {
|
|
728
|
-
if (item?.type !== "reasoning" || !item.id) continue;
|
|
729
|
-
const state = reasoningStates.get(item.id);
|
|
730
|
-
if (state?.completed) continue;
|
|
731
|
-
const extracted = extractReasoningFromOutputItem(item);
|
|
732
|
-
const text =
|
|
733
|
-
extracted.text ||
|
|
734
|
-
(state && state.doneTexts.length > 0 ? state.doneTexts.join("\n") : "");
|
|
735
|
-
// Only complete if we already started this item (otherwise aggregator has no active item).
|
|
736
|
-
if (state || reasoningStates.has(item.id)) {
|
|
737
|
-
yield* completeReasoning(item.id, text, extracted.visibility);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
if (sseEvent.type === "response.failed") {
|
|
743
|
-
yield factory.responseWarning(
|
|
744
|
-
`Response failed: ${extractFailureMessage(data.response)}`,
|
|
745
|
-
"PROVIDER_FAILURE",
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
continue;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
|
|
752
|
-
unknownEventsWarned = true;
|
|
753
|
-
yield factory.responseWarning(
|
|
754
|
-
`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`,
|
|
755
|
-
"UNKNOWN_PROVIDER_EVENT",
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
let rawResponseId: string | undefined;
|
|
762
|
-
if (completedResponse) {
|
|
763
|
-
rawResponseId = completedResponse.id;
|
|
764
|
-
if (completedResponse.usage) {
|
|
765
|
-
auxiliary.recordUsage(usageFromOpenAIResponses(completedResponse.usage), "final", completedResponse.usage);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const replay = [...replayFromOutput(output)];
|
|
770
|
-
if (completedResponse?.id) {
|
|
771
|
-
// 同时保留 id(向后兼容)与 previous_response_id(语义明确)
|
|
772
|
-
replay.push(
|
|
773
|
-
opaqueItem("responses", "replay", {
|
|
774
|
-
id: completedResponse.id,
|
|
775
|
-
previous_response_id: completedResponse.id,
|
|
776
|
-
}),
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const stopReason = completedResponse ? this.inferStopReason(completedResponse) : undefined;
|
|
781
|
-
|
|
782
|
-
if (gate.tryComplete()) {
|
|
783
|
-
yield* this.emitStreamCompleted(factory, request, auxiliary, {
|
|
784
|
-
output,
|
|
785
|
-
replay,
|
|
786
|
-
stopReason,
|
|
787
|
-
rawResponseId,
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// ── 辅助方法 ──────────────────────────────────────────────
|
|
793
|
-
|
|
794
|
-
private inferStopReason(response: ResponsesAPIResponse): import("../index.js").StopReason {
|
|
795
|
-
if (response.status === "failed") return "error";
|
|
796
|
-
|
|
797
|
-
if (response.status === "incomplete") {
|
|
798
|
-
const reason = response.incomplete_details?.reason;
|
|
799
|
-
if (reason === "content_filter") return "content_filter";
|
|
800
|
-
if (reason === "max_output_tokens") return "max_output_tokens";
|
|
801
|
-
return "max_output_tokens";
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const output = response.output;
|
|
805
|
-
if (!output || output.length === 0) {
|
|
806
|
-
return response.status === "completed" ? "end_turn" : "unknown";
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const hasFunctionCall = output.some((item) => item.type === "function_call");
|
|
810
|
-
if (hasFunctionCall) return "tool_call";
|
|
811
|
-
|
|
812
|
-
const lastItem = output[output.length - 1];
|
|
813
|
-
if (lastItem?.status === "failed") return "error";
|
|
814
|
-
if (lastItem?.status === "incomplete") return "max_output_tokens";
|
|
815
|
-
|
|
816
|
-
return "end_turn";
|
|
817
|
-
}
|
|
818
|
-
}
|