@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
package/dist/index.mjs
CHANGED
|
@@ -11,8 +11,10 @@ var AIError = class extends Error {
|
|
|
11
11
|
};
|
|
12
12
|
/** 请求构造失败 — 参数校验不通过。在进入 adapter 前同步抛错。 */
|
|
13
13
|
var AIRequestError = class extends AIError {
|
|
14
|
-
|
|
14
|
+
issues;
|
|
15
|
+
constructor(message, code, issues) {
|
|
15
16
|
super(message, code, "AIRequestError");
|
|
17
|
+
this.issues = issues;
|
|
16
18
|
}
|
|
17
19
|
};
|
|
18
20
|
/** Provider 调用失败 — HTTP 非 2xx、网络错误。由 AdapterBase 捕获转为 warning。 */
|
|
@@ -59,7 +61,9 @@ const WarningCode = {
|
|
|
59
61
|
/** 能力降级 */
|
|
60
62
|
CAPABILITY_DOWNGRADE: "CAPABILITY_DOWNGRADE",
|
|
61
63
|
/** 模拟流式 */
|
|
62
|
-
SYNTHETIC_STREAM: "SYNTHETIC_STREAM"
|
|
64
|
+
SYNTHETIC_STREAM: "SYNTHETIC_STREAM",
|
|
65
|
+
/** 工具调用以批量方式到达(非 token 级流式) */
|
|
66
|
+
TOOL_CALL_BATCHED: "TOOL_CALL_BATCHED"
|
|
63
67
|
};
|
|
64
68
|
//#endregion
|
|
65
69
|
//#region src/core/validation.ts
|
|
@@ -194,6 +198,16 @@ function validateToolChoice(toolChoice, issues) {
|
|
|
194
198
|
if (toolChoice === "auto" || toolChoice === "none") return;
|
|
195
199
|
if (!isRecord(toolChoice) || toolChoice.type !== "tool" || typeof toolChoice.name !== "string" || toolChoice.name.length === 0) pushIssue(issues, "toolChoice", "TOOL_CHOICE_INVALID", "toolChoice must be auto, none, or { type: \"tool\", name }");
|
|
196
200
|
}
|
|
201
|
+
/** Validate include settings, appending issues to the given array. */
|
|
202
|
+
function validateInclude(include, issues) {
|
|
203
|
+
if (!isRecord(include)) {
|
|
204
|
+
pushIssue(issues, "include", "INCLUDE_INVALID", "include must be an object");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (include.usage !== void 0 && (typeof include.usage !== "string" || !INCLUDE_MODES.has(include.usage))) pushIssue(issues, "include.usage", "INCLUDE_USAGE_INVALID", "include.usage must be off or best_effort");
|
|
208
|
+
if (include.billing !== void 0 && (typeof include.billing !== "string" || !INCLUDE_MODES.has(include.billing))) pushIssue(issues, "include.billing", "INCLUDE_BILLING_INVALID", "include.billing must be off or best_effort");
|
|
209
|
+
if (include.providerMetadata !== void 0 && (typeof include.providerMetadata !== "string" || !INCLUDE_MODES.has(include.providerMetadata))) pushIssue(issues, "include.providerMetadata", "INCLUDE_PROVIDER_METADATA_INVALID", "include.providerMetadata must be off or best_effort");
|
|
210
|
+
}
|
|
197
211
|
/**
|
|
198
212
|
* 校验 AIRequest,返回校验问题列表。
|
|
199
213
|
* 空数组表示无问题。
|
|
@@ -205,7 +219,7 @@ function validateRequest(request) {
|
|
|
205
219
|
if (!Array.isArray(request.input) || request.input.length === 0) pushIssue(issues, "input", "INPUT_EMPTY", "input must be a non-empty array");
|
|
206
220
|
if (Array.isArray(request.input)) for (let i = 0; i < request.input.length; i++) validateInputItem(request.input[i], `input[${i}]`, issues);
|
|
207
221
|
if (request.temperature !== void 0) {
|
|
208
|
-
if (typeof request.temperature !== "number" ||
|
|
222
|
+
if (typeof request.temperature !== "number" || !Number.isFinite(request.temperature)) issues.push({
|
|
209
223
|
field: "temperature",
|
|
210
224
|
code: "TEMPERATURE_NOT_NUMBER",
|
|
211
225
|
message: "temperature must be a number"
|
|
@@ -217,7 +231,7 @@ function validateRequest(request) {
|
|
|
217
231
|
});
|
|
218
232
|
}
|
|
219
233
|
if (request.maxOutputTokens !== void 0) {
|
|
220
|
-
if (typeof request.maxOutputTokens !== "number" ||
|
|
234
|
+
if (typeof request.maxOutputTokens !== "number" || !Number.isFinite(request.maxOutputTokens)) issues.push({
|
|
221
235
|
field: "maxOutputTokens",
|
|
222
236
|
code: "MAX_OUTPUT_TOKENS_NOT_NUMBER",
|
|
223
237
|
message: "maxOutputTokens must be a number"
|
|
@@ -228,12 +242,7 @@ function validateRequest(request) {
|
|
|
228
242
|
message: "maxOutputTokens must be a positive integer"
|
|
229
243
|
});
|
|
230
244
|
}
|
|
231
|
-
if (request.include !== void 0)
|
|
232
|
-
else {
|
|
233
|
-
if (request.include.usage !== void 0 && !INCLUDE_MODES.has(request.include.usage)) pushIssue(issues, "include.usage", "INCLUDE_USAGE_INVALID", "include.usage must be off or best_effort");
|
|
234
|
-
if (request.include.billing !== void 0 && !INCLUDE_MODES.has(request.include.billing)) pushIssue(issues, "include.billing", "INCLUDE_BILLING_INVALID", "include.billing must be off or best_effort");
|
|
235
|
-
if (request.include.providerMetadata !== void 0 && !INCLUDE_MODES.has(request.include.providerMetadata)) pushIssue(issues, "include.providerMetadata", "INCLUDE_PROVIDER_METADATA_INVALID", "include.providerMetadata must be off or best_effort");
|
|
236
|
-
}
|
|
245
|
+
if (request.include !== void 0) validateInclude(request.include, issues);
|
|
237
246
|
if (request.metadata !== void 0) {
|
|
238
247
|
if (!isRecord(request.metadata)) pushIssue(issues, "metadata", "METADATA_INVALID", "metadata must be an object");
|
|
239
248
|
else for (const [key, value] of Object.entries(request.metadata)) if (typeof value !== "string") pushIssue(issues, `metadata.${key}`, "METADATA_VALUE_INVALID", `metadata.${key} must be a string`);
|
|
@@ -260,8 +269,9 @@ function validateRequest(request) {
|
|
|
260
269
|
* 适用于客户端入口的快速失败检查。
|
|
261
270
|
*/
|
|
262
271
|
function assertValidRequest(request) {
|
|
263
|
-
const
|
|
264
|
-
|
|
272
|
+
const issues = validateRequest(request);
|
|
273
|
+
const first = issues[0];
|
|
274
|
+
if (first) throw new AIRequestError(first.message, first.code, issues);
|
|
265
275
|
}
|
|
266
276
|
//#endregion
|
|
267
277
|
//#region src/core/normalize.ts
|
|
@@ -279,6 +289,11 @@ const DEFAULT_INCLUDE = {
|
|
|
279
289
|
*/
|
|
280
290
|
function normalizeRequest(request, options) {
|
|
281
291
|
const { model, defaults } = options;
|
|
292
|
+
const earlyIncludeIssues = [];
|
|
293
|
+
if (request.include !== void 0) validateInclude(request.include, earlyIncludeIssues);
|
|
294
|
+
if (defaults?.include !== void 0) validateInclude(defaults.include, earlyIncludeIssues);
|
|
295
|
+
const firstIncludeIssue = earlyIncludeIssues[0];
|
|
296
|
+
if (firstIncludeIssue) throw new AIRequestError(firstIncludeIssue.message, firstIncludeIssue.code, earlyIncludeIssues);
|
|
282
297
|
const merged = {
|
|
283
298
|
...defaults,
|
|
284
299
|
...request,
|
|
@@ -350,11 +365,11 @@ function createEventFactory(state) {
|
|
|
350
365
|
...data
|
|
351
366
|
};
|
|
352
367
|
},
|
|
353
|
-
responseCompleted(
|
|
368
|
+
responseCompleted(completion) {
|
|
354
369
|
return {
|
|
355
370
|
...base(),
|
|
356
371
|
type: "response.completed",
|
|
357
|
-
|
|
372
|
+
...completion
|
|
358
373
|
};
|
|
359
374
|
},
|
|
360
375
|
messageStarted(id) {
|
|
@@ -367,22 +382,19 @@ function createEventFactory(state) {
|
|
|
367
382
|
}
|
|
368
383
|
};
|
|
369
384
|
},
|
|
370
|
-
messageDelta(itemId,
|
|
385
|
+
messageDelta(itemId, delta) {
|
|
371
386
|
return {
|
|
372
387
|
...base(),
|
|
373
388
|
type: "message.delta",
|
|
374
389
|
itemId,
|
|
375
|
-
delta
|
|
376
|
-
type: "text",
|
|
377
|
-
text
|
|
378
|
-
}
|
|
390
|
+
delta
|
|
379
391
|
};
|
|
380
392
|
},
|
|
381
|
-
messageCompleted(
|
|
393
|
+
messageCompleted(itemId) {
|
|
382
394
|
return {
|
|
383
395
|
...base(),
|
|
384
396
|
type: "message.completed",
|
|
385
|
-
|
|
397
|
+
itemId
|
|
386
398
|
};
|
|
387
399
|
},
|
|
388
400
|
reasoningStarted(id, visibility) {
|
|
@@ -403,11 +415,11 @@ function createEventFactory(state) {
|
|
|
403
415
|
delta
|
|
404
416
|
};
|
|
405
417
|
},
|
|
406
|
-
reasoningCompleted(
|
|
418
|
+
reasoningCompleted(itemId) {
|
|
407
419
|
return {
|
|
408
420
|
...base(),
|
|
409
421
|
type: "reasoning.completed",
|
|
410
|
-
|
|
422
|
+
itemId
|
|
411
423
|
};
|
|
412
424
|
},
|
|
413
425
|
toolCallStarted(id, name) {
|
|
@@ -428,11 +440,11 @@ function createEventFactory(state) {
|
|
|
428
440
|
delta
|
|
429
441
|
};
|
|
430
442
|
},
|
|
431
|
-
toolCallCompleted(
|
|
443
|
+
toolCallCompleted(itemId) {
|
|
432
444
|
return {
|
|
433
445
|
...base(),
|
|
434
446
|
type: "tool_call.completed",
|
|
435
|
-
|
|
447
|
+
itemId
|
|
436
448
|
};
|
|
437
449
|
},
|
|
438
450
|
/** 返回当前已发出的 sequence 计数(用于断言) */
|
|
@@ -446,6 +458,20 @@ function createEventFactory(state) {
|
|
|
446
458
|
};
|
|
447
459
|
}
|
|
448
460
|
//#endregion
|
|
461
|
+
//#region src/core/merge-auxiliary.ts
|
|
462
|
+
function mergeAuxiliary(base, patch) {
|
|
463
|
+
if (!base && !patch) return void 0;
|
|
464
|
+
const merged = {
|
|
465
|
+
...base,
|
|
466
|
+
...patch
|
|
467
|
+
};
|
|
468
|
+
if (base?.providerMetadata || patch?.providerMetadata) merged.providerMetadata = {
|
|
469
|
+
...base?.providerMetadata,
|
|
470
|
+
...patch?.providerMetadata
|
|
471
|
+
};
|
|
472
|
+
return merged;
|
|
473
|
+
}
|
|
474
|
+
//#endregion
|
|
449
475
|
//#region src/core/aggregator.ts
|
|
450
476
|
function createAggregatorState() {
|
|
451
477
|
return {
|
|
@@ -454,10 +480,47 @@ function createAggregatorState() {
|
|
|
454
480
|
warningSet: /* @__PURE__ */ new Set(),
|
|
455
481
|
output: [],
|
|
456
482
|
textParts: [],
|
|
457
|
-
toolCalls: []
|
|
483
|
+
toolCalls: [],
|
|
484
|
+
started: false,
|
|
485
|
+
completed: false,
|
|
486
|
+
activeItems: /* @__PURE__ */ new Map(),
|
|
487
|
+
itemOrder: [],
|
|
488
|
+
completedItems: /* @__PURE__ */ new Map()
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function getActiveItem(state, itemId, expectedType) {
|
|
492
|
+
const item = state.activeItems.get(itemId);
|
|
493
|
+
if (!item) throw streamProtocolError(`Received ${expectedType} delta/completed for unknown item: ${itemId}`);
|
|
494
|
+
if (item.type !== expectedType) throw streamProtocolError(`Item ${itemId} started as ${item.type} but received ${expectedType} event`);
|
|
495
|
+
return item;
|
|
496
|
+
}
|
|
497
|
+
function finalizeMessage(active) {
|
|
498
|
+
return {
|
|
499
|
+
type: "message",
|
|
500
|
+
id: active.id,
|
|
501
|
+
role: active.role,
|
|
502
|
+
content: active.content
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function finalizeReasoning(active) {
|
|
506
|
+
return {
|
|
507
|
+
type: "reasoning",
|
|
508
|
+
id: active.id,
|
|
509
|
+
visibility: active.visibility,
|
|
510
|
+
content: active.content
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function finalizeToolCall(active) {
|
|
514
|
+
return {
|
|
515
|
+
type: "tool_call",
|
|
516
|
+
id: active.id,
|
|
517
|
+
name: active.name,
|
|
518
|
+
argumentsText: active.argumentsText
|
|
458
519
|
};
|
|
459
520
|
}
|
|
460
521
|
function handleResponseStarted(state, event) {
|
|
522
|
+
if (state.started) throw streamProtocolError("Stream must contain exactly one response.started event");
|
|
523
|
+
state.started = true;
|
|
461
524
|
state.responseId = event.responseId;
|
|
462
525
|
state.model = event.model;
|
|
463
526
|
state.backendInfo = event.backend;
|
|
@@ -474,48 +537,106 @@ function handleResponseAuxiliary(state, event) {
|
|
|
474
537
|
...state.billing,
|
|
475
538
|
...event.billing
|
|
476
539
|
};
|
|
477
|
-
if (event.auxiliary) state.auxiliary = mergeAuxiliary
|
|
540
|
+
if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
|
|
541
|
+
}
|
|
542
|
+
function handleMessageStarted(state, event) {
|
|
543
|
+
const id = event.item.id;
|
|
544
|
+
if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
|
|
545
|
+
state.activeItems.set(id, {
|
|
546
|
+
type: "message",
|
|
547
|
+
id,
|
|
548
|
+
role: event.item.role,
|
|
549
|
+
content: []
|
|
550
|
+
});
|
|
551
|
+
state.itemOrder.push(id);
|
|
552
|
+
}
|
|
553
|
+
function handleMessageDelta(state, event) {
|
|
554
|
+
getActiveItem(state, event.itemId, "message").content.push(event.delta);
|
|
478
555
|
}
|
|
479
556
|
function handleMessageCompleted(state, event) {
|
|
480
|
-
state
|
|
481
|
-
|
|
557
|
+
const active = getActiveItem(state, event.itemId, "message");
|
|
558
|
+
state.activeItems.delete(event.itemId);
|
|
559
|
+
const item = finalizeMessage(active);
|
|
560
|
+
state.completedItems.set(event.itemId, item);
|
|
561
|
+
pushMessageText(state, item);
|
|
562
|
+
}
|
|
563
|
+
function handleReasoningStarted(state, event) {
|
|
564
|
+
const id = event.item.id;
|
|
565
|
+
if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
|
|
566
|
+
state.activeItems.set(id, {
|
|
567
|
+
type: "reasoning",
|
|
568
|
+
id,
|
|
569
|
+
visibility: event.item.visibility,
|
|
570
|
+
content: []
|
|
571
|
+
});
|
|
572
|
+
state.itemOrder.push(id);
|
|
573
|
+
}
|
|
574
|
+
function handleReasoningDelta(state, event) {
|
|
575
|
+
getActiveItem(state, event.itemId, "reasoning").content.push(event.delta);
|
|
482
576
|
}
|
|
483
577
|
function handleReasoningCompleted(state, event) {
|
|
484
|
-
state
|
|
578
|
+
const active = getActiveItem(state, event.itemId, "reasoning");
|
|
579
|
+
state.activeItems.delete(event.itemId);
|
|
580
|
+
state.completedItems.set(event.itemId, finalizeReasoning(active));
|
|
581
|
+
}
|
|
582
|
+
function handleToolCallStarted(state, event) {
|
|
583
|
+
const id = event.item.id;
|
|
584
|
+
if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
|
|
585
|
+
state.activeItems.set(id, {
|
|
586
|
+
type: "tool_call",
|
|
587
|
+
id,
|
|
588
|
+
name: event.item.name,
|
|
589
|
+
argumentsText: ""
|
|
590
|
+
});
|
|
591
|
+
state.itemOrder.push(id);
|
|
592
|
+
}
|
|
593
|
+
function handleToolCallDelta(state, event) {
|
|
594
|
+
const active = getActiveItem(state, event.itemId, "tool_call");
|
|
595
|
+
if (event.delta.argumentsText) active.argumentsText += event.delta.argumentsText;
|
|
485
596
|
}
|
|
486
597
|
function handleToolCallCompleted(state, event) {
|
|
487
|
-
state
|
|
488
|
-
state.
|
|
598
|
+
const active = getActiveItem(state, event.itemId, "tool_call");
|
|
599
|
+
state.activeItems.delete(event.itemId);
|
|
600
|
+
const item = finalizeToolCall(active);
|
|
601
|
+
state.completedItems.set(event.itemId, item);
|
|
602
|
+
state.toolCalls.push(item);
|
|
489
603
|
}
|
|
490
604
|
function handleResponseCompleted(state, event) {
|
|
491
|
-
state.
|
|
492
|
-
state.
|
|
493
|
-
state.
|
|
494
|
-
state.
|
|
495
|
-
|
|
605
|
+
if (state.activeItems.size > 0) throw streamProtocolError("response.completed received while active items still pending");
|
|
606
|
+
state.completed = true;
|
|
607
|
+
state.replayFromAdapter = event.replay;
|
|
608
|
+
state.stopReasonFromAdapter = event.stopReason;
|
|
609
|
+
state.backendFromAdapter = event.trace;
|
|
610
|
+
if (event.usage) state.usage = {
|
|
496
611
|
...state.usage,
|
|
497
|
-
...event.
|
|
612
|
+
...event.usage
|
|
498
613
|
};
|
|
499
|
-
if (event.
|
|
614
|
+
if (event.billing) state.billing = {
|
|
500
615
|
...state.billing,
|
|
501
|
-
...event.
|
|
616
|
+
...event.billing
|
|
502
617
|
};
|
|
503
|
-
if (event.
|
|
504
|
-
if (event.
|
|
618
|
+
if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
|
|
619
|
+
if (event.warnings) pushWarnings(state, event.warnings);
|
|
620
|
+
if (event.opaqueOutput) state.output.push(...event.opaqueOutput);
|
|
505
621
|
}
|
|
506
622
|
function buildResponse(state) {
|
|
507
|
-
const
|
|
623
|
+
const backendFromCompleted = state.backendFromAdapter;
|
|
508
624
|
const backend = {
|
|
509
|
-
adapter:
|
|
510
|
-
isSyntheticStream:
|
|
511
|
-
requestId:
|
|
512
|
-
rawResponseId:
|
|
513
|
-
metadataSources:
|
|
514
|
-
warnings:
|
|
625
|
+
adapter: backendFromCompleted?.adapter ?? state.backendInfo?.kind ?? "unknown",
|
|
626
|
+
isSyntheticStream: backendFromCompleted?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
|
|
627
|
+
requestId: backendFromCompleted?.requestId ?? state.responseId,
|
|
628
|
+
rawResponseId: backendFromCompleted?.rawResponseId,
|
|
629
|
+
metadataSources: backendFromCompleted?.metadataSources,
|
|
630
|
+
warnings: backendFromCompleted?.warnings
|
|
515
631
|
};
|
|
632
|
+
const orderedOutput = state.itemOrder.map((id) => {
|
|
633
|
+
const item = state.completedItems.get(id);
|
|
634
|
+
if (!item) throw streamProtocolError(`Item ${id} was started but not completed`);
|
|
635
|
+
return item;
|
|
636
|
+
});
|
|
516
637
|
return {
|
|
517
|
-
id: state.
|
|
518
|
-
output: state.output,
|
|
638
|
+
id: state.responseId,
|
|
639
|
+
output: [...orderedOutput, ...state.output],
|
|
519
640
|
replay: state.replayFromAdapter ?? [],
|
|
520
641
|
text: state.textParts.join(""),
|
|
521
642
|
toolCalls: state.toolCalls,
|
|
@@ -537,6 +658,7 @@ function aggregateEvents(events) {
|
|
|
537
658
|
return finalizeAggregation(state);
|
|
538
659
|
}
|
|
539
660
|
function aggregateEvent(state, event) {
|
|
661
|
+
validateEventEnvelope(state, event);
|
|
540
662
|
state.lastEventType = event.type;
|
|
541
663
|
switch (event.type) {
|
|
542
664
|
case "response.started":
|
|
@@ -549,17 +671,29 @@ function aggregateEvent(state, event) {
|
|
|
549
671
|
handleResponseAuxiliary(state, event);
|
|
550
672
|
break;
|
|
551
673
|
case "message.started":
|
|
674
|
+
handleMessageStarted(state, event);
|
|
675
|
+
break;
|
|
552
676
|
case "message.delta":
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
case "tool_call.started":
|
|
556
|
-
case "tool_call.delta": break;
|
|
677
|
+
handleMessageDelta(state, event);
|
|
678
|
+
break;
|
|
557
679
|
case "message.completed":
|
|
558
680
|
handleMessageCompleted(state, event);
|
|
559
681
|
break;
|
|
682
|
+
case "reasoning.started":
|
|
683
|
+
handleReasoningStarted(state, event);
|
|
684
|
+
break;
|
|
685
|
+
case "reasoning.delta":
|
|
686
|
+
handleReasoningDelta(state, event);
|
|
687
|
+
break;
|
|
560
688
|
case "reasoning.completed":
|
|
561
689
|
handleReasoningCompleted(state, event);
|
|
562
690
|
break;
|
|
691
|
+
case "tool_call.started":
|
|
692
|
+
handleToolCallStarted(state, event);
|
|
693
|
+
break;
|
|
694
|
+
case "tool_call.delta":
|
|
695
|
+
handleToolCallDelta(state, event);
|
|
696
|
+
break;
|
|
563
697
|
case "tool_call.completed":
|
|
564
698
|
handleToolCallCompleted(state, event);
|
|
565
699
|
break;
|
|
@@ -569,20 +703,10 @@ function aggregateEvent(state, event) {
|
|
|
569
703
|
}
|
|
570
704
|
}
|
|
571
705
|
function finalizeAggregation(state) {
|
|
572
|
-
if (state.
|
|
706
|
+
if (!state.started) throw streamProtocolError("Stream must start with response.started event");
|
|
707
|
+
if (!state.completed || state.lastEventType !== "response.completed") throw streamProtocolError("Stream must end with response.completed event to produce a valid AIResponse");
|
|
573
708
|
return buildResponse(state);
|
|
574
709
|
}
|
|
575
|
-
function mergeAuxiliary$1(base, patch) {
|
|
576
|
-
const merged = {
|
|
577
|
-
...base,
|
|
578
|
-
...patch
|
|
579
|
-
};
|
|
580
|
-
if (base.providerMetadata || patch.providerMetadata) merged.providerMetadata = {
|
|
581
|
-
...base.providerMetadata,
|
|
582
|
-
...patch.providerMetadata
|
|
583
|
-
};
|
|
584
|
-
return merged;
|
|
585
|
-
}
|
|
586
710
|
function pushWarnings(state, warnings) {
|
|
587
711
|
for (const warning of warnings) if (!state.warningSet.has(warning)) {
|
|
588
712
|
state.warningSet.add(warning);
|
|
@@ -592,6 +716,16 @@ function pushWarnings(state, warnings) {
|
|
|
592
716
|
function pushMessageText(state, item) {
|
|
593
717
|
for (const block of item.content) if (block.type === "text") state.textParts.push(block.text);
|
|
594
718
|
}
|
|
719
|
+
function validateEventEnvelope(state, event) {
|
|
720
|
+
if (state.completed) throw streamProtocolError("response.completed must be the final stream event");
|
|
721
|
+
if (!state.started && event.type !== "response.started") throw streamProtocolError("Stream must start with response.started event");
|
|
722
|
+
if (state.responseId !== void 0 && event.responseId !== state.responseId) throw streamProtocolError("All stream events must use the same responseId");
|
|
723
|
+
if (state.nextSequence !== void 0 && event.sequence !== state.nextSequence) throw streamProtocolError(`Expected event sequence ${state.nextSequence}, received ${event.sequence}`);
|
|
724
|
+
state.nextSequence = event.sequence + 1;
|
|
725
|
+
}
|
|
726
|
+
function streamProtocolError(message) {
|
|
727
|
+
return new AIStreamError(message, "STREAM_PROTOCOL_ERROR");
|
|
728
|
+
}
|
|
595
729
|
//#endregion
|
|
596
730
|
//#region src/core/collect-stream.ts
|
|
597
731
|
async function collectStream(stream) {
|
|
@@ -937,7 +1071,7 @@ var AdapterBase = class {
|
|
|
937
1071
|
responseId: request.requestId,
|
|
938
1072
|
backend: {
|
|
939
1073
|
kind: this.kind,
|
|
940
|
-
isSynthetic:
|
|
1074
|
+
isSynthetic: this.capabilities.textStreaming === "synthetic"
|
|
941
1075
|
}
|
|
942
1076
|
});
|
|
943
1077
|
yield factory.responseStarted(request.model);
|
|
@@ -945,12 +1079,25 @@ var AdapterBase = class {
|
|
|
945
1079
|
const providerRequest = await this.buildRequest(request);
|
|
946
1080
|
yield* this.runStream(providerRequest, factory, request);
|
|
947
1081
|
} catch (err) {
|
|
948
|
-
if (err instanceof AIRequestError || err instanceof
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1082
|
+
if (err instanceof AIRequestError || err instanceof AIProviderError || err instanceof AIStreamError) throw err;
|
|
1083
|
+
if (err instanceof AIMappingError) {
|
|
1084
|
+
yield factory.responseWarning(err.message, "MAPPING_ERROR");
|
|
1085
|
+
const errorResp = this.buildResponse(request, {
|
|
1086
|
+
output: [],
|
|
1087
|
+
replay: []
|
|
1088
|
+
}, factory);
|
|
1089
|
+
yield factory.responseCompleted({
|
|
1090
|
+
replay: errorResp.replay,
|
|
1091
|
+
stopReason: errorResp.stopReason,
|
|
1092
|
+
trace: errorResp.backend,
|
|
1093
|
+
usage: errorResp.usage,
|
|
1094
|
+
billing: errorResp.billing,
|
|
1095
|
+
auxiliary: errorResp.auxiliary,
|
|
1096
|
+
warnings: errorResp.warnings
|
|
1097
|
+
});
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
throw err;
|
|
954
1101
|
}
|
|
955
1102
|
}
|
|
956
1103
|
/**
|
|
@@ -976,7 +1123,7 @@ var AdapterBase = class {
|
|
|
976
1123
|
requestId: request.requestId,
|
|
977
1124
|
rawResponseId: result.rawResponseId,
|
|
978
1125
|
adapter: this.kind,
|
|
979
|
-
isSyntheticStream:
|
|
1126
|
+
isSyntheticStream: this.capabilities.textStreaming === "synthetic",
|
|
980
1127
|
metadataSources: result.metadataSources,
|
|
981
1128
|
warnings
|
|
982
1129
|
}
|
|
@@ -990,18 +1137,6 @@ var AdapterBase = class {
|
|
|
990
1137
|
return new AdapterAuxiliaryState(request);
|
|
991
1138
|
}
|
|
992
1139
|
};
|
|
993
|
-
function mergeAuxiliary(base, patch) {
|
|
994
|
-
if (!base && !patch) return void 0;
|
|
995
|
-
const merged = {
|
|
996
|
-
...base,
|
|
997
|
-
...patch
|
|
998
|
-
};
|
|
999
|
-
if (base?.providerMetadata || patch?.providerMetadata) merged.providerMetadata = {
|
|
1000
|
-
...base?.providerMetadata,
|
|
1001
|
-
...patch?.providerMetadata
|
|
1002
|
-
};
|
|
1003
|
-
return merged;
|
|
1004
|
-
}
|
|
1005
1140
|
function mergeWarnings(...groups) {
|
|
1006
1141
|
const merged = [];
|
|
1007
1142
|
for (const group of groups) {
|
|
@@ -1011,6 +1146,91 @@ function mergeWarnings(...groups) {
|
|
|
1011
1146
|
return merged.length > 0 ? merged : void 0;
|
|
1012
1147
|
}
|
|
1013
1148
|
//#endregion
|
|
1149
|
+
//#region src/helpers/adapter-security.ts
|
|
1150
|
+
/**
|
|
1151
|
+
* Adapter 边界安全辅助
|
|
1152
|
+
*
|
|
1153
|
+
* - opaque replay 入站 envelope(大小 / 深度)
|
|
1154
|
+
* - provider HTTP 错误 body 出站脱敏
|
|
1155
|
+
*/
|
|
1156
|
+
const MAX_OPAQUE_PAYLOAD_BYTES = 65536;
|
|
1157
|
+
const MAX_OPAQUE_JSON_DEPTH = 8;
|
|
1158
|
+
const PROVIDER_ERROR_MESSAGE_MAX_LEN = 500;
|
|
1159
|
+
const PROVIDER_ERROR_RAW_BODY_THRESHOLD = 200;
|
|
1160
|
+
/** 测量 JSON 值嵌套深度(对象/数组);循环引用按已访问节点深度计。 */
|
|
1161
|
+
function measureJsonDepth(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1162
|
+
if (value === null || typeof value !== "object") return 0;
|
|
1163
|
+
if (seen.has(value)) return 0;
|
|
1164
|
+
seen.add(value);
|
|
1165
|
+
let maxChild = 0;
|
|
1166
|
+
if (Array.isArray(value)) for (const item of value) maxChild = Math.max(maxChild, measureJsonDepth(item, seen));
|
|
1167
|
+
else for (const key of Object.keys(value)) maxChild = Math.max(maxChild, measureJsonDepth(value[key], seen));
|
|
1168
|
+
return 1 + maxChild;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Opaque replay 通用 envelope:必须是 object、体积 ≤ 64KB、深度 ≤ 8。
|
|
1172
|
+
* 不校验 adapter 专用字段形状。
|
|
1173
|
+
*/
|
|
1174
|
+
function validateOpaqueReplayEnvelope(payload) {
|
|
1175
|
+
if (typeof payload !== "object" || payload === null) return {
|
|
1176
|
+
ok: false,
|
|
1177
|
+
reason: "payload must be an object"
|
|
1178
|
+
};
|
|
1179
|
+
let raw;
|
|
1180
|
+
try {
|
|
1181
|
+
raw = JSON.stringify(payload);
|
|
1182
|
+
} catch {
|
|
1183
|
+
return {
|
|
1184
|
+
ok: false,
|
|
1185
|
+
reason: "payload is not JSON-serializable"
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
if (raw === void 0) return {
|
|
1189
|
+
ok: false,
|
|
1190
|
+
reason: "payload is not JSON-serializable"
|
|
1191
|
+
};
|
|
1192
|
+
if (raw.length > 65536) return {
|
|
1193
|
+
ok: false,
|
|
1194
|
+
reason: `opaque payload exceeds max size (${raw.length} > ${MAX_OPAQUE_PAYLOAD_BYTES})`
|
|
1195
|
+
};
|
|
1196
|
+
const depth = measureJsonDepth(payload);
|
|
1197
|
+
if (depth > 8) return {
|
|
1198
|
+
ok: false,
|
|
1199
|
+
reason: `opaque payload nesting depth (${depth}) exceeds max (8)`
|
|
1200
|
+
};
|
|
1201
|
+
return { ok: true };
|
|
1202
|
+
}
|
|
1203
|
+
/** envelope 失败时抛 AIRequestError。 */
|
|
1204
|
+
function assertOpaqueReplayEnvelope(payload) {
|
|
1205
|
+
const result = validateOpaqueReplayEnvelope(payload);
|
|
1206
|
+
if (!result.ok) throw new AIRequestError(`Invalid opaque replay payload: ${result.reason}`, "INVALID_OPAQUE_REPLAY");
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* 从 provider HTTP 错误 body 提取可对外暴露的短消息,避免泄漏 HTML / 内部路径等。
|
|
1210
|
+
*/
|
|
1211
|
+
function extractProviderErrorMessage(body, status) {
|
|
1212
|
+
if (!body) return `HTTP ${status}`;
|
|
1213
|
+
try {
|
|
1214
|
+
const parsed = JSON.parse(body);
|
|
1215
|
+
if (parsed && typeof parsed === "object") {
|
|
1216
|
+
const record = parsed;
|
|
1217
|
+
const errorField = record.error;
|
|
1218
|
+
let msg;
|
|
1219
|
+
if (errorField && typeof errorField === "object" && errorField !== null) msg = errorField.message;
|
|
1220
|
+
if (typeof msg !== "string") msg = typeof errorField === "string" ? errorField : record.message;
|
|
1221
|
+
if (typeof msg === "string" && msg.length > 0) return msg.slice(0, 500);
|
|
1222
|
+
}
|
|
1223
|
+
} catch {}
|
|
1224
|
+
const trimmed = body.trimStart();
|
|
1225
|
+
if (trimmed.startsWith("<!") || trimmed.startsWith("<html") || body.length > 200) return `HTTP ${status}. Body omitted (${body.length} bytes)`;
|
|
1226
|
+
return body.slice(0, 500);
|
|
1227
|
+
}
|
|
1228
|
+
/** 统一构造脱敏后的 AIProviderError。 */
|
|
1229
|
+
function providerHttpError(status, body) {
|
|
1230
|
+
const safe = extractProviderErrorMessage(body, status);
|
|
1231
|
+
return new AIProviderError(`Provider returned ${status}: ${safe}`, "PROVIDER_ERROR", status, safe);
|
|
1232
|
+
}
|
|
1233
|
+
//#endregion
|
|
1014
1234
|
//#region src/helpers/usage-mapping.ts
|
|
1015
1235
|
function num(value) {
|
|
1016
1236
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
@@ -1090,43 +1310,55 @@ function usageFromOllama(raw) {
|
|
|
1090
1310
|
* - 未完成的行保留在 rest 中,等待下次 chunk 补全
|
|
1091
1311
|
* - 支持跨 chunk 的 event 分片
|
|
1092
1312
|
*/
|
|
1093
|
-
function parseSSEEvents(chunk) {
|
|
1313
|
+
function parseSSEEvents(chunk, options = {}) {
|
|
1094
1314
|
const events = [];
|
|
1095
1315
|
let eventType = "";
|
|
1096
1316
|
let dataLines = [];
|
|
1097
1317
|
let consumedUntil = 0;
|
|
1098
1318
|
let cursor = 0;
|
|
1099
1319
|
let malformedEvents = 0;
|
|
1320
|
+
const emitEvent = (consumedCursor) => {
|
|
1321
|
+
const dataStr = dataLines.join("\n");
|
|
1322
|
+
if (dataStr === "[DONE]") {
|
|
1323
|
+
eventType = "";
|
|
1324
|
+
dataLines = [];
|
|
1325
|
+
consumedUntil = consumedCursor;
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
try {
|
|
1329
|
+
const data = JSON.parse(dataStr);
|
|
1330
|
+
events.push({
|
|
1331
|
+
type: eventType,
|
|
1332
|
+
data
|
|
1333
|
+
});
|
|
1334
|
+
} catch {
|
|
1335
|
+
malformedEvents++;
|
|
1336
|
+
}
|
|
1337
|
+
eventType = "";
|
|
1338
|
+
dataLines = [];
|
|
1339
|
+
consumedUntil = consumedCursor;
|
|
1340
|
+
};
|
|
1341
|
+
const consumeLine = (line, consumedCursor) => {
|
|
1342
|
+
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
1343
|
+
else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
|
|
1344
|
+
else if (line === "" && eventType && dataLines.length > 0) emitEvent(consumedCursor);
|
|
1345
|
+
else if (line === "" && !eventType && dataLines.length === 0) consumedUntil = consumedCursor;
|
|
1346
|
+
};
|
|
1100
1347
|
while (cursor < chunk.length) {
|
|
1101
1348
|
const lineEnd = chunk.indexOf("\n", cursor);
|
|
1102
1349
|
if (lineEnd === -1) break;
|
|
1103
1350
|
let line = chunk.slice(cursor, lineEnd);
|
|
1104
1351
|
cursor = lineEnd + 1;
|
|
1105
1352
|
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
consumedUntil = cursor;
|
|
1114
|
-
continue;
|
|
1115
|
-
}
|
|
1116
|
-
try {
|
|
1117
|
-
const data = JSON.parse(dataStr);
|
|
1118
|
-
events.push({
|
|
1119
|
-
type: eventType,
|
|
1120
|
-
data
|
|
1121
|
-
});
|
|
1122
|
-
} catch {
|
|
1123
|
-
malformedEvents++;
|
|
1124
|
-
}
|
|
1125
|
-
eventType = "";
|
|
1126
|
-
dataLines = [];
|
|
1127
|
-
consumedUntil = cursor;
|
|
1128
|
-
} else if (line === "" && !eventType && dataLines.length === 0) consumedUntil = cursor;
|
|
1353
|
+
consumeLine(line, cursor);
|
|
1354
|
+
}
|
|
1355
|
+
if (options.allowEOF && cursor < chunk.length) {
|
|
1356
|
+
let line = chunk.slice(cursor);
|
|
1357
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
1358
|
+
consumeLine(line, chunk.length);
|
|
1359
|
+
cursor = chunk.length;
|
|
1129
1360
|
}
|
|
1361
|
+
if (options.allowEOF && eventType && dataLines.length > 0) emitEvent(chunk.length);
|
|
1130
1362
|
return {
|
|
1131
1363
|
events,
|
|
1132
1364
|
rest: chunk.slice(consumedUntil),
|
|
@@ -1134,70 +1366,287 @@ function parseSSEEvents(chunk) {
|
|
|
1134
1366
|
};
|
|
1135
1367
|
}
|
|
1136
1368
|
//#endregion
|
|
1137
|
-
//#region src/
|
|
1369
|
+
//#region src/helpers/synthetic-stream.ts
|
|
1138
1370
|
/**
|
|
1139
|
-
*
|
|
1371
|
+
* 模拟流式 (Synthetic Streaming)
|
|
1140
1372
|
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
* 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
|
|
1373
|
+
* 将一组已解析的 canonical OutputItem 包装为规范事件流。
|
|
1374
|
+
* 适用于非原生流式后端:adapter 拿到完整响应后,调用此函数
|
|
1375
|
+
* 即可产出一致的事件序列,无需自己逐事件组装。
|
|
1145
1376
|
*
|
|
1146
|
-
*
|
|
1377
|
+
* 约束:
|
|
1378
|
+
* - 每个 item 只发一块完整 delta(不模拟逐 token)
|
|
1379
|
+
* - 保持 item 边界
|
|
1380
|
+
* - 保持后端原始顺序
|
|
1381
|
+
* - 不发明 reasoning
|
|
1382
|
+
* - 不改写工具参数
|
|
1383
|
+
*/
|
|
1384
|
+
/**
|
|
1385
|
+
* 将已解析的 output items 包装为完整规范事件流。
|
|
1386
|
+
*
|
|
1387
|
+
* 用法示例(在 adapter 的 runStream 中):
|
|
1388
|
+
* ```ts
|
|
1389
|
+
* const result = parseNonStreamingResponse(data);
|
|
1390
|
+
* yield* syntheticStream({
|
|
1391
|
+
* model: request.model,
|
|
1392
|
+
* responseId: request.requestId,
|
|
1393
|
+
* backend: { kind: "chat-completions" },
|
|
1394
|
+
* output: result.output,
|
|
1395
|
+
* stopReason: result.stopReason,
|
|
1396
|
+
* usage: result.usage,
|
|
1397
|
+
* });
|
|
1398
|
+
* ```
|
|
1147
1399
|
*/
|
|
1148
|
-
function
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1400
|
+
async function* syntheticStream(options) {
|
|
1401
|
+
const { model, responseId, backend, output, replay, stopReason, usage, billing, providerMetadata, rawResponseId, warnings: extraWarnings } = options;
|
|
1402
|
+
const factory = createEventFactory({
|
|
1403
|
+
responseId,
|
|
1404
|
+
backend: {
|
|
1405
|
+
kind: backend.kind,
|
|
1406
|
+
isSynthetic: true
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
yield factory.responseStarted(model);
|
|
1410
|
+
for (const item of output) yield* emitItemEvents(item, factory);
|
|
1411
|
+
if (usage || billing) yield factory.responseAuxiliary({
|
|
1412
|
+
usage,
|
|
1413
|
+
billing
|
|
1414
|
+
});
|
|
1415
|
+
const finalReplay = replay ?? replayFromOutput(output);
|
|
1416
|
+
const allWarnings = [];
|
|
1417
|
+
allWarnings.push("Response is synthetically streamed; delta granularity may differ from native streaming");
|
|
1418
|
+
if (extraWarnings) allWarnings.push(...extraWarnings);
|
|
1419
|
+
yield factory.responseCompleted({
|
|
1420
|
+
replay: finalReplay,
|
|
1421
|
+
stopReason,
|
|
1422
|
+
usage,
|
|
1423
|
+
billing,
|
|
1424
|
+
auxiliary: providerMetadata ? { providerMetadata } : void 0,
|
|
1425
|
+
opaqueOutput: output.filter((item) => item.type === "opaque"),
|
|
1426
|
+
warnings: allWarnings.length > 0 ? allWarnings : void 0,
|
|
1427
|
+
trace: {
|
|
1428
|
+
requestId: responseId,
|
|
1429
|
+
rawResponseId,
|
|
1430
|
+
adapter: backend.kind,
|
|
1431
|
+
isSyntheticStream: true,
|
|
1432
|
+
warnings: allWarnings.length > 0 ? allWarnings : void 0
|
|
1433
|
+
}
|
|
1160
1434
|
});
|
|
1161
1435
|
}
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1436
|
+
function* emitItemEvents(item, factory) {
|
|
1437
|
+
switch (item.type) {
|
|
1438
|
+
case "message":
|
|
1439
|
+
yield* emitMessageEvents(item, factory);
|
|
1440
|
+
break;
|
|
1441
|
+
case "reasoning":
|
|
1442
|
+
yield* emitReasoningEvents(item, factory);
|
|
1443
|
+
break;
|
|
1444
|
+
case "tool_call":
|
|
1445
|
+
yield* emitToolCallEvents(item, factory);
|
|
1446
|
+
break;
|
|
1447
|
+
case "opaque": break;
|
|
1448
|
+
}
|
|
1164
1449
|
}
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1450
|
+
function* emitMessageEvents(item, factory) {
|
|
1451
|
+
const id = item.id ?? `syn-msg-${crypto.randomUUID()}`;
|
|
1452
|
+
yield factory.messageStarted(id);
|
|
1453
|
+
for (const block of item.content) yield factory.messageDelta(id, block);
|
|
1454
|
+
yield factory.messageCompleted(id);
|
|
1167
1455
|
}
|
|
1168
|
-
function
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
malformedEvents: result.malformedEvents
|
|
1174
|
-
};
|
|
1456
|
+
function* emitReasoningEvents(item, factory) {
|
|
1457
|
+
const id = item.id ?? `syn-reason-${crypto.randomUUID()}`;
|
|
1458
|
+
yield factory.reasoningStarted(id, item.visibility);
|
|
1459
|
+
for (const block of item.content) yield factory.reasoningDelta(id, block);
|
|
1460
|
+
yield factory.reasoningCompleted(id);
|
|
1175
1461
|
}
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1462
|
+
function* emitToolCallEvents(item, factory) {
|
|
1463
|
+
yield factory.toolCallStarted(item.id, item.name);
|
|
1464
|
+
if (item.argumentsText) yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
|
|
1465
|
+
yield factory.toolCallCompleted(item.id);
|
|
1178
1466
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1467
|
+
//#endregion
|
|
1468
|
+
//#region src/helpers/incremental-stream-parser.ts
|
|
1469
|
+
var IncrementalStreamParser = class {
|
|
1470
|
+
split;
|
|
1471
|
+
parse;
|
|
1472
|
+
buffer = "";
|
|
1473
|
+
decoder = new TextDecoder();
|
|
1474
|
+
constructor(split, parse) {
|
|
1475
|
+
this.split = split;
|
|
1476
|
+
this.parse = parse;
|
|
1477
|
+
}
|
|
1478
|
+
feed(value) {
|
|
1479
|
+
this.buffer += this.decoder.decode(value, { stream: true });
|
|
1480
|
+
return this.consume(false);
|
|
1481
|
+
}
|
|
1482
|
+
flush() {
|
|
1483
|
+
this.buffer += this.decoder.decode();
|
|
1484
|
+
return this.consume(true);
|
|
1485
|
+
}
|
|
1486
|
+
getRemaining() {
|
|
1487
|
+
return this.buffer;
|
|
1488
|
+
}
|
|
1489
|
+
consume(allowEOF) {
|
|
1490
|
+
const split = this.split(this.buffer, allowEOF);
|
|
1491
|
+
this.buffer = split.rest;
|
|
1492
|
+
const items = [];
|
|
1493
|
+
let malformed = 0;
|
|
1494
|
+
for (const rawItem of split.items) {
|
|
1495
|
+
const result = this.parse(rawItem);
|
|
1496
|
+
if (result.status === "parsed") items.push(result.value);
|
|
1497
|
+
else if (result.status === "malformed") malformed++;
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
items,
|
|
1501
|
+
malformed
|
|
1502
|
+
};
|
|
1184
1503
|
}
|
|
1185
|
-
}
|
|
1186
|
-
function
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1504
|
+
};
|
|
1505
|
+
function splitLines(buffer, allowEOF) {
|
|
1506
|
+
const items = [];
|
|
1507
|
+
let cursor = 0;
|
|
1508
|
+
while (true) {
|
|
1509
|
+
const lineEnd = buffer.indexOf("\n", cursor);
|
|
1510
|
+
if (lineEnd === -1) break;
|
|
1511
|
+
items.push(buffer.slice(cursor, lineEnd));
|
|
1512
|
+
cursor = lineEnd + 1;
|
|
1513
|
+
}
|
|
1514
|
+
if (allowEOF && cursor < buffer.length) {
|
|
1515
|
+
items.push(buffer.slice(cursor));
|
|
1516
|
+
cursor = buffer.length;
|
|
1517
|
+
}
|
|
1518
|
+
return {
|
|
1519
|
+
items,
|
|
1520
|
+
rest: buffer.slice(cursor)
|
|
1190
1521
|
};
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1522
|
+
}
|
|
1523
|
+
function splitSSEFrames(buffer, allowEOF) {
|
|
1524
|
+
const normalized = buffer.replaceAll("\r\n", "\n");
|
|
1525
|
+
const items = [];
|
|
1526
|
+
let cursor = 0;
|
|
1527
|
+
while (true) {
|
|
1528
|
+
const frameEnd = normalized.indexOf("\n\n", cursor);
|
|
1529
|
+
if (frameEnd === -1) break;
|
|
1530
|
+
items.push(normalized.slice(cursor, frameEnd));
|
|
1531
|
+
cursor = frameEnd + 2;
|
|
1532
|
+
}
|
|
1533
|
+
if (allowEOF && cursor < normalized.length) {
|
|
1534
|
+
items.push(normalized.slice(cursor));
|
|
1535
|
+
cursor = normalized.length;
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
items,
|
|
1539
|
+
rest: normalized.slice(cursor)
|
|
1194
1540
|
};
|
|
1195
|
-
throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
1196
1541
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1542
|
+
//#endregion
|
|
1543
|
+
//#region src/helpers/request-mapper.ts
|
|
1544
|
+
var NormalizedRequestMapper = class {
|
|
1545
|
+
profile;
|
|
1546
|
+
constructor(profile) {
|
|
1547
|
+
this.profile = profile;
|
|
1548
|
+
}
|
|
1549
|
+
mapInstructions(instructions) {
|
|
1550
|
+
return typeof instructions === "string" ? instructions : contentBlocksToText(this.ensureTextBlocks(instructions, "instructions"));
|
|
1551
|
+
}
|
|
1552
|
+
ensureTextBlocks(blocks, field) {
|
|
1553
|
+
return this.ensureBlocks(blocks, field, this.profile.supportedBlockTypes, "only text/json blocks are supported");
|
|
1554
|
+
}
|
|
1555
|
+
ensureReasoningBlocks(blocks, field) {
|
|
1556
|
+
return this.ensureBlocks(blocks, field, this.profile.reasoningBlockTypes, "reasoning only supports text blocks");
|
|
1557
|
+
}
|
|
1558
|
+
assertToolResultOutcome(outcome) {
|
|
1559
|
+
if (this.profile.capabilities.toolResultOutcomes.includes(outcome)) return;
|
|
1560
|
+
const outcomes = this.profile.capabilities.toolResultOutcomes;
|
|
1561
|
+
const supported = outcomes.map((value) => `"${value}"`).join(" and ");
|
|
1562
|
+
const verb = outcomes.length > 1 ? "are" : "is";
|
|
1563
|
+
throw new AIRequestError(`${this.profile.kind} does not preserve tool_result outcome "${outcome}"; only ${supported} ${verb} supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
|
|
1564
|
+
}
|
|
1565
|
+
rollbackTrailingAssistantMessages(messages) {
|
|
1566
|
+
while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
|
|
1567
|
+
}
|
|
1568
|
+
ensureBlocks(blocks, field, supportedTypes, description) {
|
|
1569
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
1570
|
+
const block = blocks[i];
|
|
1571
|
+
if (block && !supportedTypes.includes(block.type)) throw new AIRequestError(`${this.profile.kind} does not support ${field}[${i}] of type "${block.type}"; ${description}`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
1572
|
+
}
|
|
1573
|
+
return blocks;
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
//#endregion
|
|
1577
|
+
//#region src/adapters/responses.ts
|
|
1578
|
+
/**
|
|
1579
|
+
* Responses Adapter
|
|
1580
|
+
*
|
|
1581
|
+
* 接入 OpenAI Responses API (responses 端点)。
|
|
1582
|
+
* 职责分层:
|
|
1583
|
+
* 1. buildRequest — 将 NormalizedRequest 转换为 Responses API 请求
|
|
1584
|
+
* 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
|
|
1585
|
+
*
|
|
1586
|
+
* 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
|
|
1587
|
+
*/
|
|
1588
|
+
const profile$3 = {
|
|
1589
|
+
kind: "responses",
|
|
1590
|
+
instructionsMode: "instructions_field",
|
|
1591
|
+
supportedBlockTypes: ["text", "json"],
|
|
1592
|
+
reasoningBlockTypes: ["text"],
|
|
1593
|
+
capabilities: {
|
|
1594
|
+
textStreaming: "native",
|
|
1595
|
+
reasoningStreaming: "native",
|
|
1596
|
+
toolCallStreaming: "native",
|
|
1597
|
+
replay: "opaque",
|
|
1598
|
+
usage: "final",
|
|
1599
|
+
toolResultOutcomes: ["success"]
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
const mapper$3 = new NormalizedRequestMapper(profile$3);
|
|
1603
|
+
/** 已处理或可安全忽略的 Responses SSE 类型(未知类型会 warning 一次)。 */
|
|
1604
|
+
const KNOWN_RESPONSES_SSE_TYPES = /* @__PURE__ */ new Set([
|
|
1605
|
+
"response.output_item.added",
|
|
1606
|
+
"response.output_item.done",
|
|
1607
|
+
"response.output_text.delta",
|
|
1608
|
+
"response.output_text.done",
|
|
1609
|
+
"response.reasoning.delta",
|
|
1610
|
+
"response.reasoning.done",
|
|
1611
|
+
"response.tool_call.delta",
|
|
1612
|
+
"response.tool_call.done",
|
|
1613
|
+
"response.function_call_arguments.delta",
|
|
1614
|
+
"response.function_call_arguments.done",
|
|
1615
|
+
"response.content_part.added",
|
|
1616
|
+
"response.content_part.done",
|
|
1617
|
+
"response.refusal.delta",
|
|
1618
|
+
"response.refusal.done",
|
|
1619
|
+
"response.in_progress",
|
|
1620
|
+
"response.created",
|
|
1621
|
+
"response.completed",
|
|
1622
|
+
"response.failed",
|
|
1623
|
+
"response.incomplete",
|
|
1624
|
+
"error"
|
|
1625
|
+
]);
|
|
1626
|
+
function isReplayCanonicalInput(item) {
|
|
1627
|
+
return item.type === "message" && item.role === "assistant" || item.type === "reasoning" || item.type === "function_call";
|
|
1628
|
+
}
|
|
1629
|
+
function hasReplayCanonicalInput(input) {
|
|
1630
|
+
return input.some(isReplayCanonicalInput);
|
|
1631
|
+
}
|
|
1632
|
+
function extractFailureMessage(response) {
|
|
1633
|
+
return response.error?.message ?? response.failure?.message ?? "unknown";
|
|
1634
|
+
}
|
|
1635
|
+
function canonicalToResponsesBlock(b) {
|
|
1636
|
+
if (b.type === "text") return {
|
|
1637
|
+
type: "text",
|
|
1638
|
+
text: b.text
|
|
1639
|
+
};
|
|
1640
|
+
if (b.type === "json") return {
|
|
1641
|
+
type: "text",
|
|
1642
|
+
text: JSON.stringify(b.json)
|
|
1643
|
+
};
|
|
1644
|
+
throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
1645
|
+
}
|
|
1646
|
+
var ResponsesAdapter = class extends AdapterBase {
|
|
1647
|
+
kind = "responses";
|
|
1648
|
+
capabilities = profile$3.capabilities;
|
|
1649
|
+
apiKey;
|
|
1201
1650
|
baseUrl;
|
|
1202
1651
|
fetchFn;
|
|
1203
1652
|
constructor(options) {
|
|
@@ -1211,7 +1660,7 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1211
1660
|
for (const item of request.input) switch (item.type) {
|
|
1212
1661
|
case "message":
|
|
1213
1662
|
if (item.role === "assistant") {
|
|
1214
|
-
const blocks =
|
|
1663
|
+
const blocks = mapper$3.ensureTextBlocks(item.content, `assistant message (${item.role}) content`).map(canonicalToResponsesBlock);
|
|
1215
1664
|
input.push({
|
|
1216
1665
|
type: "message",
|
|
1217
1666
|
role: item.role,
|
|
@@ -1220,11 +1669,11 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1220
1669
|
} else input.push({
|
|
1221
1670
|
type: "message",
|
|
1222
1671
|
role: item.role,
|
|
1223
|
-
content: contentBlocksToText(
|
|
1672
|
+
content: contentBlocksToText(mapper$3.ensureTextBlocks(item.content, `input message (${item.role}) content`))
|
|
1224
1673
|
});
|
|
1225
1674
|
break;
|
|
1226
1675
|
case "reasoning": {
|
|
1227
|
-
const blocks =
|
|
1676
|
+
const blocks = mapper$3.ensureReasoningBlocks(item.content, "reasoning content").map((b) => ({
|
|
1228
1677
|
type: "reasoning",
|
|
1229
1678
|
text: b.text
|
|
1230
1679
|
}));
|
|
@@ -1243,8 +1692,8 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1243
1692
|
});
|
|
1244
1693
|
break;
|
|
1245
1694
|
case "tool_result": {
|
|
1246
|
-
|
|
1247
|
-
const output =
|
|
1695
|
+
mapper$3.assertToolResultOutcome(item.outcome);
|
|
1696
|
+
const output = mapper$3.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
|
|
1248
1697
|
input.push({
|
|
1249
1698
|
type: "function_call_output",
|
|
1250
1699
|
call_id: item.callId,
|
|
@@ -1252,25 +1701,26 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1252
1701
|
});
|
|
1253
1702
|
break;
|
|
1254
1703
|
}
|
|
1255
|
-
case "opaque":
|
|
1256
|
-
if (item.source
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
}
|
|
1704
|
+
case "opaque": {
|
|
1705
|
+
if (item.source !== "responses" || item.purpose !== "replay") break;
|
|
1706
|
+
assertOpaqueReplayEnvelope(item.payload);
|
|
1707
|
+
const payload = item.payload;
|
|
1708
|
+
if ("id" in payload) {
|
|
1709
|
+
if (typeof payload.id !== "string" || payload.id.length === 0 || payload.id.length > 256) throw new AIRequestError("Invalid opaque replay payload: id must be a non-empty string (max 256)", "INVALID_OPAQUE_REPLAY");
|
|
1710
|
+
if (!hasReplayCanonicalInput(input)) input.push({
|
|
1711
|
+
type: "item_reference",
|
|
1712
|
+
id: payload.id
|
|
1713
|
+
});
|
|
1265
1714
|
}
|
|
1266
1715
|
break;
|
|
1716
|
+
}
|
|
1267
1717
|
}
|
|
1268
1718
|
const body = {
|
|
1269
1719
|
model: request.model,
|
|
1270
1720
|
input,
|
|
1271
1721
|
stream: true
|
|
1272
1722
|
};
|
|
1273
|
-
if (request.instructions) body.instructions =
|
|
1723
|
+
if (request.instructions) body.instructions = mapper$3.mapInstructions(request.instructions);
|
|
1274
1724
|
if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
|
|
1275
1725
|
type: "function",
|
|
1276
1726
|
name: t.name,
|
|
@@ -1292,31 +1742,59 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1292
1742
|
}
|
|
1293
1743
|
async *runStream(providerRequest, factory, request) {
|
|
1294
1744
|
const auxiliary = this.createAuxiliaryState(request);
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1745
|
+
let response;
|
|
1746
|
+
try {
|
|
1747
|
+
response = await this.fetchFn(`${this.baseUrl}/responses`, {
|
|
1748
|
+
method: "POST",
|
|
1749
|
+
headers: {
|
|
1750
|
+
"Content-Type": "application/json",
|
|
1751
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1752
|
+
},
|
|
1753
|
+
body: JSON.stringify(providerRequest)
|
|
1754
|
+
});
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
1757
|
+
}
|
|
1303
1758
|
if (!response.ok) {
|
|
1304
|
-
const
|
|
1305
|
-
throw
|
|
1759
|
+
const errorBody = await response.text().catch(() => "");
|
|
1760
|
+
throw providerHttpError(response.status, errorBody);
|
|
1306
1761
|
}
|
|
1307
1762
|
const reader = response.body?.getReader();
|
|
1308
|
-
if (!reader) throw new
|
|
1763
|
+
if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
1764
|
+
const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
|
|
1765
|
+
let eventType = "";
|
|
1766
|
+
let dataStr = "";
|
|
1767
|
+
for (const rawLine of frame.split("\n")) {
|
|
1768
|
+
const line = rawLine.trim();
|
|
1769
|
+
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
1770
|
+
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
1771
|
+
}
|
|
1772
|
+
if (!eventType) return { status: "ignored" };
|
|
1773
|
+
try {
|
|
1774
|
+
const data = JSON.parse(dataStr);
|
|
1775
|
+
return {
|
|
1776
|
+
status: "parsed",
|
|
1777
|
+
value: {
|
|
1778
|
+
type: eventType,
|
|
1779
|
+
data
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
} catch {
|
|
1783
|
+
return { status: "malformed" };
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1309
1786
|
const output = [];
|
|
1310
|
-
|
|
1311
|
-
let buffer = "";
|
|
1787
|
+
let streamDone = false;
|
|
1312
1788
|
let completedResponse;
|
|
1789
|
+
let completedEmitted = false;
|
|
1790
|
+
let unknownEventsWarned = false;
|
|
1791
|
+
const messageItemsWithDelta = /* @__PURE__ */ new Set();
|
|
1313
1792
|
try {
|
|
1314
1793
|
while (true) {
|
|
1315
|
-
const { done, value } = await reader.read()
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
const { events,
|
|
1319
|
-
buffer = rest;
|
|
1794
|
+
const { done, value } = await reader.read().catch((err) => {
|
|
1795
|
+
throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
|
|
1796
|
+
});
|
|
1797
|
+
const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
|
|
1320
1798
|
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
1321
1799
|
count: malformedEvents,
|
|
1322
1800
|
providerLabel: "Responses",
|
|
@@ -1325,7 +1803,8 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1325
1803
|
if (malformedWarning) yield malformedWarning;
|
|
1326
1804
|
for (const sseEvent of events) {
|
|
1327
1805
|
if (sseEvent.type === "error") {
|
|
1328
|
-
|
|
1806
|
+
const data = sseEvent.data;
|
|
1807
|
+
yield factory.responseWarning(data.message ?? "Provider error event", data.code);
|
|
1329
1808
|
continue;
|
|
1330
1809
|
}
|
|
1331
1810
|
if (sseEvent.type === "response.output_item.added") {
|
|
@@ -1344,40 +1823,69 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1344
1823
|
continue;
|
|
1345
1824
|
}
|
|
1346
1825
|
if (sseEvent.type === "response.output_text.delta") {
|
|
1347
|
-
|
|
1826
|
+
const data = sseEvent.data;
|
|
1827
|
+
yield factory.messageDelta(data.item_id, textBlock(data.delta));
|
|
1828
|
+
messageItemsWithDelta.add(data.item_id);
|
|
1348
1829
|
continue;
|
|
1349
1830
|
}
|
|
1350
1831
|
if (sseEvent.type === "response.output_text.done") {
|
|
1351
|
-
|
|
1352
|
-
|
|
1832
|
+
const data = sseEvent.data;
|
|
1833
|
+
if (!messageItemsWithDelta.has(data.item_id) && data.text) yield factory.messageDelta(data.item_id, textBlock(data.text));
|
|
1834
|
+
yield factory.messageCompleted(data.item_id);
|
|
1835
|
+
output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
|
|
1353
1836
|
continue;
|
|
1354
1837
|
}
|
|
1355
1838
|
if (sseEvent.type === "response.reasoning.delta") {
|
|
1356
|
-
|
|
1839
|
+
const data = sseEvent.data;
|
|
1840
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
1357
1841
|
continue;
|
|
1358
1842
|
}
|
|
1359
1843
|
if (sseEvent.type === "response.reasoning.done") {
|
|
1360
|
-
|
|
1361
|
-
|
|
1844
|
+
const data = sseEvent.data;
|
|
1845
|
+
yield factory.reasoningCompleted(data.item_id);
|
|
1846
|
+
output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
|
|
1362
1847
|
continue;
|
|
1363
1848
|
}
|
|
1364
1849
|
if (sseEvent.type === "response.tool_call.delta") {
|
|
1365
|
-
|
|
1850
|
+
const data = sseEvent.data;
|
|
1851
|
+
if (data.delta.arguments) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta.arguments });
|
|
1366
1852
|
continue;
|
|
1367
1853
|
}
|
|
1368
1854
|
if (sseEvent.type === "response.tool_call.done") {
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1855
|
+
const data = sseEvent.data;
|
|
1856
|
+
const tcItem = toolCallItem(data.item_id, data.name ?? "unknown", data.arguments ?? "");
|
|
1857
|
+
yield factory.toolCallCompleted(data.item_id);
|
|
1371
1858
|
output.push(tcItem);
|
|
1372
1859
|
continue;
|
|
1373
1860
|
}
|
|
1374
|
-
if (sseEvent.type === "response.completed"
|
|
1861
|
+
if (sseEvent.type === "response.completed" || sseEvent.type === "response.failed" || sseEvent.type === "response.incomplete") {
|
|
1862
|
+
const data = sseEvent.data;
|
|
1863
|
+
if (completedResponse) {
|
|
1864
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
completedResponse = data.response;
|
|
1868
|
+
if (sseEvent.type === "response.failed") yield factory.responseWarning(`Response failed: ${extractFailureMessage(data.response)}`, "PROVIDER_FAILURE");
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
|
|
1872
|
+
unknownEventsWarned = true;
|
|
1873
|
+
yield factory.responseWarning(`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`, "UNKNOWN_PROVIDER_EVENT");
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (done) {
|
|
1877
|
+
streamDone = true;
|
|
1878
|
+
break;
|
|
1375
1879
|
}
|
|
1376
1880
|
}
|
|
1377
1881
|
} finally {
|
|
1378
|
-
|
|
1882
|
+
try {
|
|
1883
|
+
if (!streamDone) await reader.cancel().catch(() => void 0);
|
|
1884
|
+
} finally {
|
|
1885
|
+
reader.releaseLock();
|
|
1886
|
+
}
|
|
1379
1887
|
}
|
|
1380
|
-
if (
|
|
1888
|
+
if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
|
|
1381
1889
|
let rawResponseId;
|
|
1382
1890
|
if (completedResponse) {
|
|
1383
1891
|
rawResponseId = completedResponse.id;
|
|
@@ -1388,23 +1896,44 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1388
1896
|
const stopReason = completedResponse ? this.inferStopReason(completedResponse) : void 0;
|
|
1389
1897
|
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
1390
1898
|
for (const event of auxiliaryResult.events) yield event;
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1899
|
+
if (!completedEmitted) {
|
|
1900
|
+
completedEmitted = true;
|
|
1901
|
+
const finalResponse = this.buildResponse(request, {
|
|
1902
|
+
output,
|
|
1903
|
+
replay,
|
|
1904
|
+
stopReason,
|
|
1905
|
+
usage: auxiliaryResult.usage,
|
|
1906
|
+
billing: auxiliaryResult.billing,
|
|
1907
|
+
auxiliary: auxiliaryResult.auxiliary,
|
|
1908
|
+
warnings: auxiliaryResult.warnings,
|
|
1909
|
+
metadataSources: auxiliaryResult.metadataSources,
|
|
1910
|
+
rawResponseId
|
|
1911
|
+
}, factory);
|
|
1912
|
+
yield factory.responseCompleted({
|
|
1913
|
+
replay: finalResponse.replay,
|
|
1914
|
+
stopReason: finalResponse.stopReason,
|
|
1915
|
+
trace: finalResponse.backend,
|
|
1916
|
+
usage: finalResponse.usage,
|
|
1917
|
+
billing: finalResponse.billing,
|
|
1918
|
+
auxiliary: finalResponse.auxiliary,
|
|
1919
|
+
warnings: finalResponse.warnings
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1402
1922
|
}
|
|
1403
1923
|
inferStopReason(response) {
|
|
1924
|
+
if (response.status === "failed") return "error";
|
|
1925
|
+
if (response.status === "incomplete") {
|
|
1926
|
+
const reason = response.incomplete_details?.reason;
|
|
1927
|
+
if (reason === "content_filter") return "content_filter";
|
|
1928
|
+
if (reason === "max_output_tokens") return "max_output_tokens";
|
|
1929
|
+
return "max_output_tokens";
|
|
1930
|
+
}
|
|
1404
1931
|
const output = response.output;
|
|
1405
|
-
if (!output || output.length === 0) return "unknown";
|
|
1932
|
+
if (!output || output.length === 0) return response.status === "completed" ? "end_turn" : "unknown";
|
|
1406
1933
|
if (output.some((item) => item.type === "function_call")) return "tool_call";
|
|
1407
|
-
|
|
1934
|
+
const lastItem = output[output.length - 1];
|
|
1935
|
+
if (lastItem?.status === "failed") return "error";
|
|
1936
|
+
if (lastItem?.status === "incomplete") return "max_output_tokens";
|
|
1408
1937
|
return "end_turn";
|
|
1409
1938
|
}
|
|
1410
1939
|
};
|
|
@@ -1421,36 +1950,41 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1421
1950
|
* - 高保真 replay(含 opaque continuation)
|
|
1422
1951
|
* - 能力降级 warning
|
|
1423
1952
|
*/
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1953
|
+
const profile$2 = {
|
|
1954
|
+
kind: "messages",
|
|
1955
|
+
instructionsMode: "system_message",
|
|
1956
|
+
supportedBlockTypes: ["text", "json"],
|
|
1957
|
+
reasoningBlockTypes: ["text"],
|
|
1958
|
+
capabilities: {
|
|
1959
|
+
textStreaming: "native",
|
|
1960
|
+
reasoningStreaming: "native",
|
|
1961
|
+
toolCallStreaming: "synthetic",
|
|
1962
|
+
replay: "opaque",
|
|
1963
|
+
usage: "stream",
|
|
1964
|
+
toolResultOutcomes: ["success", "error"]
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
const mapper$2 = new NormalizedRequestMapper(profile$2);
|
|
1968
|
+
function isMessagesReplayContentBlock(value) {
|
|
1969
|
+
if (!value || typeof value !== "object" || !("type" in value)) return false;
|
|
1970
|
+
const block = value;
|
|
1971
|
+
switch (block.type) {
|
|
1972
|
+
case "text": return typeof block.text === "string";
|
|
1973
|
+
case "thinking": return typeof block.thinking === "string" && (block.signature === void 0 || typeof block.signature === "string");
|
|
1974
|
+
case "redacted_thinking": return typeof block.data === "string";
|
|
1975
|
+
case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && !!block.input && typeof block.input === "object" && !Array.isArray(block.input);
|
|
1976
|
+
case "tool_result":
|
|
1977
|
+
if (typeof block.tool_use_id !== "string") return false;
|
|
1978
|
+
if (block.is_error !== void 0 && typeof block.is_error !== "boolean") return false;
|
|
1979
|
+
if (typeof block.content === "string") return true;
|
|
1980
|
+
if (!Array.isArray(block.content)) return false;
|
|
1981
|
+
return block.content.every(isMessagesReplayContentBlock);
|
|
1982
|
+
default: return false;
|
|
1983
|
+
}
|
|
1451
1984
|
}
|
|
1452
|
-
function
|
|
1453
|
-
|
|
1985
|
+
function assertMessagesReplayContent(content) {
|
|
1986
|
+
if (!Array.isArray(content)) throw new AIRequestError("Invalid opaque replay payload: content must be an array", "INVALID_OPAQUE_REPLAY");
|
|
1987
|
+
for (let i = 0; i < content.length; i++) if (!isMessagesReplayContentBlock(content[i])) throw new AIRequestError(`Invalid opaque replay payload: content[${i}] is not a valid Messages content block`, "INVALID_OPAQUE_REPLAY");
|
|
1454
1988
|
}
|
|
1455
1989
|
/** 用 response 级别的命名空间合成 content block 的 item ID,避免多轮工具循环 ID 碰撞 */
|
|
1456
1990
|
function synthesizeItemId(kind, blockIndex, responseId) {
|
|
@@ -1500,34 +2034,29 @@ function buildStreamMetadata(options) {
|
|
|
1500
2034
|
}
|
|
1501
2035
|
var MessagesAdapter = class extends AdapterBase {
|
|
1502
2036
|
kind = "messages";
|
|
1503
|
-
|
|
2037
|
+
capabilities = profile$2.capabilities;
|
|
1504
2038
|
apiKey;
|
|
1505
2039
|
apiVersion;
|
|
1506
2040
|
baseUrl;
|
|
1507
2041
|
fetchFn;
|
|
1508
|
-
warningAccumulator;
|
|
1509
2042
|
constructor(options) {
|
|
1510
2043
|
super();
|
|
1511
2044
|
this.apiKey = options.apiKey;
|
|
1512
2045
|
this.apiVersion = options.apiVersion ?? "2023-06-01";
|
|
1513
2046
|
this.baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
1514
2047
|
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
1515
|
-
this.warningAccumulator = [];
|
|
1516
|
-
}
|
|
1517
|
-
warn(message, _code) {
|
|
1518
|
-
this.warningAccumulator.push(message);
|
|
1519
2048
|
}
|
|
1520
2049
|
buildRequest(request) {
|
|
1521
2050
|
const messages = [];
|
|
1522
2051
|
let systemPrompt;
|
|
1523
2052
|
let pendingToolResultMessage;
|
|
1524
|
-
if (request.instructions) systemPrompt =
|
|
2053
|
+
if (request.instructions) systemPrompt = mapper$2.mapInstructions(request.instructions);
|
|
1525
2054
|
for (const item of request.input) {
|
|
1526
2055
|
if (item.type !== "tool_result") pendingToolResultMessage = void 0;
|
|
1527
2056
|
switch (item.type) {
|
|
1528
2057
|
case "message": {
|
|
1529
2058
|
const role = item.role === "user" ? "user" : "assistant";
|
|
1530
|
-
const supportedContent =
|
|
2059
|
+
const supportedContent = mapper$2.ensureTextBlocks(item.content, `input message (${item.role}) content`);
|
|
1531
2060
|
if (supportedContent.length === 1 && supportedContent[0]?.type === "text") messages.push({
|
|
1532
2061
|
role,
|
|
1533
2062
|
content: supportedContent[0].text
|
|
@@ -1554,8 +2083,8 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1554
2083
|
break;
|
|
1555
2084
|
}
|
|
1556
2085
|
case "tool_result": {
|
|
1557
|
-
|
|
1558
|
-
const content =
|
|
2086
|
+
mapper$2.assertToolResultOutcome(item.outcome);
|
|
2087
|
+
const content = mapper$2.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
|
|
1559
2088
|
const block = {
|
|
1560
2089
|
type: "tool_result",
|
|
1561
2090
|
tool_use_id: item.callId,
|
|
@@ -1575,7 +2104,7 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1575
2104
|
case "reasoning": {
|
|
1576
2105
|
const block = {
|
|
1577
2106
|
type: "thinking",
|
|
1578
|
-
thinking: contentBlocksToText(
|
|
2107
|
+
thinking: contentBlocksToText(mapper$2.ensureReasoningBlocks(item.content, "reasoning content"))
|
|
1579
2108
|
};
|
|
1580
2109
|
const lastMsg = messages[messages.length - 1];
|
|
1581
2110
|
if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(block);
|
|
@@ -1585,20 +2114,20 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1585
2114
|
});
|
|
1586
2115
|
break;
|
|
1587
2116
|
}
|
|
1588
|
-
case "opaque":
|
|
1589
|
-
if (item.purpose
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
}
|
|
2117
|
+
case "opaque": {
|
|
2118
|
+
if (item.purpose !== "replay") break;
|
|
2119
|
+
assertOpaqueReplayEnvelope(item.payload);
|
|
2120
|
+
const payload = item.payload;
|
|
2121
|
+
if (payload.role === "assistant" && "content" in payload) {
|
|
2122
|
+
assertMessagesReplayContent(payload.content);
|
|
2123
|
+
mapper$2.rollbackTrailingAssistantMessages(messages);
|
|
2124
|
+
messages.push({
|
|
2125
|
+
role: "assistant",
|
|
2126
|
+
content: payload.content
|
|
2127
|
+
});
|
|
1600
2128
|
}
|
|
1601
2129
|
break;
|
|
2130
|
+
}
|
|
1602
2131
|
}
|
|
1603
2132
|
}
|
|
1604
2133
|
const body = {
|
|
@@ -1625,27 +2154,53 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1625
2154
|
return body;
|
|
1626
2155
|
}
|
|
1627
2156
|
async *runStream(providerRequest, factory, request) {
|
|
1628
|
-
this.warningAccumulator = [];
|
|
1629
2157
|
const auxiliary = this.createAuxiliaryState(request);
|
|
2158
|
+
let completedEmitted = false;
|
|
1630
2159
|
if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Messages adapter", "UNSUPPORTED_METADATA");
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
2160
|
+
let response;
|
|
2161
|
+
try {
|
|
2162
|
+
response = await this.fetchFn(`${this.baseUrl}/messages`, {
|
|
2163
|
+
method: "POST",
|
|
2164
|
+
headers: {
|
|
2165
|
+
"Content-Type": "application/json",
|
|
2166
|
+
"x-api-key": this.apiKey,
|
|
2167
|
+
"anthropic-version": this.apiVersion
|
|
2168
|
+
},
|
|
2169
|
+
body: JSON.stringify(providerRequest)
|
|
2170
|
+
});
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
2173
|
+
}
|
|
1640
2174
|
if (!response.ok) {
|
|
1641
|
-
const
|
|
1642
|
-
throw
|
|
2175
|
+
const errorBody = await response.text().catch(() => "");
|
|
2176
|
+
throw providerHttpError(response.status, errorBody);
|
|
1643
2177
|
}
|
|
1644
2178
|
const reader = response.body?.getReader();
|
|
1645
|
-
if (!reader) throw new
|
|
2179
|
+
if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
2180
|
+
const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
|
|
2181
|
+
let eventType = "";
|
|
2182
|
+
let dataStr = "";
|
|
2183
|
+
for (const rawLine of frame.split("\n")) {
|
|
2184
|
+
const line = rawLine.trim();
|
|
2185
|
+
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
2186
|
+
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
2187
|
+
}
|
|
2188
|
+
if (!eventType) return { status: "ignored" };
|
|
2189
|
+
try {
|
|
2190
|
+
const data = JSON.parse(dataStr);
|
|
2191
|
+
return {
|
|
2192
|
+
status: "parsed",
|
|
2193
|
+
value: {
|
|
2194
|
+
type: eventType,
|
|
2195
|
+
data
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
} catch {
|
|
2199
|
+
return { status: "malformed" };
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
1646
2202
|
const output = [];
|
|
1647
|
-
|
|
1648
|
-
let buffer = "";
|
|
2203
|
+
let streamDone = false;
|
|
1649
2204
|
let messageResponse;
|
|
1650
2205
|
let currentContentBlockIndex = -1;
|
|
1651
2206
|
let currentItemType = null;
|
|
@@ -1667,11 +2222,10 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1667
2222
|
}
|
|
1668
2223
|
try {
|
|
1669
2224
|
while (true) {
|
|
1670
|
-
const { done, value } = await reader.read()
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
const { events,
|
|
1674
|
-
buffer = rest;
|
|
2225
|
+
const { done, value } = await reader.read().catch((err) => {
|
|
2226
|
+
throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
|
|
2227
|
+
});
|
|
2228
|
+
const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
|
|
1675
2229
|
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
1676
2230
|
count: malformedEvents,
|
|
1677
2231
|
providerLabel: "Messages",
|
|
@@ -1683,7 +2237,6 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1683
2237
|
case "error": {
|
|
1684
2238
|
const err = sseEvent.data.error;
|
|
1685
2239
|
yield factory.responseWarning(err.message, err.type);
|
|
1686
|
-
this.warn(err.message, err.type);
|
|
1687
2240
|
continue;
|
|
1688
2241
|
}
|
|
1689
2242
|
case "message_start":
|
|
@@ -1718,7 +2271,7 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1718
2271
|
yield factory.reasoningStarted(currentItemId, "redacted");
|
|
1719
2272
|
yield factory.reasoningDelta(currentItemId, textBlock(data));
|
|
1720
2273
|
const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
|
|
1721
|
-
yield factory.reasoningCompleted(
|
|
2274
|
+
yield factory.reasoningCompleted(currentItemId);
|
|
1722
2275
|
output.push(redactedItem);
|
|
1723
2276
|
rawReplayContent.push({
|
|
1724
2277
|
type: "redacted_thinking",
|
|
@@ -1747,7 +2300,7 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1747
2300
|
if (currentItemType === "message" && currentItemId) {
|
|
1748
2301
|
const txt = delta.text;
|
|
1749
2302
|
textBuffer += txt;
|
|
1750
|
-
yield factory.messageDelta(currentItemId, txt);
|
|
2303
|
+
yield factory.messageDelta(currentItemId, textBlock(txt));
|
|
1751
2304
|
}
|
|
1752
2305
|
break;
|
|
1753
2306
|
case "thinking_delta":
|
|
@@ -1769,14 +2322,14 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1769
2322
|
}
|
|
1770
2323
|
case "content_block_stop":
|
|
1771
2324
|
if (currentItemType === "message" && currentItemId) {
|
|
1772
|
-
yield factory.messageCompleted(
|
|
2325
|
+
yield factory.messageCompleted(currentItemId);
|
|
1773
2326
|
output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
|
|
1774
2327
|
rawReplayContent.push({
|
|
1775
2328
|
type: "text",
|
|
1776
2329
|
text: textBuffer
|
|
1777
2330
|
});
|
|
1778
2331
|
} else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
|
|
1779
|
-
yield factory.reasoningCompleted(
|
|
2332
|
+
yield factory.reasoningCompleted(currentItemId);
|
|
1780
2333
|
output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
|
|
1781
2334
|
rawReplayContent.push({
|
|
1782
2335
|
type: "thinking",
|
|
@@ -1784,7 +2337,7 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1784
2337
|
});
|
|
1785
2338
|
} else if (currentItemType === "tool_call" && currentItemId) {
|
|
1786
2339
|
const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
|
|
1787
|
-
yield factory.toolCallCompleted(
|
|
2340
|
+
yield factory.toolCallCompleted(currentItemId);
|
|
1788
2341
|
output.push(tcItem);
|
|
1789
2342
|
rawReplayContent.push({
|
|
1790
2343
|
type: "tool_use",
|
|
@@ -1805,11 +2358,19 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1805
2358
|
}
|
|
1806
2359
|
case "message_stop": break;
|
|
1807
2360
|
}
|
|
2361
|
+
if (done) {
|
|
2362
|
+
streamDone = true;
|
|
2363
|
+
break;
|
|
2364
|
+
}
|
|
1808
2365
|
}
|
|
1809
2366
|
} finally {
|
|
1810
|
-
|
|
2367
|
+
try {
|
|
2368
|
+
if (!streamDone) await reader.cancel().catch(() => void 0);
|
|
2369
|
+
} finally {
|
|
2370
|
+
reader.releaseLock();
|
|
2371
|
+
}
|
|
1811
2372
|
}
|
|
1812
|
-
if (
|
|
2373
|
+
if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
|
|
1813
2374
|
const replay = [...replayFromOutput(output)];
|
|
1814
2375
|
if (messageResponse) {
|
|
1815
2376
|
const replayContent = rawReplayContent.length > 0 ? rawReplayContent : messageResponse.content;
|
|
@@ -1830,17 +2391,29 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1830
2391
|
if (!hasStreamedReasoning) {}
|
|
1831
2392
|
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
1832
2393
|
for (const event of auxiliaryResult.events) yield event;
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2394
|
+
if (!completedEmitted) {
|
|
2395
|
+
completedEmitted = true;
|
|
2396
|
+
const finalResponse = this.buildResponse(request, {
|
|
2397
|
+
output,
|
|
2398
|
+
replay,
|
|
2399
|
+
stopReason: stopReason ? mapStopReason(stopReason) : void 0,
|
|
2400
|
+
usage: auxiliaryResult.usage,
|
|
2401
|
+
billing: auxiliaryResult.billing,
|
|
2402
|
+
auxiliary: auxiliaryResult.auxiliary,
|
|
2403
|
+
warnings: auxiliaryResult.warnings,
|
|
2404
|
+
metadataSources: auxiliaryResult.metadataSources,
|
|
2405
|
+
rawResponseId
|
|
2406
|
+
}, factory);
|
|
2407
|
+
yield factory.responseCompleted({
|
|
2408
|
+
replay: finalResponse.replay,
|
|
2409
|
+
stopReason: finalResponse.stopReason,
|
|
2410
|
+
trace: finalResponse.backend,
|
|
2411
|
+
usage: finalResponse.usage,
|
|
2412
|
+
billing: finalResponse.billing,
|
|
2413
|
+
auxiliary: finalResponse.auxiliary,
|
|
2414
|
+
warnings: finalResponse.warnings
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
1844
2417
|
}
|
|
1845
2418
|
};
|
|
1846
2419
|
//#endregion
|
|
@@ -1855,51 +2428,21 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1855
2428
|
* - replay fidelity 依赖 provider 是否暴露可回放的 assistant turn 字段
|
|
1856
2429
|
*/
|
|
1857
2430
|
const REASONING_FIELDS = ["reasoning_content", "reasoning"];
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
let rest = buffer;
|
|
1871
|
-
let malformedEvents = 0;
|
|
1872
|
-
while (true) {
|
|
1873
|
-
const lineEnd = rest.indexOf("\n");
|
|
1874
|
-
if (lineEnd === -1) break;
|
|
1875
|
-
const line = rest.slice(0, lineEnd).trim();
|
|
1876
|
-
rest = rest.slice(lineEnd + 1);
|
|
1877
|
-
if (!line.startsWith("data: ")) continue;
|
|
1878
|
-
const data = line.slice(6).trim();
|
|
1879
|
-
if (data === "[DONE]") continue;
|
|
1880
|
-
try {
|
|
1881
|
-
chunks.push(JSON.parse(data));
|
|
1882
|
-
} catch {
|
|
1883
|
-
malformedEvents++;
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
return {
|
|
1887
|
-
chunks,
|
|
1888
|
-
rest,
|
|
1889
|
-
malformedEvents
|
|
1890
|
-
};
|
|
1891
|
-
}
|
|
1892
|
-
function ensureTextCompatibleBlocks(blocks, field) {
|
|
1893
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1894
|
-
const block = blocks[i];
|
|
1895
|
-
if (!block) continue;
|
|
1896
|
-
if (block.type !== "text" && block.type !== "json") throw new AIRequestError(`chat-completions does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
2431
|
+
const profile$1 = {
|
|
2432
|
+
kind: "chat-completions",
|
|
2433
|
+
instructionsMode: "system_message",
|
|
2434
|
+
supportedBlockTypes: ["text", "json"],
|
|
2435
|
+
reasoningBlockTypes: ["text"],
|
|
2436
|
+
capabilities: {
|
|
2437
|
+
textStreaming: "native",
|
|
2438
|
+
reasoningStreaming: "native",
|
|
2439
|
+
toolCallStreaming: "native",
|
|
2440
|
+
replay: "opaque",
|
|
2441
|
+
usage: "final",
|
|
2442
|
+
toolResultOutcomes: ["success"]
|
|
1897
2443
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
function contentBlocksToChatText(blocks, field) {
|
|
1901
|
-
return contentBlocksToText(ensureTextCompatibleBlocks(blocks, field));
|
|
1902
|
-
}
|
|
2444
|
+
};
|
|
2445
|
+
const mapper$1 = new NormalizedRequestMapper(profile$1);
|
|
1903
2446
|
function extractReasoningText(value) {
|
|
1904
2447
|
if (typeof value === "string") return value;
|
|
1905
2448
|
if (Array.isArray(value)) return value.map(extractReasoningText).join("");
|
|
@@ -1930,8 +2473,31 @@ function extractReasoningDeltas(delta) {
|
|
|
1930
2473
|
}
|
|
1931
2474
|
return deltas;
|
|
1932
2475
|
}
|
|
1933
|
-
function
|
|
1934
|
-
|
|
2476
|
+
function isChatReplayToolCall(value) {
|
|
2477
|
+
if (!value || typeof value !== "object") return false;
|
|
2478
|
+
const entry = value;
|
|
2479
|
+
if (typeof entry.id !== "string" || entry.type !== "function") return false;
|
|
2480
|
+
const fn = entry.function;
|
|
2481
|
+
if (!fn || typeof fn !== "object") return false;
|
|
2482
|
+
const f = fn;
|
|
2483
|
+
return typeof f.name === "string" && typeof f.arguments === "string";
|
|
2484
|
+
}
|
|
2485
|
+
function isChatReplayMessage(value) {
|
|
2486
|
+
if (!value || typeof value !== "object") return false;
|
|
2487
|
+
const msg = value;
|
|
2488
|
+
const role = msg.role;
|
|
2489
|
+
if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return false;
|
|
2490
|
+
if (!(msg.content === null || typeof msg.content === "string")) return false;
|
|
2491
|
+
if (msg.tool_calls !== void 0) {
|
|
2492
|
+
if (!Array.isArray(msg.tool_calls) || !msg.tool_calls.every(isChatReplayToolCall)) return false;
|
|
2493
|
+
}
|
|
2494
|
+
if (msg.tool_call_id !== void 0 && typeof msg.tool_call_id !== "string") return false;
|
|
2495
|
+
if (msg.name !== void 0 && typeof msg.name !== "string") return false;
|
|
2496
|
+
return true;
|
|
2497
|
+
}
|
|
2498
|
+
function assertChatReplayMessages(messages, field) {
|
|
2499
|
+
if (!Array.isArray(messages)) throw new AIRequestError(`Invalid opaque replay payload: ${field} must be an array`, "INVALID_OPAQUE_REPLAY");
|
|
2500
|
+
for (let i = 0; i < messages.length; i++) if (!isChatReplayMessage(messages[i])) throw new AIRequestError(`Invalid opaque replay payload: ${field}[${i}] is not a valid chat message`, "INVALID_OPAQUE_REPLAY");
|
|
1935
2501
|
}
|
|
1936
2502
|
function buildAssistantReplayMessage(params) {
|
|
1937
2503
|
const { content, reasoningByField, toolCalls } = params;
|
|
@@ -1953,7 +2519,7 @@ function buildAssistantReplayMessage(params) {
|
|
|
1953
2519
|
}
|
|
1954
2520
|
var ChatCompletionsAdapter = class extends AdapterBase {
|
|
1955
2521
|
kind = "chat-completions";
|
|
1956
|
-
|
|
2522
|
+
capabilities = profile$1.capabilities;
|
|
1957
2523
|
apiKey;
|
|
1958
2524
|
baseUrl;
|
|
1959
2525
|
fetchFn;
|
|
@@ -1965,17 +2531,14 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
1965
2531
|
}
|
|
1966
2532
|
buildRequest(request) {
|
|
1967
2533
|
const messages = [];
|
|
1968
|
-
if (request.instructions) {
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
content
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
2534
|
+
if (request.instructions) messages.push({
|
|
2535
|
+
role: "system",
|
|
2536
|
+
content: mapper$1.mapInstructions(request.instructions)
|
|
2537
|
+
});
|
|
1975
2538
|
for (const item of request.input) switch (item.type) {
|
|
1976
2539
|
case "message": {
|
|
1977
2540
|
const role = item.role;
|
|
1978
|
-
const text =
|
|
2541
|
+
const text = contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `input message (${item.role}) content`));
|
|
1979
2542
|
messages.push({
|
|
1980
2543
|
role,
|
|
1981
2544
|
content: text || null
|
|
@@ -2001,38 +2564,44 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2001
2564
|
break;
|
|
2002
2565
|
}
|
|
2003
2566
|
case "tool_result":
|
|
2004
|
-
|
|
2567
|
+
mapper$1.assertToolResultOutcome(item.outcome);
|
|
2005
2568
|
messages.push({
|
|
2006
2569
|
role: "tool",
|
|
2007
2570
|
tool_call_id: item.callId,
|
|
2008
2571
|
name: item.toolName,
|
|
2009
|
-
content:
|
|
2572
|
+
content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
|
|
2010
2573
|
});
|
|
2011
2574
|
break;
|
|
2012
2575
|
case "reasoning":
|
|
2013
2576
|
messages.push({
|
|
2014
2577
|
role: "assistant",
|
|
2015
|
-
content:
|
|
2578
|
+
content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, "reasoning content"))
|
|
2016
2579
|
});
|
|
2017
2580
|
break;
|
|
2018
|
-
case "opaque":
|
|
2019
|
-
if (item.purpose
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2581
|
+
case "opaque": {
|
|
2582
|
+
if (item.purpose !== "replay") break;
|
|
2583
|
+
assertOpaqueReplayEnvelope(item.payload);
|
|
2584
|
+
const payload = item.payload;
|
|
2585
|
+
if (payload.role === "assistant" && typeof payload.content === "string") messages.push({
|
|
2586
|
+
role: "assistant",
|
|
2587
|
+
content: payload.content
|
|
2588
|
+
});
|
|
2589
|
+
else if (payload.replaceCanonical === true && "messages" in payload) {
|
|
2590
|
+
assertChatReplayMessages(payload.messages, "messages");
|
|
2591
|
+
mapper$1.rollbackTrailingAssistantMessages(messages);
|
|
2592
|
+
for (const m of payload.messages) messages.push(m);
|
|
2593
|
+
} else if ("messages" in payload) {
|
|
2594
|
+
assertChatReplayMessages(payload.messages, "messages");
|
|
2595
|
+
for (const m of payload.messages) messages.push(m);
|
|
2029
2596
|
}
|
|
2030
2597
|
break;
|
|
2598
|
+
}
|
|
2031
2599
|
}
|
|
2032
2600
|
const body = {
|
|
2033
2601
|
model: request.model,
|
|
2034
2602
|
messages,
|
|
2035
|
-
stream: true
|
|
2603
|
+
stream: true,
|
|
2604
|
+
n: 1
|
|
2036
2605
|
};
|
|
2037
2606
|
if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
|
|
2038
2607
|
type: "function",
|
|
@@ -2057,23 +2626,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2057
2626
|
}
|
|
2058
2627
|
async *runStream(providerRequest, factory, request) {
|
|
2059
2628
|
const auxiliary = this.createAuxiliaryState(request);
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2629
|
+
let response;
|
|
2630
|
+
try {
|
|
2631
|
+
response = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
|
|
2632
|
+
method: "POST",
|
|
2633
|
+
headers: {
|
|
2634
|
+
"Content-Type": "application/json",
|
|
2635
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
2636
|
+
},
|
|
2637
|
+
body: JSON.stringify(providerRequest)
|
|
2638
|
+
});
|
|
2639
|
+
} catch (err) {
|
|
2640
|
+
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
2641
|
+
}
|
|
2068
2642
|
if (!response.ok) {
|
|
2069
|
-
const
|
|
2070
|
-
throw
|
|
2643
|
+
const errorBody = await response.text().catch(() => "");
|
|
2644
|
+
throw providerHttpError(response.status, errorBody);
|
|
2071
2645
|
}
|
|
2072
2646
|
const reader = response.body?.getReader();
|
|
2073
|
-
if (!reader) throw new
|
|
2647
|
+
if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
2648
|
+
const parser = new IncrementalStreamParser(splitLines, (item) => {
|
|
2649
|
+
const trimmed = item.trim();
|
|
2650
|
+
if (!trimmed.startsWith("data: ")) return { status: "ignored" };
|
|
2651
|
+
const data = trimmed.slice(6).trim();
|
|
2652
|
+
if (data === "[DONE]") return { status: "ignored" };
|
|
2653
|
+
try {
|
|
2654
|
+
return {
|
|
2655
|
+
status: "parsed",
|
|
2656
|
+
value: JSON.parse(data)
|
|
2657
|
+
};
|
|
2658
|
+
} catch {
|
|
2659
|
+
return { status: "malformed" };
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2074
2662
|
const output = [];
|
|
2075
|
-
|
|
2076
|
-
let buffer = "";
|
|
2663
|
+
let streamDone = false;
|
|
2077
2664
|
let responseId;
|
|
2078
2665
|
let accumulatedContent = "";
|
|
2079
2666
|
let accumulatedReasoning = "";
|
|
@@ -2081,6 +2668,9 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2081
2668
|
let currentReasoningId = "";
|
|
2082
2669
|
let hasMessageStarted = false;
|
|
2083
2670
|
let hasReasoningStarted = false;
|
|
2671
|
+
let completedEmitted = false;
|
|
2672
|
+
let warnedNonZeroChoice = false;
|
|
2673
|
+
const buildResponse = this.buildResponse.bind(this);
|
|
2084
2674
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2085
2675
|
const reasoningByField = /* @__PURE__ */ new Map();
|
|
2086
2676
|
const finalizePendingTurn = () => {
|
|
@@ -2089,17 +2679,17 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2089
2679
|
const finalizedReasoningByField = new Map(reasoningByField);
|
|
2090
2680
|
if (hasReasoningStarted && accumulatedReasoning) {
|
|
2091
2681
|
const reasoning = reasoningItem([textBlock(accumulatedReasoning)], "full", currentReasoningId);
|
|
2092
|
-
events.push(factory.reasoningCompleted(
|
|
2682
|
+
events.push(factory.reasoningCompleted(currentReasoningId));
|
|
2093
2683
|
output.push(reasoning);
|
|
2094
2684
|
}
|
|
2095
|
-
if (hasMessageStarted
|
|
2685
|
+
if (hasMessageStarted) {
|
|
2096
2686
|
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
2097
|
-
events.push(factory.messageCompleted(
|
|
2687
|
+
events.push(factory.messageCompleted(currentMessageId));
|
|
2098
2688
|
output.push(message);
|
|
2099
2689
|
}
|
|
2100
2690
|
for (const pending of finalizedToolCalls) {
|
|
2101
2691
|
const toolCall = toolCallItem(pending.id, pending.name, pending.args);
|
|
2102
|
-
events.push(factory.toolCallCompleted(
|
|
2692
|
+
events.push(factory.toolCallCompleted(pending.id));
|
|
2103
2693
|
output.push(toolCall);
|
|
2104
2694
|
}
|
|
2105
2695
|
const assistantReplayMessage = buildAssistantReplayMessage({
|
|
@@ -2120,13 +2710,46 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2120
2710
|
assistantReplayMessage
|
|
2121
2711
|
};
|
|
2122
2712
|
};
|
|
2713
|
+
const emitCompleted = async function* (stopReason, assistantReplayMessage, rawResponseId) {
|
|
2714
|
+
if (completedEmitted) {
|
|
2715
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
completedEmitted = true;
|
|
2719
|
+
const replay = [...replayFromOutput(output)];
|
|
2720
|
+
if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
|
|
2721
|
+
replaceCanonical: true,
|
|
2722
|
+
messages: [assistantReplayMessage]
|
|
2723
|
+
}));
|
|
2724
|
+
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
2725
|
+
for (const event of auxiliaryResult.events) yield event;
|
|
2726
|
+
const finalResponse = buildResponse(request, {
|
|
2727
|
+
output,
|
|
2728
|
+
replay,
|
|
2729
|
+
stopReason,
|
|
2730
|
+
usage: auxiliaryResult.usage,
|
|
2731
|
+
billing: auxiliaryResult.billing,
|
|
2732
|
+
auxiliary: auxiliaryResult.auxiliary,
|
|
2733
|
+
warnings: auxiliaryResult.warnings,
|
|
2734
|
+
metadataSources: auxiliaryResult.metadataSources,
|
|
2735
|
+
rawResponseId
|
|
2736
|
+
}, factory);
|
|
2737
|
+
yield factory.responseCompleted({
|
|
2738
|
+
replay: finalResponse.replay,
|
|
2739
|
+
stopReason: finalResponse.stopReason,
|
|
2740
|
+
trace: finalResponse.backend,
|
|
2741
|
+
usage: finalResponse.usage,
|
|
2742
|
+
billing: finalResponse.billing,
|
|
2743
|
+
auxiliary: finalResponse.auxiliary,
|
|
2744
|
+
warnings: finalResponse.warnings
|
|
2745
|
+
});
|
|
2746
|
+
};
|
|
2123
2747
|
try {
|
|
2124
2748
|
while (true) {
|
|
2125
|
-
const { done, value } = await reader.read()
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
const { chunks,
|
|
2129
|
-
buffer = rest;
|
|
2749
|
+
const { done, value } = await reader.read().catch((err) => {
|
|
2750
|
+
throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
|
|
2751
|
+
});
|
|
2752
|
+
const { items: chunks, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
|
|
2130
2753
|
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
2131
2754
|
count: malformedEvents,
|
|
2132
2755
|
providerLabel: "Chat Completions",
|
|
@@ -2137,16 +2760,26 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2137
2760
|
responseId = chunk.id;
|
|
2138
2761
|
if (chunk.usage) auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
|
|
2139
2762
|
for (const choice of chunk.choices) {
|
|
2140
|
-
if (choice.index !== 0)
|
|
2763
|
+
if (choice.index !== 0) {
|
|
2764
|
+
if (!warnedNonZeroChoice) {
|
|
2765
|
+
yield factory.responseWarning(`Chat Completions returned choice index ${choice.index}; only the first choice (index 0) is supported. This choice is ignored.`, "MULTIPLE_CHOICES_IGNORED");
|
|
2766
|
+
warnedNonZeroChoice = true;
|
|
2767
|
+
}
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
if (completedEmitted) {
|
|
2771
|
+
if (choice.finish_reason) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2141
2774
|
const delta = choice.delta;
|
|
2142
2775
|
const finishReason = choice.finish_reason;
|
|
2143
2776
|
const reasoningDeltas = extractReasoningDeltas(delta);
|
|
2144
|
-
|
|
2777
|
+
const ensureMessageStarted = () => {
|
|
2778
|
+
if (hasMessageStarted) return;
|
|
2145
2779
|
currentMessageId = `msg-${chunk.id}`;
|
|
2146
2780
|
hasMessageStarted = true;
|
|
2147
2781
|
accumulatedContent = "";
|
|
2148
|
-
|
|
2149
|
-
}
|
|
2782
|
+
};
|
|
2150
2783
|
if (reasoningDeltas.length > 0) {
|
|
2151
2784
|
if (!hasReasoningStarted) {
|
|
2152
2785
|
currentReasoningId = `reason-${chunk.id}`;
|
|
@@ -2162,32 +2795,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2162
2795
|
}
|
|
2163
2796
|
if (delta.content) {
|
|
2164
2797
|
if (!hasMessageStarted) {
|
|
2165
|
-
|
|
2166
|
-
hasMessageStarted = true;
|
|
2798
|
+
ensureMessageStarted();
|
|
2167
2799
|
yield factory.messageStarted(currentMessageId);
|
|
2168
2800
|
}
|
|
2169
2801
|
accumulatedContent += delta.content;
|
|
2170
|
-
yield factory.messageDelta(currentMessageId, delta.content);
|
|
2802
|
+
yield factory.messageDelta(currentMessageId, textBlock(delta.content));
|
|
2171
2803
|
}
|
|
2172
|
-
if (delta.tool_calls)
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
id: tc.id,
|
|
2177
|
-
name: tc.function?.name ?? "",
|
|
2178
|
-
args: ""
|
|
2179
|
-
});
|
|
2180
|
-
yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
|
|
2804
|
+
if (delta.tool_calls) {
|
|
2805
|
+
if (!hasMessageStarted) {
|
|
2806
|
+
ensureMessageStarted();
|
|
2807
|
+
yield factory.messageStarted(currentMessageId);
|
|
2181
2808
|
}
|
|
2182
|
-
|
|
2183
|
-
const
|
|
2184
|
-
if (
|
|
2185
|
-
|
|
2186
|
-
|
|
2809
|
+
for (const tc of delta.tool_calls) {
|
|
2810
|
+
const idx = tc.index;
|
|
2811
|
+
if (tc.id) {
|
|
2812
|
+
pendingToolCalls.set(idx, {
|
|
2813
|
+
id: tc.id,
|
|
2814
|
+
name: tc.function?.name ?? "",
|
|
2815
|
+
args: ""
|
|
2816
|
+
});
|
|
2817
|
+
yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
|
|
2818
|
+
}
|
|
2819
|
+
if (tc.function?.arguments) {
|
|
2820
|
+
const pending = pendingToolCalls.get(idx);
|
|
2821
|
+
if (pending) {
|
|
2822
|
+
pending.args += tc.function.arguments;
|
|
2823
|
+
yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
|
|
2824
|
+
}
|
|
2187
2825
|
}
|
|
2188
2826
|
}
|
|
2189
2827
|
}
|
|
2190
2828
|
if (delta.function_call) {
|
|
2829
|
+
if (!hasMessageStarted) {
|
|
2830
|
+
ensureMessageStarted();
|
|
2831
|
+
yield factory.messageStarted(currentMessageId);
|
|
2832
|
+
}
|
|
2191
2833
|
if (delta.function_call.name) {
|
|
2192
2834
|
const fcId = `fc-${chunk.id}-0`;
|
|
2193
2835
|
pendingToolCalls.set(0, {
|
|
@@ -2208,54 +2850,28 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2208
2850
|
if (finishReason && finishReason !== null) {
|
|
2209
2851
|
const { events, assistantReplayMessage } = finalizePendingTurn();
|
|
2210
2852
|
for (const event of events) yield event;
|
|
2211
|
-
|
|
2212
|
-
const replay = [...replayFromOutput(output)];
|
|
2213
|
-
if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
|
|
2214
|
-
replaceCanonical: true,
|
|
2215
|
-
messages: [assistantReplayMessage]
|
|
2216
|
-
}));
|
|
2217
|
-
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
2218
|
-
for (const event of auxiliaryResult.events) yield event;
|
|
2219
|
-
yield factory.responseCompleted(this.buildResponse(request, {
|
|
2220
|
-
output,
|
|
2221
|
-
replay,
|
|
2222
|
-
stopReason,
|
|
2223
|
-
usage: auxiliaryResult.usage,
|
|
2224
|
-
billing: auxiliaryResult.billing,
|
|
2225
|
-
auxiliary: auxiliaryResult.auxiliary,
|
|
2226
|
-
warnings: auxiliaryResult.warnings,
|
|
2227
|
-
metadataSources: auxiliaryResult.metadataSources,
|
|
2228
|
-
rawResponseId: chunk.id
|
|
2229
|
-
}, factory));
|
|
2853
|
+
yield* emitCompleted(mapStopReason(finishReason), assistantReplayMessage, chunk.id);
|
|
2230
2854
|
}
|
|
2231
2855
|
}
|
|
2232
2856
|
}
|
|
2857
|
+
if (done) {
|
|
2858
|
+
streamDone = true;
|
|
2859
|
+
break;
|
|
2860
|
+
}
|
|
2233
2861
|
}
|
|
2234
2862
|
} finally {
|
|
2235
|
-
|
|
2863
|
+
try {
|
|
2864
|
+
if (!streamDone) await reader.cancel().catch(() => void 0);
|
|
2865
|
+
} finally {
|
|
2866
|
+
reader.releaseLock();
|
|
2867
|
+
}
|
|
2236
2868
|
}
|
|
2237
|
-
if (
|
|
2238
|
-
if (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0) {
|
|
2869
|
+
if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
|
|
2870
|
+
if (!completedEmitted && (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0)) {
|
|
2239
2871
|
yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
|
|
2240
2872
|
const { events, assistantReplayMessage } = finalizePendingTurn();
|
|
2241
2873
|
for (const event of events) yield event;
|
|
2242
|
-
|
|
2243
|
-
if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
|
|
2244
|
-
replaceCanonical: true,
|
|
2245
|
-
messages: [assistantReplayMessage]
|
|
2246
|
-
}));
|
|
2247
|
-
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
2248
|
-
for (const event of auxiliaryResult.events) yield event;
|
|
2249
|
-
yield factory.responseCompleted(this.buildResponse(request, {
|
|
2250
|
-
output,
|
|
2251
|
-
replay,
|
|
2252
|
-
usage: auxiliaryResult.usage,
|
|
2253
|
-
billing: auxiliaryResult.billing,
|
|
2254
|
-
auxiliary: auxiliaryResult.auxiliary,
|
|
2255
|
-
warnings: auxiliaryResult.warnings,
|
|
2256
|
-
metadataSources: auxiliaryResult.metadataSources,
|
|
2257
|
-
rawResponseId: responseId
|
|
2258
|
-
}, factory));
|
|
2874
|
+
yield* emitCompleted(void 0, assistantReplayMessage, responseId);
|
|
2259
2875
|
}
|
|
2260
2876
|
}
|
|
2261
2877
|
};
|
|
@@ -2277,23 +2893,21 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2277
2893
|
* - tool_call 不支持逐 token 流式
|
|
2278
2894
|
* - replay 保真度低(无 opaque continuation 机制)
|
|
2279
2895
|
*/
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
return typeof instructions === "string" ? instructions : contentBlocksToText(ensureOllamaTextBlocks(instructions, "instructions"));
|
|
2296
|
-
}
|
|
2896
|
+
const profile = {
|
|
2897
|
+
kind: "ollama",
|
|
2898
|
+
instructionsMode: "system_message",
|
|
2899
|
+
supportedBlockTypes: ["text", "json"],
|
|
2900
|
+
reasoningBlockTypes: ["text"],
|
|
2901
|
+
capabilities: {
|
|
2902
|
+
textStreaming: "native",
|
|
2903
|
+
reasoningStreaming: "none",
|
|
2904
|
+
toolCallStreaming: "synthetic",
|
|
2905
|
+
replay: "opaque",
|
|
2906
|
+
usage: "final",
|
|
2907
|
+
toolResultOutcomes: ["success"]
|
|
2908
|
+
}
|
|
2909
|
+
};
|
|
2910
|
+
const mapper = new NormalizedRequestMapper(profile);
|
|
2297
2911
|
function parseOllamaToolArguments(item) {
|
|
2298
2912
|
if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) return item.argumentsJson;
|
|
2299
2913
|
try {
|
|
@@ -2302,46 +2916,24 @@ function parseOllamaToolArguments(item) {
|
|
|
2302
2916
|
} catch {}
|
|
2303
2917
|
throw new AIRequestError("ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent", "TOOL_CALL_ARGUMENTS_INVALID");
|
|
2304
2918
|
}
|
|
2305
|
-
function
|
|
2306
|
-
if (outcome !== "success") throw new AIRequestError(`ollama does not preserve tool_result outcome "${outcome}"; only "success" is supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
|
|
2307
|
-
}
|
|
2308
|
-
function parseOllamaNDJSON(buffer) {
|
|
2309
|
-
const chunks = [];
|
|
2310
|
-
let rest = buffer;
|
|
2311
|
-
let malformedLines = 0;
|
|
2312
|
-
while (true) {
|
|
2313
|
-
const lineEnd = rest.indexOf("\n");
|
|
2314
|
-
if (lineEnd === -1) break;
|
|
2315
|
-
const line = rest.slice(0, lineEnd).trim();
|
|
2316
|
-
rest = rest.slice(lineEnd + 1);
|
|
2317
|
-
if (!line) continue;
|
|
2318
|
-
try {
|
|
2319
|
-
const parsed = JSON.parse(line);
|
|
2320
|
-
if (parsed && typeof parsed === "object" && "message" in parsed) chunks.push(parsed);
|
|
2321
|
-
else malformedLines++;
|
|
2322
|
-
} catch {
|
|
2323
|
-
malformedLines++;
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
return {
|
|
2327
|
-
chunks,
|
|
2328
|
-
rest,
|
|
2329
|
-
malformedLines
|
|
2330
|
-
};
|
|
2331
|
-
}
|
|
2332
|
-
function rollbackTrailingAssistantMessages(messages) {
|
|
2333
|
-
while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
|
|
2334
|
-
}
|
|
2335
|
-
function isOllamaToolCalls(value) {
|
|
2919
|
+
function isOllamaReplayToolCalls(value) {
|
|
2336
2920
|
return Array.isArray(value) && value.every((entry) => {
|
|
2337
2921
|
if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
|
|
2338
2922
|
const fn = entry.function;
|
|
2923
|
+
const id = entry.id;
|
|
2924
|
+
if (id !== void 0 && typeof id !== "string") return false;
|
|
2339
2925
|
return !!fn && typeof fn === "object" && "name" in fn && typeof fn.name === "string" && "arguments" in fn && typeof fn.arguments === "object" && fn.arguments !== null;
|
|
2340
2926
|
});
|
|
2341
2927
|
}
|
|
2928
|
+
function toWireOllamaToolCalls(toolCalls) {
|
|
2929
|
+
return toolCalls.map((tc) => ({ function: {
|
|
2930
|
+
name: tc.function.name,
|
|
2931
|
+
arguments: tc.function.arguments
|
|
2932
|
+
} }));
|
|
2933
|
+
}
|
|
2342
2934
|
var OllamaAdapter = class extends AdapterBase {
|
|
2343
2935
|
kind = "ollama";
|
|
2344
|
-
|
|
2936
|
+
capabilities = profile.capabilities;
|
|
2345
2937
|
baseUrl;
|
|
2346
2938
|
apiKey;
|
|
2347
2939
|
fetchFn;
|
|
@@ -2354,16 +2946,18 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2354
2946
|
buildRequest(request) {
|
|
2355
2947
|
if (request.toolChoice && request.toolChoice !== "auto") throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
|
|
2356
2948
|
const messages = [];
|
|
2949
|
+
/** Local-only name → call id queue for best-effort tool_result association (not sent to Ollama). */
|
|
2950
|
+
const callIdsByName = /* @__PURE__ */ new Map();
|
|
2357
2951
|
if (request.instructions) messages.push({
|
|
2358
2952
|
role: "system",
|
|
2359
|
-
content:
|
|
2953
|
+
content: mapper.mapInstructions(request.instructions)
|
|
2360
2954
|
});
|
|
2361
2955
|
for (const item of request.input) switch (item.type) {
|
|
2362
2956
|
case "message": {
|
|
2363
2957
|
const role = item.role;
|
|
2364
2958
|
messages.push({
|
|
2365
2959
|
role,
|
|
2366
|
-
content: contentBlocksToText(
|
|
2960
|
+
content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`))
|
|
2367
2961
|
});
|
|
2368
2962
|
break;
|
|
2369
2963
|
}
|
|
@@ -2373,6 +2967,9 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2373
2967
|
name: item.name,
|
|
2374
2968
|
arguments: parseOllamaToolArguments(item)
|
|
2375
2969
|
} };
|
|
2970
|
+
const queue = callIdsByName.get(item.name) ?? [];
|
|
2971
|
+
queue.push(item.id);
|
|
2972
|
+
callIdsByName.set(item.name, queue);
|
|
2376
2973
|
if (lastAssistant) lastAssistant.tool_calls = [...lastAssistant.tool_calls ?? [], tc];
|
|
2377
2974
|
else messages.push({
|
|
2378
2975
|
role: "assistant",
|
|
@@ -2381,32 +2978,48 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2381
2978
|
});
|
|
2382
2979
|
break;
|
|
2383
2980
|
}
|
|
2384
|
-
case "tool_result":
|
|
2385
|
-
|
|
2981
|
+
case "tool_result": {
|
|
2982
|
+
mapper.assertToolResultOutcome(item.outcome);
|
|
2983
|
+
const queue = callIdsByName.get(item.toolName);
|
|
2984
|
+
if (queue && queue.length > 0) queue.shift();
|
|
2386
2985
|
messages.push({
|
|
2387
2986
|
role: "tool",
|
|
2388
|
-
content: contentBlocksToText(
|
|
2987
|
+
content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
|
|
2389
2988
|
});
|
|
2390
2989
|
break;
|
|
2990
|
+
}
|
|
2391
2991
|
case "reasoning":
|
|
2392
2992
|
messages.push({
|
|
2393
2993
|
role: "assistant",
|
|
2394
|
-
content: contentBlocksToText(
|
|
2994
|
+
content: contentBlocksToText(mapper.ensureReasoningBlocks(item.content, "reasoning content"))
|
|
2395
2995
|
});
|
|
2396
2996
|
break;
|
|
2397
|
-
case "opaque":
|
|
2398
|
-
if (item.source
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2997
|
+
case "opaque": {
|
|
2998
|
+
if (item.source !== "ollama" || item.purpose !== "replay") break;
|
|
2999
|
+
assertOpaqueReplayEnvelope(item.payload);
|
|
3000
|
+
const payload = item.payload;
|
|
3001
|
+
if (payload.role === "assistant" && typeof payload.content === "string") {
|
|
3002
|
+
mapper.rollbackTrailingAssistantMessages(messages);
|
|
3003
|
+
let replayToolCalls;
|
|
3004
|
+
if ("tool_calls" in payload && payload.tool_calls !== void 0) {
|
|
3005
|
+
if (!isOllamaReplayToolCalls(payload.tool_calls)) throw new AIRequestError("Invalid opaque replay payload: tool_calls is not a valid ollama tool_calls array", "INVALID_OPAQUE_REPLAY");
|
|
3006
|
+
replayToolCalls = payload.tool_calls;
|
|
2407
3007
|
}
|
|
3008
|
+
if (replayToolCalls) {
|
|
3009
|
+
for (const tc of replayToolCalls) if (tc.id) {
|
|
3010
|
+
const queue = callIdsByName.get(tc.function.name) ?? [];
|
|
3011
|
+
queue.push(tc.id);
|
|
3012
|
+
callIdsByName.set(tc.function.name, queue);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
messages.push({
|
|
3016
|
+
role: "assistant",
|
|
3017
|
+
content: payload.content,
|
|
3018
|
+
tool_calls: replayToolCalls ? toWireOllamaToolCalls(replayToolCalls) : void 0
|
|
3019
|
+
});
|
|
2408
3020
|
}
|
|
2409
3021
|
break;
|
|
3022
|
+
}
|
|
2410
3023
|
}
|
|
2411
3024
|
const body = {
|
|
2412
3025
|
model: request.model,
|
|
@@ -2430,35 +3043,96 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2430
3043
|
}
|
|
2431
3044
|
async *runStream(providerRequest, factory, request) {
|
|
2432
3045
|
const auxiliary = this.createAuxiliaryState(request);
|
|
3046
|
+
let completedEmitted = false;
|
|
2433
3047
|
if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
|
|
2434
3048
|
const headers = { "Content-Type": "application/json" };
|
|
2435
3049
|
if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
3050
|
+
let response;
|
|
3051
|
+
try {
|
|
3052
|
+
response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
|
|
3053
|
+
method: "POST",
|
|
3054
|
+
headers,
|
|
3055
|
+
body: JSON.stringify(providerRequest)
|
|
3056
|
+
});
|
|
3057
|
+
} catch (err) {
|
|
3058
|
+
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
3059
|
+
}
|
|
2441
3060
|
if (!response.ok) {
|
|
2442
|
-
const
|
|
2443
|
-
throw
|
|
3061
|
+
const errorBody = await response.text().catch(() => "");
|
|
3062
|
+
throw providerHttpError(response.status, errorBody);
|
|
2444
3063
|
}
|
|
2445
3064
|
const reader = response.body?.getReader();
|
|
2446
|
-
if (!reader) throw new
|
|
3065
|
+
if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
3066
|
+
const parser = new IncrementalStreamParser(splitLines, (item) => {
|
|
3067
|
+
const trimmed = item.trim();
|
|
3068
|
+
if (!trimmed) return { status: "ignored" };
|
|
3069
|
+
try {
|
|
3070
|
+
const parsed = JSON.parse(trimmed);
|
|
3071
|
+
if (parsed && typeof parsed === "object" && "message" in parsed) return {
|
|
3072
|
+
status: "parsed",
|
|
3073
|
+
value: parsed
|
|
3074
|
+
};
|
|
3075
|
+
return { status: "malformed" };
|
|
3076
|
+
} catch {
|
|
3077
|
+
return { status: "malformed" };
|
|
3078
|
+
}
|
|
3079
|
+
});
|
|
2447
3080
|
const output = [];
|
|
2448
|
-
|
|
2449
|
-
let buffer = "";
|
|
3081
|
+
let streamDone = false;
|
|
2450
3082
|
let responseId;
|
|
2451
3083
|
let accumulatedContent = "";
|
|
2452
3084
|
let currentMessageId = "";
|
|
2453
3085
|
let hasMessageStarted = false;
|
|
2454
3086
|
let pendingToolCalls = [];
|
|
3087
|
+
let toolCallIndex = 0;
|
|
3088
|
+
const buildResponse = this.buildResponse.bind(this);
|
|
3089
|
+
const emitCompleted = async function* (stopReason, rawResponseId) {
|
|
3090
|
+
if (completedEmitted) {
|
|
3091
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
completedEmitted = true;
|
|
3095
|
+
const replay = replayFromOutput(output);
|
|
3096
|
+
if (accumulatedContent || pendingToolCalls.length > 0) replay.push(opaqueItem("ollama", "replay", {
|
|
3097
|
+
role: "assistant",
|
|
3098
|
+
content: accumulatedContent,
|
|
3099
|
+
tool_calls: pendingToolCalls.map((tc) => ({
|
|
3100
|
+
id: tc.id,
|
|
3101
|
+
function: {
|
|
3102
|
+
name: tc.name,
|
|
3103
|
+
arguments: tc.argumentsJson
|
|
3104
|
+
}
|
|
3105
|
+
}))
|
|
3106
|
+
}));
|
|
3107
|
+
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
3108
|
+
for (const event of auxiliaryResult.events) yield event;
|
|
3109
|
+
const finalResponse = buildResponse(request, {
|
|
3110
|
+
output,
|
|
3111
|
+
replay,
|
|
3112
|
+
stopReason,
|
|
3113
|
+
usage: auxiliaryResult.usage,
|
|
3114
|
+
billing: auxiliaryResult.billing,
|
|
3115
|
+
auxiliary: auxiliaryResult.auxiliary,
|
|
3116
|
+
warnings: auxiliaryResult.warnings,
|
|
3117
|
+
metadataSources: auxiliaryResult.metadataSources,
|
|
3118
|
+
rawResponseId
|
|
3119
|
+
}, factory);
|
|
3120
|
+
yield factory.responseCompleted({
|
|
3121
|
+
replay: finalResponse.replay,
|
|
3122
|
+
stopReason: finalResponse.stopReason,
|
|
3123
|
+
trace: finalResponse.backend,
|
|
3124
|
+
usage: finalResponse.usage,
|
|
3125
|
+
billing: finalResponse.billing,
|
|
3126
|
+
auxiliary: finalResponse.auxiliary,
|
|
3127
|
+
warnings: finalResponse.warnings
|
|
3128
|
+
});
|
|
3129
|
+
};
|
|
2455
3130
|
try {
|
|
2456
3131
|
while (true) {
|
|
2457
|
-
const { done, value } = await reader.read()
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
const { chunks,
|
|
2461
|
-
buffer = rest;
|
|
3132
|
+
const { done, value } = await reader.read().catch((err) => {
|
|
3133
|
+
throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
|
|
3134
|
+
});
|
|
3135
|
+
const { items: chunks, malformed: malformedLines } = done ? parser.flush() : parser.feed(value);
|
|
2462
3136
|
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
2463
3137
|
count: malformedLines,
|
|
2464
3138
|
providerLabel: "Ollama",
|
|
@@ -2467,6 +3141,10 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2467
3141
|
if (malformedWarning) yield malformedWarning;
|
|
2468
3142
|
for (const chunk of chunks) {
|
|
2469
3143
|
responseId = chunk.created_at;
|
|
3144
|
+
if (completedEmitted) {
|
|
3145
|
+
if (chunk.done) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
2470
3148
|
const msg = chunk.message;
|
|
2471
3149
|
if (msg.content) {
|
|
2472
3150
|
if (!hasMessageStarted) {
|
|
@@ -2475,10 +3153,10 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2475
3153
|
yield factory.messageStarted(currentMessageId);
|
|
2476
3154
|
}
|
|
2477
3155
|
accumulatedContent += msg.content;
|
|
2478
|
-
yield factory.messageDelta(currentMessageId, msg.content);
|
|
3156
|
+
yield factory.messageDelta(currentMessageId, textBlock(msg.content));
|
|
2479
3157
|
}
|
|
2480
3158
|
if (msg.tool_calls && msg.tool_calls.length > 0) for (const tc of msg.tool_calls) {
|
|
2481
|
-
const tcId = `tc-${
|
|
3159
|
+
const tcId = `ollama-tc-${request.requestId}-${toolCallIndex++}`;
|
|
2482
3160
|
const argsText = JSON.stringify(tc.function.arguments);
|
|
2483
3161
|
pendingToolCalls.push({
|
|
2484
3162
|
id: tcId,
|
|
@@ -2495,14 +3173,15 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2495
3173
|
}
|
|
2496
3174
|
if (hasMessageStarted) {
|
|
2497
3175
|
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
2498
|
-
yield factory.messageCompleted(
|
|
3176
|
+
yield factory.messageCompleted(currentMessageId);
|
|
2499
3177
|
if (accumulatedContent) output.push(message);
|
|
2500
3178
|
}
|
|
3179
|
+
if (pendingToolCalls.length > 0) yield factory.responseWarning(`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`, WarningCode.TOOL_CALL_BATCHED);
|
|
2501
3180
|
for (const pending of pendingToolCalls) {
|
|
2502
3181
|
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
|
|
2503
3182
|
yield factory.toolCallStarted(pending.id, pending.name);
|
|
2504
3183
|
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
2505
|
-
yield factory.toolCallCompleted(
|
|
3184
|
+
yield factory.toolCallCompleted(pending.id);
|
|
2506
3185
|
output.push(toolCall);
|
|
2507
3186
|
}
|
|
2508
3187
|
if (request.include?.usage !== "off" && (chunk.prompt_eval_count !== void 0 || chunk.eval_count !== void 0)) auxiliary.recordUsage(usageFromOllama({
|
|
@@ -2512,67 +3191,42 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2512
3191
|
prompt_eval_count: chunk.prompt_eval_count,
|
|
2513
3192
|
eval_count: chunk.eval_count
|
|
2514
3193
|
});
|
|
2515
|
-
|
|
2516
|
-
const replay = replayFromOutput(output);
|
|
2517
|
-
if (accumulatedContent || pendingToolCalls.length > 0) replay.push(opaqueItem("ollama", "replay", {
|
|
2518
|
-
role: "assistant",
|
|
2519
|
-
content: accumulatedContent,
|
|
2520
|
-
tool_calls: pendingToolCalls.map((tc) => ({ function: {
|
|
2521
|
-
name: tc.name,
|
|
2522
|
-
arguments: tc.argumentsJson
|
|
2523
|
-
} }))
|
|
2524
|
-
}));
|
|
2525
|
-
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
2526
|
-
for (const event of auxiliaryResult.events) yield event;
|
|
2527
|
-
yield factory.responseCompleted(this.buildResponse(request, {
|
|
2528
|
-
output,
|
|
2529
|
-
replay,
|
|
2530
|
-
stopReason,
|
|
2531
|
-
usage: auxiliaryResult.usage,
|
|
2532
|
-
billing: auxiliaryResult.billing,
|
|
2533
|
-
auxiliary: auxiliaryResult.auxiliary,
|
|
2534
|
-
warnings: auxiliaryResult.warnings,
|
|
2535
|
-
metadataSources: auxiliaryResult.metadataSources,
|
|
2536
|
-
rawResponseId: chunk.created_at
|
|
2537
|
-
}, factory));
|
|
3194
|
+
yield* emitCompleted(chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0, chunk.created_at);
|
|
2538
3195
|
accumulatedContent = "";
|
|
2539
3196
|
currentMessageId = "";
|
|
2540
3197
|
hasMessageStarted = false;
|
|
2541
3198
|
pendingToolCalls = [];
|
|
2542
3199
|
}
|
|
2543
3200
|
}
|
|
3201
|
+
if (done) {
|
|
3202
|
+
streamDone = true;
|
|
3203
|
+
break;
|
|
3204
|
+
}
|
|
2544
3205
|
}
|
|
2545
3206
|
} finally {
|
|
2546
|
-
|
|
3207
|
+
try {
|
|
3208
|
+
if (!streamDone) await reader.cancel().catch(() => void 0);
|
|
3209
|
+
} finally {
|
|
3210
|
+
reader.releaseLock();
|
|
3211
|
+
}
|
|
2547
3212
|
}
|
|
2548
|
-
if (
|
|
2549
|
-
if (hasMessageStarted || pendingToolCalls.length > 0) {
|
|
3213
|
+
if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
|
|
3214
|
+
if (!completedEmitted && (hasMessageStarted || pendingToolCalls.length > 0)) {
|
|
2550
3215
|
yield factory.responseWarning("Stream ended without a done signal", "INCOMPLETE_STREAM");
|
|
2551
3216
|
if (hasMessageStarted) {
|
|
2552
3217
|
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
2553
|
-
yield factory.messageCompleted(
|
|
3218
|
+
yield factory.messageCompleted(currentMessageId);
|
|
2554
3219
|
if (accumulatedContent) output.push(message);
|
|
2555
3220
|
}
|
|
3221
|
+
if (pendingToolCalls.length > 0) yield factory.responseWarning(`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`, WarningCode.TOOL_CALL_BATCHED);
|
|
2556
3222
|
for (const pending of pendingToolCalls) {
|
|
2557
3223
|
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
|
|
2558
3224
|
yield factory.toolCallStarted(pending.id, pending.name);
|
|
2559
3225
|
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
2560
|
-
yield factory.toolCallCompleted(
|
|
3226
|
+
yield factory.toolCallCompleted(pending.id);
|
|
2561
3227
|
output.push(toolCall);
|
|
2562
3228
|
}
|
|
2563
|
-
|
|
2564
|
-
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
2565
|
-
for (const event of auxiliaryResult.events) yield event;
|
|
2566
|
-
yield factory.responseCompleted(this.buildResponse(request, {
|
|
2567
|
-
output,
|
|
2568
|
-
replay,
|
|
2569
|
-
usage: auxiliaryResult.usage,
|
|
2570
|
-
billing: auxiliaryResult.billing,
|
|
2571
|
-
auxiliary: auxiliaryResult.auxiliary,
|
|
2572
|
-
warnings: auxiliaryResult.warnings,
|
|
2573
|
-
metadataSources: auxiliaryResult.metadataSources,
|
|
2574
|
-
rawResponseId: responseId
|
|
2575
|
-
}, factory));
|
|
3229
|
+
yield* emitCompleted(void 0, responseId);
|
|
2576
3230
|
}
|
|
2577
3231
|
}
|
|
2578
3232
|
};
|
|
@@ -2607,7 +3261,18 @@ function assertMockRequest(request, expectation, context) {
|
|
|
2607
3261
|
}
|
|
2608
3262
|
var MockAdapter = class extends AdapterBase {
|
|
2609
3263
|
kind = "mock";
|
|
2610
|
-
|
|
3264
|
+
capabilities = {
|
|
3265
|
+
textStreaming: "synthetic",
|
|
3266
|
+
reasoningStreaming: "synthetic",
|
|
3267
|
+
toolCallStreaming: "synthetic",
|
|
3268
|
+
replay: "canonical",
|
|
3269
|
+
usage: "final",
|
|
3270
|
+
toolResultOutcomes: [
|
|
3271
|
+
"success",
|
|
3272
|
+
"error",
|
|
3273
|
+
"rejected"
|
|
3274
|
+
]
|
|
3275
|
+
};
|
|
2611
3276
|
handler;
|
|
2612
3277
|
providerMetadata;
|
|
2613
3278
|
cursor = 0;
|
|
@@ -2679,18 +3344,34 @@ var MockAdapter = class extends AdapterBase {
|
|
|
2679
3344
|
break;
|
|
2680
3345
|
}
|
|
2681
3346
|
case "complete": {
|
|
2682
|
-
const
|
|
2683
|
-
yield factory.responseCompleted(
|
|
3347
|
+
const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
|
|
3348
|
+
yield factory.responseCompleted({
|
|
3349
|
+
replay: finalResponse.replay,
|
|
3350
|
+
stopReason: finalResponse.stopReason,
|
|
3351
|
+
trace: finalResponse.backend,
|
|
3352
|
+
usage: finalResponse.usage,
|
|
3353
|
+
billing: finalResponse.billing,
|
|
3354
|
+
auxiliary: finalResponse.auxiliary,
|
|
3355
|
+
warnings: finalResponse.warnings
|
|
3356
|
+
});
|
|
2684
3357
|
return;
|
|
2685
3358
|
}
|
|
2686
3359
|
case "error": {
|
|
2687
3360
|
yield factory.responseWarning(step.message, step.code);
|
|
2688
|
-
const
|
|
3361
|
+
const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, {
|
|
2689
3362
|
type: "complete",
|
|
2690
3363
|
stopReason: step.stopReason ?? "error",
|
|
2691
3364
|
providerMetadata: step.providerMetadata
|
|
2692
3365
|
}, stepCount);
|
|
2693
|
-
yield factory.responseCompleted(
|
|
3366
|
+
yield factory.responseCompleted({
|
|
3367
|
+
replay: finalResponse.replay,
|
|
3368
|
+
stopReason: finalResponse.stopReason,
|
|
3369
|
+
trace: finalResponse.backend,
|
|
3370
|
+
usage: finalResponse.usage,
|
|
3371
|
+
billing: finalResponse.billing,
|
|
3372
|
+
auxiliary: finalResponse.auxiliary,
|
|
3373
|
+
warnings: finalResponse.warnings
|
|
3374
|
+
});
|
|
2694
3375
|
return;
|
|
2695
3376
|
}
|
|
2696
3377
|
case "interrupt":
|
|
@@ -2699,8 +3380,16 @@ var MockAdapter = class extends AdapterBase {
|
|
|
2699
3380
|
case "throw": throw typeof step.error === "string" ? new Error(step.error) : step.error;
|
|
2700
3381
|
}
|
|
2701
3382
|
}
|
|
2702
|
-
const
|
|
2703
|
-
yield factory.responseCompleted(
|
|
3383
|
+
const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" }, stepCount);
|
|
3384
|
+
yield factory.responseCompleted({
|
|
3385
|
+
replay: finalResponse.replay,
|
|
3386
|
+
stopReason: finalResponse.stopReason,
|
|
3387
|
+
trace: finalResponse.backend,
|
|
3388
|
+
usage: finalResponse.usage,
|
|
3389
|
+
billing: finalResponse.billing,
|
|
3390
|
+
auxiliary: finalResponse.auxiliary,
|
|
3391
|
+
warnings: finalResponse.warnings
|
|
3392
|
+
});
|
|
2704
3393
|
} finally {
|
|
2705
3394
|
this.activeStream = false;
|
|
2706
3395
|
}
|
|
@@ -2828,10 +3517,11 @@ async function* emitMessage(factory, item, stream) {
|
|
|
2828
3517
|
let chunkIndex = 0;
|
|
2829
3518
|
for (const block of item.content) if (block.type === "text") for (const chunk of chunkText(block.text, stream)) {
|
|
2830
3519
|
await delayForChunk(stream, chunkIndex, chunk.length);
|
|
2831
|
-
yield factory.messageDelta(item.id, chunk);
|
|
3520
|
+
yield factory.messageDelta(item.id, textBlock(chunk));
|
|
2832
3521
|
chunkIndex += 1;
|
|
2833
3522
|
}
|
|
2834
|
-
yield factory.
|
|
3523
|
+
else yield factory.messageDelta(item.id, block);
|
|
3524
|
+
yield factory.messageCompleted(item.id);
|
|
2835
3525
|
}
|
|
2836
3526
|
async function* emitReasoning(factory, item, stream) {
|
|
2837
3527
|
if (!item.id) throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
|
|
@@ -2848,7 +3538,7 @@ async function* emitReasoning(factory, item, stream) {
|
|
|
2848
3538
|
chunkIndex += 1;
|
|
2849
3539
|
}
|
|
2850
3540
|
}
|
|
2851
|
-
yield factory.reasoningCompleted(item);
|
|
3541
|
+
yield factory.reasoningCompleted(item.id);
|
|
2852
3542
|
}
|
|
2853
3543
|
async function* emitToolCall(factory, item, streamArguments, stream) {
|
|
2854
3544
|
yield factory.toolCallStarted(item.id, item.name);
|
|
@@ -2860,7 +3550,7 @@ async function* emitToolCall(factory, item, streamArguments, stream) {
|
|
|
2860
3550
|
chunkIndex += 1;
|
|
2861
3551
|
}
|
|
2862
3552
|
}
|
|
2863
|
-
yield factory.toolCallCompleted(item);
|
|
3553
|
+
yield factory.toolCallCompleted(item.id);
|
|
2864
3554
|
}
|
|
2865
3555
|
function resolveStepStreamOptions(defaults, override, label) {
|
|
2866
3556
|
if (override === false) return;
|
|
@@ -2971,108 +3661,6 @@ function cloneItem(item) {
|
|
|
2971
3661
|
return structuredClone(item);
|
|
2972
3662
|
}
|
|
2973
3663
|
//#endregion
|
|
2974
|
-
|
|
2975
|
-
/**
|
|
2976
|
-
* 模拟流式 (Synthetic Streaming)
|
|
2977
|
-
*
|
|
2978
|
-
* 将一组已解析的 canonical OutputItem 包装为规范事件流。
|
|
2979
|
-
* 适用于非原生流式后端:adapter 拿到完整响应后,调用此函数
|
|
2980
|
-
* 即可产出一致的事件序列,无需自己逐事件组装。
|
|
2981
|
-
*
|
|
2982
|
-
* 约束:
|
|
2983
|
-
* - 每个 item 只发一块完整 delta(不模拟逐 token)
|
|
2984
|
-
* - 保持 item 边界
|
|
2985
|
-
* - 保持后端原始顺序
|
|
2986
|
-
* - 不发明 reasoning
|
|
2987
|
-
* - 不改写工具参数
|
|
2988
|
-
*/
|
|
2989
|
-
/**
|
|
2990
|
-
* 将已解析的 output items 包装为完整规范事件流。
|
|
2991
|
-
*
|
|
2992
|
-
* 用法示例(在 adapter 的 runStream 中):
|
|
2993
|
-
* ```ts
|
|
2994
|
-
* const result = parseNonStreamingResponse(data);
|
|
2995
|
-
* yield* syntheticStream({
|
|
2996
|
-
* model: request.model,
|
|
2997
|
-
* responseId: request.requestId,
|
|
2998
|
-
* backend: { kind: "chat-completions" },
|
|
2999
|
-
* output: result.output,
|
|
3000
|
-
* stopReason: result.stopReason,
|
|
3001
|
-
* usage: result.usage,
|
|
3002
|
-
* });
|
|
3003
|
-
* ```
|
|
3004
|
-
*/
|
|
3005
|
-
async function* syntheticStream(options) {
|
|
3006
|
-
const { model, responseId, backend, output, replay, stopReason, usage, billing, providerMetadata, rawResponseId, warnings: extraWarnings } = options;
|
|
3007
|
-
const factory = createEventFactory({
|
|
3008
|
-
responseId,
|
|
3009
|
-
backend: {
|
|
3010
|
-
kind: backend.kind,
|
|
3011
|
-
isSynthetic: true
|
|
3012
|
-
}
|
|
3013
|
-
});
|
|
3014
|
-
yield factory.responseStarted(model);
|
|
3015
|
-
for (const item of output) yield* emitItemEvents(item, factory);
|
|
3016
|
-
if (usage || billing) yield factory.responseAuxiliary({
|
|
3017
|
-
usage,
|
|
3018
|
-
billing
|
|
3019
|
-
});
|
|
3020
|
-
const finalReplay = replay ?? replayFromOutput(output);
|
|
3021
|
-
const allWarnings = [];
|
|
3022
|
-
allWarnings.push("Response is synthetically streamed; delta granularity may differ from native streaming");
|
|
3023
|
-
if (extraWarnings) allWarnings.push(...extraWarnings);
|
|
3024
|
-
const response = {
|
|
3025
|
-
id: responseId,
|
|
3026
|
-
output,
|
|
3027
|
-
replay: finalReplay,
|
|
3028
|
-
text: extractText(output),
|
|
3029
|
-
toolCalls: output.filter((item) => item.type === "tool_call"),
|
|
3030
|
-
stopReason,
|
|
3031
|
-
usage,
|
|
3032
|
-
billing,
|
|
3033
|
-
auxiliary: providerMetadata ? { providerMetadata } : void 0,
|
|
3034
|
-
warnings: allWarnings.length > 0 ? allWarnings : void 0,
|
|
3035
|
-
backend: {
|
|
3036
|
-
requestId: responseId,
|
|
3037
|
-
rawResponseId,
|
|
3038
|
-
adapter: backend.kind,
|
|
3039
|
-
isSyntheticStream: true
|
|
3040
|
-
}
|
|
3041
|
-
};
|
|
3042
|
-
yield factory.responseCompleted(response);
|
|
3043
|
-
}
|
|
3044
|
-
function* emitItemEvents(item, factory) {
|
|
3045
|
-
switch (item.type) {
|
|
3046
|
-
case "message":
|
|
3047
|
-
yield* emitMessageEvents(item, factory);
|
|
3048
|
-
break;
|
|
3049
|
-
case "reasoning":
|
|
3050
|
-
yield* emitReasoningEvents(item, factory);
|
|
3051
|
-
break;
|
|
3052
|
-
case "tool_call":
|
|
3053
|
-
yield* emitToolCallEvents(item, factory);
|
|
3054
|
-
break;
|
|
3055
|
-
case "opaque": break;
|
|
3056
|
-
}
|
|
3057
|
-
}
|
|
3058
|
-
function* emitMessageEvents(item, factory) {
|
|
3059
|
-
const id = item.id ?? `syn-msg-${crypto.randomUUID()}`;
|
|
3060
|
-
yield factory.messageStarted(id);
|
|
3061
|
-
for (const block of item.content) if (block.type === "text") yield factory.messageDelta(id, block.text);
|
|
3062
|
-
yield factory.messageCompleted(item);
|
|
3063
|
-
}
|
|
3064
|
-
function* emitReasoningEvents(item, factory) {
|
|
3065
|
-
const id = item.id ?? `syn-reason-${crypto.randomUUID()}`;
|
|
3066
|
-
yield factory.reasoningStarted(id, item.visibility);
|
|
3067
|
-
for (const block of item.content) if (block.type === "text") yield factory.reasoningDelta(id, block);
|
|
3068
|
-
yield factory.reasoningCompleted(item);
|
|
3069
|
-
}
|
|
3070
|
-
function* emitToolCallEvents(item, factory) {
|
|
3071
|
-
yield factory.toolCallStarted(item.id, item.name);
|
|
3072
|
-
if (item.argumentsText) yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
|
|
3073
|
-
yield factory.toolCallCompleted(item);
|
|
3074
|
-
}
|
|
3075
|
-
//#endregion
|
|
3076
|
-
export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, ChatCompletionsAdapter, MessagesAdapter, MockAdapter, OllamaAdapter, ResponsesAdapter, WarningCode, aggregateEvents, assertMockRequest, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createEventFactory, emitMalformedStreamWarning, extractText, imageBlock, instructionsToText, jsonBlock, mapReasoningVisibility, mapStopReason, messageItem, metadataSourceList, normalizeRequest, opaqueBlock, opaqueItem, parseSSEEvents, reasoningItem, replayFromOutput, syntheticStream, textBlock, toolCallItem, toolResultItem, usageFromAnthropicMessages, usageFromChatCompletions, usageFromOllama, usageFromOpenAIResponses, validateRequest, withMockStreaming };
|
|
3664
|
+
export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, ChatCompletionsAdapter, IncrementalStreamParser, MAX_OPAQUE_JSON_DEPTH, MAX_OPAQUE_PAYLOAD_BYTES, MessagesAdapter, MockAdapter, NormalizedRequestMapper, OllamaAdapter, PROVIDER_ERROR_MESSAGE_MAX_LEN, PROVIDER_ERROR_RAW_BODY_THRESHOLD, ResponsesAdapter, WarningCode, aggregateEvents, assertMockRequest, assertOpaqueReplayEnvelope, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createEventFactory, emitMalformedStreamWarning, extractProviderErrorMessage, extractText, imageBlock, instructionsToText, jsonBlock, mapReasoningVisibility, mapStopReason, measureJsonDepth, messageItem, metadataSourceList, normalizeRequest, opaqueBlock, opaqueItem, parseSSEEvents, providerHttpError, reasoningItem, replayFromOutput, splitLines, splitSSEFrames, syntheticStream, textBlock, toolCallItem, toolResultItem, usageFromAnthropicMessages, usageFromChatCompletions, usageFromOllama, usageFromOpenAIResponses, validateOpaqueReplayEnvelope, validateRequest, withMockStreaming };
|
|
3077
3665
|
|
|
3078
3666
|
//# sourceMappingURL=index.mjs.map
|