@codehz/ai 0.2.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -8
- package/dist/index.d.mts +79 -89
- package/dist/index.mjs +717 -931
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +153 -266
- package/src/adapters/messages.ts +179 -301
- package/src/adapters/mock.ts +1 -10
- package/src/adapters/ollama.ts +142 -257
- package/src/adapters/responses.ts +141 -251
- package/src/core/validation.ts +19 -0
- package/src/helpers/adapter-auxiliary.ts +1 -23
- package/src/helpers/adapter-base.ts +41 -7
- package/src/helpers/incremental-stream-parser.ts +58 -0
- package/src/helpers/index.ts +20 -8
- package/src/helpers/mapping.ts +1 -10
- package/src/helpers/provider-stream.ts +147 -0
- package/src/helpers/request-mapper.ts +47 -25
- package/src/helpers/usage-mapping.ts +36 -39
- package/src/types/adapter.ts +1 -12
- package/src/types/index.ts +1 -9
- package/src/types/items.ts +0 -1
- package/src/helpers/sse-parser.ts +0 -113
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { AdapterBase } from "../helpers/adapter-base.js";
|
|
12
|
-
import {
|
|
12
|
+
import { AIRequestError } from "../core/errors.js";
|
|
13
13
|
import {
|
|
14
14
|
textBlock,
|
|
15
15
|
messageItem,
|
|
@@ -18,15 +18,18 @@ import {
|
|
|
18
18
|
opaqueItem,
|
|
19
19
|
replayFromOutput,
|
|
20
20
|
mapStopReason,
|
|
21
|
-
contentBlocksToText,
|
|
22
21
|
} from "../helpers/mapping.js";
|
|
23
|
-
import {
|
|
24
|
-
import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
|
|
22
|
+
import { assertOpaqueReplayEnvelope } from "../helpers/adapter-security.js";
|
|
25
23
|
import { usageFromChatCompletions } from "../helpers/usage-mapping.js";
|
|
26
|
-
import {
|
|
27
|
-
|
|
24
|
+
import {
|
|
25
|
+
NormalizedRequestMapper,
|
|
26
|
+
createChatCompletionsSseParser,
|
|
27
|
+
openProviderJsonStream,
|
|
28
|
+
iterateProviderStreamBatches,
|
|
29
|
+
createCompletionGate,
|
|
30
|
+
} from "../helpers/index.js";
|
|
28
31
|
|
|
29
|
-
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
32
|
+
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn, StopReason } from "../index.js";
|
|
30
33
|
|
|
31
34
|
// ── 类型 ──────────────────────────────────────────────────────
|
|
32
35
|
|
|
@@ -118,24 +121,7 @@ type ReasoningFieldName = "reasoning" | "reasoning_content";
|
|
|
118
121
|
|
|
119
122
|
const REASONING_FIELDS: readonly ReasoningFieldName[] = ["reasoning_content", "reasoning"];
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const profile: ProviderProfile = {
|
|
124
|
-
kind: "chat-completions",
|
|
125
|
-
instructionsMode: "system_message",
|
|
126
|
-
supportedBlockTypes: ["text", "json"] as const,
|
|
127
|
-
reasoningBlockTypes: ["text"] as const,
|
|
128
|
-
capabilities: {
|
|
129
|
-
textStreaming: "native",
|
|
130
|
-
reasoningStreaming: "native",
|
|
131
|
-
toolCallStreaming: "native",
|
|
132
|
-
replay: "opaque",
|
|
133
|
-
usage: "final",
|
|
134
|
-
toolResultOutcomes: ["success"],
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const mapper = new NormalizedRequestMapper(profile);
|
|
124
|
+
const mapper = new NormalizedRequestMapper("chat-completions");
|
|
139
125
|
|
|
140
126
|
function extractReasoningText(value: unknown): string {
|
|
141
127
|
if (typeof value === "string") return value;
|
|
@@ -251,7 +237,7 @@ function buildAssistantReplayMessage(params: {
|
|
|
251
237
|
|
|
252
238
|
export class ChatCompletionsAdapter extends AdapterBase {
|
|
253
239
|
readonly kind = "chat-completions" as const;
|
|
254
|
-
readonly
|
|
240
|
+
readonly isSyntheticStream = false;
|
|
255
241
|
|
|
256
242
|
private apiKey: string;
|
|
257
243
|
private baseUrl: string;
|
|
@@ -278,9 +264,7 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
278
264
|
switch (item.type) {
|
|
279
265
|
case "message": {
|
|
280
266
|
const role = item.role;
|
|
281
|
-
const text =
|
|
282
|
-
mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`),
|
|
283
|
-
);
|
|
267
|
+
const text = mapper.textFromBlocks(item.content, `input message (${item.role}) content`);
|
|
284
268
|
messages.push({ role, content: text || null });
|
|
285
269
|
break;
|
|
286
270
|
}
|
|
@@ -303,12 +287,11 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
303
287
|
break;
|
|
304
288
|
}
|
|
305
289
|
case "tool_result": {
|
|
306
|
-
mapper.assertToolResultOutcome(item.outcome);
|
|
307
290
|
messages.push({
|
|
308
291
|
role: "tool",
|
|
309
292
|
tool_call_id: item.callId,
|
|
310
293
|
name: item.toolName,
|
|
311
|
-
content:
|
|
294
|
+
content: mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`),
|
|
312
295
|
});
|
|
313
296
|
break;
|
|
314
297
|
}
|
|
@@ -317,13 +300,13 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
317
300
|
// Convert to a text message for best-effort
|
|
318
301
|
messages.push({
|
|
319
302
|
role: "assistant",
|
|
320
|
-
content:
|
|
303
|
+
content: mapper.textFromBlocks(item.content, "reasoning content"),
|
|
321
304
|
});
|
|
322
305
|
break;
|
|
323
306
|
}
|
|
324
307
|
case "opaque": {
|
|
325
308
|
// Try to restore from opaque replay
|
|
326
|
-
if (item.purpose !== "replay") break;
|
|
309
|
+
if (item.source !== "chat.completions" || item.purpose !== "replay") break;
|
|
327
310
|
assertOpaqueReplayEnvelope(item.payload);
|
|
328
311
|
const payload = item.payload as Record<string, unknown>;
|
|
329
312
|
if (payload.role === "assistant" && typeof payload.content === "string") {
|
|
@@ -352,26 +335,23 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
352
335
|
n: 1,
|
|
353
336
|
};
|
|
354
337
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
338
|
+
body.tools = mapper.mapToolsIfPresent(
|
|
339
|
+
request.tools,
|
|
340
|
+
(t): ChatTool => ({
|
|
341
|
+
type: "function",
|
|
342
|
+
function: {
|
|
343
|
+
name: t.name,
|
|
344
|
+
description: t.description,
|
|
345
|
+
parameters: t.inputSchema as Record<string, unknown>,
|
|
346
|
+
},
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
367
349
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
}
|
|
350
|
+
body.tool_choice = mapper.mapToolChoice<Exclude<ChatRequest["tool_choice"], undefined>>(request.toolChoice, {
|
|
351
|
+
auto: "auto",
|
|
352
|
+
none: "none",
|
|
353
|
+
tool: (name) => ({ type: "function" as const, function: { name } }),
|
|
354
|
+
});
|
|
375
355
|
|
|
376
356
|
if (request.temperature !== undefined) body.temperature = request.temperature;
|
|
377
357
|
if (request.maxOutputTokens !== undefined) body.max_tokens = request.maxOutputTokens;
|
|
@@ -388,46 +368,21 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
388
368
|
request: NormalizedRequest,
|
|
389
369
|
): AsyncIterable<AIStreamEvent> {
|
|
390
370
|
const auxiliary = this.createAuxiliaryState(request);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
});
|
|
403
|
-
} catch (err) {
|
|
404
|
-
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (!response.ok) {
|
|
408
|
-
const errorBody = await response.text().catch(() => "");
|
|
409
|
-
throw providerHttpError(response.status, errorBody);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const reader = response.body?.getReader();
|
|
413
|
-
if (!reader) {
|
|
414
|
-
throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const parser = new IncrementalStreamParser<ChatChunk>(splitLines, (item: string) => {
|
|
418
|
-
const trimmed = item.trim();
|
|
419
|
-
if (!trimmed.startsWith("data: ")) return { status: "ignored" };
|
|
420
|
-
const data = trimmed.slice(6).trim();
|
|
421
|
-
if (data === "[DONE]") return { status: "ignored" };
|
|
422
|
-
try {
|
|
423
|
-
return { status: "parsed", value: JSON.parse(data) as ChatChunk };
|
|
424
|
-
} catch {
|
|
425
|
-
return { status: "malformed" };
|
|
426
|
-
}
|
|
371
|
+
const gate = createCompletionGate();
|
|
372
|
+
|
|
373
|
+
const { reader } = await openProviderJsonStream({
|
|
374
|
+
fetchFn: this.fetchFn,
|
|
375
|
+
url: `${this.baseUrl}/chat/completions`,
|
|
376
|
+
headers: {
|
|
377
|
+
"Content-Type": "application/json",
|
|
378
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
379
|
+
},
|
|
380
|
+
body: providerRequest,
|
|
381
|
+
signal: request.signal,
|
|
427
382
|
});
|
|
428
383
|
|
|
384
|
+
const parser = createChatCompletionsSseParser<ChatChunk>();
|
|
429
385
|
const output: OutputItem[] = [];
|
|
430
|
-
let streamDone = false;
|
|
431
386
|
|
|
432
387
|
// 累积状态 — 支持多 choice,此处只取 index 0
|
|
433
388
|
let responseId: string | undefined;
|
|
@@ -437,9 +392,7 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
437
392
|
let currentReasoningId = "";
|
|
438
393
|
let hasMessageStarted = false;
|
|
439
394
|
let hasReasoningStarted = false;
|
|
440
|
-
let completedEmitted = false;
|
|
441
395
|
let warnedNonZeroChoice = false;
|
|
442
|
-
const buildResponse = this.buildResponse.bind(this);
|
|
443
396
|
|
|
444
397
|
// tool_calls 累积: tool call index → { id, name, args }
|
|
445
398
|
const pendingToolCalls = new Map<number, PendingToolCall>();
|
|
@@ -487,19 +440,17 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
487
440
|
};
|
|
488
441
|
|
|
489
442
|
const emitCompleted = async function* (
|
|
490
|
-
|
|
443
|
+
this: ChatCompletionsAdapter,
|
|
444
|
+
stopReason: StopReason | undefined,
|
|
491
445
|
assistantReplayMessage: ChatMessage | null,
|
|
492
446
|
rawResponseId: string | undefined,
|
|
493
447
|
): AsyncIterable<AIStreamEvent> {
|
|
494
|
-
if (
|
|
448
|
+
if (!gate.tryComplete()) {
|
|
495
449
|
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
496
450
|
return;
|
|
497
451
|
}
|
|
498
452
|
|
|
499
|
-
completedEmitted = true;
|
|
500
|
-
|
|
501
453
|
const replay = [...replayFromOutput(output)];
|
|
502
|
-
|
|
503
454
|
if (assistantReplayMessage) {
|
|
504
455
|
replay.push(
|
|
505
456
|
opaqueItem("chat.completions", "replay", {
|
|
@@ -509,209 +460,145 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
509
460
|
);
|
|
510
461
|
}
|
|
511
462
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const finalResponse = buildResponse(
|
|
518
|
-
request,
|
|
519
|
-
{
|
|
520
|
-
output,
|
|
521
|
-
replay,
|
|
522
|
-
stopReason,
|
|
523
|
-
usage: auxiliaryResult.usage,
|
|
524
|
-
billing: auxiliaryResult.billing,
|
|
525
|
-
auxiliary: auxiliaryResult.auxiliary,
|
|
526
|
-
warnings: auxiliaryResult.warnings,
|
|
527
|
-
metadataSources: auxiliaryResult.metadataSources,
|
|
528
|
-
rawResponseId,
|
|
529
|
-
},
|
|
530
|
-
factory,
|
|
531
|
-
);
|
|
532
|
-
yield factory.responseCompleted({
|
|
533
|
-
replay: finalResponse.replay,
|
|
534
|
-
stopReason: finalResponse.stopReason,
|
|
535
|
-
trace: finalResponse.backend,
|
|
536
|
-
usage: finalResponse.usage,
|
|
537
|
-
billing: finalResponse.billing,
|
|
538
|
-
auxiliary: finalResponse.auxiliary,
|
|
539
|
-
warnings: finalResponse.warnings,
|
|
463
|
+
yield* this.emitStreamCompleted(factory, request, auxiliary, {
|
|
464
|
+
output,
|
|
465
|
+
replay,
|
|
466
|
+
stopReason,
|
|
467
|
+
rawResponseId,
|
|
540
468
|
});
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
});
|
|
559
|
-
if (malformedWarning) {
|
|
560
|
-
yield malformedWarning;
|
|
469
|
+
}.bind(this);
|
|
470
|
+
|
|
471
|
+
for await (const batch of iterateProviderStreamBatches({
|
|
472
|
+
reader,
|
|
473
|
+
parser,
|
|
474
|
+
factory,
|
|
475
|
+
providerLabel: "Chat Completions",
|
|
476
|
+
transportLabel: "SSE event(s)",
|
|
477
|
+
incompleteMessage: "Stream ended with an incomplete Chat Completions SSE frame",
|
|
478
|
+
})) {
|
|
479
|
+
for (const warning of batch.warnings) yield warning;
|
|
480
|
+
|
|
481
|
+
for (const chunk of batch.items) {
|
|
482
|
+
responseId = chunk.id;
|
|
483
|
+
|
|
484
|
+
if (chunk.usage) {
|
|
485
|
+
auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
|
|
561
486
|
}
|
|
562
487
|
|
|
563
|
-
for (const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
488
|
+
for (const choice of chunk.choices) {
|
|
489
|
+
if (choice.index !== 0) {
|
|
490
|
+
if (!warnedNonZeroChoice) {
|
|
491
|
+
yield factory.responseWarning(
|
|
492
|
+
`Chat Completions returned choice index ${choice.index}; only the first choice (index 0) is supported. This choice is ignored.`,
|
|
493
|
+
"MULTIPLE_CHOICES_IGNORED",
|
|
494
|
+
);
|
|
495
|
+
warnedNonZeroChoice = true;
|
|
496
|
+
}
|
|
497
|
+
continue;
|
|
569
498
|
}
|
|
570
499
|
|
|
571
|
-
|
|
572
|
-
if (choice.
|
|
573
|
-
|
|
574
|
-
yield factory.responseWarning(
|
|
575
|
-
`Chat Completions returned choice index ${choice.index}; only the first choice (index 0) is supported. This choice is ignored.`,
|
|
576
|
-
"MULTIPLE_CHOICES_IGNORED",
|
|
577
|
-
);
|
|
578
|
-
warnedNonZeroChoice = true;
|
|
579
|
-
}
|
|
580
|
-
continue;
|
|
500
|
+
if (gate.completed) {
|
|
501
|
+
if (choice.finish_reason) {
|
|
502
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
581
503
|
}
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
582
506
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
507
|
+
const delta = choice.delta;
|
|
508
|
+
const finishReason = choice.finish_reason;
|
|
509
|
+
const reasoningDeltas = extractReasoningDeltas(delta);
|
|
589
510
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
currentMessageId = `msg-${chunk.id}`;
|
|
597
|
-
hasMessageStarted = true;
|
|
598
|
-
accumulatedContent = "";
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
// 处理 third-party reasoning delta
|
|
602
|
-
if (reasoningDeltas.length > 0) {
|
|
603
|
-
if (!hasReasoningStarted) {
|
|
604
|
-
currentReasoningId = `reason-${chunk.id}`;
|
|
605
|
-
hasReasoningStarted = true;
|
|
606
|
-
accumulatedReasoning = "";
|
|
607
|
-
yield factory.reasoningStarted(currentReasoningId, "full");
|
|
608
|
-
}
|
|
511
|
+
const ensureMessageStarted = (): void => {
|
|
512
|
+
if (hasMessageStarted) return;
|
|
513
|
+
currentMessageId = `msg-${chunk.id}`;
|
|
514
|
+
hasMessageStarted = true;
|
|
515
|
+
accumulatedContent = "";
|
|
516
|
+
};
|
|
609
517
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
yield factory.reasoningDelta(currentReasoningId, textBlock(reasoningDelta.text));
|
|
617
|
-
}
|
|
518
|
+
if (reasoningDeltas.length > 0) {
|
|
519
|
+
if (!hasReasoningStarted) {
|
|
520
|
+
currentReasoningId = `reason-${chunk.id}`;
|
|
521
|
+
hasReasoningStarted = true;
|
|
522
|
+
accumulatedReasoning = "";
|
|
523
|
+
yield factory.reasoningStarted(currentReasoningId, "full");
|
|
618
524
|
}
|
|
619
525
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
yield factory.messageDelta(currentMessageId, textBlock(delta.content));
|
|
526
|
+
for (const reasoningDelta of reasoningDeltas) {
|
|
527
|
+
accumulatedReasoning += reasoningDelta.text;
|
|
528
|
+
reasoningByField.set(
|
|
529
|
+
reasoningDelta.field,
|
|
530
|
+
(reasoningByField.get(reasoningDelta.field) ?? "") + reasoningDelta.text,
|
|
531
|
+
);
|
|
532
|
+
yield factory.reasoningDelta(currentReasoningId, textBlock(reasoningDelta.text));
|
|
628
533
|
}
|
|
534
|
+
}
|
|
629
535
|
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const idx = tc.index;
|
|
639
|
-
|
|
640
|
-
if (tc.id) {
|
|
641
|
-
pendingToolCalls.set(idx, { id: tc.id, name: tc.function?.name ?? "", args: "" });
|
|
642
|
-
yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
|
|
643
|
-
}
|
|
536
|
+
if (delta.content) {
|
|
537
|
+
if (!hasMessageStarted) {
|
|
538
|
+
ensureMessageStarted();
|
|
539
|
+
yield factory.messageStarted(currentMessageId);
|
|
540
|
+
}
|
|
541
|
+
accumulatedContent += delta.content;
|
|
542
|
+
yield factory.messageDelta(currentMessageId, textBlock(delta.content));
|
|
543
|
+
}
|
|
644
544
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
545
|
+
if (delta.tool_calls) {
|
|
546
|
+
if (!hasMessageStarted) {
|
|
547
|
+
ensureMessageStarted();
|
|
548
|
+
yield factory.messageStarted(currentMessageId);
|
|
653
549
|
}
|
|
654
550
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if (!hasMessageStarted) {
|
|
658
|
-
ensureMessageStarted();
|
|
659
|
-
yield factory.messageStarted(currentMessageId);
|
|
660
|
-
}
|
|
551
|
+
for (const tc of delta.tool_calls) {
|
|
552
|
+
const idx = tc.index;
|
|
661
553
|
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
yield factory.toolCallStarted(fcId, delta.function_call.name);
|
|
554
|
+
if (tc.id) {
|
|
555
|
+
pendingToolCalls.set(idx, { id: tc.id, name: tc.function?.name ?? "", args: "" });
|
|
556
|
+
yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
|
|
666
557
|
}
|
|
667
|
-
|
|
668
|
-
|
|
558
|
+
|
|
559
|
+
if (tc.function?.arguments) {
|
|
560
|
+
const pending = pendingToolCalls.get(idx);
|
|
669
561
|
if (pending) {
|
|
670
|
-
pending.args +=
|
|
671
|
-
yield factory.toolCallDelta(pending.id, { argumentsText:
|
|
562
|
+
pending.args += tc.function.arguments;
|
|
563
|
+
yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
|
|
672
564
|
}
|
|
673
565
|
}
|
|
674
566
|
}
|
|
567
|
+
}
|
|
675
568
|
|
|
676
|
-
|
|
677
|
-
if (
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
569
|
+
if (delta.function_call) {
|
|
570
|
+
if (!hasMessageStarted) {
|
|
571
|
+
ensureMessageStarted();
|
|
572
|
+
yield factory.messageStarted(currentMessageId);
|
|
573
|
+
}
|
|
682
574
|
|
|
683
|
-
|
|
684
|
-
|
|
575
|
+
if (delta.function_call.name) {
|
|
576
|
+
const fcId = `fc-${chunk.id}-0`;
|
|
577
|
+
pendingToolCalls.set(0, { id: fcId, name: delta.function_call.name, args: "" });
|
|
578
|
+
yield factory.toolCallStarted(fcId, delta.function_call.name);
|
|
579
|
+
}
|
|
580
|
+
if (delta.function_call.arguments) {
|
|
581
|
+
const pending = pendingToolCalls.get(0);
|
|
582
|
+
if (pending) {
|
|
583
|
+
pending.args += delta.function_call.arguments;
|
|
584
|
+
yield factory.toolCallDelta(pending.id, { argumentsText: delta.function_call.arguments });
|
|
585
|
+
}
|
|
685
586
|
}
|
|
686
587
|
}
|
|
687
|
-
}
|
|
688
588
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
589
|
+
if (finishReason && finishReason !== null) {
|
|
590
|
+
const { events, assistantReplayMessage } = finalizePendingTurn();
|
|
591
|
+
for (const event of events) yield event;
|
|
592
|
+
yield* emitCompleted(mapStopReason(finishReason), assistantReplayMessage, chunk.id);
|
|
593
|
+
}
|
|
692
594
|
}
|
|
693
595
|
}
|
|
694
|
-
} finally {
|
|
695
|
-
try {
|
|
696
|
-
if (!streamDone) await reader.cancel().catch(() => undefined);
|
|
697
|
-
} finally {
|
|
698
|
-
reader.releaseLock();
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (parser.getRemaining().trim().length > 0) {
|
|
703
|
-
yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
|
|
704
596
|
}
|
|
705
597
|
|
|
706
|
-
|
|
707
|
-
if (!completedEmitted && (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0)) {
|
|
598
|
+
if (!gate.completed && (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0)) {
|
|
708
599
|
yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
|
|
709
|
-
|
|
710
600
|
const { events, assistantReplayMessage } = finalizePendingTurn();
|
|
711
|
-
for (const event of events)
|
|
712
|
-
yield event;
|
|
713
|
-
}
|
|
714
|
-
|
|
601
|
+
for (const event of events) yield event;
|
|
715
602
|
yield* emitCompleted(undefined, assistantReplayMessage, responseId);
|
|
716
603
|
}
|
|
717
604
|
}
|