@codehz/ai 0.4.3 → 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 +66 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/responses.ts +165 -50
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");
|
|
@@ -209,11 +256,12 @@ type ResponsesAPIOutputItem = {
|
|
|
209
256
|
id: string;
|
|
210
257
|
type: "message" | "reasoning" | "function_call" | string;
|
|
211
258
|
role?: string;
|
|
212
|
-
content?:
|
|
259
|
+
content?: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
213
260
|
summary?: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
214
261
|
encrypted_content?: string | null;
|
|
215
262
|
name?: string;
|
|
216
263
|
arguments?: string;
|
|
264
|
+
call_id?: string;
|
|
217
265
|
status?: string;
|
|
218
266
|
[key: string]: unknown;
|
|
219
267
|
};
|
|
@@ -232,15 +280,56 @@ function extractFailureMessage(response: ResponsesAPIResponse): string {
|
|
|
232
280
|
return response.error?.message ?? response.failure?.message ?? "unknown";
|
|
233
281
|
}
|
|
234
282
|
|
|
235
|
-
|
|
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
|
+
}
|
|
236
295
|
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`responses does not support content block type "${b.type}" in canonical mapping`,
|
|
242
|
-
"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",
|
|
243
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 };
|
|
244
333
|
}
|
|
245
334
|
|
|
246
335
|
// ── Adapter ───────────────────────────────────────────────────
|
|
@@ -264,36 +353,30 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
264
353
|
|
|
265
354
|
protected buildRequest(request: NormalizedRequest): ResponsesAPIRequest {
|
|
266
355
|
const input: ResponsesInputItem[] = [];
|
|
356
|
+
let previousResponseId: string | undefined;
|
|
357
|
+
let reasoningIndex = 0;
|
|
267
358
|
|
|
268
359
|
for (const item of request.input) {
|
|
269
360
|
switch (item.type) {
|
|
270
361
|
case "message": {
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
input.push({
|
|
279
|
-
type: "message",
|
|
280
|
-
role: item.role,
|
|
281
|
-
content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`),
|
|
282
|
-
});
|
|
283
|
-
}
|
|
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
|
+
});
|
|
284
369
|
break;
|
|
285
370
|
}
|
|
286
371
|
case "reasoning": {
|
|
287
|
-
|
|
288
|
-
.ensureReasoningBlocks(item.content, "reasoning content")
|
|
289
|
-
.map((b): ResponsesContentBlock => ({ type: "reasoning", text: b.text }));
|
|
290
|
-
input.push({ type: "reasoning", content: blocks });
|
|
372
|
+
input.push(mapReasoningInput(item, reasoningIndex++));
|
|
291
373
|
break;
|
|
292
374
|
}
|
|
293
375
|
case "tool_call": {
|
|
376
|
+
// call_id 必填;canonical ToolCallItem.id 即 call_id(流里会优先取 call_id)
|
|
294
377
|
input.push({
|
|
295
378
|
type: "function_call",
|
|
296
|
-
|
|
379
|
+
call_id: item.id,
|
|
297
380
|
name: item.name,
|
|
298
381
|
arguments: item.argumentsText,
|
|
299
382
|
});
|
|
@@ -309,20 +392,28 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
309
392
|
break;
|
|
310
393
|
}
|
|
311
394
|
case "opaque": {
|
|
312
|
-
// Canonical replay
|
|
313
|
-
//
|
|
395
|
+
// Canonical replay 优先;否则用 previous_response_id 做服务端续写。
|
|
396
|
+
// 注意:response id 不能塞进 item_reference(那是 item id)。
|
|
314
397
|
if (item.source !== "responses" || item.purpose !== "replay") break;
|
|
315
398
|
assertOpaqueReplayEnvelope(item.payload);
|
|
316
399
|
const payload = item.payload as Record<string, unknown>;
|
|
317
|
-
|
|
318
|
-
|
|
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)) {
|
|
319
404
|
throw new AIRequestError(
|
|
320
|
-
|
|
405
|
+
`Invalid opaque replay payload: ${key} must be a non-empty string (max 256)`,
|
|
321
406
|
"INVALID_OPAQUE_REPLAY",
|
|
322
407
|
);
|
|
323
408
|
}
|
|
324
|
-
|
|
325
|
-
|
|
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 });
|
|
326
417
|
}
|
|
327
418
|
}
|
|
328
419
|
break;
|
|
@@ -336,6 +427,10 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
336
427
|
stream: true,
|
|
337
428
|
};
|
|
338
429
|
|
|
430
|
+
if (previousResponseId) {
|
|
431
|
+
body.previous_response_id = previousResponseId;
|
|
432
|
+
}
|
|
433
|
+
|
|
339
434
|
if (request.instructions) {
|
|
340
435
|
body.instructions = mapper.mapInstructions(request.instructions);
|
|
341
436
|
}
|
|
@@ -392,9 +487,14 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
392
487
|
let completedResponse: ResponsesAPIResponse | undefined;
|
|
393
488
|
let unknownEventsWarned = false;
|
|
394
489
|
const messageItemsWithDelta = new Set<string>();
|
|
490
|
+
/** item_id → function name */
|
|
395
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>();
|
|
396
494
|
const reasoningStates = new Map<string, ReasoningStreamState>();
|
|
397
495
|
|
|
496
|
+
const resolveToolCallId = (itemId: string): string => toolCallIds.get(itemId) ?? itemId;
|
|
497
|
+
|
|
398
498
|
const ensureReasoningState = (itemId: string, visibility: ReasoningVisibility = "summary"): ReasoningStreamState => {
|
|
399
499
|
let state = reasoningStates.get(itemId);
|
|
400
500
|
if (!state) {
|
|
@@ -449,8 +549,12 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
449
549
|
}
|
|
450
550
|
case "function_call": {
|
|
451
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;
|
|
452
555
|
toolCallNames.set(item.id, name);
|
|
453
|
-
|
|
556
|
+
toolCallIds.set(item.id, callId);
|
|
557
|
+
yield factory.toolCallStarted(callId, name);
|
|
454
558
|
break;
|
|
455
559
|
}
|
|
456
560
|
}
|
|
@@ -569,14 +673,19 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
569
673
|
|
|
570
674
|
if (sseEvent.type === "response.function_call_arguments.delta") {
|
|
571
675
|
const data = sseEvent.data as { item_id: string; delta: string };
|
|
572
|
-
if (data.delta)
|
|
676
|
+
if (data.delta) {
|
|
677
|
+
yield factory.toolCallDelta(resolveToolCallId(data.item_id), { argumentsText: data.delta });
|
|
678
|
+
}
|
|
573
679
|
continue;
|
|
574
680
|
}
|
|
575
681
|
|
|
576
682
|
if (sseEvent.type === "response.function_call_arguments.done") {
|
|
577
683
|
const data = sseEvent.data as { item_id: string; arguments: string };
|
|
578
|
-
const
|
|
579
|
-
|
|
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);
|
|
580
689
|
output.push(tcItem);
|
|
581
690
|
continue;
|
|
582
691
|
}
|
|
@@ -640,7 +749,13 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
640
749
|
|
|
641
750
|
const replay = [...replayFromOutput(output)];
|
|
642
751
|
if (completedResponse?.id) {
|
|
643
|
-
|
|
752
|
+
// 同时保留 id(向后兼容)与 previous_response_id(语义明确)
|
|
753
|
+
replay.push(
|
|
754
|
+
opaqueItem("responses", "replay", {
|
|
755
|
+
id: completedResponse.id,
|
|
756
|
+
previous_response_id: completedResponse.id,
|
|
757
|
+
}),
|
|
758
|
+
);
|
|
644
759
|
}
|
|
645
760
|
|
|
646
761
|
const stopReason = completedResponse ? this.inferStopReason(completedResponse) : undefined;
|