@codehz/ai 0.2.4 → 0.4.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 +3 -8
- package/dist/index.d.mts +79 -89
- package/dist/index.mjs +717 -931
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +153 -266
- package/src/adapters/messages.ts +179 -301
- package/src/adapters/mock.ts +1 -10
- package/src/adapters/ollama.ts +142 -257
- package/src/adapters/responses.ts +141 -251
- package/src/core/validation.ts +19 -0
- package/src/helpers/adapter-auxiliary.ts +1 -23
- package/src/helpers/adapter-base.ts +41 -7
- package/src/helpers/incremental-stream-parser.ts +58 -0
- package/src/helpers/index.ts +20 -8
- package/src/helpers/mapping.ts +1 -10
- package/src/helpers/provider-stream.ts +147 -0
- package/src/helpers/request-mapper.ts +47 -25
- package/src/helpers/usage-mapping.ts +36 -39
- package/src/types/adapter.ts +1 -12
- package/src/types/index.ts +1 -9
- package/src/types/items.ts +0 -1
- package/src/helpers/sse-parser.ts +0 -113
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { AdapterBase } from "../helpers/adapter-base.js";
|
|
13
|
-
import {
|
|
13
|
+
import { AIRequestError } from "../core/errors.js";
|
|
14
14
|
import {
|
|
15
15
|
textBlock,
|
|
16
16
|
messageItem,
|
|
@@ -18,14 +18,16 @@ import {
|
|
|
18
18
|
toolCallItem,
|
|
19
19
|
opaqueItem,
|
|
20
20
|
replayFromOutput,
|
|
21
|
-
blockToText,
|
|
22
|
-
contentBlocksToText,
|
|
23
21
|
} from "../helpers/mapping.js";
|
|
24
|
-
import {
|
|
25
|
-
import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
|
|
22
|
+
import { assertOpaqueReplayEnvelope } from "../helpers/adapter-security.js";
|
|
26
23
|
import { usageFromOpenAIResponses } from "../helpers/usage-mapping.js";
|
|
27
|
-
import {
|
|
28
|
-
|
|
24
|
+
import {
|
|
25
|
+
NormalizedRequestMapper,
|
|
26
|
+
createSseJsonParser,
|
|
27
|
+
openProviderJsonStream,
|
|
28
|
+
iterateProviderStreamBatches,
|
|
29
|
+
createCompletionGate,
|
|
30
|
+
} from "../helpers/index.js";
|
|
29
31
|
|
|
30
32
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
31
33
|
|
|
@@ -72,24 +74,7 @@ type ResponsesTool = {
|
|
|
72
74
|
input_schema: Record<string, unknown>;
|
|
73
75
|
};
|
|
74
76
|
|
|
75
|
-
|
|
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
|
-
};
|
|
91
|
-
|
|
92
|
-
const mapper = new NormalizedRequestMapper(profile);
|
|
77
|
+
const mapper = new NormalizedRequestMapper("responses");
|
|
93
78
|
|
|
94
79
|
// ── SSE 事件类型 ──────────────────────────────────────────────
|
|
95
80
|
|
|
@@ -99,8 +84,8 @@ type ResponsesSSEEvent =
|
|
|
99
84
|
| { type: "response.output_text.done"; data: { item_id: string; text: string } }
|
|
100
85
|
| { type: "response.reasoning.delta"; data: { item_id: string; delta: string } }
|
|
101
86
|
| { type: "response.reasoning.done"; data: { item_id: string; text: string } }
|
|
102
|
-
| { type: "response.
|
|
103
|
-
| { type: "response.
|
|
87
|
+
| { type: "response.function_call_arguments.delta"; data: { item_id: string; delta: string } }
|
|
88
|
+
| { type: "response.function_call_arguments.done"; data: { item_id: string; arguments: string } }
|
|
104
89
|
| { type: "response.completed"; data: { response: ResponsesAPIResponse } }
|
|
105
90
|
| { type: "response.failed"; data: { response: ResponsesAPIResponse } }
|
|
106
91
|
| { type: "response.incomplete"; data: { response: ResponsesAPIResponse } }
|
|
@@ -115,8 +100,6 @@ const KNOWN_RESPONSES_SSE_TYPES = new Set([
|
|
|
115
100
|
"response.output_text.done",
|
|
116
101
|
"response.reasoning.delta",
|
|
117
102
|
"response.reasoning.done",
|
|
118
|
-
"response.tool_call.delta",
|
|
119
|
-
"response.tool_call.done",
|
|
120
103
|
"response.function_call_arguments.delta",
|
|
121
104
|
"response.function_call_arguments.done",
|
|
122
105
|
"response.content_part.added",
|
|
@@ -187,7 +170,7 @@ function canonicalToResponsesBlock(b: import("../index.js").ContentBlock): Respo
|
|
|
187
170
|
|
|
188
171
|
export class ResponsesAdapter extends AdapterBase {
|
|
189
172
|
readonly kind = "responses" as const;
|
|
190
|
-
readonly
|
|
173
|
+
readonly isSyntheticStream = false;
|
|
191
174
|
|
|
192
175
|
private apiKey: string;
|
|
193
176
|
private baseUrl: string;
|
|
@@ -218,9 +201,7 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
218
201
|
input.push({
|
|
219
202
|
type: "message",
|
|
220
203
|
role: item.role,
|
|
221
|
-
content:
|
|
222
|
-
mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`),
|
|
223
|
-
),
|
|
204
|
+
content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`),
|
|
224
205
|
});
|
|
225
206
|
}
|
|
226
207
|
break;
|
|
@@ -242,11 +223,7 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
242
223
|
break;
|
|
243
224
|
}
|
|
244
225
|
case "tool_result": {
|
|
245
|
-
mapper.
|
|
246
|
-
const output = mapper
|
|
247
|
-
.ensureTextBlocks(item.content, `tool_result ${item.callId} content`)
|
|
248
|
-
.map(blockToText)
|
|
249
|
-
.join("\n");
|
|
226
|
+
const output = mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`);
|
|
250
227
|
input.push({
|
|
251
228
|
type: "function_call_output",
|
|
252
229
|
call_id: item.callId,
|
|
@@ -286,24 +263,24 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
286
263
|
body.instructions = mapper.mapInstructions(request.instructions);
|
|
287
264
|
}
|
|
288
265
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
266
|
+
body.tools = mapper.mapToolsIfPresent(
|
|
267
|
+
request.tools,
|
|
268
|
+
(t): ResponsesTool => ({
|
|
269
|
+
type: "function",
|
|
270
|
+
name: t.name,
|
|
271
|
+
description: t.description,
|
|
272
|
+
input_schema: t.inputSchema,
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
body.tool_choice = mapper.mapToolChoice<Exclude<ResponsesAPIRequest["tool_choice"], undefined>>(
|
|
277
|
+
request.toolChoice,
|
|
278
|
+
{
|
|
279
|
+
auto: "auto",
|
|
280
|
+
none: "none",
|
|
281
|
+
tool: (name) => ({ type: "function" as const, name }),
|
|
282
|
+
},
|
|
283
|
+
);
|
|
307
284
|
|
|
308
285
|
if (request.temperature !== undefined) body.temperature = request.temperature;
|
|
309
286
|
if (request.maxOutputTokens !== undefined) body.max_output_tokens = request.maxOutputTokens;
|
|
@@ -320,198 +297,139 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
320
297
|
request: NormalizedRequest,
|
|
321
298
|
): AsyncIterable<AIStreamEvent> {
|
|
322
299
|
const auxiliary = this.createAuxiliaryState(request);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
});
|
|
335
|
-
} catch (err) {
|
|
336
|
-
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!response.ok) {
|
|
340
|
-
const errorBody = await response.text().catch(() => "");
|
|
341
|
-
throw providerHttpError(response.status, errorBody);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const reader = response.body?.getReader();
|
|
345
|
-
if (!reader) {
|
|
346
|
-
throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// 流式累积状态
|
|
350
|
-
const parser = new IncrementalStreamParser<ResponsesSSEEvent>(splitSSEFrames, (frame: string) => {
|
|
351
|
-
let eventType = "";
|
|
352
|
-
let dataStr = "";
|
|
353
|
-
for (const rawLine of frame.split("\n")) {
|
|
354
|
-
const line = rawLine.trim();
|
|
355
|
-
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
356
|
-
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
357
|
-
}
|
|
358
|
-
if (!eventType) return { status: "ignored" };
|
|
359
|
-
try {
|
|
360
|
-
const data = JSON.parse(dataStr);
|
|
361
|
-
return { status: "parsed", value: { type: eventType, data } as ResponsesSSEEvent };
|
|
362
|
-
} catch {
|
|
363
|
-
return { status: "malformed" };
|
|
364
|
-
}
|
|
300
|
+
const gate = createCompletionGate();
|
|
301
|
+
|
|
302
|
+
const { reader } = await openProviderJsonStream({
|
|
303
|
+
fetchFn: this.fetchFn,
|
|
304
|
+
url: `${this.baseUrl}/responses`,
|
|
305
|
+
headers: {
|
|
306
|
+
"Content-Type": "application/json",
|
|
307
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
308
|
+
},
|
|
309
|
+
body: providerRequest,
|
|
310
|
+
signal: request.signal,
|
|
365
311
|
});
|
|
366
312
|
|
|
313
|
+
const parser = createSseJsonParser<ResponsesSSEEvent>();
|
|
367
314
|
const output: OutputItem[] = [];
|
|
368
|
-
let streamDone = false;
|
|
369
315
|
let completedResponse: ResponsesAPIResponse | undefined;
|
|
370
|
-
let completedEmitted = false;
|
|
371
316
|
let unknownEventsWarned = false;
|
|
372
317
|
const messageItemsWithDelta = new Set<string>();
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (malformedWarning) {
|
|
391
|
-
yield malformedWarning;
|
|
318
|
+
const toolCallNames = new Map<string, string>();
|
|
319
|
+
|
|
320
|
+
for await (const batch of iterateProviderStreamBatches({
|
|
321
|
+
reader,
|
|
322
|
+
parser,
|
|
323
|
+
factory,
|
|
324
|
+
providerLabel: "Responses",
|
|
325
|
+
transportLabel: "SSE event(s)",
|
|
326
|
+
incompleteMessage: "Stream ended with an incomplete Responses SSE frame",
|
|
327
|
+
})) {
|
|
328
|
+
for (const warning of batch.warnings) yield warning;
|
|
329
|
+
|
|
330
|
+
for (const sseEvent of batch.items) {
|
|
331
|
+
if (sseEvent.type === "error") {
|
|
332
|
+
const data = sseEvent.data as { message?: string; code?: string };
|
|
333
|
+
yield factory.responseWarning(data.message ?? "Provider error event", data.code);
|
|
334
|
+
continue;
|
|
392
335
|
}
|
|
393
336
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
case "reasoning":
|
|
409
|
-
yield factory.reasoningStarted(item.id, "full");
|
|
410
|
-
break;
|
|
411
|
-
case "function_call":
|
|
412
|
-
yield factory.toolCallStarted(item.id, ((item as Record<string, unknown>).name as string) ?? "unknown");
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (sseEvent.type === "response.output_text.delta") {
|
|
419
|
-
const data = sseEvent.data as { item_id: string; delta: string };
|
|
420
|
-
yield factory.messageDelta(data.item_id, textBlock(data.delta));
|
|
421
|
-
messageItemsWithDelta.add(data.item_id);
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (sseEvent.type === "response.output_text.done") {
|
|
426
|
-
const data = sseEvent.data as { item_id: string; text: string };
|
|
427
|
-
if (!messageItemsWithDelta.has(data.item_id) && data.text) {
|
|
428
|
-
yield factory.messageDelta(data.item_id, textBlock(data.text));
|
|
337
|
+
if (sseEvent.type === "response.output_item.added") {
|
|
338
|
+
const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
|
|
339
|
+
switch (item.type) {
|
|
340
|
+
case "message":
|
|
341
|
+
yield factory.messageStarted(item.id);
|
|
342
|
+
break;
|
|
343
|
+
case "reasoning":
|
|
344
|
+
yield factory.reasoningStarted(item.id, "full");
|
|
345
|
+
break;
|
|
346
|
+
case "function_call": {
|
|
347
|
+
const name = typeof item.name === "string" ? item.name : "unknown";
|
|
348
|
+
toolCallNames.set(item.id, name);
|
|
349
|
+
yield factory.toolCallStarted(item.id, name);
|
|
350
|
+
break;
|
|
429
351
|
}
|
|
430
|
-
yield factory.messageCompleted(data.item_id);
|
|
431
|
-
output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
|
|
432
|
-
continue;
|
|
433
352
|
}
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
434
355
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
356
|
+
if (sseEvent.type === "response.output_text.delta") {
|
|
357
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
358
|
+
yield factory.messageDelta(data.item_id, textBlock(data.delta));
|
|
359
|
+
messageItemsWithDelta.add(data.item_id);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
440
362
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
continue;
|
|
363
|
+
if (sseEvent.type === "response.output_text.done") {
|
|
364
|
+
const data = sseEvent.data as { item_id: string; text: string };
|
|
365
|
+
if (!messageItemsWithDelta.has(data.item_id) && data.text) {
|
|
366
|
+
yield factory.messageDelta(data.item_id, textBlock(data.text));
|
|
446
367
|
}
|
|
368
|
+
yield factory.messageCompleted(data.item_id);
|
|
369
|
+
output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
447
372
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
373
|
+
if (sseEvent.type === "response.reasoning.delta") {
|
|
374
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
375
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
455
378
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
379
|
+
if (sseEvent.type === "response.reasoning.done") {
|
|
380
|
+
const data = sseEvent.data as { item_id: string; text: string };
|
|
381
|
+
yield factory.reasoningCompleted(data.item_id);
|
|
382
|
+
output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
463
385
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const data = sseEvent.data as { response: ResponsesAPIResponse };
|
|
470
|
-
if (completedResponse) {
|
|
471
|
-
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
386
|
+
if (sseEvent.type === "response.function_call_arguments.delta") {
|
|
387
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
388
|
+
if (data.delta) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta });
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
474
391
|
|
|
475
|
-
|
|
392
|
+
if (sseEvent.type === "response.function_call_arguments.done") {
|
|
393
|
+
const data = sseEvent.data as { item_id: string; arguments: string };
|
|
394
|
+
const tcItem = toolCallItem(data.item_id, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
|
|
395
|
+
yield factory.toolCallCompleted(data.item_id);
|
|
396
|
+
output.push(tcItem);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
476
399
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
400
|
+
if (
|
|
401
|
+
sseEvent.type === "response.completed" ||
|
|
402
|
+
sseEvent.type === "response.failed" ||
|
|
403
|
+
sseEvent.type === "response.incomplete"
|
|
404
|
+
) {
|
|
405
|
+
const data = sseEvent.data as { response: ResponsesAPIResponse };
|
|
406
|
+
if (completedResponse) {
|
|
407
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
483
408
|
continue;
|
|
484
409
|
}
|
|
485
410
|
|
|
486
|
-
|
|
487
|
-
|
|
411
|
+
completedResponse = data.response;
|
|
412
|
+
|
|
413
|
+
if (sseEvent.type === "response.failed") {
|
|
488
414
|
yield factory.responseWarning(
|
|
489
|
-
`
|
|
490
|
-
"
|
|
415
|
+
`Response failed: ${extractFailureMessage(data.response)}`,
|
|
416
|
+
"PROVIDER_FAILURE",
|
|
491
417
|
);
|
|
492
418
|
}
|
|
419
|
+
continue;
|
|
493
420
|
}
|
|
494
421
|
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
422
|
+
if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
|
|
423
|
+
unknownEventsWarned = true;
|
|
424
|
+
yield factory.responseWarning(
|
|
425
|
+
`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`,
|
|
426
|
+
"UNKNOWN_PROVIDER_EVENT",
|
|
427
|
+
);
|
|
498
428
|
}
|
|
499
429
|
}
|
|
500
|
-
} finally {
|
|
501
|
-
try {
|
|
502
|
-
if (!streamDone) await reader.cancel().catch(() => undefined);
|
|
503
|
-
} finally {
|
|
504
|
-
reader.releaseLock();
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (parser.getRemaining().trim().length > 0) {
|
|
509
|
-
yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
|
|
510
430
|
}
|
|
511
431
|
|
|
512
|
-
// 解析完成响应中的 usage 和 replay
|
|
513
432
|
let rawResponseId: string | undefined;
|
|
514
|
-
|
|
515
433
|
if (completedResponse) {
|
|
516
434
|
rawResponseId = completedResponse.id;
|
|
517
435
|
if (completedResponse.usage) {
|
|
@@ -519,47 +437,19 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
519
437
|
}
|
|
520
438
|
}
|
|
521
439
|
|
|
522
|
-
// 构造 replay:在 output 基础上追加 opaque continuation
|
|
523
440
|
const replay = [...replayFromOutput(output)];
|
|
524
|
-
|
|
525
|
-
// 如果有 provider continuation id,附加 opaque replay item
|
|
526
441
|
if (completedResponse?.id) {
|
|
527
442
|
replay.push(opaqueItem("responses", "replay", { id: completedResponse.id }));
|
|
528
443
|
}
|
|
529
444
|
|
|
530
|
-
// 从 completedResponse 推断 stop reason
|
|
531
445
|
const stopReason = completedResponse ? this.inferStopReason(completedResponse) : undefined;
|
|
532
446
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
completedEmitted = true;
|
|
540
|
-
const finalResponse = this.buildResponse(
|
|
541
|
-
request,
|
|
542
|
-
{
|
|
543
|
-
output,
|
|
544
|
-
replay,
|
|
545
|
-
stopReason,
|
|
546
|
-
usage: auxiliaryResult.usage,
|
|
547
|
-
billing: auxiliaryResult.billing,
|
|
548
|
-
auxiliary: auxiliaryResult.auxiliary,
|
|
549
|
-
warnings: auxiliaryResult.warnings,
|
|
550
|
-
metadataSources: auxiliaryResult.metadataSources,
|
|
551
|
-
rawResponseId,
|
|
552
|
-
},
|
|
553
|
-
factory,
|
|
554
|
-
);
|
|
555
|
-
yield factory.responseCompleted({
|
|
556
|
-
replay: finalResponse.replay,
|
|
557
|
-
stopReason: finalResponse.stopReason,
|
|
558
|
-
trace: finalResponse.backend,
|
|
559
|
-
usage: finalResponse.usage,
|
|
560
|
-
billing: finalResponse.billing,
|
|
561
|
-
auxiliary: finalResponse.auxiliary,
|
|
562
|
-
warnings: finalResponse.warnings,
|
|
447
|
+
if (gate.tryComplete()) {
|
|
448
|
+
yield* this.emitStreamCompleted(factory, request, auxiliary, {
|
|
449
|
+
output,
|
|
450
|
+
replay,
|
|
451
|
+
stopReason,
|
|
452
|
+
rawResponseId,
|
|
563
453
|
});
|
|
564
454
|
}
|
|
565
455
|
}
|
package/src/core/validation.ts
CHANGED
|
@@ -136,6 +136,25 @@ function validateInputItem(item: unknown, field: string, issues: ValidationIssue
|
|
|
136
136
|
"TOOL_CALL_ARGUMENTS_INVALID",
|
|
137
137
|
`${field}.argumentsText must be a string`,
|
|
138
138
|
);
|
|
139
|
+
} else {
|
|
140
|
+
try {
|
|
141
|
+
const parsed: unknown = JSON.parse(item.argumentsText);
|
|
142
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
143
|
+
pushIssue(
|
|
144
|
+
issues,
|
|
145
|
+
`${field}.argumentsText`,
|
|
146
|
+
"TOOL_CALL_ARGUMENTS_INVALID",
|
|
147
|
+
`${field}.argumentsText must encode a JSON object`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
pushIssue(
|
|
152
|
+
issues,
|
|
153
|
+
`${field}.argumentsText`,
|
|
154
|
+
"TOOL_CALL_ARGUMENTS_INVALID",
|
|
155
|
+
`${field}.argumentsText must encode a JSON object`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
139
158
|
}
|
|
140
159
|
return;
|
|
141
160
|
case "tool_result":
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { WarningCode } from "../core/errors.js";
|
|
2
2
|
import type { EventFactory } from "../core/event-factory.js";
|
|
3
|
-
import type {
|
|
4
|
-
AIStreamEvent,
|
|
5
|
-
BillingInfo,
|
|
6
|
-
NormalizedRequest,
|
|
7
|
-
Usage,
|
|
8
|
-
AuxiliaryInfo,
|
|
9
|
-
BackendTrace,
|
|
10
|
-
} from "../types/index.js";
|
|
3
|
+
import type { AIStreamEvent, BillingInfo, NormalizedRequest, Usage, AuxiliaryInfo } from "../types/index.js";
|
|
11
4
|
import { AuxiliaryCollector, type BillingSource, type LookupResult, type UsageSource } from "./auxiliary-collector.js";
|
|
12
5
|
|
|
13
6
|
type MaybePromise<T> = T | Promise<T>;
|
|
@@ -157,21 +150,6 @@ export function emitMalformedStreamWarning(
|
|
|
157
150
|
);
|
|
158
151
|
}
|
|
159
152
|
|
|
160
|
-
export function metadataSourceList(
|
|
161
|
-
...groups: Array<Array<NonNullable<BackendTrace["metadataSources"]>[number]> | undefined>
|
|
162
|
-
): string[] | undefined {
|
|
163
|
-
const sources = new Set<string>();
|
|
164
|
-
|
|
165
|
-
for (const group of groups) {
|
|
166
|
-
if (!group) continue;
|
|
167
|
-
for (const source of group) {
|
|
168
|
-
sources.add(source);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return sources.size > 0 ? [...sources] : undefined;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
153
|
function isEmptyRecord(value: object): boolean {
|
|
176
154
|
return Object.keys(value).length === 0;
|
|
177
155
|
}
|
|
@@ -56,7 +56,7 @@ export type StreamResult = {
|
|
|
56
56
|
|
|
57
57
|
export abstract class AdapterBase implements BackendAdapter {
|
|
58
58
|
abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
|
|
59
|
-
abstract readonly
|
|
59
|
+
abstract readonly isSyntheticStream: boolean;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* stream 模板方法:
|
|
@@ -70,7 +70,7 @@ export abstract class AdapterBase implements BackendAdapter {
|
|
|
70
70
|
|
|
71
71
|
const factory = createEventFactory({
|
|
72
72
|
responseId: request.requestId,
|
|
73
|
-
backend: { kind: this.kind, isSynthetic: this.
|
|
73
|
+
backend: { kind: this.kind, isSynthetic: this.isSyntheticStream },
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
yield factory.responseStarted(request.model);
|
|
@@ -129,7 +129,7 @@ export abstract class AdapterBase implements BackendAdapter {
|
|
|
129
129
|
* 子类可在返回前自定义覆盖。
|
|
130
130
|
*/
|
|
131
131
|
protected buildResponse(request: NormalizedRequest, result: StreamResult, _factory: EventFactory): AIResponse {
|
|
132
|
-
const text =
|
|
132
|
+
const text = extractText(result.output);
|
|
133
133
|
const warnings = mergeWarnings(result.warnings, _factory.warnings);
|
|
134
134
|
const auxiliary = mergeAuxiliary(
|
|
135
135
|
result.auxiliary,
|
|
@@ -151,16 +151,50 @@ export abstract class AdapterBase implements BackendAdapter {
|
|
|
151
151
|
requestId: request.requestId,
|
|
152
152
|
rawResponseId: result.rawResponseId,
|
|
153
153
|
adapter: this.kind,
|
|
154
|
-
isSyntheticStream: this.
|
|
154
|
+
isSyntheticStream: this.isSyntheticStream,
|
|
155
155
|
metadataSources: result.metadataSources,
|
|
156
156
|
warnings,
|
|
157
157
|
},
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
/**
|
|
162
|
+
* 统一 finalize auxiliary → response.completed。
|
|
163
|
+
* adapter 在调用前组装 output / replay / stopReason 等业务字段。
|
|
164
|
+
*/
|
|
165
|
+
protected async *emitStreamCompleted(
|
|
166
|
+
factory: EventFactory,
|
|
167
|
+
request: NormalizedRequest,
|
|
168
|
+
auxiliary: AdapterAuxiliaryState,
|
|
169
|
+
result: StreamResult,
|
|
170
|
+
): AsyncIterable<AIStreamEvent> {
|
|
171
|
+
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
172
|
+
for (const event of auxiliaryResult.events) {
|
|
173
|
+
yield event;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const finalResponse = this.buildResponse(
|
|
177
|
+
request,
|
|
178
|
+
{
|
|
179
|
+
...result,
|
|
180
|
+
usage: result.usage ?? auxiliaryResult.usage,
|
|
181
|
+
billing: result.billing ?? auxiliaryResult.billing,
|
|
182
|
+
auxiliary: mergeAuxiliary(result.auxiliary, auxiliaryResult.auxiliary),
|
|
183
|
+
warnings: mergeWarnings(result.warnings, auxiliaryResult.warnings),
|
|
184
|
+
metadataSources: result.metadataSources ?? auxiliaryResult.metadataSources,
|
|
185
|
+
},
|
|
186
|
+
factory,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
yield factory.responseCompleted({
|
|
190
|
+
replay: finalResponse.replay,
|
|
191
|
+
stopReason: finalResponse.stopReason,
|
|
192
|
+
trace: finalResponse.backend,
|
|
193
|
+
usage: finalResponse.usage,
|
|
194
|
+
billing: finalResponse.billing,
|
|
195
|
+
auxiliary: finalResponse.auxiliary,
|
|
196
|
+
warnings: finalResponse.warnings,
|
|
197
|
+
});
|
|
164
198
|
}
|
|
165
199
|
|
|
166
200
|
protected createAuxiliaryState(request: NormalizedRequest): AdapterAuxiliaryState {
|