@codehz/ai 0.2.0 → 0.2.1
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 +1284 -702
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +243 -196
- 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 +218 -61
- 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,45 @@ 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
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function getActiveItem(state, itemId, expectedType) {
|
|
490
|
+
const item = state.activeItems.get(itemId);
|
|
491
|
+
if (!item) throw streamProtocolError(`Received ${expectedType} delta/completed for unknown item: ${itemId}`);
|
|
492
|
+
if (item.type !== expectedType) throw streamProtocolError(`Item ${itemId} started as ${item.type} but received ${expectedType} event`);
|
|
493
|
+
return item;
|
|
494
|
+
}
|
|
495
|
+
function finalizeMessage(active) {
|
|
496
|
+
return {
|
|
497
|
+
type: "message",
|
|
498
|
+
id: active.id,
|
|
499
|
+
role: active.role,
|
|
500
|
+
content: active.content
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function finalizeReasoning(active) {
|
|
504
|
+
return {
|
|
505
|
+
type: "reasoning",
|
|
506
|
+
id: active.id,
|
|
507
|
+
visibility: active.visibility,
|
|
508
|
+
content: active.content
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function finalizeToolCall(active) {
|
|
512
|
+
return {
|
|
513
|
+
type: "tool_call",
|
|
514
|
+
id: active.id,
|
|
515
|
+
name: active.name,
|
|
516
|
+
argumentsText: active.argumentsText
|
|
458
517
|
};
|
|
459
518
|
}
|
|
460
519
|
function handleResponseStarted(state, event) {
|
|
520
|
+
if (state.started) throw streamProtocolError("Stream must contain exactly one response.started event");
|
|
521
|
+
state.started = true;
|
|
461
522
|
state.responseId = event.responseId;
|
|
462
523
|
state.model = event.model;
|
|
463
524
|
state.backendInfo = event.backend;
|
|
@@ -474,47 +535,97 @@ function handleResponseAuxiliary(state, event) {
|
|
|
474
535
|
...state.billing,
|
|
475
536
|
...event.billing
|
|
476
537
|
};
|
|
477
|
-
if (event.auxiliary) state.auxiliary = mergeAuxiliary
|
|
538
|
+
if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
|
|
539
|
+
}
|
|
540
|
+
function handleMessageStarted(state, event) {
|
|
541
|
+
const id = event.item.id;
|
|
542
|
+
if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
|
|
543
|
+
state.activeItems.set(id, {
|
|
544
|
+
type: "message",
|
|
545
|
+
id,
|
|
546
|
+
role: event.item.role,
|
|
547
|
+
content: []
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
function handleMessageDelta(state, event) {
|
|
551
|
+
getActiveItem(state, event.itemId, "message").content.push(event.delta);
|
|
478
552
|
}
|
|
479
553
|
function handleMessageCompleted(state, event) {
|
|
480
|
-
state
|
|
481
|
-
|
|
554
|
+
const active = getActiveItem(state, event.itemId, "message");
|
|
555
|
+
state.activeItems.delete(event.itemId);
|
|
556
|
+
const item = finalizeMessage(active);
|
|
557
|
+
state.output.push(item);
|
|
558
|
+
pushMessageText(state, item);
|
|
559
|
+
}
|
|
560
|
+
function handleReasoningStarted(state, event) {
|
|
561
|
+
const id = event.item.id;
|
|
562
|
+
if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
|
|
563
|
+
state.activeItems.set(id, {
|
|
564
|
+
type: "reasoning",
|
|
565
|
+
id,
|
|
566
|
+
visibility: event.item.visibility,
|
|
567
|
+
content: []
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
function handleReasoningDelta(state, event) {
|
|
571
|
+
getActiveItem(state, event.itemId, "reasoning").content.push(event.delta);
|
|
482
572
|
}
|
|
483
573
|
function handleReasoningCompleted(state, event) {
|
|
484
|
-
state
|
|
574
|
+
const active = getActiveItem(state, event.itemId, "reasoning");
|
|
575
|
+
state.activeItems.delete(event.itemId);
|
|
576
|
+
state.output.push(finalizeReasoning(active));
|
|
577
|
+
}
|
|
578
|
+
function handleToolCallStarted(state, event) {
|
|
579
|
+
const id = event.item.id;
|
|
580
|
+
if (state.activeItems.has(id)) throw streamProtocolError(`Item with id ${id} is already active`);
|
|
581
|
+
state.activeItems.set(id, {
|
|
582
|
+
type: "tool_call",
|
|
583
|
+
id,
|
|
584
|
+
name: event.item.name,
|
|
585
|
+
argumentsText: ""
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function handleToolCallDelta(state, event) {
|
|
589
|
+
const active = getActiveItem(state, event.itemId, "tool_call");
|
|
590
|
+
if (event.delta.argumentsText) active.argumentsText += event.delta.argumentsText;
|
|
485
591
|
}
|
|
486
592
|
function handleToolCallCompleted(state, event) {
|
|
487
|
-
state
|
|
488
|
-
state.
|
|
593
|
+
const active = getActiveItem(state, event.itemId, "tool_call");
|
|
594
|
+
state.activeItems.delete(event.itemId);
|
|
595
|
+
const item = finalizeToolCall(active);
|
|
596
|
+
state.output.push(item);
|
|
597
|
+
state.toolCalls.push(item);
|
|
489
598
|
}
|
|
490
599
|
function handleResponseCompleted(state, event) {
|
|
491
|
-
state.
|
|
492
|
-
state.
|
|
493
|
-
state.
|
|
494
|
-
state.
|
|
495
|
-
|
|
600
|
+
if (state.activeItems.size > 0) throw streamProtocolError("response.completed received while active items still pending");
|
|
601
|
+
state.completed = true;
|
|
602
|
+
state.replayFromAdapter = event.replay;
|
|
603
|
+
state.stopReasonFromAdapter = event.stopReason;
|
|
604
|
+
state.backendFromAdapter = event.trace;
|
|
605
|
+
if (event.usage) state.usage = {
|
|
496
606
|
...state.usage,
|
|
497
|
-
...event.
|
|
607
|
+
...event.usage
|
|
498
608
|
};
|
|
499
|
-
if (event.
|
|
609
|
+
if (event.billing) state.billing = {
|
|
500
610
|
...state.billing,
|
|
501
|
-
...event.
|
|
611
|
+
...event.billing
|
|
502
612
|
};
|
|
503
|
-
if (event.
|
|
504
|
-
if (event.
|
|
613
|
+
if (event.auxiliary) state.auxiliary = mergeAuxiliary(state.auxiliary, event.auxiliary) ?? {};
|
|
614
|
+
if (event.warnings) pushWarnings(state, event.warnings);
|
|
615
|
+
if (event.opaqueOutput) state.output.push(...event.opaqueOutput);
|
|
505
616
|
}
|
|
506
617
|
function buildResponse(state) {
|
|
507
|
-
const
|
|
618
|
+
const backendFromCompleted = state.backendFromAdapter;
|
|
508
619
|
const backend = {
|
|
509
|
-
adapter:
|
|
510
|
-
isSyntheticStream:
|
|
511
|
-
requestId:
|
|
512
|
-
rawResponseId:
|
|
513
|
-
metadataSources:
|
|
514
|
-
warnings:
|
|
620
|
+
adapter: backendFromCompleted?.adapter ?? state.backendInfo?.kind ?? "unknown",
|
|
621
|
+
isSyntheticStream: backendFromCompleted?.isSyntheticStream ?? state.backendInfo?.isSynthetic ?? false,
|
|
622
|
+
requestId: backendFromCompleted?.requestId ?? state.responseId,
|
|
623
|
+
rawResponseId: backendFromCompleted?.rawResponseId,
|
|
624
|
+
metadataSources: backendFromCompleted?.metadataSources,
|
|
625
|
+
warnings: backendFromCompleted?.warnings
|
|
515
626
|
};
|
|
516
627
|
return {
|
|
517
|
-
id: state.
|
|
628
|
+
id: state.responseId,
|
|
518
629
|
output: state.output,
|
|
519
630
|
replay: state.replayFromAdapter ?? [],
|
|
520
631
|
text: state.textParts.join(""),
|
|
@@ -537,6 +648,7 @@ function aggregateEvents(events) {
|
|
|
537
648
|
return finalizeAggregation(state);
|
|
538
649
|
}
|
|
539
650
|
function aggregateEvent(state, event) {
|
|
651
|
+
validateEventEnvelope(state, event);
|
|
540
652
|
state.lastEventType = event.type;
|
|
541
653
|
switch (event.type) {
|
|
542
654
|
case "response.started":
|
|
@@ -549,17 +661,29 @@ function aggregateEvent(state, event) {
|
|
|
549
661
|
handleResponseAuxiliary(state, event);
|
|
550
662
|
break;
|
|
551
663
|
case "message.started":
|
|
664
|
+
handleMessageStarted(state, event);
|
|
665
|
+
break;
|
|
552
666
|
case "message.delta":
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
case "tool_call.started":
|
|
556
|
-
case "tool_call.delta": break;
|
|
667
|
+
handleMessageDelta(state, event);
|
|
668
|
+
break;
|
|
557
669
|
case "message.completed":
|
|
558
670
|
handleMessageCompleted(state, event);
|
|
559
671
|
break;
|
|
672
|
+
case "reasoning.started":
|
|
673
|
+
handleReasoningStarted(state, event);
|
|
674
|
+
break;
|
|
675
|
+
case "reasoning.delta":
|
|
676
|
+
handleReasoningDelta(state, event);
|
|
677
|
+
break;
|
|
560
678
|
case "reasoning.completed":
|
|
561
679
|
handleReasoningCompleted(state, event);
|
|
562
680
|
break;
|
|
681
|
+
case "tool_call.started":
|
|
682
|
+
handleToolCallStarted(state, event);
|
|
683
|
+
break;
|
|
684
|
+
case "tool_call.delta":
|
|
685
|
+
handleToolCallDelta(state, event);
|
|
686
|
+
break;
|
|
563
687
|
case "tool_call.completed":
|
|
564
688
|
handleToolCallCompleted(state, event);
|
|
565
689
|
break;
|
|
@@ -569,20 +693,10 @@ function aggregateEvent(state, event) {
|
|
|
569
693
|
}
|
|
570
694
|
}
|
|
571
695
|
function finalizeAggregation(state) {
|
|
572
|
-
if (state.
|
|
696
|
+
if (!state.started) throw streamProtocolError("Stream must start with response.started event");
|
|
697
|
+
if (!state.completed || state.lastEventType !== "response.completed") throw streamProtocolError("Stream must end with response.completed event to produce a valid AIResponse");
|
|
573
698
|
return buildResponse(state);
|
|
574
699
|
}
|
|
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
700
|
function pushWarnings(state, warnings) {
|
|
587
701
|
for (const warning of warnings) if (!state.warningSet.has(warning)) {
|
|
588
702
|
state.warningSet.add(warning);
|
|
@@ -592,6 +706,16 @@ function pushWarnings(state, warnings) {
|
|
|
592
706
|
function pushMessageText(state, item) {
|
|
593
707
|
for (const block of item.content) if (block.type === "text") state.textParts.push(block.text);
|
|
594
708
|
}
|
|
709
|
+
function validateEventEnvelope(state, event) {
|
|
710
|
+
if (state.completed) throw streamProtocolError("response.completed must be the final stream event");
|
|
711
|
+
if (!state.started && event.type !== "response.started") throw streamProtocolError("Stream must start with response.started event");
|
|
712
|
+
if (state.responseId !== void 0 && event.responseId !== state.responseId) throw streamProtocolError("All stream events must use the same responseId");
|
|
713
|
+
if (state.nextSequence !== void 0 && event.sequence !== state.nextSequence) throw streamProtocolError(`Expected event sequence ${state.nextSequence}, received ${event.sequence}`);
|
|
714
|
+
state.nextSequence = event.sequence + 1;
|
|
715
|
+
}
|
|
716
|
+
function streamProtocolError(message) {
|
|
717
|
+
return new AIStreamError(message, "STREAM_PROTOCOL_ERROR");
|
|
718
|
+
}
|
|
595
719
|
//#endregion
|
|
596
720
|
//#region src/core/collect-stream.ts
|
|
597
721
|
async function collectStream(stream) {
|
|
@@ -937,7 +1061,7 @@ var AdapterBase = class {
|
|
|
937
1061
|
responseId: request.requestId,
|
|
938
1062
|
backend: {
|
|
939
1063
|
kind: this.kind,
|
|
940
|
-
isSynthetic:
|
|
1064
|
+
isSynthetic: this.capabilities.textStreaming === "synthetic"
|
|
941
1065
|
}
|
|
942
1066
|
});
|
|
943
1067
|
yield factory.responseStarted(request.model);
|
|
@@ -945,12 +1069,25 @@ var AdapterBase = class {
|
|
|
945
1069
|
const providerRequest = await this.buildRequest(request);
|
|
946
1070
|
yield* this.runStream(providerRequest, factory, request);
|
|
947
1071
|
} catch (err) {
|
|
948
|
-
if (err instanceof AIRequestError || err instanceof
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1072
|
+
if (err instanceof AIRequestError || err instanceof AIProviderError || err instanceof AIStreamError) throw err;
|
|
1073
|
+
if (err instanceof AIMappingError) {
|
|
1074
|
+
yield factory.responseWarning(err.message, "MAPPING_ERROR");
|
|
1075
|
+
const errorResp = this.buildResponse(request, {
|
|
1076
|
+
output: [],
|
|
1077
|
+
replay: []
|
|
1078
|
+
}, factory);
|
|
1079
|
+
yield factory.responseCompleted({
|
|
1080
|
+
replay: errorResp.replay,
|
|
1081
|
+
stopReason: errorResp.stopReason,
|
|
1082
|
+
trace: errorResp.backend,
|
|
1083
|
+
usage: errorResp.usage,
|
|
1084
|
+
billing: errorResp.billing,
|
|
1085
|
+
auxiliary: errorResp.auxiliary,
|
|
1086
|
+
warnings: errorResp.warnings
|
|
1087
|
+
});
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
throw err;
|
|
954
1091
|
}
|
|
955
1092
|
}
|
|
956
1093
|
/**
|
|
@@ -976,7 +1113,7 @@ var AdapterBase = class {
|
|
|
976
1113
|
requestId: request.requestId,
|
|
977
1114
|
rawResponseId: result.rawResponseId,
|
|
978
1115
|
adapter: this.kind,
|
|
979
|
-
isSyntheticStream:
|
|
1116
|
+
isSyntheticStream: this.capabilities.textStreaming === "synthetic",
|
|
980
1117
|
metadataSources: result.metadataSources,
|
|
981
1118
|
warnings
|
|
982
1119
|
}
|
|
@@ -990,18 +1127,6 @@ var AdapterBase = class {
|
|
|
990
1127
|
return new AdapterAuxiliaryState(request);
|
|
991
1128
|
}
|
|
992
1129
|
};
|
|
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
1130
|
function mergeWarnings(...groups) {
|
|
1006
1131
|
const merged = [];
|
|
1007
1132
|
for (const group of groups) {
|
|
@@ -1011,6 +1136,91 @@ function mergeWarnings(...groups) {
|
|
|
1011
1136
|
return merged.length > 0 ? merged : void 0;
|
|
1012
1137
|
}
|
|
1013
1138
|
//#endregion
|
|
1139
|
+
//#region src/helpers/adapter-security.ts
|
|
1140
|
+
/**
|
|
1141
|
+
* Adapter 边界安全辅助
|
|
1142
|
+
*
|
|
1143
|
+
* - opaque replay 入站 envelope(大小 / 深度)
|
|
1144
|
+
* - provider HTTP 错误 body 出站脱敏
|
|
1145
|
+
*/
|
|
1146
|
+
const MAX_OPAQUE_PAYLOAD_BYTES = 65536;
|
|
1147
|
+
const MAX_OPAQUE_JSON_DEPTH = 8;
|
|
1148
|
+
const PROVIDER_ERROR_MESSAGE_MAX_LEN = 500;
|
|
1149
|
+
const PROVIDER_ERROR_RAW_BODY_THRESHOLD = 200;
|
|
1150
|
+
/** 测量 JSON 值嵌套深度(对象/数组);循环引用按已访问节点深度计。 */
|
|
1151
|
+
function measureJsonDepth(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1152
|
+
if (value === null || typeof value !== "object") return 0;
|
|
1153
|
+
if (seen.has(value)) return 0;
|
|
1154
|
+
seen.add(value);
|
|
1155
|
+
let maxChild = 0;
|
|
1156
|
+
if (Array.isArray(value)) for (const item of value) maxChild = Math.max(maxChild, measureJsonDepth(item, seen));
|
|
1157
|
+
else for (const key of Object.keys(value)) maxChild = Math.max(maxChild, measureJsonDepth(value[key], seen));
|
|
1158
|
+
return 1 + maxChild;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Opaque replay 通用 envelope:必须是 object、体积 ≤ 64KB、深度 ≤ 8。
|
|
1162
|
+
* 不校验 adapter 专用字段形状。
|
|
1163
|
+
*/
|
|
1164
|
+
function validateOpaqueReplayEnvelope(payload) {
|
|
1165
|
+
if (typeof payload !== "object" || payload === null) return {
|
|
1166
|
+
ok: false,
|
|
1167
|
+
reason: "payload must be an object"
|
|
1168
|
+
};
|
|
1169
|
+
let raw;
|
|
1170
|
+
try {
|
|
1171
|
+
raw = JSON.stringify(payload);
|
|
1172
|
+
} catch {
|
|
1173
|
+
return {
|
|
1174
|
+
ok: false,
|
|
1175
|
+
reason: "payload is not JSON-serializable"
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
if (raw === void 0) return {
|
|
1179
|
+
ok: false,
|
|
1180
|
+
reason: "payload is not JSON-serializable"
|
|
1181
|
+
};
|
|
1182
|
+
if (raw.length > 65536) return {
|
|
1183
|
+
ok: false,
|
|
1184
|
+
reason: `opaque payload exceeds max size (${raw.length} > ${MAX_OPAQUE_PAYLOAD_BYTES})`
|
|
1185
|
+
};
|
|
1186
|
+
const depth = measureJsonDepth(payload);
|
|
1187
|
+
if (depth > 8) return {
|
|
1188
|
+
ok: false,
|
|
1189
|
+
reason: `opaque payload nesting depth (${depth}) exceeds max (8)`
|
|
1190
|
+
};
|
|
1191
|
+
return { ok: true };
|
|
1192
|
+
}
|
|
1193
|
+
/** envelope 失败时抛 AIRequestError。 */
|
|
1194
|
+
function assertOpaqueReplayEnvelope(payload) {
|
|
1195
|
+
const result = validateOpaqueReplayEnvelope(payload);
|
|
1196
|
+
if (!result.ok) throw new AIRequestError(`Invalid opaque replay payload: ${result.reason}`, "INVALID_OPAQUE_REPLAY");
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* 从 provider HTTP 错误 body 提取可对外暴露的短消息,避免泄漏 HTML / 内部路径等。
|
|
1200
|
+
*/
|
|
1201
|
+
function extractProviderErrorMessage(body, status) {
|
|
1202
|
+
if (!body) return `HTTP ${status}`;
|
|
1203
|
+
try {
|
|
1204
|
+
const parsed = JSON.parse(body);
|
|
1205
|
+
if (parsed && typeof parsed === "object") {
|
|
1206
|
+
const record = parsed;
|
|
1207
|
+
const errorField = record.error;
|
|
1208
|
+
let msg;
|
|
1209
|
+
if (errorField && typeof errorField === "object" && errorField !== null) msg = errorField.message;
|
|
1210
|
+
if (typeof msg !== "string") msg = typeof errorField === "string" ? errorField : record.message;
|
|
1211
|
+
if (typeof msg === "string" && msg.length > 0) return msg.slice(0, 500);
|
|
1212
|
+
}
|
|
1213
|
+
} catch {}
|
|
1214
|
+
const trimmed = body.trimStart();
|
|
1215
|
+
if (trimmed.startsWith("<!") || trimmed.startsWith("<html") || body.length > 200) return `HTTP ${status}. Body omitted (${body.length} bytes)`;
|
|
1216
|
+
return body.slice(0, 500);
|
|
1217
|
+
}
|
|
1218
|
+
/** 统一构造脱敏后的 AIProviderError。 */
|
|
1219
|
+
function providerHttpError(status, body) {
|
|
1220
|
+
const safe = extractProviderErrorMessage(body, status);
|
|
1221
|
+
return new AIProviderError(`Provider returned ${status}: ${safe}`, "PROVIDER_ERROR", status, safe);
|
|
1222
|
+
}
|
|
1223
|
+
//#endregion
|
|
1014
1224
|
//#region src/helpers/usage-mapping.ts
|
|
1015
1225
|
function num(value) {
|
|
1016
1226
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
@@ -1090,43 +1300,55 @@ function usageFromOllama(raw) {
|
|
|
1090
1300
|
* - 未完成的行保留在 rest 中,等待下次 chunk 补全
|
|
1091
1301
|
* - 支持跨 chunk 的 event 分片
|
|
1092
1302
|
*/
|
|
1093
|
-
function parseSSEEvents(chunk) {
|
|
1303
|
+
function parseSSEEvents(chunk, options = {}) {
|
|
1094
1304
|
const events = [];
|
|
1095
1305
|
let eventType = "";
|
|
1096
1306
|
let dataLines = [];
|
|
1097
1307
|
let consumedUntil = 0;
|
|
1098
1308
|
let cursor = 0;
|
|
1099
1309
|
let malformedEvents = 0;
|
|
1310
|
+
const emitEvent = (consumedCursor) => {
|
|
1311
|
+
const dataStr = dataLines.join("\n");
|
|
1312
|
+
if (dataStr === "[DONE]") {
|
|
1313
|
+
eventType = "";
|
|
1314
|
+
dataLines = [];
|
|
1315
|
+
consumedUntil = consumedCursor;
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
try {
|
|
1319
|
+
const data = JSON.parse(dataStr);
|
|
1320
|
+
events.push({
|
|
1321
|
+
type: eventType,
|
|
1322
|
+
data
|
|
1323
|
+
});
|
|
1324
|
+
} catch {
|
|
1325
|
+
malformedEvents++;
|
|
1326
|
+
}
|
|
1327
|
+
eventType = "";
|
|
1328
|
+
dataLines = [];
|
|
1329
|
+
consumedUntil = consumedCursor;
|
|
1330
|
+
};
|
|
1331
|
+
const consumeLine = (line, consumedCursor) => {
|
|
1332
|
+
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
1333
|
+
else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
|
|
1334
|
+
else if (line === "" && eventType && dataLines.length > 0) emitEvent(consumedCursor);
|
|
1335
|
+
else if (line === "" && !eventType && dataLines.length === 0) consumedUntil = consumedCursor;
|
|
1336
|
+
};
|
|
1100
1337
|
while (cursor < chunk.length) {
|
|
1101
1338
|
const lineEnd = chunk.indexOf("\n", cursor);
|
|
1102
1339
|
if (lineEnd === -1) break;
|
|
1103
1340
|
let line = chunk.slice(cursor, lineEnd);
|
|
1104
1341
|
cursor = lineEnd + 1;
|
|
1105
1342
|
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;
|
|
1343
|
+
consumeLine(line, cursor);
|
|
1344
|
+
}
|
|
1345
|
+
if (options.allowEOF && cursor < chunk.length) {
|
|
1346
|
+
let line = chunk.slice(cursor);
|
|
1347
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
1348
|
+
consumeLine(line, chunk.length);
|
|
1349
|
+
cursor = chunk.length;
|
|
1129
1350
|
}
|
|
1351
|
+
if (options.allowEOF && eventType && dataLines.length > 0) emitEvent(chunk.length);
|
|
1130
1352
|
return {
|
|
1131
1353
|
events,
|
|
1132
1354
|
rest: chunk.slice(consumedUntil),
|
|
@@ -1134,71 +1356,288 @@ function parseSSEEvents(chunk) {
|
|
|
1134
1356
|
};
|
|
1135
1357
|
}
|
|
1136
1358
|
//#endregion
|
|
1137
|
-
//#region src/
|
|
1359
|
+
//#region src/helpers/synthetic-stream.ts
|
|
1138
1360
|
/**
|
|
1139
|
-
*
|
|
1361
|
+
* 模拟流式 (Synthetic Streaming)
|
|
1140
1362
|
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
* 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
|
|
1363
|
+
* 将一组已解析的 canonical OutputItem 包装为规范事件流。
|
|
1364
|
+
* 适用于非原生流式后端:adapter 拿到完整响应后,调用此函数
|
|
1365
|
+
* 即可产出一致的事件序列,无需自己逐事件组装。
|
|
1145
1366
|
*
|
|
1146
|
-
*
|
|
1367
|
+
* 约束:
|
|
1368
|
+
* - 每个 item 只发一块完整 delta(不模拟逐 token)
|
|
1369
|
+
* - 保持 item 边界
|
|
1370
|
+
* - 保持后端原始顺序
|
|
1371
|
+
* - 不发明 reasoning
|
|
1372
|
+
* - 不改写工具参数
|
|
1373
|
+
*/
|
|
1374
|
+
/**
|
|
1375
|
+
* 将已解析的 output items 包装为完整规范事件流。
|
|
1376
|
+
*
|
|
1377
|
+
* 用法示例(在 adapter 的 runStream 中):
|
|
1378
|
+
* ```ts
|
|
1379
|
+
* const result = parseNonStreamingResponse(data);
|
|
1380
|
+
* yield* syntheticStream({
|
|
1381
|
+
* model: request.model,
|
|
1382
|
+
* responseId: request.requestId,
|
|
1383
|
+
* backend: { kind: "chat-completions" },
|
|
1384
|
+
* output: result.output,
|
|
1385
|
+
* stopReason: result.stopReason,
|
|
1386
|
+
* usage: result.usage,
|
|
1387
|
+
* });
|
|
1388
|
+
* ```
|
|
1147
1389
|
*/
|
|
1148
|
-
function
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1390
|
+
async function* syntheticStream(options) {
|
|
1391
|
+
const { model, responseId, backend, output, replay, stopReason, usage, billing, providerMetadata, rawResponseId, warnings: extraWarnings } = options;
|
|
1392
|
+
const factory = createEventFactory({
|
|
1393
|
+
responseId,
|
|
1394
|
+
backend: {
|
|
1395
|
+
kind: backend.kind,
|
|
1396
|
+
isSynthetic: true
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
yield factory.responseStarted(model);
|
|
1400
|
+
for (const item of output) yield* emitItemEvents(item, factory);
|
|
1401
|
+
if (usage || billing) yield factory.responseAuxiliary({
|
|
1402
|
+
usage,
|
|
1403
|
+
billing
|
|
1404
|
+
});
|
|
1405
|
+
const finalReplay = replay ?? replayFromOutput(output);
|
|
1406
|
+
const allWarnings = [];
|
|
1407
|
+
allWarnings.push("Response is synthetically streamed; delta granularity may differ from native streaming");
|
|
1408
|
+
if (extraWarnings) allWarnings.push(...extraWarnings);
|
|
1409
|
+
yield factory.responseCompleted({
|
|
1410
|
+
replay: finalReplay,
|
|
1411
|
+
stopReason,
|
|
1412
|
+
usage,
|
|
1413
|
+
billing,
|
|
1414
|
+
auxiliary: providerMetadata ? { providerMetadata } : void 0,
|
|
1415
|
+
opaqueOutput: output.filter((item) => item.type === "opaque"),
|
|
1416
|
+
warnings: allWarnings.length > 0 ? allWarnings : void 0,
|
|
1417
|
+
trace: {
|
|
1418
|
+
requestId: responseId,
|
|
1419
|
+
rawResponseId,
|
|
1420
|
+
adapter: backend.kind,
|
|
1421
|
+
isSyntheticStream: true,
|
|
1422
|
+
warnings: allWarnings.length > 0 ? allWarnings : void 0
|
|
1423
|
+
}
|
|
1160
1424
|
});
|
|
1161
1425
|
}
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1426
|
+
function* emitItemEvents(item, factory) {
|
|
1427
|
+
switch (item.type) {
|
|
1428
|
+
case "message":
|
|
1429
|
+
yield* emitMessageEvents(item, factory);
|
|
1430
|
+
break;
|
|
1431
|
+
case "reasoning":
|
|
1432
|
+
yield* emitReasoningEvents(item, factory);
|
|
1433
|
+
break;
|
|
1434
|
+
case "tool_call":
|
|
1435
|
+
yield* emitToolCallEvents(item, factory);
|
|
1436
|
+
break;
|
|
1437
|
+
case "opaque": break;
|
|
1438
|
+
}
|
|
1164
1439
|
}
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1440
|
+
function* emitMessageEvents(item, factory) {
|
|
1441
|
+
const id = item.id ?? `syn-msg-${crypto.randomUUID()}`;
|
|
1442
|
+
yield factory.messageStarted(id);
|
|
1443
|
+
for (const block of item.content) yield factory.messageDelta(id, block);
|
|
1444
|
+
yield factory.messageCompleted(id);
|
|
1167
1445
|
}
|
|
1168
|
-
function
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
malformedEvents: result.malformedEvents
|
|
1174
|
-
};
|
|
1446
|
+
function* emitReasoningEvents(item, factory) {
|
|
1447
|
+
const id = item.id ?? `syn-reason-${crypto.randomUUID()}`;
|
|
1448
|
+
yield factory.reasoningStarted(id, item.visibility);
|
|
1449
|
+
for (const block of item.content) yield factory.reasoningDelta(id, block);
|
|
1450
|
+
yield factory.reasoningCompleted(id);
|
|
1175
1451
|
}
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1452
|
+
function* emitToolCallEvents(item, factory) {
|
|
1453
|
+
yield factory.toolCallStarted(item.id, item.name);
|
|
1454
|
+
if (item.argumentsText) yield factory.toolCallDelta(item.id, { argumentsText: item.argumentsText });
|
|
1455
|
+
yield factory.toolCallCompleted(item.id);
|
|
1178
1456
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1457
|
+
//#endregion
|
|
1458
|
+
//#region src/helpers/incremental-stream-parser.ts
|
|
1459
|
+
var IncrementalStreamParser = class {
|
|
1460
|
+
split;
|
|
1461
|
+
parse;
|
|
1462
|
+
buffer = "";
|
|
1463
|
+
decoder = new TextDecoder();
|
|
1464
|
+
constructor(split, parse) {
|
|
1465
|
+
this.split = split;
|
|
1466
|
+
this.parse = parse;
|
|
1467
|
+
}
|
|
1468
|
+
feed(value) {
|
|
1469
|
+
this.buffer += this.decoder.decode(value, { stream: true });
|
|
1470
|
+
return this.consume(false);
|
|
1471
|
+
}
|
|
1472
|
+
flush() {
|
|
1473
|
+
this.buffer += this.decoder.decode();
|
|
1474
|
+
return this.consume(true);
|
|
1475
|
+
}
|
|
1476
|
+
getRemaining() {
|
|
1477
|
+
return this.buffer;
|
|
1478
|
+
}
|
|
1479
|
+
consume(allowEOF) {
|
|
1480
|
+
const split = this.split(this.buffer, allowEOF);
|
|
1481
|
+
this.buffer = split.rest;
|
|
1482
|
+
const items = [];
|
|
1483
|
+
let malformed = 0;
|
|
1484
|
+
for (const rawItem of split.items) {
|
|
1485
|
+
const result = this.parse(rawItem);
|
|
1486
|
+
if (result.status === "parsed") items.push(result.value);
|
|
1487
|
+
else if (result.status === "malformed") malformed++;
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
items,
|
|
1491
|
+
malformed
|
|
1492
|
+
};
|
|
1184
1493
|
}
|
|
1185
|
-
}
|
|
1186
|
-
function
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1494
|
+
};
|
|
1495
|
+
function splitLines(buffer, allowEOF) {
|
|
1496
|
+
const items = [];
|
|
1497
|
+
let cursor = 0;
|
|
1498
|
+
while (true) {
|
|
1499
|
+
const lineEnd = buffer.indexOf("\n", cursor);
|
|
1500
|
+
if (lineEnd === -1) break;
|
|
1501
|
+
items.push(buffer.slice(cursor, lineEnd));
|
|
1502
|
+
cursor = lineEnd + 1;
|
|
1503
|
+
}
|
|
1504
|
+
if (allowEOF && cursor < buffer.length) {
|
|
1505
|
+
items.push(buffer.slice(cursor));
|
|
1506
|
+
cursor = buffer.length;
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
items,
|
|
1510
|
+
rest: buffer.slice(cursor)
|
|
1190
1511
|
};
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1512
|
+
}
|
|
1513
|
+
function splitSSEFrames(buffer, allowEOF) {
|
|
1514
|
+
const normalized = buffer.replaceAll("\r\n", "\n");
|
|
1515
|
+
const items = [];
|
|
1516
|
+
let cursor = 0;
|
|
1517
|
+
while (true) {
|
|
1518
|
+
const frameEnd = normalized.indexOf("\n\n", cursor);
|
|
1519
|
+
if (frameEnd === -1) break;
|
|
1520
|
+
items.push(normalized.slice(cursor, frameEnd));
|
|
1521
|
+
cursor = frameEnd + 2;
|
|
1522
|
+
}
|
|
1523
|
+
if (allowEOF && cursor < normalized.length) {
|
|
1524
|
+
items.push(normalized.slice(cursor));
|
|
1525
|
+
cursor = normalized.length;
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
items,
|
|
1529
|
+
rest: normalized.slice(cursor)
|
|
1194
1530
|
};
|
|
1195
|
-
throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
1196
1531
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1532
|
+
//#endregion
|
|
1533
|
+
//#region src/helpers/request-mapper.ts
|
|
1534
|
+
var NormalizedRequestMapper = class {
|
|
1535
|
+
profile;
|
|
1536
|
+
constructor(profile) {
|
|
1537
|
+
this.profile = profile;
|
|
1538
|
+
}
|
|
1539
|
+
mapInstructions(instructions) {
|
|
1540
|
+
return typeof instructions === "string" ? instructions : contentBlocksToText(this.ensureTextBlocks(instructions, "instructions"));
|
|
1541
|
+
}
|
|
1542
|
+
ensureTextBlocks(blocks, field) {
|
|
1543
|
+
return this.ensureBlocks(blocks, field, this.profile.supportedBlockTypes, "only text/json blocks are supported");
|
|
1544
|
+
}
|
|
1545
|
+
ensureReasoningBlocks(blocks, field) {
|
|
1546
|
+
return this.ensureBlocks(blocks, field, this.profile.reasoningBlockTypes, "reasoning only supports text blocks");
|
|
1547
|
+
}
|
|
1548
|
+
assertToolResultOutcome(outcome) {
|
|
1549
|
+
if (this.profile.capabilities.toolResultOutcomes.includes(outcome)) return;
|
|
1550
|
+
const outcomes = this.profile.capabilities.toolResultOutcomes;
|
|
1551
|
+
const supported = outcomes.map((value) => `"${value}"`).join(" and ");
|
|
1552
|
+
const verb = outcomes.length > 1 ? "are" : "is";
|
|
1553
|
+
throw new AIRequestError(`${this.profile.kind} does not preserve tool_result outcome "${outcome}"; only ${supported} ${verb} supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
|
|
1554
|
+
}
|
|
1555
|
+
rollbackTrailingAssistantMessages(messages) {
|
|
1556
|
+
while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
|
|
1557
|
+
}
|
|
1558
|
+
ensureBlocks(blocks, field, supportedTypes, description) {
|
|
1559
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
1560
|
+
const block = blocks[i];
|
|
1561
|
+
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");
|
|
1562
|
+
}
|
|
1563
|
+
return blocks;
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
//#endregion
|
|
1567
|
+
//#region src/adapters/responses.ts
|
|
1568
|
+
/**
|
|
1569
|
+
* Responses Adapter
|
|
1570
|
+
*
|
|
1571
|
+
* 接入 OpenAI Responses API (responses 端点)。
|
|
1572
|
+
* 职责分层:
|
|
1573
|
+
* 1. buildRequest — 将 NormalizedRequest 转换为 Responses API 请求
|
|
1574
|
+
* 2. runStream — 调用 API、解析 SSE、发射 canonical 事件
|
|
1575
|
+
*
|
|
1576
|
+
* 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
|
|
1577
|
+
*/
|
|
1578
|
+
const profile$3 = {
|
|
1579
|
+
kind: "responses",
|
|
1580
|
+
instructionsMode: "instructions_field",
|
|
1581
|
+
supportedBlockTypes: ["text", "json"],
|
|
1582
|
+
reasoningBlockTypes: ["text"],
|
|
1583
|
+
capabilities: {
|
|
1584
|
+
textStreaming: "native",
|
|
1585
|
+
reasoningStreaming: "native",
|
|
1586
|
+
toolCallStreaming: "native",
|
|
1587
|
+
replay: "opaque",
|
|
1588
|
+
usage: "final",
|
|
1589
|
+
toolResultOutcomes: ["success"]
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
const mapper$3 = new NormalizedRequestMapper(profile$3);
|
|
1593
|
+
/** 已处理或可安全忽略的 Responses SSE 类型(未知类型会 warning 一次)。 */
|
|
1594
|
+
const KNOWN_RESPONSES_SSE_TYPES = /* @__PURE__ */ new Set([
|
|
1595
|
+
"response.output_item.added",
|
|
1596
|
+
"response.output_item.done",
|
|
1597
|
+
"response.output_text.delta",
|
|
1598
|
+
"response.output_text.done",
|
|
1599
|
+
"response.reasoning.delta",
|
|
1600
|
+
"response.reasoning.done",
|
|
1601
|
+
"response.tool_call.delta",
|
|
1602
|
+
"response.tool_call.done",
|
|
1603
|
+
"response.function_call_arguments.delta",
|
|
1604
|
+
"response.function_call_arguments.done",
|
|
1605
|
+
"response.content_part.added",
|
|
1606
|
+
"response.content_part.done",
|
|
1607
|
+
"response.refusal.delta",
|
|
1608
|
+
"response.refusal.done",
|
|
1609
|
+
"response.in_progress",
|
|
1610
|
+
"response.created",
|
|
1611
|
+
"response.completed",
|
|
1612
|
+
"response.failed",
|
|
1613
|
+
"response.incomplete",
|
|
1614
|
+
"error"
|
|
1615
|
+
]);
|
|
1616
|
+
function isReplayCanonicalInput(item) {
|
|
1617
|
+
return item.type === "message" && item.role === "assistant" || item.type === "reasoning" || item.type === "function_call";
|
|
1618
|
+
}
|
|
1619
|
+
function hasReplayCanonicalInput(input) {
|
|
1620
|
+
return input.some(isReplayCanonicalInput);
|
|
1621
|
+
}
|
|
1622
|
+
function extractFailureMessage(response) {
|
|
1623
|
+
return response.error?.message ?? response.failure?.message ?? "unknown";
|
|
1624
|
+
}
|
|
1625
|
+
function canonicalToResponsesBlock(b) {
|
|
1626
|
+
if (b.type === "text") return {
|
|
1627
|
+
type: "text",
|
|
1628
|
+
text: b.text
|
|
1629
|
+
};
|
|
1630
|
+
if (b.type === "json") return {
|
|
1631
|
+
type: "text",
|
|
1632
|
+
text: JSON.stringify(b.json)
|
|
1633
|
+
};
|
|
1634
|
+
throw new AIRequestError(`responses does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
1635
|
+
}
|
|
1636
|
+
var ResponsesAdapter = class extends AdapterBase {
|
|
1637
|
+
kind = "responses";
|
|
1638
|
+
capabilities = profile$3.capabilities;
|
|
1639
|
+
apiKey;
|
|
1640
|
+
baseUrl;
|
|
1202
1641
|
fetchFn;
|
|
1203
1642
|
constructor(options) {
|
|
1204
1643
|
super();
|
|
@@ -1211,7 +1650,7 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1211
1650
|
for (const item of request.input) switch (item.type) {
|
|
1212
1651
|
case "message":
|
|
1213
1652
|
if (item.role === "assistant") {
|
|
1214
|
-
const blocks =
|
|
1653
|
+
const blocks = mapper$3.ensureTextBlocks(item.content, `assistant message (${item.role}) content`).map(canonicalToResponsesBlock);
|
|
1215
1654
|
input.push({
|
|
1216
1655
|
type: "message",
|
|
1217
1656
|
role: item.role,
|
|
@@ -1220,11 +1659,11 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1220
1659
|
} else input.push({
|
|
1221
1660
|
type: "message",
|
|
1222
1661
|
role: item.role,
|
|
1223
|
-
content: contentBlocksToText(
|
|
1662
|
+
content: contentBlocksToText(mapper$3.ensureTextBlocks(item.content, `input message (${item.role}) content`))
|
|
1224
1663
|
});
|
|
1225
1664
|
break;
|
|
1226
1665
|
case "reasoning": {
|
|
1227
|
-
const blocks =
|
|
1666
|
+
const blocks = mapper$3.ensureReasoningBlocks(item.content, "reasoning content").map((b) => ({
|
|
1228
1667
|
type: "reasoning",
|
|
1229
1668
|
text: b.text
|
|
1230
1669
|
}));
|
|
@@ -1243,8 +1682,8 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1243
1682
|
});
|
|
1244
1683
|
break;
|
|
1245
1684
|
case "tool_result": {
|
|
1246
|
-
|
|
1247
|
-
const output =
|
|
1685
|
+
mapper$3.assertToolResultOutcome(item.outcome);
|
|
1686
|
+
const output = mapper$3.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
|
|
1248
1687
|
input.push({
|
|
1249
1688
|
type: "function_call_output",
|
|
1250
1689
|
call_id: item.callId,
|
|
@@ -1252,25 +1691,26 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1252
1691
|
});
|
|
1253
1692
|
break;
|
|
1254
1693
|
}
|
|
1255
|
-
case "opaque":
|
|
1256
|
-
if (item.source
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
}
|
|
1694
|
+
case "opaque": {
|
|
1695
|
+
if (item.source !== "responses" || item.purpose !== "replay") break;
|
|
1696
|
+
assertOpaqueReplayEnvelope(item.payload);
|
|
1697
|
+
const payload = item.payload;
|
|
1698
|
+
if ("id" in payload) {
|
|
1699
|
+
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");
|
|
1700
|
+
if (!hasReplayCanonicalInput(input)) input.push({
|
|
1701
|
+
type: "item_reference",
|
|
1702
|
+
id: payload.id
|
|
1703
|
+
});
|
|
1265
1704
|
}
|
|
1266
1705
|
break;
|
|
1706
|
+
}
|
|
1267
1707
|
}
|
|
1268
1708
|
const body = {
|
|
1269
1709
|
model: request.model,
|
|
1270
1710
|
input,
|
|
1271
1711
|
stream: true
|
|
1272
1712
|
};
|
|
1273
|
-
if (request.instructions) body.instructions =
|
|
1713
|
+
if (request.instructions) body.instructions = mapper$3.mapInstructions(request.instructions);
|
|
1274
1714
|
if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
|
|
1275
1715
|
type: "function",
|
|
1276
1716
|
name: t.name,
|
|
@@ -1292,31 +1732,59 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1292
1732
|
}
|
|
1293
1733
|
async *runStream(providerRequest, factory, request) {
|
|
1294
1734
|
const auxiliary = this.createAuxiliaryState(request);
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1735
|
+
let response;
|
|
1736
|
+
try {
|
|
1737
|
+
response = await this.fetchFn(`${this.baseUrl}/responses`, {
|
|
1738
|
+
method: "POST",
|
|
1739
|
+
headers: {
|
|
1740
|
+
"Content-Type": "application/json",
|
|
1741
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1742
|
+
},
|
|
1743
|
+
body: JSON.stringify(providerRequest)
|
|
1744
|
+
});
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
1747
|
+
}
|
|
1303
1748
|
if (!response.ok) {
|
|
1304
|
-
const
|
|
1305
|
-
throw
|
|
1749
|
+
const errorBody = await response.text().catch(() => "");
|
|
1750
|
+
throw providerHttpError(response.status, errorBody);
|
|
1306
1751
|
}
|
|
1307
1752
|
const reader = response.body?.getReader();
|
|
1308
|
-
if (!reader) throw new
|
|
1753
|
+
if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
1754
|
+
const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
|
|
1755
|
+
let eventType = "";
|
|
1756
|
+
let dataStr = "";
|
|
1757
|
+
for (const rawLine of frame.split("\n")) {
|
|
1758
|
+
const line = rawLine.trim();
|
|
1759
|
+
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
1760
|
+
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
1761
|
+
}
|
|
1762
|
+
if (!eventType) return { status: "ignored" };
|
|
1763
|
+
try {
|
|
1764
|
+
const data = JSON.parse(dataStr);
|
|
1765
|
+
return {
|
|
1766
|
+
status: "parsed",
|
|
1767
|
+
value: {
|
|
1768
|
+
type: eventType,
|
|
1769
|
+
data
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
} catch {
|
|
1773
|
+
return { status: "malformed" };
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1309
1776
|
const output = [];
|
|
1310
|
-
|
|
1311
|
-
let buffer = "";
|
|
1777
|
+
let streamDone = false;
|
|
1312
1778
|
let completedResponse;
|
|
1779
|
+
let completedEmitted = false;
|
|
1780
|
+
let unknownEventsWarned = false;
|
|
1781
|
+
const messageItemsWithDelta = /* @__PURE__ */ new Set();
|
|
1313
1782
|
try {
|
|
1314
1783
|
while (true) {
|
|
1315
|
-
const { done, value } = await reader.read()
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
const { events,
|
|
1319
|
-
buffer = rest;
|
|
1784
|
+
const { done, value } = await reader.read().catch((err) => {
|
|
1785
|
+
throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
|
|
1786
|
+
});
|
|
1787
|
+
const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
|
|
1320
1788
|
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
1321
1789
|
count: malformedEvents,
|
|
1322
1790
|
providerLabel: "Responses",
|
|
@@ -1325,7 +1793,8 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1325
1793
|
if (malformedWarning) yield malformedWarning;
|
|
1326
1794
|
for (const sseEvent of events) {
|
|
1327
1795
|
if (sseEvent.type === "error") {
|
|
1328
|
-
|
|
1796
|
+
const data = sseEvent.data;
|
|
1797
|
+
yield factory.responseWarning(data.message ?? "Provider error event", data.code);
|
|
1329
1798
|
continue;
|
|
1330
1799
|
}
|
|
1331
1800
|
if (sseEvent.type === "response.output_item.added") {
|
|
@@ -1344,40 +1813,69 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1344
1813
|
continue;
|
|
1345
1814
|
}
|
|
1346
1815
|
if (sseEvent.type === "response.output_text.delta") {
|
|
1347
|
-
|
|
1816
|
+
const data = sseEvent.data;
|
|
1817
|
+
yield factory.messageDelta(data.item_id, textBlock(data.delta));
|
|
1818
|
+
messageItemsWithDelta.add(data.item_id);
|
|
1348
1819
|
continue;
|
|
1349
1820
|
}
|
|
1350
1821
|
if (sseEvent.type === "response.output_text.done") {
|
|
1351
|
-
|
|
1352
|
-
|
|
1822
|
+
const data = sseEvent.data;
|
|
1823
|
+
if (!messageItemsWithDelta.has(data.item_id) && data.text) yield factory.messageDelta(data.item_id, textBlock(data.text));
|
|
1824
|
+
yield factory.messageCompleted(data.item_id);
|
|
1825
|
+
output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
|
|
1353
1826
|
continue;
|
|
1354
1827
|
}
|
|
1355
1828
|
if (sseEvent.type === "response.reasoning.delta") {
|
|
1356
|
-
|
|
1829
|
+
const data = sseEvent.data;
|
|
1830
|
+
yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
|
|
1357
1831
|
continue;
|
|
1358
1832
|
}
|
|
1359
1833
|
if (sseEvent.type === "response.reasoning.done") {
|
|
1360
|
-
|
|
1361
|
-
|
|
1834
|
+
const data = sseEvent.data;
|
|
1835
|
+
yield factory.reasoningCompleted(data.item_id);
|
|
1836
|
+
output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
|
|
1362
1837
|
continue;
|
|
1363
1838
|
}
|
|
1364
1839
|
if (sseEvent.type === "response.tool_call.delta") {
|
|
1365
|
-
|
|
1840
|
+
const data = sseEvent.data;
|
|
1841
|
+
if (data.delta.arguments) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta.arguments });
|
|
1366
1842
|
continue;
|
|
1367
1843
|
}
|
|
1368
1844
|
if (sseEvent.type === "response.tool_call.done") {
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1845
|
+
const data = sseEvent.data;
|
|
1846
|
+
const tcItem = toolCallItem(data.item_id, data.name ?? "unknown", data.arguments ?? "");
|
|
1847
|
+
yield factory.toolCallCompleted(data.item_id);
|
|
1371
1848
|
output.push(tcItem);
|
|
1372
1849
|
continue;
|
|
1373
1850
|
}
|
|
1374
|
-
if (sseEvent.type === "response.completed"
|
|
1851
|
+
if (sseEvent.type === "response.completed" || sseEvent.type === "response.failed" || sseEvent.type === "response.incomplete") {
|
|
1852
|
+
const data = sseEvent.data;
|
|
1853
|
+
if (completedResponse) {
|
|
1854
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
completedResponse = data.response;
|
|
1858
|
+
if (sseEvent.type === "response.failed") yield factory.responseWarning(`Response failed: ${extractFailureMessage(data.response)}`, "PROVIDER_FAILURE");
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
|
|
1862
|
+
unknownEventsWarned = true;
|
|
1863
|
+
yield factory.responseWarning(`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`, "UNKNOWN_PROVIDER_EVENT");
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
if (done) {
|
|
1867
|
+
streamDone = true;
|
|
1868
|
+
break;
|
|
1375
1869
|
}
|
|
1376
1870
|
}
|
|
1377
1871
|
} finally {
|
|
1378
|
-
|
|
1872
|
+
try {
|
|
1873
|
+
if (!streamDone) await reader.cancel().catch(() => void 0);
|
|
1874
|
+
} finally {
|
|
1875
|
+
reader.releaseLock();
|
|
1876
|
+
}
|
|
1379
1877
|
}
|
|
1380
|
-
if (
|
|
1878
|
+
if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
|
|
1381
1879
|
let rawResponseId;
|
|
1382
1880
|
if (completedResponse) {
|
|
1383
1881
|
rawResponseId = completedResponse.id;
|
|
@@ -1388,23 +1886,44 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1388
1886
|
const stopReason = completedResponse ? this.inferStopReason(completedResponse) : void 0;
|
|
1389
1887
|
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
1390
1888
|
for (const event of auxiliaryResult.events) yield event;
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1889
|
+
if (!completedEmitted) {
|
|
1890
|
+
completedEmitted = true;
|
|
1891
|
+
const finalResponse = this.buildResponse(request, {
|
|
1892
|
+
output,
|
|
1893
|
+
replay,
|
|
1894
|
+
stopReason,
|
|
1895
|
+
usage: auxiliaryResult.usage,
|
|
1896
|
+
billing: auxiliaryResult.billing,
|
|
1897
|
+
auxiliary: auxiliaryResult.auxiliary,
|
|
1898
|
+
warnings: auxiliaryResult.warnings,
|
|
1899
|
+
metadataSources: auxiliaryResult.metadataSources,
|
|
1900
|
+
rawResponseId
|
|
1901
|
+
}, factory);
|
|
1902
|
+
yield factory.responseCompleted({
|
|
1903
|
+
replay: finalResponse.replay,
|
|
1904
|
+
stopReason: finalResponse.stopReason,
|
|
1905
|
+
trace: finalResponse.backend,
|
|
1906
|
+
usage: finalResponse.usage,
|
|
1907
|
+
billing: finalResponse.billing,
|
|
1908
|
+
auxiliary: finalResponse.auxiliary,
|
|
1909
|
+
warnings: finalResponse.warnings
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1402
1912
|
}
|
|
1403
1913
|
inferStopReason(response) {
|
|
1914
|
+
if (response.status === "failed") return "error";
|
|
1915
|
+
if (response.status === "incomplete") {
|
|
1916
|
+
const reason = response.incomplete_details?.reason;
|
|
1917
|
+
if (reason === "content_filter") return "content_filter";
|
|
1918
|
+
if (reason === "max_output_tokens") return "max_output_tokens";
|
|
1919
|
+
return "max_output_tokens";
|
|
1920
|
+
}
|
|
1404
1921
|
const output = response.output;
|
|
1405
|
-
if (!output || output.length === 0) return "unknown";
|
|
1922
|
+
if (!output || output.length === 0) return response.status === "completed" ? "end_turn" : "unknown";
|
|
1406
1923
|
if (output.some((item) => item.type === "function_call")) return "tool_call";
|
|
1407
|
-
|
|
1924
|
+
const lastItem = output[output.length - 1];
|
|
1925
|
+
if (lastItem?.status === "failed") return "error";
|
|
1926
|
+
if (lastItem?.status === "incomplete") return "max_output_tokens";
|
|
1408
1927
|
return "end_turn";
|
|
1409
1928
|
}
|
|
1410
1929
|
};
|
|
@@ -1421,36 +1940,41 @@ var ResponsesAdapter = class extends AdapterBase {
|
|
|
1421
1940
|
* - 高保真 replay(含 opaque continuation)
|
|
1422
1941
|
* - 能力降级 warning
|
|
1423
1942
|
*/
|
|
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
|
-
|
|
1943
|
+
const profile$2 = {
|
|
1944
|
+
kind: "messages",
|
|
1945
|
+
instructionsMode: "system_message",
|
|
1946
|
+
supportedBlockTypes: ["text", "json"],
|
|
1947
|
+
reasoningBlockTypes: ["text"],
|
|
1948
|
+
capabilities: {
|
|
1949
|
+
textStreaming: "native",
|
|
1950
|
+
reasoningStreaming: "native",
|
|
1951
|
+
toolCallStreaming: "synthetic",
|
|
1952
|
+
replay: "opaque",
|
|
1953
|
+
usage: "stream",
|
|
1954
|
+
toolResultOutcomes: ["success", "error"]
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
const mapper$2 = new NormalizedRequestMapper(profile$2);
|
|
1958
|
+
function isMessagesReplayContentBlock(value) {
|
|
1959
|
+
if (!value || typeof value !== "object" || !("type" in value)) return false;
|
|
1960
|
+
const block = value;
|
|
1961
|
+
switch (block.type) {
|
|
1962
|
+
case "text": return typeof block.text === "string";
|
|
1963
|
+
case "thinking": return typeof block.thinking === "string" && (block.signature === void 0 || typeof block.signature === "string");
|
|
1964
|
+
case "redacted_thinking": return typeof block.data === "string";
|
|
1965
|
+
case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && !!block.input && typeof block.input === "object" && !Array.isArray(block.input);
|
|
1966
|
+
case "tool_result":
|
|
1967
|
+
if (typeof block.tool_use_id !== "string") return false;
|
|
1968
|
+
if (block.is_error !== void 0 && typeof block.is_error !== "boolean") return false;
|
|
1969
|
+
if (typeof block.content === "string") return true;
|
|
1970
|
+
if (!Array.isArray(block.content)) return false;
|
|
1971
|
+
return block.content.every(isMessagesReplayContentBlock);
|
|
1972
|
+
default: return false;
|
|
1973
|
+
}
|
|
1451
1974
|
}
|
|
1452
|
-
function
|
|
1453
|
-
|
|
1975
|
+
function assertMessagesReplayContent(content) {
|
|
1976
|
+
if (!Array.isArray(content)) throw new AIRequestError("Invalid opaque replay payload: content must be an array", "INVALID_OPAQUE_REPLAY");
|
|
1977
|
+
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
1978
|
}
|
|
1455
1979
|
/** 用 response 级别的命名空间合成 content block 的 item ID,避免多轮工具循环 ID 碰撞 */
|
|
1456
1980
|
function synthesizeItemId(kind, blockIndex, responseId) {
|
|
@@ -1500,34 +2024,29 @@ function buildStreamMetadata(options) {
|
|
|
1500
2024
|
}
|
|
1501
2025
|
var MessagesAdapter = class extends AdapterBase {
|
|
1502
2026
|
kind = "messages";
|
|
1503
|
-
|
|
2027
|
+
capabilities = profile$2.capabilities;
|
|
1504
2028
|
apiKey;
|
|
1505
2029
|
apiVersion;
|
|
1506
2030
|
baseUrl;
|
|
1507
2031
|
fetchFn;
|
|
1508
|
-
warningAccumulator;
|
|
1509
2032
|
constructor(options) {
|
|
1510
2033
|
super();
|
|
1511
2034
|
this.apiKey = options.apiKey;
|
|
1512
2035
|
this.apiVersion = options.apiVersion ?? "2023-06-01";
|
|
1513
2036
|
this.baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
1514
2037
|
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
1515
|
-
this.warningAccumulator = [];
|
|
1516
|
-
}
|
|
1517
|
-
warn(message, _code) {
|
|
1518
|
-
this.warningAccumulator.push(message);
|
|
1519
2038
|
}
|
|
1520
2039
|
buildRequest(request) {
|
|
1521
2040
|
const messages = [];
|
|
1522
2041
|
let systemPrompt;
|
|
1523
2042
|
let pendingToolResultMessage;
|
|
1524
|
-
if (request.instructions) systemPrompt =
|
|
2043
|
+
if (request.instructions) systemPrompt = mapper$2.mapInstructions(request.instructions);
|
|
1525
2044
|
for (const item of request.input) {
|
|
1526
2045
|
if (item.type !== "tool_result") pendingToolResultMessage = void 0;
|
|
1527
2046
|
switch (item.type) {
|
|
1528
2047
|
case "message": {
|
|
1529
2048
|
const role = item.role === "user" ? "user" : "assistant";
|
|
1530
|
-
const supportedContent =
|
|
2049
|
+
const supportedContent = mapper$2.ensureTextBlocks(item.content, `input message (${item.role}) content`);
|
|
1531
2050
|
if (supportedContent.length === 1 && supportedContent[0]?.type === "text") messages.push({
|
|
1532
2051
|
role,
|
|
1533
2052
|
content: supportedContent[0].text
|
|
@@ -1554,8 +2073,8 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1554
2073
|
break;
|
|
1555
2074
|
}
|
|
1556
2075
|
case "tool_result": {
|
|
1557
|
-
|
|
1558
|
-
const content =
|
|
2076
|
+
mapper$2.assertToolResultOutcome(item.outcome);
|
|
2077
|
+
const content = mapper$2.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
|
|
1559
2078
|
const block = {
|
|
1560
2079
|
type: "tool_result",
|
|
1561
2080
|
tool_use_id: item.callId,
|
|
@@ -1575,7 +2094,7 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1575
2094
|
case "reasoning": {
|
|
1576
2095
|
const block = {
|
|
1577
2096
|
type: "thinking",
|
|
1578
|
-
thinking: contentBlocksToText(
|
|
2097
|
+
thinking: contentBlocksToText(mapper$2.ensureReasoningBlocks(item.content, "reasoning content"))
|
|
1579
2098
|
};
|
|
1580
2099
|
const lastMsg = messages[messages.length - 1];
|
|
1581
2100
|
if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(block);
|
|
@@ -1585,20 +2104,20 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1585
2104
|
});
|
|
1586
2105
|
break;
|
|
1587
2106
|
}
|
|
1588
|
-
case "opaque":
|
|
1589
|
-
if (item.purpose
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
}
|
|
2107
|
+
case "opaque": {
|
|
2108
|
+
if (item.purpose !== "replay") break;
|
|
2109
|
+
assertOpaqueReplayEnvelope(item.payload);
|
|
2110
|
+
const payload = item.payload;
|
|
2111
|
+
if (payload.role === "assistant" && "content" in payload) {
|
|
2112
|
+
assertMessagesReplayContent(payload.content);
|
|
2113
|
+
mapper$2.rollbackTrailingAssistantMessages(messages);
|
|
2114
|
+
messages.push({
|
|
2115
|
+
role: "assistant",
|
|
2116
|
+
content: payload.content
|
|
2117
|
+
});
|
|
1600
2118
|
}
|
|
1601
2119
|
break;
|
|
2120
|
+
}
|
|
1602
2121
|
}
|
|
1603
2122
|
}
|
|
1604
2123
|
const body = {
|
|
@@ -1625,27 +2144,53 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1625
2144
|
return body;
|
|
1626
2145
|
}
|
|
1627
2146
|
async *runStream(providerRequest, factory, request) {
|
|
1628
|
-
this.warningAccumulator = [];
|
|
1629
2147
|
const auxiliary = this.createAuxiliaryState(request);
|
|
2148
|
+
let completedEmitted = false;
|
|
1630
2149
|
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
|
-
|
|
2150
|
+
let response;
|
|
2151
|
+
try {
|
|
2152
|
+
response = await this.fetchFn(`${this.baseUrl}/messages`, {
|
|
2153
|
+
method: "POST",
|
|
2154
|
+
headers: {
|
|
2155
|
+
"Content-Type": "application/json",
|
|
2156
|
+
"x-api-key": this.apiKey,
|
|
2157
|
+
"anthropic-version": this.apiVersion
|
|
2158
|
+
},
|
|
2159
|
+
body: JSON.stringify(providerRequest)
|
|
2160
|
+
});
|
|
2161
|
+
} catch (err) {
|
|
2162
|
+
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
2163
|
+
}
|
|
1640
2164
|
if (!response.ok) {
|
|
1641
|
-
const
|
|
1642
|
-
throw
|
|
2165
|
+
const errorBody = await response.text().catch(() => "");
|
|
2166
|
+
throw providerHttpError(response.status, errorBody);
|
|
1643
2167
|
}
|
|
1644
2168
|
const reader = response.body?.getReader();
|
|
1645
|
-
if (!reader) throw new
|
|
2169
|
+
if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
2170
|
+
const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
|
|
2171
|
+
let eventType = "";
|
|
2172
|
+
let dataStr = "";
|
|
2173
|
+
for (const rawLine of frame.split("\n")) {
|
|
2174
|
+
const line = rawLine.trim();
|
|
2175
|
+
if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
2176
|
+
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
2177
|
+
}
|
|
2178
|
+
if (!eventType) return { status: "ignored" };
|
|
2179
|
+
try {
|
|
2180
|
+
const data = JSON.parse(dataStr);
|
|
2181
|
+
return {
|
|
2182
|
+
status: "parsed",
|
|
2183
|
+
value: {
|
|
2184
|
+
type: eventType,
|
|
2185
|
+
data
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
} catch {
|
|
2189
|
+
return { status: "malformed" };
|
|
2190
|
+
}
|
|
2191
|
+
});
|
|
1646
2192
|
const output = [];
|
|
1647
|
-
|
|
1648
|
-
let buffer = "";
|
|
2193
|
+
let streamDone = false;
|
|
1649
2194
|
let messageResponse;
|
|
1650
2195
|
let currentContentBlockIndex = -1;
|
|
1651
2196
|
let currentItemType = null;
|
|
@@ -1667,11 +2212,10 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1667
2212
|
}
|
|
1668
2213
|
try {
|
|
1669
2214
|
while (true) {
|
|
1670
|
-
const { done, value } = await reader.read()
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
const { events,
|
|
1674
|
-
buffer = rest;
|
|
2215
|
+
const { done, value } = await reader.read().catch((err) => {
|
|
2216
|
+
throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
|
|
2217
|
+
});
|
|
2218
|
+
const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
|
|
1675
2219
|
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
1676
2220
|
count: malformedEvents,
|
|
1677
2221
|
providerLabel: "Messages",
|
|
@@ -1683,7 +2227,6 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1683
2227
|
case "error": {
|
|
1684
2228
|
const err = sseEvent.data.error;
|
|
1685
2229
|
yield factory.responseWarning(err.message, err.type);
|
|
1686
|
-
this.warn(err.message, err.type);
|
|
1687
2230
|
continue;
|
|
1688
2231
|
}
|
|
1689
2232
|
case "message_start":
|
|
@@ -1718,7 +2261,7 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1718
2261
|
yield factory.reasoningStarted(currentItemId, "redacted");
|
|
1719
2262
|
yield factory.reasoningDelta(currentItemId, textBlock(data));
|
|
1720
2263
|
const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
|
|
1721
|
-
yield factory.reasoningCompleted(
|
|
2264
|
+
yield factory.reasoningCompleted(currentItemId);
|
|
1722
2265
|
output.push(redactedItem);
|
|
1723
2266
|
rawReplayContent.push({
|
|
1724
2267
|
type: "redacted_thinking",
|
|
@@ -1747,7 +2290,7 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1747
2290
|
if (currentItemType === "message" && currentItemId) {
|
|
1748
2291
|
const txt = delta.text;
|
|
1749
2292
|
textBuffer += txt;
|
|
1750
|
-
yield factory.messageDelta(currentItemId, txt);
|
|
2293
|
+
yield factory.messageDelta(currentItemId, textBlock(txt));
|
|
1751
2294
|
}
|
|
1752
2295
|
break;
|
|
1753
2296
|
case "thinking_delta":
|
|
@@ -1769,14 +2312,14 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1769
2312
|
}
|
|
1770
2313
|
case "content_block_stop":
|
|
1771
2314
|
if (currentItemType === "message" && currentItemId) {
|
|
1772
|
-
yield factory.messageCompleted(
|
|
2315
|
+
yield factory.messageCompleted(currentItemId);
|
|
1773
2316
|
output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
|
|
1774
2317
|
rawReplayContent.push({
|
|
1775
2318
|
type: "text",
|
|
1776
2319
|
text: textBuffer
|
|
1777
2320
|
});
|
|
1778
2321
|
} else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
|
|
1779
|
-
yield factory.reasoningCompleted(
|
|
2322
|
+
yield factory.reasoningCompleted(currentItemId);
|
|
1780
2323
|
output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
|
|
1781
2324
|
rawReplayContent.push({
|
|
1782
2325
|
type: "thinking",
|
|
@@ -1784,7 +2327,7 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1784
2327
|
});
|
|
1785
2328
|
} else if (currentItemType === "tool_call" && currentItemId) {
|
|
1786
2329
|
const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
|
|
1787
|
-
yield factory.toolCallCompleted(
|
|
2330
|
+
yield factory.toolCallCompleted(currentItemId);
|
|
1788
2331
|
output.push(tcItem);
|
|
1789
2332
|
rawReplayContent.push({
|
|
1790
2333
|
type: "tool_use",
|
|
@@ -1805,11 +2348,19 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1805
2348
|
}
|
|
1806
2349
|
case "message_stop": break;
|
|
1807
2350
|
}
|
|
2351
|
+
if (done) {
|
|
2352
|
+
streamDone = true;
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
1808
2355
|
}
|
|
1809
2356
|
} finally {
|
|
1810
|
-
|
|
2357
|
+
try {
|
|
2358
|
+
if (!streamDone) await reader.cancel().catch(() => void 0);
|
|
2359
|
+
} finally {
|
|
2360
|
+
reader.releaseLock();
|
|
2361
|
+
}
|
|
1811
2362
|
}
|
|
1812
|
-
if (
|
|
2363
|
+
if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
|
|
1813
2364
|
const replay = [...replayFromOutput(output)];
|
|
1814
2365
|
if (messageResponse) {
|
|
1815
2366
|
const replayContent = rawReplayContent.length > 0 ? rawReplayContent : messageResponse.content;
|
|
@@ -1830,17 +2381,29 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1830
2381
|
if (!hasStreamedReasoning) {}
|
|
1831
2382
|
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
1832
2383
|
for (const event of auxiliaryResult.events) yield event;
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2384
|
+
if (!completedEmitted) {
|
|
2385
|
+
completedEmitted = true;
|
|
2386
|
+
const finalResponse = this.buildResponse(request, {
|
|
2387
|
+
output,
|
|
2388
|
+
replay,
|
|
2389
|
+
stopReason: stopReason ? mapStopReason(stopReason) : void 0,
|
|
2390
|
+
usage: auxiliaryResult.usage,
|
|
2391
|
+
billing: auxiliaryResult.billing,
|
|
2392
|
+
auxiliary: auxiliaryResult.auxiliary,
|
|
2393
|
+
warnings: auxiliaryResult.warnings,
|
|
2394
|
+
metadataSources: auxiliaryResult.metadataSources,
|
|
2395
|
+
rawResponseId
|
|
2396
|
+
}, factory);
|
|
2397
|
+
yield factory.responseCompleted({
|
|
2398
|
+
replay: finalResponse.replay,
|
|
2399
|
+
stopReason: finalResponse.stopReason,
|
|
2400
|
+
trace: finalResponse.backend,
|
|
2401
|
+
usage: finalResponse.usage,
|
|
2402
|
+
billing: finalResponse.billing,
|
|
2403
|
+
auxiliary: finalResponse.auxiliary,
|
|
2404
|
+
warnings: finalResponse.warnings
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
1844
2407
|
}
|
|
1845
2408
|
};
|
|
1846
2409
|
//#endregion
|
|
@@ -1855,51 +2418,21 @@ var MessagesAdapter = class extends AdapterBase {
|
|
|
1855
2418
|
* - replay fidelity 依赖 provider 是否暴露可回放的 assistant turn 字段
|
|
1856
2419
|
*/
|
|
1857
2420
|
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
|
-
}
|
|
2421
|
+
const profile$1 = {
|
|
2422
|
+
kind: "chat-completions",
|
|
2423
|
+
instructionsMode: "system_message",
|
|
2424
|
+
supportedBlockTypes: ["text", "json"],
|
|
2425
|
+
reasoningBlockTypes: ["text"],
|
|
2426
|
+
capabilities: {
|
|
2427
|
+
textStreaming: "native",
|
|
2428
|
+
reasoningStreaming: "native",
|
|
2429
|
+
toolCallStreaming: "native",
|
|
2430
|
+
replay: "opaque",
|
|
2431
|
+
usage: "final",
|
|
2432
|
+
toolResultOutcomes: ["success"]
|
|
1885
2433
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
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");
|
|
1897
|
-
}
|
|
1898
|
-
return blocks;
|
|
1899
|
-
}
|
|
1900
|
-
function contentBlocksToChatText(blocks, field) {
|
|
1901
|
-
return contentBlocksToText(ensureTextCompatibleBlocks(blocks, field));
|
|
1902
|
-
}
|
|
2434
|
+
};
|
|
2435
|
+
const mapper$1 = new NormalizedRequestMapper(profile$1);
|
|
1903
2436
|
function extractReasoningText(value) {
|
|
1904
2437
|
if (typeof value === "string") return value;
|
|
1905
2438
|
if (Array.isArray(value)) return value.map(extractReasoningText).join("");
|
|
@@ -1930,8 +2463,31 @@ function extractReasoningDeltas(delta) {
|
|
|
1930
2463
|
}
|
|
1931
2464
|
return deltas;
|
|
1932
2465
|
}
|
|
1933
|
-
function
|
|
1934
|
-
|
|
2466
|
+
function isChatReplayToolCall(value) {
|
|
2467
|
+
if (!value || typeof value !== "object") return false;
|
|
2468
|
+
const entry = value;
|
|
2469
|
+
if (typeof entry.id !== "string" || entry.type !== "function") return false;
|
|
2470
|
+
const fn = entry.function;
|
|
2471
|
+
if (!fn || typeof fn !== "object") return false;
|
|
2472
|
+
const f = fn;
|
|
2473
|
+
return typeof f.name === "string" && typeof f.arguments === "string";
|
|
2474
|
+
}
|
|
2475
|
+
function isChatReplayMessage(value) {
|
|
2476
|
+
if (!value || typeof value !== "object") return false;
|
|
2477
|
+
const msg = value;
|
|
2478
|
+
const role = msg.role;
|
|
2479
|
+
if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return false;
|
|
2480
|
+
if (!(msg.content === null || typeof msg.content === "string")) return false;
|
|
2481
|
+
if (msg.tool_calls !== void 0) {
|
|
2482
|
+
if (!Array.isArray(msg.tool_calls) || !msg.tool_calls.every(isChatReplayToolCall)) return false;
|
|
2483
|
+
}
|
|
2484
|
+
if (msg.tool_call_id !== void 0 && typeof msg.tool_call_id !== "string") return false;
|
|
2485
|
+
if (msg.name !== void 0 && typeof msg.name !== "string") return false;
|
|
2486
|
+
return true;
|
|
2487
|
+
}
|
|
2488
|
+
function assertChatReplayMessages(messages, field) {
|
|
2489
|
+
if (!Array.isArray(messages)) throw new AIRequestError(`Invalid opaque replay payload: ${field} must be an array`, "INVALID_OPAQUE_REPLAY");
|
|
2490
|
+
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
2491
|
}
|
|
1936
2492
|
function buildAssistantReplayMessage(params) {
|
|
1937
2493
|
const { content, reasoningByField, toolCalls } = params;
|
|
@@ -1953,7 +2509,7 @@ function buildAssistantReplayMessage(params) {
|
|
|
1953
2509
|
}
|
|
1954
2510
|
var ChatCompletionsAdapter = class extends AdapterBase {
|
|
1955
2511
|
kind = "chat-completions";
|
|
1956
|
-
|
|
2512
|
+
capabilities = profile$1.capabilities;
|
|
1957
2513
|
apiKey;
|
|
1958
2514
|
baseUrl;
|
|
1959
2515
|
fetchFn;
|
|
@@ -1965,17 +2521,14 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
1965
2521
|
}
|
|
1966
2522
|
buildRequest(request) {
|
|
1967
2523
|
const messages = [];
|
|
1968
|
-
if (request.instructions) {
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
content
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
2524
|
+
if (request.instructions) messages.push({
|
|
2525
|
+
role: "system",
|
|
2526
|
+
content: mapper$1.mapInstructions(request.instructions)
|
|
2527
|
+
});
|
|
1975
2528
|
for (const item of request.input) switch (item.type) {
|
|
1976
2529
|
case "message": {
|
|
1977
2530
|
const role = item.role;
|
|
1978
|
-
const text =
|
|
2531
|
+
const text = contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `input message (${item.role}) content`));
|
|
1979
2532
|
messages.push({
|
|
1980
2533
|
role,
|
|
1981
2534
|
content: text || null
|
|
@@ -2001,38 +2554,44 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2001
2554
|
break;
|
|
2002
2555
|
}
|
|
2003
2556
|
case "tool_result":
|
|
2004
|
-
|
|
2557
|
+
mapper$1.assertToolResultOutcome(item.outcome);
|
|
2005
2558
|
messages.push({
|
|
2006
2559
|
role: "tool",
|
|
2007
2560
|
tool_call_id: item.callId,
|
|
2008
2561
|
name: item.toolName,
|
|
2009
|
-
content:
|
|
2562
|
+
content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
|
|
2010
2563
|
});
|
|
2011
2564
|
break;
|
|
2012
2565
|
case "reasoning":
|
|
2013
2566
|
messages.push({
|
|
2014
2567
|
role: "assistant",
|
|
2015
|
-
content:
|
|
2568
|
+
content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, "reasoning content"))
|
|
2016
2569
|
});
|
|
2017
2570
|
break;
|
|
2018
|
-
case "opaque":
|
|
2019
|
-
if (item.purpose
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2571
|
+
case "opaque": {
|
|
2572
|
+
if (item.purpose !== "replay") break;
|
|
2573
|
+
assertOpaqueReplayEnvelope(item.payload);
|
|
2574
|
+
const payload = item.payload;
|
|
2575
|
+
if (payload.role === "assistant" && typeof payload.content === "string") messages.push({
|
|
2576
|
+
role: "assistant",
|
|
2577
|
+
content: payload.content
|
|
2578
|
+
});
|
|
2579
|
+
else if (payload.replaceCanonical === true && "messages" in payload) {
|
|
2580
|
+
assertChatReplayMessages(payload.messages, "messages");
|
|
2581
|
+
mapper$1.rollbackTrailingAssistantMessages(messages);
|
|
2582
|
+
for (const m of payload.messages) messages.push(m);
|
|
2583
|
+
} else if ("messages" in payload) {
|
|
2584
|
+
assertChatReplayMessages(payload.messages, "messages");
|
|
2585
|
+
for (const m of payload.messages) messages.push(m);
|
|
2029
2586
|
}
|
|
2030
2587
|
break;
|
|
2588
|
+
}
|
|
2031
2589
|
}
|
|
2032
2590
|
const body = {
|
|
2033
2591
|
model: request.model,
|
|
2034
2592
|
messages,
|
|
2035
|
-
stream: true
|
|
2593
|
+
stream: true,
|
|
2594
|
+
n: 1
|
|
2036
2595
|
};
|
|
2037
2596
|
if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
|
|
2038
2597
|
type: "function",
|
|
@@ -2057,23 +2616,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2057
2616
|
}
|
|
2058
2617
|
async *runStream(providerRequest, factory, request) {
|
|
2059
2618
|
const auxiliary = this.createAuxiliaryState(request);
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2619
|
+
let response;
|
|
2620
|
+
try {
|
|
2621
|
+
response = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
|
|
2622
|
+
method: "POST",
|
|
2623
|
+
headers: {
|
|
2624
|
+
"Content-Type": "application/json",
|
|
2625
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
2626
|
+
},
|
|
2627
|
+
body: JSON.stringify(providerRequest)
|
|
2628
|
+
});
|
|
2629
|
+
} catch (err) {
|
|
2630
|
+
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
2631
|
+
}
|
|
2068
2632
|
if (!response.ok) {
|
|
2069
|
-
const
|
|
2070
|
-
throw
|
|
2633
|
+
const errorBody = await response.text().catch(() => "");
|
|
2634
|
+
throw providerHttpError(response.status, errorBody);
|
|
2071
2635
|
}
|
|
2072
2636
|
const reader = response.body?.getReader();
|
|
2073
|
-
if (!reader) throw new
|
|
2637
|
+
if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
2638
|
+
const parser = new IncrementalStreamParser(splitLines, (item) => {
|
|
2639
|
+
const trimmed = item.trim();
|
|
2640
|
+
if (!trimmed.startsWith("data: ")) return { status: "ignored" };
|
|
2641
|
+
const data = trimmed.slice(6).trim();
|
|
2642
|
+
if (data === "[DONE]") return { status: "ignored" };
|
|
2643
|
+
try {
|
|
2644
|
+
return {
|
|
2645
|
+
status: "parsed",
|
|
2646
|
+
value: JSON.parse(data)
|
|
2647
|
+
};
|
|
2648
|
+
} catch {
|
|
2649
|
+
return { status: "malformed" };
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2074
2652
|
const output = [];
|
|
2075
|
-
|
|
2076
|
-
let buffer = "";
|
|
2653
|
+
let streamDone = false;
|
|
2077
2654
|
let responseId;
|
|
2078
2655
|
let accumulatedContent = "";
|
|
2079
2656
|
let accumulatedReasoning = "";
|
|
@@ -2081,6 +2658,9 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2081
2658
|
let currentReasoningId = "";
|
|
2082
2659
|
let hasMessageStarted = false;
|
|
2083
2660
|
let hasReasoningStarted = false;
|
|
2661
|
+
let completedEmitted = false;
|
|
2662
|
+
let warnedNonZeroChoice = false;
|
|
2663
|
+
const buildResponse = this.buildResponse.bind(this);
|
|
2084
2664
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2085
2665
|
const reasoningByField = /* @__PURE__ */ new Map();
|
|
2086
2666
|
const finalizePendingTurn = () => {
|
|
@@ -2089,17 +2669,17 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2089
2669
|
const finalizedReasoningByField = new Map(reasoningByField);
|
|
2090
2670
|
if (hasReasoningStarted && accumulatedReasoning) {
|
|
2091
2671
|
const reasoning = reasoningItem([textBlock(accumulatedReasoning)], "full", currentReasoningId);
|
|
2092
|
-
events.push(factory.reasoningCompleted(
|
|
2672
|
+
events.push(factory.reasoningCompleted(currentReasoningId));
|
|
2093
2673
|
output.push(reasoning);
|
|
2094
2674
|
}
|
|
2095
|
-
if (hasMessageStarted
|
|
2675
|
+
if (hasMessageStarted) {
|
|
2096
2676
|
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
2097
|
-
events.push(factory.messageCompleted(
|
|
2098
|
-
output.push(message);
|
|
2677
|
+
events.push(factory.messageCompleted(currentMessageId));
|
|
2678
|
+
if (accumulatedContent) output.push(message);
|
|
2099
2679
|
}
|
|
2100
2680
|
for (const pending of finalizedToolCalls) {
|
|
2101
2681
|
const toolCall = toolCallItem(pending.id, pending.name, pending.args);
|
|
2102
|
-
events.push(factory.toolCallCompleted(
|
|
2682
|
+
events.push(factory.toolCallCompleted(pending.id));
|
|
2103
2683
|
output.push(toolCall);
|
|
2104
2684
|
}
|
|
2105
2685
|
const assistantReplayMessage = buildAssistantReplayMessage({
|
|
@@ -2120,13 +2700,46 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2120
2700
|
assistantReplayMessage
|
|
2121
2701
|
};
|
|
2122
2702
|
};
|
|
2703
|
+
const emitCompleted = async function* (stopReason, assistantReplayMessage, rawResponseId) {
|
|
2704
|
+
if (completedEmitted) {
|
|
2705
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
completedEmitted = true;
|
|
2709
|
+
const replay = [...replayFromOutput(output)];
|
|
2710
|
+
if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
|
|
2711
|
+
replaceCanonical: true,
|
|
2712
|
+
messages: [assistantReplayMessage]
|
|
2713
|
+
}));
|
|
2714
|
+
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
2715
|
+
for (const event of auxiliaryResult.events) yield event;
|
|
2716
|
+
const finalResponse = buildResponse(request, {
|
|
2717
|
+
output,
|
|
2718
|
+
replay,
|
|
2719
|
+
stopReason,
|
|
2720
|
+
usage: auxiliaryResult.usage,
|
|
2721
|
+
billing: auxiliaryResult.billing,
|
|
2722
|
+
auxiliary: auxiliaryResult.auxiliary,
|
|
2723
|
+
warnings: auxiliaryResult.warnings,
|
|
2724
|
+
metadataSources: auxiliaryResult.metadataSources,
|
|
2725
|
+
rawResponseId
|
|
2726
|
+
}, factory);
|
|
2727
|
+
yield factory.responseCompleted({
|
|
2728
|
+
replay: finalResponse.replay,
|
|
2729
|
+
stopReason: finalResponse.stopReason,
|
|
2730
|
+
trace: finalResponse.backend,
|
|
2731
|
+
usage: finalResponse.usage,
|
|
2732
|
+
billing: finalResponse.billing,
|
|
2733
|
+
auxiliary: finalResponse.auxiliary,
|
|
2734
|
+
warnings: finalResponse.warnings
|
|
2735
|
+
});
|
|
2736
|
+
};
|
|
2123
2737
|
try {
|
|
2124
2738
|
while (true) {
|
|
2125
|
-
const { done, value } = await reader.read()
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
const { chunks,
|
|
2129
|
-
buffer = rest;
|
|
2739
|
+
const { done, value } = await reader.read().catch((err) => {
|
|
2740
|
+
throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
|
|
2741
|
+
});
|
|
2742
|
+
const { items: chunks, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
|
|
2130
2743
|
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
2131
2744
|
count: malformedEvents,
|
|
2132
2745
|
providerLabel: "Chat Completions",
|
|
@@ -2137,14 +2750,28 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2137
2750
|
responseId = chunk.id;
|
|
2138
2751
|
if (chunk.usage) auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
|
|
2139
2752
|
for (const choice of chunk.choices) {
|
|
2140
|
-
if (choice.index !== 0)
|
|
2753
|
+
if (choice.index !== 0) {
|
|
2754
|
+
if (!warnedNonZeroChoice) {
|
|
2755
|
+
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");
|
|
2756
|
+
warnedNonZeroChoice = true;
|
|
2757
|
+
}
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
if (completedEmitted) {
|
|
2761
|
+
if (choice.finish_reason) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
2762
|
+
continue;
|
|
2763
|
+
}
|
|
2141
2764
|
const delta = choice.delta;
|
|
2142
2765
|
const finishReason = choice.finish_reason;
|
|
2143
2766
|
const reasoningDeltas = extractReasoningDeltas(delta);
|
|
2144
|
-
|
|
2767
|
+
const ensureMessageStarted = () => {
|
|
2768
|
+
if (hasMessageStarted) return;
|
|
2145
2769
|
currentMessageId = `msg-${chunk.id}`;
|
|
2146
2770
|
hasMessageStarted = true;
|
|
2147
2771
|
accumulatedContent = "";
|
|
2772
|
+
};
|
|
2773
|
+
if (delta.role === "assistant" && !hasMessageStarted) {
|
|
2774
|
+
ensureMessageStarted();
|
|
2148
2775
|
yield factory.messageStarted(currentMessageId);
|
|
2149
2776
|
}
|
|
2150
2777
|
if (reasoningDeltas.length > 0) {
|
|
@@ -2162,32 +2789,41 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2162
2789
|
}
|
|
2163
2790
|
if (delta.content) {
|
|
2164
2791
|
if (!hasMessageStarted) {
|
|
2165
|
-
|
|
2166
|
-
hasMessageStarted = true;
|
|
2792
|
+
ensureMessageStarted();
|
|
2167
2793
|
yield factory.messageStarted(currentMessageId);
|
|
2168
2794
|
}
|
|
2169
2795
|
accumulatedContent += delta.content;
|
|
2170
|
-
yield factory.messageDelta(currentMessageId, delta.content);
|
|
2796
|
+
yield factory.messageDelta(currentMessageId, textBlock(delta.content));
|
|
2171
2797
|
}
|
|
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 ?? "");
|
|
2798
|
+
if (delta.tool_calls) {
|
|
2799
|
+
if (!hasMessageStarted) {
|
|
2800
|
+
ensureMessageStarted();
|
|
2801
|
+
yield factory.messageStarted(currentMessageId);
|
|
2181
2802
|
}
|
|
2182
|
-
|
|
2183
|
-
const
|
|
2184
|
-
if (
|
|
2185
|
-
|
|
2186
|
-
|
|
2803
|
+
for (const tc of delta.tool_calls) {
|
|
2804
|
+
const idx = tc.index;
|
|
2805
|
+
if (tc.id) {
|
|
2806
|
+
pendingToolCalls.set(idx, {
|
|
2807
|
+
id: tc.id,
|
|
2808
|
+
name: tc.function?.name ?? "",
|
|
2809
|
+
args: ""
|
|
2810
|
+
});
|
|
2811
|
+
yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
|
|
2812
|
+
}
|
|
2813
|
+
if (tc.function?.arguments) {
|
|
2814
|
+
const pending = pendingToolCalls.get(idx);
|
|
2815
|
+
if (pending) {
|
|
2816
|
+
pending.args += tc.function.arguments;
|
|
2817
|
+
yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
|
|
2818
|
+
}
|
|
2187
2819
|
}
|
|
2188
2820
|
}
|
|
2189
2821
|
}
|
|
2190
2822
|
if (delta.function_call) {
|
|
2823
|
+
if (!hasMessageStarted) {
|
|
2824
|
+
ensureMessageStarted();
|
|
2825
|
+
yield factory.messageStarted(currentMessageId);
|
|
2826
|
+
}
|
|
2191
2827
|
if (delta.function_call.name) {
|
|
2192
2828
|
const fcId = `fc-${chunk.id}-0`;
|
|
2193
2829
|
pendingToolCalls.set(0, {
|
|
@@ -2208,54 +2844,28 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2208
2844
|
if (finishReason && finishReason !== null) {
|
|
2209
2845
|
const { events, assistantReplayMessage } = finalizePendingTurn();
|
|
2210
2846
|
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));
|
|
2847
|
+
yield* emitCompleted(mapStopReason(finishReason), assistantReplayMessage, chunk.id);
|
|
2230
2848
|
}
|
|
2231
2849
|
}
|
|
2232
2850
|
}
|
|
2851
|
+
if (done) {
|
|
2852
|
+
streamDone = true;
|
|
2853
|
+
break;
|
|
2854
|
+
}
|
|
2233
2855
|
}
|
|
2234
2856
|
} finally {
|
|
2235
|
-
|
|
2857
|
+
try {
|
|
2858
|
+
if (!streamDone) await reader.cancel().catch(() => void 0);
|
|
2859
|
+
} finally {
|
|
2860
|
+
reader.releaseLock();
|
|
2861
|
+
}
|
|
2236
2862
|
}
|
|
2237
|
-
if (
|
|
2238
|
-
if (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0) {
|
|
2863
|
+
if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
|
|
2864
|
+
if (!completedEmitted && (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0)) {
|
|
2239
2865
|
yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
|
|
2240
2866
|
const { events, assistantReplayMessage } = finalizePendingTurn();
|
|
2241
2867
|
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));
|
|
2868
|
+
yield* emitCompleted(void 0, assistantReplayMessage, responseId);
|
|
2259
2869
|
}
|
|
2260
2870
|
}
|
|
2261
2871
|
};
|
|
@@ -2277,23 +2887,21 @@ var ChatCompletionsAdapter = class extends AdapterBase {
|
|
|
2277
2887
|
* - tool_call 不支持逐 token 流式
|
|
2278
2888
|
* - replay 保真度低(无 opaque continuation 机制)
|
|
2279
2889
|
*/
|
|
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
|
-
}
|
|
2890
|
+
const profile = {
|
|
2891
|
+
kind: "ollama",
|
|
2892
|
+
instructionsMode: "system_message",
|
|
2893
|
+
supportedBlockTypes: ["text", "json"],
|
|
2894
|
+
reasoningBlockTypes: ["text"],
|
|
2895
|
+
capabilities: {
|
|
2896
|
+
textStreaming: "native",
|
|
2897
|
+
reasoningStreaming: "none",
|
|
2898
|
+
toolCallStreaming: "synthetic",
|
|
2899
|
+
replay: "opaque",
|
|
2900
|
+
usage: "final",
|
|
2901
|
+
toolResultOutcomes: ["success"]
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
const mapper = new NormalizedRequestMapper(profile);
|
|
2297
2905
|
function parseOllamaToolArguments(item) {
|
|
2298
2906
|
if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) return item.argumentsJson;
|
|
2299
2907
|
try {
|
|
@@ -2302,46 +2910,24 @@ function parseOllamaToolArguments(item) {
|
|
|
2302
2910
|
} catch {}
|
|
2303
2911
|
throw new AIRequestError("ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent", "TOOL_CALL_ARGUMENTS_INVALID");
|
|
2304
2912
|
}
|
|
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) {
|
|
2913
|
+
function isOllamaReplayToolCalls(value) {
|
|
2336
2914
|
return Array.isArray(value) && value.every((entry) => {
|
|
2337
2915
|
if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
|
|
2338
2916
|
const fn = entry.function;
|
|
2917
|
+
const id = entry.id;
|
|
2918
|
+
if (id !== void 0 && typeof id !== "string") return false;
|
|
2339
2919
|
return !!fn && typeof fn === "object" && "name" in fn && typeof fn.name === "string" && "arguments" in fn && typeof fn.arguments === "object" && fn.arguments !== null;
|
|
2340
2920
|
});
|
|
2341
2921
|
}
|
|
2922
|
+
function toWireOllamaToolCalls(toolCalls) {
|
|
2923
|
+
return toolCalls.map((tc) => ({ function: {
|
|
2924
|
+
name: tc.function.name,
|
|
2925
|
+
arguments: tc.function.arguments
|
|
2926
|
+
} }));
|
|
2927
|
+
}
|
|
2342
2928
|
var OllamaAdapter = class extends AdapterBase {
|
|
2343
2929
|
kind = "ollama";
|
|
2344
|
-
|
|
2930
|
+
capabilities = profile.capabilities;
|
|
2345
2931
|
baseUrl;
|
|
2346
2932
|
apiKey;
|
|
2347
2933
|
fetchFn;
|
|
@@ -2354,16 +2940,18 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2354
2940
|
buildRequest(request) {
|
|
2355
2941
|
if (request.toolChoice && request.toolChoice !== "auto") throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
|
|
2356
2942
|
const messages = [];
|
|
2943
|
+
/** Local-only name → call id queue for best-effort tool_result association (not sent to Ollama). */
|
|
2944
|
+
const callIdsByName = /* @__PURE__ */ new Map();
|
|
2357
2945
|
if (request.instructions) messages.push({
|
|
2358
2946
|
role: "system",
|
|
2359
|
-
content:
|
|
2947
|
+
content: mapper.mapInstructions(request.instructions)
|
|
2360
2948
|
});
|
|
2361
2949
|
for (const item of request.input) switch (item.type) {
|
|
2362
2950
|
case "message": {
|
|
2363
2951
|
const role = item.role;
|
|
2364
2952
|
messages.push({
|
|
2365
2953
|
role,
|
|
2366
|
-
content: contentBlocksToText(
|
|
2954
|
+
content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`))
|
|
2367
2955
|
});
|
|
2368
2956
|
break;
|
|
2369
2957
|
}
|
|
@@ -2373,6 +2961,9 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2373
2961
|
name: item.name,
|
|
2374
2962
|
arguments: parseOllamaToolArguments(item)
|
|
2375
2963
|
} };
|
|
2964
|
+
const queue = callIdsByName.get(item.name) ?? [];
|
|
2965
|
+
queue.push(item.id);
|
|
2966
|
+
callIdsByName.set(item.name, queue);
|
|
2376
2967
|
if (lastAssistant) lastAssistant.tool_calls = [...lastAssistant.tool_calls ?? [], tc];
|
|
2377
2968
|
else messages.push({
|
|
2378
2969
|
role: "assistant",
|
|
@@ -2381,32 +2972,48 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2381
2972
|
});
|
|
2382
2973
|
break;
|
|
2383
2974
|
}
|
|
2384
|
-
case "tool_result":
|
|
2385
|
-
|
|
2975
|
+
case "tool_result": {
|
|
2976
|
+
mapper.assertToolResultOutcome(item.outcome);
|
|
2977
|
+
const queue = callIdsByName.get(item.toolName);
|
|
2978
|
+
if (queue && queue.length > 0) queue.shift();
|
|
2386
2979
|
messages.push({
|
|
2387
2980
|
role: "tool",
|
|
2388
|
-
content: contentBlocksToText(
|
|
2981
|
+
content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
|
|
2389
2982
|
});
|
|
2390
2983
|
break;
|
|
2984
|
+
}
|
|
2391
2985
|
case "reasoning":
|
|
2392
2986
|
messages.push({
|
|
2393
2987
|
role: "assistant",
|
|
2394
|
-
content: contentBlocksToText(
|
|
2988
|
+
content: contentBlocksToText(mapper.ensureReasoningBlocks(item.content, "reasoning content"))
|
|
2395
2989
|
});
|
|
2396
2990
|
break;
|
|
2397
|
-
case "opaque":
|
|
2398
|
-
if (item.source
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2991
|
+
case "opaque": {
|
|
2992
|
+
if (item.source !== "ollama" || item.purpose !== "replay") break;
|
|
2993
|
+
assertOpaqueReplayEnvelope(item.payload);
|
|
2994
|
+
const payload = item.payload;
|
|
2995
|
+
if (payload.role === "assistant" && typeof payload.content === "string") {
|
|
2996
|
+
mapper.rollbackTrailingAssistantMessages(messages);
|
|
2997
|
+
let replayToolCalls;
|
|
2998
|
+
if ("tool_calls" in payload && payload.tool_calls !== void 0) {
|
|
2999
|
+
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");
|
|
3000
|
+
replayToolCalls = payload.tool_calls;
|
|
2407
3001
|
}
|
|
3002
|
+
if (replayToolCalls) {
|
|
3003
|
+
for (const tc of replayToolCalls) if (tc.id) {
|
|
3004
|
+
const queue = callIdsByName.get(tc.function.name) ?? [];
|
|
3005
|
+
queue.push(tc.id);
|
|
3006
|
+
callIdsByName.set(tc.function.name, queue);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
messages.push({
|
|
3010
|
+
role: "assistant",
|
|
3011
|
+
content: payload.content,
|
|
3012
|
+
tool_calls: replayToolCalls ? toWireOllamaToolCalls(replayToolCalls) : void 0
|
|
3013
|
+
});
|
|
2408
3014
|
}
|
|
2409
3015
|
break;
|
|
3016
|
+
}
|
|
2410
3017
|
}
|
|
2411
3018
|
const body = {
|
|
2412
3019
|
model: request.model,
|
|
@@ -2430,35 +3037,96 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2430
3037
|
}
|
|
2431
3038
|
async *runStream(providerRequest, factory, request) {
|
|
2432
3039
|
const auxiliary = this.createAuxiliaryState(request);
|
|
3040
|
+
let completedEmitted = false;
|
|
2433
3041
|
if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
|
|
2434
3042
|
const headers = { "Content-Type": "application/json" };
|
|
2435
3043
|
if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
3044
|
+
let response;
|
|
3045
|
+
try {
|
|
3046
|
+
response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
|
|
3047
|
+
method: "POST",
|
|
3048
|
+
headers,
|
|
3049
|
+
body: JSON.stringify(providerRequest)
|
|
3050
|
+
});
|
|
3051
|
+
} catch (err) {
|
|
3052
|
+
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
3053
|
+
}
|
|
2441
3054
|
if (!response.ok) {
|
|
2442
|
-
const
|
|
2443
|
-
throw
|
|
3055
|
+
const errorBody = await response.text().catch(() => "");
|
|
3056
|
+
throw providerHttpError(response.status, errorBody);
|
|
2444
3057
|
}
|
|
2445
3058
|
const reader = response.body?.getReader();
|
|
2446
|
-
if (!reader) throw new
|
|
3059
|
+
if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
3060
|
+
const parser = new IncrementalStreamParser(splitLines, (item) => {
|
|
3061
|
+
const trimmed = item.trim();
|
|
3062
|
+
if (!trimmed) return { status: "ignored" };
|
|
3063
|
+
try {
|
|
3064
|
+
const parsed = JSON.parse(trimmed);
|
|
3065
|
+
if (parsed && typeof parsed === "object" && "message" in parsed) return {
|
|
3066
|
+
status: "parsed",
|
|
3067
|
+
value: parsed
|
|
3068
|
+
};
|
|
3069
|
+
return { status: "malformed" };
|
|
3070
|
+
} catch {
|
|
3071
|
+
return { status: "malformed" };
|
|
3072
|
+
}
|
|
3073
|
+
});
|
|
2447
3074
|
const output = [];
|
|
2448
|
-
|
|
2449
|
-
let buffer = "";
|
|
3075
|
+
let streamDone = false;
|
|
2450
3076
|
let responseId;
|
|
2451
3077
|
let accumulatedContent = "";
|
|
2452
3078
|
let currentMessageId = "";
|
|
2453
3079
|
let hasMessageStarted = false;
|
|
2454
3080
|
let pendingToolCalls = [];
|
|
3081
|
+
let toolCallIndex = 0;
|
|
3082
|
+
const buildResponse = this.buildResponse.bind(this);
|
|
3083
|
+
const emitCompleted = async function* (stopReason, rawResponseId) {
|
|
3084
|
+
if (completedEmitted) {
|
|
3085
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
completedEmitted = true;
|
|
3089
|
+
const replay = replayFromOutput(output);
|
|
3090
|
+
if (accumulatedContent || pendingToolCalls.length > 0) replay.push(opaqueItem("ollama", "replay", {
|
|
3091
|
+
role: "assistant",
|
|
3092
|
+
content: accumulatedContent,
|
|
3093
|
+
tool_calls: pendingToolCalls.map((tc) => ({
|
|
3094
|
+
id: tc.id,
|
|
3095
|
+
function: {
|
|
3096
|
+
name: tc.name,
|
|
3097
|
+
arguments: tc.argumentsJson
|
|
3098
|
+
}
|
|
3099
|
+
}))
|
|
3100
|
+
}));
|
|
3101
|
+
const auxiliaryResult = await auxiliary.finalize(factory);
|
|
3102
|
+
for (const event of auxiliaryResult.events) yield event;
|
|
3103
|
+
const finalResponse = buildResponse(request, {
|
|
3104
|
+
output,
|
|
3105
|
+
replay,
|
|
3106
|
+
stopReason,
|
|
3107
|
+
usage: auxiliaryResult.usage,
|
|
3108
|
+
billing: auxiliaryResult.billing,
|
|
3109
|
+
auxiliary: auxiliaryResult.auxiliary,
|
|
3110
|
+
warnings: auxiliaryResult.warnings,
|
|
3111
|
+
metadataSources: auxiliaryResult.metadataSources,
|
|
3112
|
+
rawResponseId
|
|
3113
|
+
}, factory);
|
|
3114
|
+
yield factory.responseCompleted({
|
|
3115
|
+
replay: finalResponse.replay,
|
|
3116
|
+
stopReason: finalResponse.stopReason,
|
|
3117
|
+
trace: finalResponse.backend,
|
|
3118
|
+
usage: finalResponse.usage,
|
|
3119
|
+
billing: finalResponse.billing,
|
|
3120
|
+
auxiliary: finalResponse.auxiliary,
|
|
3121
|
+
warnings: finalResponse.warnings
|
|
3122
|
+
});
|
|
3123
|
+
};
|
|
2455
3124
|
try {
|
|
2456
3125
|
while (true) {
|
|
2457
|
-
const { done, value } = await reader.read()
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
const { chunks,
|
|
2461
|
-
buffer = rest;
|
|
3126
|
+
const { done, value } = await reader.read().catch((err) => {
|
|
3127
|
+
throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
|
|
3128
|
+
});
|
|
3129
|
+
const { items: chunks, malformed: malformedLines } = done ? parser.flush() : parser.feed(value);
|
|
2462
3130
|
const malformedWarning = emitMalformedStreamWarning(factory, {
|
|
2463
3131
|
count: malformedLines,
|
|
2464
3132
|
providerLabel: "Ollama",
|
|
@@ -2467,6 +3135,10 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2467
3135
|
if (malformedWarning) yield malformedWarning;
|
|
2468
3136
|
for (const chunk of chunks) {
|
|
2469
3137
|
responseId = chunk.created_at;
|
|
3138
|
+
if (completedEmitted) {
|
|
3139
|
+
if (chunk.done) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
3140
|
+
continue;
|
|
3141
|
+
}
|
|
2470
3142
|
const msg = chunk.message;
|
|
2471
3143
|
if (msg.content) {
|
|
2472
3144
|
if (!hasMessageStarted) {
|
|
@@ -2475,10 +3147,10 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2475
3147
|
yield factory.messageStarted(currentMessageId);
|
|
2476
3148
|
}
|
|
2477
3149
|
accumulatedContent += msg.content;
|
|
2478
|
-
yield factory.messageDelta(currentMessageId, msg.content);
|
|
3150
|
+
yield factory.messageDelta(currentMessageId, textBlock(msg.content));
|
|
2479
3151
|
}
|
|
2480
3152
|
if (msg.tool_calls && msg.tool_calls.length > 0) for (const tc of msg.tool_calls) {
|
|
2481
|
-
const tcId = `tc-${
|
|
3153
|
+
const tcId = `ollama-tc-${request.requestId}-${toolCallIndex++}`;
|
|
2482
3154
|
const argsText = JSON.stringify(tc.function.arguments);
|
|
2483
3155
|
pendingToolCalls.push({
|
|
2484
3156
|
id: tcId,
|
|
@@ -2495,14 +3167,15 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2495
3167
|
}
|
|
2496
3168
|
if (hasMessageStarted) {
|
|
2497
3169
|
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
2498
|
-
yield factory.messageCompleted(
|
|
3170
|
+
yield factory.messageCompleted(currentMessageId);
|
|
2499
3171
|
if (accumulatedContent) output.push(message);
|
|
2500
3172
|
}
|
|
3173
|
+
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
3174
|
for (const pending of pendingToolCalls) {
|
|
2502
3175
|
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
|
|
2503
3176
|
yield factory.toolCallStarted(pending.id, pending.name);
|
|
2504
3177
|
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
2505
|
-
yield factory.toolCallCompleted(
|
|
3178
|
+
yield factory.toolCallCompleted(pending.id);
|
|
2506
3179
|
output.push(toolCall);
|
|
2507
3180
|
}
|
|
2508
3181
|
if (request.include?.usage !== "off" && (chunk.prompt_eval_count !== void 0 || chunk.eval_count !== void 0)) auxiliary.recordUsage(usageFromOllama({
|
|
@@ -2512,67 +3185,42 @@ var OllamaAdapter = class extends AdapterBase {
|
|
|
2512
3185
|
prompt_eval_count: chunk.prompt_eval_count,
|
|
2513
3186
|
eval_count: chunk.eval_count
|
|
2514
3187
|
});
|
|
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));
|
|
3188
|
+
yield* emitCompleted(chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0, chunk.created_at);
|
|
2538
3189
|
accumulatedContent = "";
|
|
2539
3190
|
currentMessageId = "";
|
|
2540
3191
|
hasMessageStarted = false;
|
|
2541
3192
|
pendingToolCalls = [];
|
|
2542
3193
|
}
|
|
2543
3194
|
}
|
|
3195
|
+
if (done) {
|
|
3196
|
+
streamDone = true;
|
|
3197
|
+
break;
|
|
3198
|
+
}
|
|
2544
3199
|
}
|
|
2545
3200
|
} finally {
|
|
2546
|
-
|
|
3201
|
+
try {
|
|
3202
|
+
if (!streamDone) await reader.cancel().catch(() => void 0);
|
|
3203
|
+
} finally {
|
|
3204
|
+
reader.releaseLock();
|
|
3205
|
+
}
|
|
2547
3206
|
}
|
|
2548
|
-
if (
|
|
2549
|
-
if (hasMessageStarted || pendingToolCalls.length > 0) {
|
|
3207
|
+
if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
|
|
3208
|
+
if (!completedEmitted && (hasMessageStarted || pendingToolCalls.length > 0)) {
|
|
2550
3209
|
yield factory.responseWarning("Stream ended without a done signal", "INCOMPLETE_STREAM");
|
|
2551
3210
|
if (hasMessageStarted) {
|
|
2552
3211
|
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
2553
|
-
yield factory.messageCompleted(
|
|
3212
|
+
yield factory.messageCompleted(currentMessageId);
|
|
2554
3213
|
if (accumulatedContent) output.push(message);
|
|
2555
3214
|
}
|
|
3215
|
+
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
3216
|
for (const pending of pendingToolCalls) {
|
|
2557
3217
|
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
|
|
2558
3218
|
yield factory.toolCallStarted(pending.id, pending.name);
|
|
2559
3219
|
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
2560
|
-
yield factory.toolCallCompleted(
|
|
3220
|
+
yield factory.toolCallCompleted(pending.id);
|
|
2561
3221
|
output.push(toolCall);
|
|
2562
3222
|
}
|
|
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));
|
|
3223
|
+
yield* emitCompleted(void 0, responseId);
|
|
2576
3224
|
}
|
|
2577
3225
|
}
|
|
2578
3226
|
};
|
|
@@ -2607,7 +3255,18 @@ function assertMockRequest(request, expectation, context) {
|
|
|
2607
3255
|
}
|
|
2608
3256
|
var MockAdapter = class extends AdapterBase {
|
|
2609
3257
|
kind = "mock";
|
|
2610
|
-
|
|
3258
|
+
capabilities = {
|
|
3259
|
+
textStreaming: "synthetic",
|
|
3260
|
+
reasoningStreaming: "synthetic",
|
|
3261
|
+
toolCallStreaming: "synthetic",
|
|
3262
|
+
replay: "canonical",
|
|
3263
|
+
usage: "final",
|
|
3264
|
+
toolResultOutcomes: [
|
|
3265
|
+
"success",
|
|
3266
|
+
"error",
|
|
3267
|
+
"rejected"
|
|
3268
|
+
]
|
|
3269
|
+
};
|
|
2611
3270
|
handler;
|
|
2612
3271
|
providerMetadata;
|
|
2613
3272
|
cursor = 0;
|
|
@@ -2679,18 +3338,34 @@ var MockAdapter = class extends AdapterBase {
|
|
|
2679
3338
|
break;
|
|
2680
3339
|
}
|
|
2681
3340
|
case "complete": {
|
|
2682
|
-
const
|
|
2683
|
-
yield factory.responseCompleted(
|
|
3341
|
+
const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
|
|
3342
|
+
yield factory.responseCompleted({
|
|
3343
|
+
replay: finalResponse.replay,
|
|
3344
|
+
stopReason: finalResponse.stopReason,
|
|
3345
|
+
trace: finalResponse.backend,
|
|
3346
|
+
usage: finalResponse.usage,
|
|
3347
|
+
billing: finalResponse.billing,
|
|
3348
|
+
auxiliary: finalResponse.auxiliary,
|
|
3349
|
+
warnings: finalResponse.warnings
|
|
3350
|
+
});
|
|
2684
3351
|
return;
|
|
2685
3352
|
}
|
|
2686
3353
|
case "error": {
|
|
2687
3354
|
yield factory.responseWarning(step.message, step.code);
|
|
2688
|
-
const
|
|
3355
|
+
const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, {
|
|
2689
3356
|
type: "complete",
|
|
2690
3357
|
stopReason: step.stopReason ?? "error",
|
|
2691
3358
|
providerMetadata: step.providerMetadata
|
|
2692
3359
|
}, stepCount);
|
|
2693
|
-
yield factory.responseCompleted(
|
|
3360
|
+
yield factory.responseCompleted({
|
|
3361
|
+
replay: finalResponse.replay,
|
|
3362
|
+
stopReason: finalResponse.stopReason,
|
|
3363
|
+
trace: finalResponse.backend,
|
|
3364
|
+
usage: finalResponse.usage,
|
|
3365
|
+
billing: finalResponse.billing,
|
|
3366
|
+
auxiliary: finalResponse.auxiliary,
|
|
3367
|
+
warnings: finalResponse.warnings
|
|
3368
|
+
});
|
|
2694
3369
|
return;
|
|
2695
3370
|
}
|
|
2696
3371
|
case "interrupt":
|
|
@@ -2699,8 +3374,16 @@ var MockAdapter = class extends AdapterBase {
|
|
|
2699
3374
|
case "throw": throw typeof step.error === "string" ? new Error(step.error) : step.error;
|
|
2700
3375
|
}
|
|
2701
3376
|
}
|
|
2702
|
-
const
|
|
2703
|
-
yield factory.responseCompleted(
|
|
3377
|
+
const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, { type: "complete" }, stepCount);
|
|
3378
|
+
yield factory.responseCompleted({
|
|
3379
|
+
replay: finalResponse.replay,
|
|
3380
|
+
stopReason: finalResponse.stopReason,
|
|
3381
|
+
trace: finalResponse.backend,
|
|
3382
|
+
usage: finalResponse.usage,
|
|
3383
|
+
billing: finalResponse.billing,
|
|
3384
|
+
auxiliary: finalResponse.auxiliary,
|
|
3385
|
+
warnings: finalResponse.warnings
|
|
3386
|
+
});
|
|
2704
3387
|
} finally {
|
|
2705
3388
|
this.activeStream = false;
|
|
2706
3389
|
}
|
|
@@ -2828,10 +3511,11 @@ async function* emitMessage(factory, item, stream) {
|
|
|
2828
3511
|
let chunkIndex = 0;
|
|
2829
3512
|
for (const block of item.content) if (block.type === "text") for (const chunk of chunkText(block.text, stream)) {
|
|
2830
3513
|
await delayForChunk(stream, chunkIndex, chunk.length);
|
|
2831
|
-
yield factory.messageDelta(item.id, chunk);
|
|
3514
|
+
yield factory.messageDelta(item.id, textBlock(chunk));
|
|
2832
3515
|
chunkIndex += 1;
|
|
2833
3516
|
}
|
|
2834
|
-
yield factory.
|
|
3517
|
+
else yield factory.messageDelta(item.id, block);
|
|
3518
|
+
yield factory.messageCompleted(item.id);
|
|
2835
3519
|
}
|
|
2836
3520
|
async function* emitReasoning(factory, item, stream) {
|
|
2837
3521
|
if (!item.id) throw new AIRequestError("Mock reasoning output requires an id after normalization", "MOCK_REASONING_ID_MISSING");
|
|
@@ -2848,7 +3532,7 @@ async function* emitReasoning(factory, item, stream) {
|
|
|
2848
3532
|
chunkIndex += 1;
|
|
2849
3533
|
}
|
|
2850
3534
|
}
|
|
2851
|
-
yield factory.reasoningCompleted(item);
|
|
3535
|
+
yield factory.reasoningCompleted(item.id);
|
|
2852
3536
|
}
|
|
2853
3537
|
async function* emitToolCall(factory, item, streamArguments, stream) {
|
|
2854
3538
|
yield factory.toolCallStarted(item.id, item.name);
|
|
@@ -2860,7 +3544,7 @@ async function* emitToolCall(factory, item, streamArguments, stream) {
|
|
|
2860
3544
|
chunkIndex += 1;
|
|
2861
3545
|
}
|
|
2862
3546
|
}
|
|
2863
|
-
yield factory.toolCallCompleted(item);
|
|
3547
|
+
yield factory.toolCallCompleted(item.id);
|
|
2864
3548
|
}
|
|
2865
3549
|
function resolveStepStreamOptions(defaults, override, label) {
|
|
2866
3550
|
if (override === false) return;
|
|
@@ -2971,108 +3655,6 @@ function cloneItem(item) {
|
|
|
2971
3655
|
return structuredClone(item);
|
|
2972
3656
|
}
|
|
2973
3657
|
//#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 };
|
|
3658
|
+
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
3659
|
|
|
3078
3660
|
//# sourceMappingURL=index.mjs.map
|