@oh-my-pi/pi-ai 18.8.7 → 18.8.8
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/CHANGELOG.md +9 -0
- package/dist/types/providers/google-shared.d.ts +7 -0
- package/dist/types/providers/openai-responses.d.ts +7 -0
- package/dist/types/providers/openai-shared.d.ts +2 -1
- package/package.json +6 -6
- package/src/providers/factory-droid/gemini.ts +24 -6
- package/src/providers/google-shared.ts +18 -13
- package/src/providers/openai-codex-responses.ts +7 -1
- package/src/providers/openai-responses.ts +18 -4
- package/src/providers/openai-shared.ts +15 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.8.8] - 2026-10-10
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed OpenAI Responses and Codex tool calls running with empty `{}` arguments when a compatible host or proxy ends the call with an empty arguments payload after streaming the real ones ([#15099](https://github.com/can1357/oh-my-pi/pull/15099) by [@will-bogusz](https://github.com/will-bogusz))
|
|
10
|
+
- Fixed truncated Factory Droid Gemini responses ending as a hard error or a half-received tool call instead of being retried ([#15103](https://github.com/can1357/oh-my-pi/pull/15103) by [@will-bogusz](https://github.com/will-bogusz))
|
|
11
|
+
- Fixed Factory Droid Gemini ignoring an error the server reports mid-response, which hid its status and retried errors that cannot succeed ([#15103](https://github.com/can1357/oh-my-pi/pull/15103) by [@will-bogusz](https://github.com/will-bogusz))
|
|
12
|
+
- Fixed resumed OpenAI Responses sessions (xAI, Factory Droid, OpenAI and other hosts) dropping all earlier encrypted reasoning on their first request; GitHub Copilot still rebuilds history until its first response ([#15148](https://github.com/can1357/oh-my-pi/pull/15148) by [@will-bogusz](https://github.com/will-bogusz))
|
|
13
|
+
|
|
5
14
|
## [18.8.7] - 2026-10-09
|
|
6
15
|
|
|
7
16
|
### Added
|
|
@@ -153,6 +153,13 @@ export declare function pushToolCallEvents(toolCall: ToolCall, contentIndex: num
|
|
|
153
153
|
export declare function startTextOrThinkingBlock(isThinking: true, output: AssistantMessage, stream: AssistantMessageEventStream, onBeforeStartEvent?: () => void): ThinkingContent;
|
|
154
154
|
export declare function startTextOrThinkingBlock(isThinking: false, output: AssistantMessage, stream: AssistantMessageEventStream, onBeforeStartEvent?: () => void): TextContent;
|
|
155
155
|
export declare function startTextOrThinkingBlock(isThinking: boolean, output: AssistantMessage, stream: AssistantMessageEventStream, onBeforeStartEvent?: () => void): TextContent | ThinkingContent;
|
|
156
|
+
/**
|
|
157
|
+
* Error for an in-band `error` frame in a 200 generateContent stream. Keeps the
|
|
158
|
+
* RPC status alongside the message: an in-band quota failure is classified
|
|
159
|
+
* from this text, and `RESOURCE_EXHAUSTED` is the only account-exhaustion
|
|
160
|
+
* signal some of these chunks carry (#13090).
|
|
161
|
+
*/
|
|
162
|
+
export declare function googleStreamChunkError(error: NonNullable<GenerateContentResponse["error"]>, provider: string): Error;
|
|
156
163
|
/**
|
|
157
164
|
* Drives the chunked `generateContentStream` iterator into an `AssistantMessage` and
|
|
158
165
|
* the corresponding `AssistantMessageEventStream`. Shared between `streamGoogle` and
|
|
@@ -82,7 +82,14 @@ export declare function pollOpenAIResponsesResultForCompletion(args: {
|
|
|
82
82
|
wait?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
83
83
|
}): Promise<Record<string, unknown> | undefined>;
|
|
84
84
|
interface OpenAIResponsesProviderSessionState extends ProviderSessionState, OpenAIStrictToolsState, OpenAIReasoningEffortFallbackState {
|
|
85
|
+
/**
|
|
86
|
+
* Replay native history items. Starts false only on connection-bound hosts;
|
|
87
|
+
* `close()` clears it so the next request rebuilds history from message
|
|
88
|
+
* content; the first successful response sets it.
|
|
89
|
+
*/
|
|
85
90
|
nativeHistoryReplayWarmed: boolean;
|
|
91
|
+
/** Bumped by `close()`; a response warms the state only if no close happened since its request started. */
|
|
92
|
+
closeCount: number;
|
|
86
93
|
/** Stateful `previous_response_id` chain baselines, keyed by baseUrl/model/session. */
|
|
87
94
|
chains: Map<string, OpenAIResponsesChainState>;
|
|
88
95
|
/** `configuration_update` effort baselines, keyed by baseUrl/model/session. */
|
|
@@ -622,7 +622,8 @@ export declare function accumulateToolCallArgumentsDelta(block: ResponsesToolCal
|
|
|
622
622
|
* Finalize streamed function-call arguments from the authoritative `.done`
|
|
623
623
|
* payload. The caller owns the `argumentsDone` flag (generic Responses sets it;
|
|
624
624
|
* Codex's block shape has no such field), so this only rewrites `arguments` and
|
|
625
|
-
* drops the transient accumulation fields.
|
|
625
|
+
* drops the transient accumulation fields. A blank payload is not authoritative:
|
|
626
|
+
* it parses the buffered deltas and keeps them so the final item can still win.
|
|
626
627
|
*/
|
|
627
628
|
export declare function finalizeToolCallArgumentsDone(block: ResponsesToolCallBlock, args: string): void;
|
|
628
629
|
export declare function accumulateCustomToolCallInputDelta(block: ResponsesToolCallBlock, delta: string, stream: AssistantMessageEventStream, output: AssistantMessage, contentIndex: number): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.8.
|
|
3
|
+
"version": "18.8.8",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.8.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.8.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.8.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.8.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.8.
|
|
158
|
+
"@oh-my-pi/omptype": "18.8.8",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.8.8",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.8.8",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.8.8",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.8.8"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
|
@@ -15,6 +15,7 @@ import { notifyProviderResponse } from "../../utils/provider-response";
|
|
|
15
15
|
import { dereferenceJsonSchema, normalizeSchemaForFactoryDroid, toolWireSchema } from "../../utils/schema";
|
|
16
16
|
import {
|
|
17
17
|
extractGoogleErrorMessage,
|
|
18
|
+
googleStreamChunkError,
|
|
18
19
|
mapGoogleUsage,
|
|
19
20
|
mapStopReasonString,
|
|
20
21
|
nextToolCallId,
|
|
@@ -87,22 +88,22 @@ const FACTORY_DROID_BLOCK_REASONS: Record<string, true> = {
|
|
|
87
88
|
* "unknown" bucket has no StopReason equivalent, so unknown terminators
|
|
88
89
|
* surface as errors instead of masquerading as a clean stop.
|
|
89
90
|
*/
|
|
90
|
-
function mapFactoryDroidFinishReason(reason: string
|
|
91
|
+
function mapFactoryDroidFinishReason(reason: string): {
|
|
91
92
|
stopReason: "stop" | "length" | "error";
|
|
92
93
|
errorMessage?: string;
|
|
93
94
|
} {
|
|
94
95
|
// mapStopReasonString already implements the CLI's outcome table
|
|
95
96
|
// (STOP→stop, MAX_TOKENS→length, everything else→error); the cast narrows
|
|
96
97
|
// its wide StopReason return to the three values it ever produces.
|
|
97
|
-
const stopReason = mapStopReasonString(reason
|
|
98
|
+
const stopReason = mapStopReasonString(reason) as "stop" | "length" | "error";
|
|
98
99
|
if (stopReason !== "error") return { stopReason };
|
|
99
|
-
if (
|
|
100
|
+
if (FACTORY_DROID_BLOCK_REASONS[reason]) {
|
|
100
101
|
return { stopReason: "error", errorMessage: `Generation was blocked by content filters (${reason})` };
|
|
101
102
|
}
|
|
102
103
|
if (reason === "MALFORMED_FUNCTION_CALL") {
|
|
103
104
|
return { stopReason: "error", errorMessage: `Generation failed with finish reason: ${reason}` };
|
|
104
105
|
}
|
|
105
|
-
return { stopReason: "error", errorMessage: `Unknown finish reason: ${reason
|
|
106
|
+
return { stopReason: "error", errorMessage: `Unknown finish reason: ${reason}` };
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
/** Transports whose outputs carry Gemini-verifiable signatures (Factory's own outputs are stamped google-generative-ai). */
|
|
@@ -414,6 +415,9 @@ export function streamFactoryDroidGemini(
|
|
|
414
415
|
},
|
|
415
416
|
);
|
|
416
417
|
for await (const chunk of chunks) {
|
|
418
|
+
// A server-declared error keeps its status and the same classification
|
|
419
|
+
// as the Google provider; only a stream that just stops is a premature close.
|
|
420
|
+
if (chunk.error) throw googleStreamChunkError(chunk.error, model.provider);
|
|
417
421
|
if (firstTokenTime === undefined && chunk.candidates?.[0]?.content?.parts?.some(part => part.text)) {
|
|
418
422
|
firstTokenTime = performance.now();
|
|
419
423
|
}
|
|
@@ -484,6 +488,20 @@ export function streamFactoryDroidGemini(
|
|
|
484
488
|
if (firstTokenTime !== undefined) output.ttft = firstTokenTime - startTime;
|
|
485
489
|
|
|
486
490
|
closeBlock();
|
|
491
|
+
// A stream that reaches EOF without a finishReason (and without a
|
|
492
|
+
// promptFeedback block) was truncated, even when it already carried
|
|
493
|
+
// functionCall parts; fail it as retryable instead of finishing the turn.
|
|
494
|
+
if (finishReason === undefined && !blockReason) {
|
|
495
|
+
// Worded like the other incomplete-stream errors so turn recovery
|
|
496
|
+
// continues a stream that already rendered text.
|
|
497
|
+
throw new AIError.ProviderResponseError(
|
|
498
|
+
"Factory Droid Gemini stream closed before a finish_reason was received",
|
|
499
|
+
{
|
|
500
|
+
provider: model.provider,
|
|
501
|
+
kind: "incomplete-stream",
|
|
502
|
+
},
|
|
503
|
+
);
|
|
504
|
+
}
|
|
487
505
|
for (const contentIndex of toolCallIndices) {
|
|
488
506
|
const toolCall = output.content[contentIndex] as ToolCall;
|
|
489
507
|
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
@@ -499,12 +517,12 @@ export function streamFactoryDroidGemini(
|
|
|
499
517
|
output.errorMessage = `Generation was blocked by content filters (${blockReason})`;
|
|
500
518
|
output.stopDetails = { type: "content_filter", category: blockReason };
|
|
501
519
|
stream.push({ type: "error", reason: "error", error: output });
|
|
502
|
-
} else {
|
|
520
|
+
} else if (finishReason !== undefined) {
|
|
503
521
|
const mapped = mapFactoryDroidFinishReason(finishReason);
|
|
504
522
|
output.stopReason = mapped.stopReason;
|
|
505
523
|
if (mapped.errorMessage) {
|
|
506
524
|
output.errorMessage = mapped.errorMessage;
|
|
507
|
-
if (mapped.stopReason === "error" &&
|
|
525
|
+
if (mapped.stopReason === "error" && FACTORY_DROID_BLOCK_REASONS[finishReason]) {
|
|
508
526
|
output.stopDetails = { type: "content_filter", category: finishReason };
|
|
509
527
|
}
|
|
510
528
|
}
|
|
@@ -595,6 +595,23 @@ export function startTextOrThinkingBlock(
|
|
|
595
595
|
return block;
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Error for an in-band `error` frame in a 200 generateContent stream. Keeps the
|
|
600
|
+
* RPC status alongside the message: an in-band quota failure is classified
|
|
601
|
+
* from this text, and `RESOURCE_EXHAUSTED` is the only account-exhaustion
|
|
602
|
+
* signal some of these chunks carry (#13090).
|
|
603
|
+
*/
|
|
604
|
+
export function googleStreamChunkError(error: NonNullable<GenerateContentResponse["error"]>, provider: string): Error {
|
|
605
|
+
const detail =
|
|
606
|
+
error.message && error.status
|
|
607
|
+
? `${error.message} (${error.status})`
|
|
608
|
+
: error.message || error.status || "unknown error";
|
|
609
|
+
const message = `Google API stream error: ${detail}`;
|
|
610
|
+
return typeof error.code === "number" && error.code >= 400
|
|
611
|
+
? new AIError.GoogleApiError(message, error.code)
|
|
612
|
+
: new AIError.ProviderResponseError(message, { provider, kind: "output" });
|
|
613
|
+
}
|
|
614
|
+
|
|
598
615
|
/**
|
|
599
616
|
* Drives the chunked `generateContentStream` iterator into an `AssistantMessage` and
|
|
600
617
|
* the corresponding `AssistantMessageEventStream`. Shared between `streamGoogle` and
|
|
@@ -645,19 +662,7 @@ export async function consumeGoogleStream<T extends GoogleApiType>(args: {
|
|
|
645
662
|
};
|
|
646
663
|
|
|
647
664
|
for await (const chunk of googleStream) {
|
|
648
|
-
if (chunk.error)
|
|
649
|
-
// Keep the RPC status alongside the message: an in-band quota failure
|
|
650
|
-
// is classified from this text, and `RESOURCE_EXHAUSTED` is the only
|
|
651
|
-
// account-exhaustion signal some of these chunks carry (#13090).
|
|
652
|
-
const detail =
|
|
653
|
-
chunk.error.message && chunk.error.status
|
|
654
|
-
? `${chunk.error.message} (${chunk.error.status})`
|
|
655
|
-
: chunk.error.message || chunk.error.status || "unknown error";
|
|
656
|
-
const message = `Google API stream error: ${detail}`;
|
|
657
|
-
throw typeof chunk.error.code === "number" && chunk.error.code >= 400
|
|
658
|
-
? new AIError.GoogleApiError(message, chunk.error.code)
|
|
659
|
-
: new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
|
|
660
|
-
}
|
|
665
|
+
if (chunk.error) throw googleStreamChunkError(chunk.error, model.provider);
|
|
661
666
|
if (!chunk.candidates?.length && chunk.promptFeedback?.blockReason) {
|
|
662
667
|
const detail = chunk.promptFeedback.blockReasonMessage;
|
|
663
668
|
throw new AIError.ProviderResponseError(
|
|
@@ -2637,11 +2637,17 @@ class CodexStreamProcessor {
|
|
|
2637
2637
|
}
|
|
2638
2638
|
|
|
2639
2639
|
if (item.type === "function_call") {
|
|
2640
|
+
const partial = block?.type === "toolCall" ? block[kStreamingPartialJson] : undefined;
|
|
2640
2641
|
const toolCall: ToolCall = {
|
|
2641
2642
|
type: "toolCall",
|
|
2642
2643
|
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2643
2644
|
name: item.name,
|
|
2644
|
-
|
|
2645
|
+
// An empty terminal item falls back to what `.delta`/`.done` already delivered.
|
|
2646
|
+
arguments: item.arguments
|
|
2647
|
+
? parseToolCallArguments(item.arguments)
|
|
2648
|
+
: block?.type === "toolCall" && !partial
|
|
2649
|
+
? block.arguments
|
|
2650
|
+
: parseToolCallArguments(partial),
|
|
2645
2651
|
};
|
|
2646
2652
|
item.arguments = replayableToolCallArguments(item.arguments, toolCall.arguments);
|
|
2647
2653
|
if (block?.type === "toolCall") {
|
|
@@ -395,7 +395,14 @@ async function* resumeOpenAIResponsesEventStream(
|
|
|
395
395
|
|
|
396
396
|
interface OpenAIResponsesProviderSessionState
|
|
397
397
|
extends ProviderSessionState, OpenAIStrictToolsState, OpenAIReasoningEffortFallbackState {
|
|
398
|
+
/**
|
|
399
|
+
* Replay native history items. Starts false only on connection-bound hosts;
|
|
400
|
+
* `close()` clears it so the next request rebuilds history from message
|
|
401
|
+
* content; the first successful response sets it.
|
|
402
|
+
*/
|
|
398
403
|
nativeHistoryReplayWarmed: boolean;
|
|
404
|
+
/** Bumped by `close()`; a response warms the state only if no close happened since its request started. */
|
|
405
|
+
closeCount: number;
|
|
399
406
|
/** Stateful `previous_response_id` chain baselines, keyed by baseUrl/model/session. */
|
|
400
407
|
chains: Map<string, OpenAIResponsesChainState>;
|
|
401
408
|
/** `configuration_update` effort baselines, keyed by baseUrl/model/session. */
|
|
@@ -423,13 +430,16 @@ interface OpenAIResponsesChainState {
|
|
|
423
430
|
disabled: boolean;
|
|
424
431
|
}
|
|
425
432
|
|
|
426
|
-
function createOpenAIResponsesProviderSessionState(
|
|
433
|
+
function createOpenAIResponsesProviderSessionState(
|
|
434
|
+
model: Model<"openai-responses">,
|
|
435
|
+
): OpenAIResponsesProviderSessionState {
|
|
427
436
|
const strictToolsState = createOpenAIStrictToolsState();
|
|
428
437
|
const reasoningEffortFallbackState = createOpenAIReasoningEffortFallbackState();
|
|
429
438
|
const state: OpenAIResponsesProviderSessionState = {
|
|
430
439
|
...strictToolsState,
|
|
431
440
|
...reasoningEffortFallbackState,
|
|
432
|
-
nativeHistoryReplayWarmed:
|
|
441
|
+
nativeHistoryReplayWarmed: !model.compat.connectionBoundNativeHistory,
|
|
442
|
+
closeCount: 0,
|
|
433
443
|
chains: new Map(),
|
|
434
444
|
effortControls: new Map(),
|
|
435
445
|
releaseSession: sessionId => {
|
|
@@ -442,6 +452,7 @@ function createOpenAIResponsesProviderSessionState(): OpenAIResponsesProviderSes
|
|
|
442
452
|
},
|
|
443
453
|
close: () => {
|
|
444
454
|
state.nativeHistoryReplayWarmed = false;
|
|
455
|
+
state.closeCount++;
|
|
445
456
|
state.chains.clear();
|
|
446
457
|
state.effortControls.clear();
|
|
447
458
|
clearOpenAIStrictToolsState(state);
|
|
@@ -459,7 +470,7 @@ function getOpenAIResponsesProviderSessionState(
|
|
|
459
470
|
const key = `${OPENAI_RESPONSES_PROVIDER_SESSION_STATE_PREFIX}${model.provider}`;
|
|
460
471
|
const existing = providerSessionState.get(key) as OpenAIResponsesProviderSessionState | undefined;
|
|
461
472
|
if (existing) return existing;
|
|
462
|
-
const created = createOpenAIResponsesProviderSessionState();
|
|
473
|
+
const created = createOpenAIResponsesProviderSessionState(model);
|
|
463
474
|
providerSessionState.set(key, created);
|
|
464
475
|
return created;
|
|
465
476
|
}
|
|
@@ -733,6 +744,7 @@ const streamOpenAIResponsesOnce = (
|
|
|
733
744
|
});
|
|
734
745
|
const premiumRequestsTotal = copilotPremiumRequests;
|
|
735
746
|
const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState);
|
|
747
|
+
const closeCountAtStart = providerSessionState?.closeCount;
|
|
736
748
|
const strictToolsScope = getOpenAIStrictToolsScope(model, baseUrl);
|
|
737
749
|
const promptCacheBreakpointPolicy =
|
|
738
750
|
resolveCacheRetention(options?.cacheRetention) !== "none" && options?.promptCache?.mode === "explicit"
|
|
@@ -1250,7 +1262,9 @@ const streamOpenAIResponsesOnce = (
|
|
|
1250
1262
|
{ supportsImageDetailOriginal: model.compat.supportsImageDetailOriginal },
|
|
1251
1263
|
);
|
|
1252
1264
|
if (replayableResponseItems) {
|
|
1253
|
-
if (providerSessionState
|
|
1265
|
+
if (providerSessionState && providerSessionState.closeCount === closeCountAtStart) {
|
|
1266
|
+
providerSessionState.nativeHistoryReplayWarmed = true;
|
|
1267
|
+
}
|
|
1254
1268
|
if (chainState) {
|
|
1255
1269
|
chainState.lastParams = cloneJsonTree(
|
|
1256
1270
|
activeTrailingScaffoldingItems > 0 && Array.isArray(activeParams.input)
|
|
@@ -3052,7 +3052,8 @@ export function accumulateToolCallArgumentsDelta(
|
|
|
3052
3052
|
contentIndex: number,
|
|
3053
3053
|
): void {
|
|
3054
3054
|
delta = optionalResponsesText(delta, "function call arguments delta") ?? "";
|
|
3055
|
-
|
|
3055
|
+
// A non-blank `.done` consumed the buffer; a late delta must not reopen it.
|
|
3056
|
+
if (!delta || block[kStreamingPartialJson] === undefined) return;
|
|
3056
3057
|
block[kStreamingPartialJson] += delta;
|
|
3057
3058
|
const throttled = parseStreamingJsonThrottled(block[kStreamingPartialJson], block[kStreamingLastParseLen] ?? 0);
|
|
3058
3059
|
if (throttled) {
|
|
@@ -3066,9 +3067,14 @@ export function accumulateToolCallArgumentsDelta(
|
|
|
3066
3067
|
* Finalize streamed function-call arguments from the authoritative `.done`
|
|
3067
3068
|
* payload. The caller owns the `argumentsDone` flag (generic Responses sets it;
|
|
3068
3069
|
* Codex's block shape has no such field), so this only rewrites `arguments` and
|
|
3069
|
-
* drops the transient accumulation fields.
|
|
3070
|
+
* drops the transient accumulation fields. A blank payload is not authoritative:
|
|
3071
|
+
* it parses the buffered deltas and keeps them so the final item can still win.
|
|
3070
3072
|
*/
|
|
3071
3073
|
export function finalizeToolCallArgumentsDone(block: ResponsesToolCallBlock, args: string): void {
|
|
3074
|
+
if (!args.trim()) {
|
|
3075
|
+
block.arguments = parseToolCallArguments(block[kStreamingPartialJson]);
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3072
3078
|
block[kStreamingPartialJson] = args;
|
|
3073
3079
|
block.arguments = parseToolCallArguments(block[kStreamingPartialJson]);
|
|
3074
3080
|
clearStreamingPartialJson(block);
|
|
@@ -3638,11 +3644,13 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
3638
3644
|
closeOpenItem(event.output_index, item.id, entry);
|
|
3639
3645
|
} else if (item.type === "function_call") {
|
|
3640
3646
|
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
?
|
|
3645
|
-
:
|
|
3647
|
+
// A blank `.done` leaves the delta buffer in place, so the item's own arguments still win.
|
|
3648
|
+
const args =
|
|
3649
|
+
block?.[kStreamingArgumentsDone] && block[kStreamingPartialJson] === undefined
|
|
3650
|
+
? block.arguments
|
|
3651
|
+
: item.arguments
|
|
3652
|
+
? parseToolCallArguments(item.arguments)
|
|
3653
|
+
: parseToolCallArguments(block?.[kStreamingPartialJson]);
|
|
3646
3654
|
item.arguments = replayableToolCallArguments(item.arguments, args);
|
|
3647
3655
|
const toolCall: ToolCall = {
|
|
3648
3656
|
type: "toolCall",
|