@oh-my-pi/pi-ai 15.7.2 → 15.7.5
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 +33 -0
- package/dist/types/auth-gateway/types.d.ts +3 -1
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +15 -0
- package/dist/types/providers/google-gemini-cli.d.ts +10 -1
- package/dist/types/providers/google-shared.d.ts +9 -1
- package/dist/types/stream.d.ts +4 -0
- package/dist/types/types.d.ts +15 -0
- package/dist/types/utils/idle-iterator.d.ts +14 -0
- package/dist/types/utils/json-parse.d.ts +27 -0
- package/package.json +3 -3
- package/src/auth-gateway/server.ts +1 -0
- package/src/auth-gateway/types.ts +10 -1
- package/src/providers/amazon-bedrock.ts +12 -3
- package/src/providers/anthropic-messages-server-schema.ts +13 -0
- package/src/providers/anthropic-messages-server.ts +3 -0
- package/src/providers/anthropic.ts +44 -8
- package/src/providers/azure-openai-responses.ts +3 -2
- package/src/providers/google-gemini-cli.ts +20 -6
- package/src/providers/google-shared.ts +21 -6
- package/src/providers/openai-codex-responses.ts +18 -5
- package/src/providers/openai-completions.ts +18 -12
- package/src/providers/openai-responses-shared.ts +22 -3
- package/src/providers/openai-responses.ts +3 -2
- package/src/providers/register-builtins.ts +4 -1
- package/src/stream.ts +12 -2
- package/src/types.ts +16 -0
- package/src/utils/idle-iterator.ts +27 -0
- package/src/utils/json-parse.ts +34 -0
- package/src/utils/oauth/xiaomi.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.7.5] - 2026-06-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added Anthropic task budget support, forwarding `taskBudget` as `output_config.task_budget` with the required `task-budgets-2026-03-13` beta header and accepting Anthropic gateway requests that send `output_config.task_budget`.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed OpenAI-family first-event timeouts so `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` cannot be undercut by a lower generic `PI_STREAM_FIRST_EVENT_TIMEOUT_MS` while local OpenAI-compatible servers are still processing large prompts. `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` is now available for an explicit OpenAI-specific first-event override. ([#1603](https://github.com/can1357/oh-my-pi/issues/1603))
|
|
14
|
+
|
|
15
|
+
## [15.7.4] - 2026-05-31
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed Anthropic stream idle-timeout retries after the provider stream has already begun.
|
|
20
|
+
- Fixed Xiaomi MiMo `/login` rejecting token-plan (`tp-`) keys with `401 Invalid API Key`. The validation request was still sending the legacy Anthropic `x-api-key` header against the OpenAI-compatible `/v1/chat/completions` endpoint; switched to `Authorization: Bearer`, matching the runtime path. ([#1580](https://github.com/can1357/oh-my-pi/issues/1580))
|
|
21
|
+
- Fixed OpenAI-compatible tool-call replay to send empty assistant content instead of `null`, avoiding strict custom backends that crash with `str`/`NoneType` concatenation after subagent tool results. ([#1585](https://github.com/can1357/oh-my-pi/issues/1585))
|
|
22
|
+
|
|
23
|
+
## [15.7.3] - 2026-05-31
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Throttled per-delta streaming JSON re-parsing of OpenAI Responses/Codex tool-call arguments (bounding mid-stream parse cost from O(N²) to O(N)). Finalization via `response.output_item.done` now writes the authoritative full arguments back to the persisted assistant-message block, so tool calls finalized without a trailing `response.function_call_arguments.done` no longer retain stale/empty (`{}`) arguments. ([#1507](https://github.com/can1357/oh-my-pi/pull/1507))
|
|
28
|
+
|
|
5
29
|
## [15.6.0] - 2026-05-30
|
|
6
30
|
|
|
7
31
|
### Fixed
|
|
@@ -130,6 +154,15 @@
|
|
|
130
154
|
|
|
131
155
|
- Fixed Synthetic model discovery to treat the provider `/models` response as authoritative so deprecated bundled IDs are pruned from the runtime cache, and changed Synthetic login validation to avoid probing a specific model ([#1417](https://github.com/can1357/oh-my-pi/issues/1417)).
|
|
132
156
|
|
|
157
|
+
### Added
|
|
158
|
+
|
|
159
|
+
- Added `parseStreamingJsonThrottled` to `@oh-my-pi/pi-ai/utils/json-parse` — a per-delta wrapper around `parseStreamingJson` that skips re-parses until the buffer has grown by `minGrowthBytes` (default 256). Wired into the streaming hot path of every provider's tool-call argument accumulator (`anthropic`, `amazon-bedrock`, `openai-completions`, `openai-codex-responses`, `openai-responses-shared`) so per-delta cost is O(N) in total buffer length instead of O(N²). Each provider's `toolcall_end` still runs a final unthrottled parse, so the published `block.arguments` is unchanged.
|
|
160
|
+
- Added named-tool routing support to Google providers: `GoogleSharedStreamOptions.toolChoice` and `GoogleGeminiCliOptions.toolChoice` now accept `{ mode: "ANY"; allowedFunctionNames: [string, ...string[]] }` in addition to the string forms. `mapGoogleToolChoice` converts `ToolChoice` objects of shape `{ type: "tool" | "function", name }` to the wire form. Mirrors the equivalent Anthropic mapper.
|
|
161
|
+
|
|
162
|
+
### Changed
|
|
163
|
+
|
|
164
|
+
- Changed `mapGoogleToolChoice` to be exported from `@oh-my-pi/pi-ai/stream` so callers can build the wire-shape allow-list directly without re-deriving it.
|
|
165
|
+
|
|
133
166
|
## [15.5.0] - 2026-05-26
|
|
134
167
|
### Added
|
|
135
168
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Effort } from "../model-thinking";
|
|
2
|
-
import type { AssistantMessage, AssistantMessageEventStream, CacheRetention, Context, ServiceTier } from "../types";
|
|
2
|
+
import type { AssistantMessage, AssistantMessageEventStream, CacheRetention, Context, ServiceTier, TokenTaskBudget } from "../types";
|
|
3
3
|
/**
|
|
4
4
|
* Wire types for the omp auth-gateway.
|
|
5
5
|
*
|
|
@@ -54,6 +54,8 @@ export interface AuthGatewayParsedRequestOptions {
|
|
|
54
54
|
thinkingBudgets?: Partial<Record<Effort, number>>;
|
|
55
55
|
/** Suppress the provider's reasoning summary stream. */
|
|
56
56
|
hideThinkingSummary?: boolean;
|
|
57
|
+
/** Anthropic `output_config.task_budget` advisory loop budget. */
|
|
58
|
+
taskBudget?: TokenTaskBudget;
|
|
57
59
|
/** OpenAI service tier (auto|default|flex|scale|priority). */
|
|
58
60
|
serviceTier?: ServiceTier;
|
|
59
61
|
/** Cache retention hint derived from inbound `cache_control` markers. */
|
|
@@ -429,6 +429,21 @@ export declare const anthropicMessagesRequestSchema: z.ZodObject<{
|
|
|
429
429
|
budget_tokens: z.ZodOptional<z.ZodNumber>;
|
|
430
430
|
display: z.ZodOptional<z.ZodUnknown>;
|
|
431
431
|
}, z.core.$strip>], "type">>;
|
|
432
|
+
output_config: z.ZodOptional<z.ZodObject<{
|
|
433
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
434
|
+
high: "high";
|
|
435
|
+
low: "low";
|
|
436
|
+
max: "max";
|
|
437
|
+
medium: "medium";
|
|
438
|
+
xhigh: "xhigh";
|
|
439
|
+
}>>;
|
|
440
|
+
task_budget: z.ZodOptional<z.ZodObject<{
|
|
441
|
+
type: z.ZodLiteral<"tokens">;
|
|
442
|
+
total: z.ZodNumber;
|
|
443
|
+
remaining: z.ZodOptional<z.ZodNumber>;
|
|
444
|
+
}, z.core.$strip>>;
|
|
445
|
+
format: z.ZodOptional<z.ZodUnknown>;
|
|
446
|
+
}, z.core.$strip>>;
|
|
432
447
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
433
448
|
container: z.ZodOptional<z.ZodUnknown>;
|
|
434
449
|
context_management: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -7,7 +7,15 @@ import { type GoogleThinkingLevel } from "./google-shared";
|
|
|
7
7
|
*/
|
|
8
8
|
export type { GoogleThinkingLevel };
|
|
9
9
|
export interface GoogleGeminiCliOptions extends StreamOptions {
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Tool selection mode. String forms map directly to Gemini
|
|
12
|
+
* `FunctionCallingConfigMode`. The object form forces a single named tool —
|
|
13
|
+
* `mode: "ANY"` is wire-required when `allowedFunctionNames` is set.
|
|
14
|
+
*/
|
|
15
|
+
toolChoice?: "auto" | "none" | "any" | {
|
|
16
|
+
mode: "ANY";
|
|
17
|
+
allowedFunctionNames: [string, ...string[]];
|
|
18
|
+
};
|
|
11
19
|
/**
|
|
12
20
|
* Thinking/reasoning configuration.
|
|
13
21
|
* - Gemini 2.x models: use `budgetTokens` to set the thinking budget
|
|
@@ -61,6 +69,7 @@ interface CloudCodeAssistRequest {
|
|
|
61
69
|
toolConfig?: {
|
|
62
70
|
functionCallingConfig: {
|
|
63
71
|
mode: FunctionCallingConfigMode;
|
|
72
|
+
allowedFunctionNames?: string[];
|
|
64
73
|
};
|
|
65
74
|
};
|
|
66
75
|
};
|
|
@@ -19,7 +19,15 @@ export type GoogleThinkingLevel = "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LO
|
|
|
19
19
|
* `google-gemini-cli` uses a different transport and request shape — do not extend this for it.
|
|
20
20
|
*/
|
|
21
21
|
export interface GoogleSharedStreamOptions extends StreamOptions {
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Tool selection mode. String forms map directly to Gemini
|
|
24
|
+
* `FunctionCallingConfigMode`. The object form forces a single named tool
|
|
25
|
+
* — `mode: "ANY"` is wire-required when `allowedFunctionNames` is set.
|
|
26
|
+
*/
|
|
27
|
+
toolChoice?: "auto" | "none" | "any" | {
|
|
28
|
+
mode: "ANY";
|
|
29
|
+
allowedFunctionNames: [string, ...string[]];
|
|
30
|
+
};
|
|
23
31
|
thinking?: {
|
|
24
32
|
enabled: boolean;
|
|
25
33
|
budgetTokens?: number;
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Effort } from "./model-thinking";
|
|
2
2
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
3
|
+
import type { GoogleOptions } from "./providers/google";
|
|
4
|
+
import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
|
|
5
|
+
import type { GoogleVertexOptions } from "./providers/google-vertex";
|
|
3
6
|
import type { Api, AssistantMessage, Context, Model, OptionsForApi, SimpleStreamOptions, ToolChoice } from "./types";
|
|
4
7
|
import { AssistantMessageEventStream } from "./utils/event-stream";
|
|
5
8
|
/**
|
|
@@ -22,3 +25,4 @@ export declare function completeSimple<TApi extends Api>(model: Model<TApi>, con
|
|
|
22
25
|
export declare const OUTPUT_FALLBACK_BUFFER = 4000;
|
|
23
26
|
export declare const ANTHROPIC_THINKING: Record<Effort, number>;
|
|
24
27
|
export declare function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["toolChoice"];
|
|
28
|
+
export declare function mapGoogleToolChoice(choice?: ToolChoice): GoogleOptions["toolChoice"] | GoogleGeminiCliOptions["toolChoice"] | GoogleVertexOptions["toolChoice"];
|
package/dist/types/types.d.ts
CHANGED
|
@@ -55,6 +55,11 @@ import type { Effort } from "./model-thinking";
|
|
|
55
55
|
export type ThinkingBudgets = {
|
|
56
56
|
[key in Effort]?: number;
|
|
57
57
|
};
|
|
58
|
+
export interface TokenTaskBudget {
|
|
59
|
+
type: "tokens";
|
|
60
|
+
total: number;
|
|
61
|
+
remaining?: number;
|
|
62
|
+
}
|
|
58
63
|
export type MessageAttribution = "user" | "agent";
|
|
59
64
|
export type ToolChoice = "auto" | "none" | "any" | "required" | {
|
|
60
65
|
type: "function";
|
|
@@ -184,6 +189,11 @@ export interface StreamOptions {
|
|
|
184
189
|
* For example, Anthropic uses `user_id` for abuse tracking and rate limiting.
|
|
185
190
|
*/
|
|
186
191
|
metadata?: Record<string, unknown>;
|
|
192
|
+
/**
|
|
193
|
+
* Advisory token budget for a full agentic loop. Anthropic encodes this as
|
|
194
|
+
* `output_config.task_budget` with the `task-budgets-2026-03-13` beta header.
|
|
195
|
+
*/
|
|
196
|
+
taskBudget?: TokenTaskBudget;
|
|
187
197
|
/**
|
|
188
198
|
* Optional session identifier for providers that support session-based
|
|
189
199
|
* routing, request affinity, or transport reuse. Providers may also use this
|
|
@@ -228,6 +238,11 @@ export interface StreamOptions {
|
|
|
228
238
|
* `0` to disable both layers for this request. After the first semantic
|
|
229
239
|
* event arrives, `streamIdleTimeoutMs` governs inter-event stalls. Falls
|
|
230
240
|
* back to `PI_STREAM_FIRST_EVENT_TIMEOUT_MS` and then to a 100s default.
|
|
241
|
+
* OpenAI-family transports additionally honor
|
|
242
|
+
* `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` as the most-specific override and
|
|
243
|
+
* floor the first-event budget at the resolved idle (per-call
|
|
244
|
+
* `streamIdleTimeoutMs` or `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS`) so slow local
|
|
245
|
+
* OpenAI-compatible servers are not undercut during prompt processing.
|
|
231
246
|
*
|
|
232
247
|
* Iterator-level honored by: every built-in provider (via the lazy-stream
|
|
233
248
|
* forwarder in `register-builtins`). SDK-request honored by:
|
|
@@ -32,6 +32,20 @@ export declare function getOpenAIStreamIdleTimeoutMs(fallbackMs?: number): numbe
|
|
|
32
32
|
* env overrides still trump the fallback.
|
|
33
33
|
*/
|
|
34
34
|
export declare function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number, fallbackMs?: number): number | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Returns the first-event timeout used for OpenAI-family streaming transports.
|
|
37
|
+
*
|
|
38
|
+
* Precedence: explicit `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` (including a
|
|
39
|
+
* `"0"` disable) wins outright. Otherwise the resolved idle (caller-supplied
|
|
40
|
+
* `idleTimeoutMs` — which itself already encompasses per-call
|
|
41
|
+
* `streamIdleTimeoutMs` or `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` resolved
|
|
42
|
+
* upstream) floors the first-event budget so slow local OpenAI-compatible
|
|
43
|
+
* servers are not undercut by a shorter `PI_STREAM_FIRST_EVENT_TIMEOUT_MS`
|
|
44
|
+
* or the global default during prompt processing.
|
|
45
|
+
*
|
|
46
|
+
* Returns `undefined` when an explicit env knob disables the watchdog.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs?: number, fallbackMs?: number): number | undefined;
|
|
35
49
|
export interface IdleTimeoutIteratorOptions {
|
|
36
50
|
idleTimeoutMs?: number;
|
|
37
51
|
firstItemTimeoutMs?: number;
|
|
@@ -8,3 +8,30 @@ export declare function parseJsonWithRepair<T>(json: string): T;
|
|
|
8
8
|
* @returns Parsed object or empty object if parsing fails
|
|
9
9
|
*/
|
|
10
10
|
export declare function parseStreamingJson<T = Record<string, unknown>>(partialJson: string | undefined): T;
|
|
11
|
+
/**
|
|
12
|
+
* Default minimum byte growth before `parseStreamingJsonThrottled` will
|
|
13
|
+
* re-parse a streaming tool-call argument buffer. Bounds the mid-stream
|
|
14
|
+
* partial-parse cost from quadratic to linear in N.
|
|
15
|
+
*/
|
|
16
|
+
export declare const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
|
|
17
|
+
/**
|
|
18
|
+
* Throttled variant of {@link parseStreamingJson} for the per-delta hot path.
|
|
19
|
+
*
|
|
20
|
+
* Tool calls arrive as a long sequence of small deltas — calling
|
|
21
|
+
* `parseStreamingJson(buffer)` on every delta re-parses the entire buffer
|
|
22
|
+
* each time, giving O(N²) work in the total buffer length. Throttling skips
|
|
23
|
+
* the re-parse until at least `minGrowthBytes` of new content has arrived
|
|
24
|
+
* since the last successful parse, bounding mid-stream cost to O(N).
|
|
25
|
+
*
|
|
26
|
+
* Each provider tracks the last parsed length on its tool-call block, so the
|
|
27
|
+
* final `toolcall_end` parse (which providers already perform unconditionally)
|
|
28
|
+
* is the authoritative full parse — the throttle only delays mid-stream UI
|
|
29
|
+
* updates by at most `minGrowthBytes` of accumulated partial content.
|
|
30
|
+
*
|
|
31
|
+
* @returns the parsed object plus the new `parsedLen` to persist; or `null`
|
|
32
|
+
* when the buffer has not grown enough to warrant a re-parse.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseStreamingJsonThrottled<T = Record<string, unknown>>(partialJson: string | undefined, lastParsedLen: number, minGrowthBytes?: number): {
|
|
35
|
+
value: T;
|
|
36
|
+
parsedLen: number;
|
|
37
|
+
} | null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.7.
|
|
4
|
+
"version": "15.7.5",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"check": "biome check . && bun run check:types",
|
|
33
33
|
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
34
34
|
"lint": "biome lint .",
|
|
35
|
-
"test": "bun test",
|
|
35
|
+
"test": "bun test --parallel",
|
|
36
36
|
"fix": "biome check --write --unsafe .",
|
|
37
37
|
"fmt": "biome format --write .",
|
|
38
38
|
"generate-models": "bun scripts/generate-models.ts"
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.99.0",
|
|
42
42
|
"@bufbuild/protobuf": "^2.12.0",
|
|
43
|
-
"@oh-my-pi/pi-utils": "15.7.
|
|
43
|
+
"@oh-my-pi/pi-utils": "15.7.5",
|
|
44
44
|
"openai": "^6.39.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "4.4.3"
|
|
@@ -141,6 +141,7 @@ function buildStreamOptions(parsed: ParsedFormatRequest, api: Api, signal: Abort
|
|
|
141
141
|
if (options.reasoning !== undefined) opts.reasoning = options.reasoning;
|
|
142
142
|
if (options.disableReasoning !== undefined) opts.disableReasoning = options.disableReasoning;
|
|
143
143
|
if (options.hideThinkingSummary !== undefined) opts.hideThinkingSummary = options.hideThinkingSummary;
|
|
144
|
+
if (options.taskBudget !== undefined) opts.taskBudget = options.taskBudget;
|
|
144
145
|
if (options.serviceTier !== undefined) opts.serviceTier = options.serviceTier;
|
|
145
146
|
if (options.cacheRetention !== undefined) opts.cacheRetention = options.cacheRetention;
|
|
146
147
|
// Client-supplied `prompt_cache_key` wins; otherwise derive a stable
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Effort } from "../model-thinking";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
AssistantMessageEventStream,
|
|
5
|
+
CacheRetention,
|
|
6
|
+
Context,
|
|
7
|
+
ServiceTier,
|
|
8
|
+
TokenTaskBudget,
|
|
9
|
+
} from "../types";
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Wire types for the omp auth-gateway.
|
|
@@ -61,6 +68,8 @@ export interface AuthGatewayParsedRequestOptions {
|
|
|
61
68
|
thinkingBudgets?: Partial<Record<Effort, number>>;
|
|
62
69
|
/** Suppress the provider's reasoning summary stream. */
|
|
63
70
|
hideThinkingSummary?: boolean;
|
|
71
|
+
/** Anthropic `output_config.task_budget` advisory loop budget. */
|
|
72
|
+
taskBudget?: TokenTaskBudget;
|
|
64
73
|
|
|
65
74
|
// ── Service / routing ─────────────────────────────────────────────────
|
|
66
75
|
/** OpenAI service tier (auto|default|flex|scale|priority). */
|
|
@@ -30,7 +30,7 @@ import type {
|
|
|
30
30
|
import { normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
31
31
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
32
32
|
import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
33
|
-
import { parseStreamingJson } from "../utils/json-parse";
|
|
33
|
+
import { parseStreamingJson, parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
34
34
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
35
35
|
import { resolveAwsCredentials } from "./aws-credentials";
|
|
36
36
|
import { decodeEventStream } from "./aws-eventstream";
|
|
@@ -72,7 +72,11 @@ function resolveBearerToken(options: BedrockOptions): string | undefined {
|
|
|
72
72
|
return options.bearerToken || apiKey || $env.AWS_BEARER_TOKEN_BEDROCK;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
type Block = (TextContent | ThinkingContent | ToolCall) & {
|
|
75
|
+
type Block = (TextContent | ThinkingContent | ToolCall) & {
|
|
76
|
+
index?: number;
|
|
77
|
+
partialJson?: string;
|
|
78
|
+
lastParseLen?: number;
|
|
79
|
+
};
|
|
76
80
|
|
|
77
81
|
// ---------- Bedrock wire-format types ----------
|
|
78
82
|
// Mirrors only what we actually consume from `ConverseStreamRequest` /
|
|
@@ -454,7 +458,11 @@ function handleContentBlockDelta(
|
|
|
454
458
|
}
|
|
455
459
|
} else if (delta?.toolUse && block?.type === "toolCall") {
|
|
456
460
|
block.partialJson = (block.partialJson || "") + (delta.toolUse.input || "");
|
|
457
|
-
|
|
461
|
+
const throttled = parseStreamingJsonThrottled(block.partialJson, block.lastParseLen ?? 0);
|
|
462
|
+
if (throttled) {
|
|
463
|
+
block.arguments = throttled.value;
|
|
464
|
+
block.lastParseLen = throttled.parsedLen;
|
|
465
|
+
}
|
|
458
466
|
stream.push({ type: "toolcall_delta", contentIndex: index, delta: delta.toolUse.input || "", partial: output });
|
|
459
467
|
} else if (delta?.reasoningContent) {
|
|
460
468
|
let thinkingBlock = block;
|
|
@@ -518,6 +526,7 @@ function handleContentBlockStop(
|
|
|
518
526
|
case "toolCall":
|
|
519
527
|
block.arguments = parseStreamingJson(block.partialJson);
|
|
520
528
|
delete (block as Block).partialJson;
|
|
529
|
+
delete (block as Block).lastParseLen;
|
|
521
530
|
stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: output });
|
|
522
531
|
break;
|
|
523
532
|
}
|
|
@@ -189,6 +189,18 @@ export const thinkingConfigSchema = z.discriminatedUnion("type", [
|
|
|
189
189
|
}),
|
|
190
190
|
]);
|
|
191
191
|
|
|
192
|
+
const taskBudgetSchema = z.object({
|
|
193
|
+
type: z.literal("tokens"),
|
|
194
|
+
total: z.number(),
|
|
195
|
+
remaining: z.number().optional(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const outputConfigSchema = z.object({
|
|
199
|
+
effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
|
|
200
|
+
task_budget: taskBudgetSchema.optional(),
|
|
201
|
+
format: z.unknown().optional(),
|
|
202
|
+
});
|
|
203
|
+
|
|
192
204
|
// ─── Top-level request ─────────────────────────────────────────────────────
|
|
193
205
|
|
|
194
206
|
export const anthropicMessagesRequestSchema = z.object({
|
|
@@ -204,6 +216,7 @@ export const anthropicMessagesRequestSchema = z.object({
|
|
|
204
216
|
stop_sequences: z.array(z.string()).optional(),
|
|
205
217
|
stream: z.boolean().optional(),
|
|
206
218
|
thinking: thinkingConfigSchema.optional(),
|
|
219
|
+
output_config: outputConfigSchema.optional(),
|
|
207
220
|
// Anthropic clients commonly send `metadata: { user_id }`; the walker
|
|
208
221
|
// surfaces it on `options.metadata` for downstream provider forwarding.
|
|
209
222
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -344,6 +344,9 @@ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
|
|
|
344
344
|
break;
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
|
+
if (data.output_config?.task_budget) {
|
|
348
|
+
options.taskBudget = data.output_config.task_budget;
|
|
349
|
+
}
|
|
347
350
|
const cacheRetention = deriveCacheRetention(data);
|
|
348
351
|
if (cacheRetention !== undefined) options.cacheRetention = cacheRetention;
|
|
349
352
|
// Anthropic clients commonly send `metadata: { user_id }`; forward verbatim
|
|
@@ -47,6 +47,7 @@ import type {
|
|
|
47
47
|
StreamOptions,
|
|
48
48
|
TextContent,
|
|
49
49
|
ThinkingContent,
|
|
50
|
+
TokenTaskBudget,
|
|
50
51
|
Tool,
|
|
51
52
|
ToolCall,
|
|
52
53
|
ToolResultMessage,
|
|
@@ -65,7 +66,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
65
66
|
import { isFoundryEnabled } from "../utils/foundry";
|
|
66
67
|
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
|
|
67
68
|
import { getStreamFirstEventTimeoutMs, getStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
68
|
-
import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse";
|
|
69
|
+
import { parseJsonWithRepair, parseStreamingJson, parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
69
70
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
70
71
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
71
72
|
import { isCopilotTransientModelError } from "../utils/retry";
|
|
@@ -123,6 +124,7 @@ const claudeCodeBetaDefaults = [
|
|
|
123
124
|
const fineGrainedToolStreamingBeta = "fine-grained-tool-streaming-2025-05-14";
|
|
124
125
|
const interleavedThinkingBeta = "interleaved-thinking-2025-05-14";
|
|
125
126
|
const fastModeBeta = "fast-mode-2026-02-01";
|
|
127
|
+
const taskBudgetBeta = "task-budgets-2026-03-13";
|
|
126
128
|
|
|
127
129
|
function getHeaderCaseInsensitive(headers: Record<string, string> | undefined, headerName: string): string | undefined {
|
|
128
130
|
if (!headers) return undefined;
|
|
@@ -217,6 +219,16 @@ type AnthropicSamplingParams = MessageCreateParamsStreaming & {
|
|
|
217
219
|
top_k?: number;
|
|
218
220
|
};
|
|
219
221
|
|
|
222
|
+
type AnthropicOutputConfig = NonNullable<MessageCreateParamsStreaming["output_config"]> & {
|
|
223
|
+
task_budget?: TokenTaskBudget | null;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function getAnthropicOutputConfig(params: MessageCreateParamsStreaming): AnthropicOutputConfig {
|
|
227
|
+
const outputConfig = (params.output_config ?? {}) as AnthropicOutputConfig;
|
|
228
|
+
params.output_config = outputConfig as typeof params.output_config;
|
|
229
|
+
return outputConfig;
|
|
230
|
+
}
|
|
231
|
+
|
|
220
232
|
const ANTHROPIC_STOP_SEQUENCES_MAX = 4;
|
|
221
233
|
let warnedStopSequencesTrim = false;
|
|
222
234
|
|
|
@@ -1150,6 +1162,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1150
1162
|
if (wantsAnthropicPriority && !extraBetas.includes(fastModeBeta)) {
|
|
1151
1163
|
extraBetas.push(fastModeBeta);
|
|
1152
1164
|
}
|
|
1165
|
+
if (options?.taskBudget && !extraBetas.includes(taskBudgetBeta)) {
|
|
1166
|
+
extraBetas.push(taskBudgetBeta);
|
|
1167
|
+
}
|
|
1153
1168
|
|
|
1154
1169
|
const created = createClient(model, {
|
|
1155
1170
|
model,
|
|
@@ -1204,7 +1219,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1204
1219
|
| ThinkingContent
|
|
1205
1220
|
| RedactedThinkingContent
|
|
1206
1221
|
| TextContent
|
|
1207
|
-
| (ToolCall & { partialJson: string })
|
|
1222
|
+
| (ToolCall & { partialJson: string; lastParseLen?: number })
|
|
1208
1223
|
) & { index: number };
|
|
1209
1224
|
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
|
|
1210
1225
|
const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
@@ -1378,7 +1393,11 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1378
1393
|
const block = blocks[index];
|
|
1379
1394
|
if (block && block.type === "toolCall") {
|
|
1380
1395
|
block.partialJson += event.delta.partial_json;
|
|
1381
|
-
|
|
1396
|
+
const throttled = parseStreamingJsonThrottled(block.partialJson, block.lastParseLen ?? 0);
|
|
1397
|
+
if (throttled) {
|
|
1398
|
+
block.arguments = throttled.value;
|
|
1399
|
+
block.lastParseLen = throttled.parsedLen;
|
|
1400
|
+
}
|
|
1382
1401
|
stream.push({
|
|
1383
1402
|
type: "toolcall_delta",
|
|
1384
1403
|
contentIndex: index,
|
|
@@ -1416,6 +1435,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1416
1435
|
} else if (block.type === "toolCall") {
|
|
1417
1436
|
block.arguments = parseStreamingJson(block.partialJson);
|
|
1418
1437
|
delete (block as { partialJson?: string }).partialJson;
|
|
1438
|
+
delete (block as { lastParseLen?: number }).lastParseLen;
|
|
1419
1439
|
stream.push({
|
|
1420
1440
|
type: "toolcall_end",
|
|
1421
1441
|
contentIndex: index,
|
|
@@ -1536,9 +1556,15 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1536
1556
|
}
|
|
1537
1557
|
const isTransientEnvelopeFailure =
|
|
1538
1558
|
isTransientStreamParseError(streamFailure) || isTransientStreamEnvelopeError(streamFailure);
|
|
1559
|
+
const isLocalIdleTimeout =
|
|
1560
|
+
streamFailure === idleTimeoutAbortError ||
|
|
1561
|
+
(streamFailure instanceof Error && streamFailure.message === idleTimeoutAbortError.message);
|
|
1539
1562
|
const canRetryTransientEnvelopeFailure = isTransientEnvelopeFailure && !streamedReplayUnsafeContent;
|
|
1540
1563
|
const canRetryProviderFailure =
|
|
1541
|
-
|
|
1564
|
+
!isLocalIdleTimeout &&
|
|
1565
|
+
firstTokenTime === undefined &&
|
|
1566
|
+
!streamedReplayUnsafeContent &&
|
|
1567
|
+
isProviderRetryableError(streamFailure, model.provider);
|
|
1542
1568
|
if (
|
|
1543
1569
|
activeAbortTracker.wasCallerAbort() ||
|
|
1544
1570
|
providerRetryAttempt >= PROVIDER_MAX_RETRIES ||
|
|
@@ -1574,6 +1600,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1574
1600
|
for (const block of output.content) {
|
|
1575
1601
|
delete (block as { index?: number }).index;
|
|
1576
1602
|
delete (block as { partialJson?: string }).partialJson;
|
|
1603
|
+
delete (block as { lastParseLen?: number }).lastParseLen;
|
|
1577
1604
|
}
|
|
1578
1605
|
const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
|
|
1579
1606
|
output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
@@ -1767,8 +1794,14 @@ function createClient(
|
|
|
1767
1794
|
function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming): void {
|
|
1768
1795
|
const toolChoice = params.tool_choice;
|
|
1769
1796
|
if (!toolChoice) return;
|
|
1770
|
-
if (toolChoice.type
|
|
1771
|
-
|
|
1797
|
+
if (toolChoice.type !== "any" && toolChoice.type !== "tool") return;
|
|
1798
|
+
|
|
1799
|
+
delete params.thinking;
|
|
1800
|
+
const outputConfig = params.output_config as AnthropicOutputConfig | undefined;
|
|
1801
|
+
if (!outputConfig) return;
|
|
1802
|
+
|
|
1803
|
+
delete outputConfig.effort;
|
|
1804
|
+
if (Object.keys(outputConfig).length === 0) {
|
|
1772
1805
|
delete params.output_config;
|
|
1773
1806
|
}
|
|
1774
1807
|
}
|
|
@@ -2095,7 +2128,7 @@ function buildParams(
|
|
|
2095
2128
|
if (effort) {
|
|
2096
2129
|
// SDK's OutputConfig.effort type is not yet widened to include the new "xhigh"
|
|
2097
2130
|
// level introduced with Claude Opus 4.7. Cast until the SDK catches up.
|
|
2098
|
-
params.
|
|
2131
|
+
getAnthropicOutputConfig(params).effort = effort;
|
|
2099
2132
|
}
|
|
2100
2133
|
} else {
|
|
2101
2134
|
params.thinking = {
|
|
@@ -2104,7 +2137,7 @@ function buildParams(
|
|
|
2104
2137
|
display: options.thinkingDisplay ?? "summarized",
|
|
2105
2138
|
} as typeof params.thinking;
|
|
2106
2139
|
if (mode === "anthropic-budget-effort" && effort) {
|
|
2107
|
-
params.
|
|
2140
|
+
getAnthropicOutputConfig(params).effort = effort;
|
|
2108
2141
|
}
|
|
2109
2142
|
}
|
|
2110
2143
|
} else if (options?.thinkingEnabled === false) {
|
|
@@ -2112,6 +2145,9 @@ function buildParams(
|
|
|
2112
2145
|
}
|
|
2113
2146
|
}
|
|
2114
2147
|
|
|
2148
|
+
if (options?.taskBudget) {
|
|
2149
|
+
getAnthropicOutputConfig(params).task_budget = options.taskBudget;
|
|
2150
|
+
}
|
|
2115
2151
|
const metadataUserId = resolveAnthropicMetadataUserId(options?.metadata?.user_id, isOAuthToken);
|
|
2116
2152
|
if (metadataUserId) {
|
|
2117
2153
|
params.metadata = { user_id: metadataUserId };
|
|
@@ -22,8 +22,8 @@ import { createAbortSourceTracker } from "../utils/abort";
|
|
|
22
22
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
23
23
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
24
24
|
import {
|
|
25
|
+
getOpenAIStreamFirstEventTimeoutMs,
|
|
25
26
|
getOpenAIStreamIdleTimeoutMs,
|
|
26
|
-
getStreamFirstEventTimeoutMs,
|
|
27
27
|
iterateWithIdleTimeout,
|
|
28
28
|
} from "../utils/idle-iterator";
|
|
29
29
|
import { sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
@@ -122,7 +122,8 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
|
|
|
122
122
|
const params = buildParams(model, context, options, deploymentName, baseUrl);
|
|
123
123
|
options?.onPayload?.(params);
|
|
124
124
|
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
|
|
125
|
-
const firstEventTimeoutMs =
|
|
125
|
+
const firstEventTimeoutMs =
|
|
126
|
+
options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
126
127
|
const requestTimeoutMs =
|
|
127
128
|
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
128
129
|
rawRequestDump = {
|
|
@@ -47,7 +47,12 @@ import {
|
|
|
47
47
|
export type { GoogleThinkingLevel };
|
|
48
48
|
|
|
49
49
|
export interface GoogleGeminiCliOptions extends StreamOptions {
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Tool selection mode. String forms map directly to Gemini
|
|
52
|
+
* `FunctionCallingConfigMode`. The object form forces a single named tool —
|
|
53
|
+
* `mode: "ANY"` is wire-required when `allowedFunctionNames` is set.
|
|
54
|
+
*/
|
|
55
|
+
toolChoice?: "auto" | "none" | "any" | { mode: "ANY"; allowedFunctionNames: [string, ...string[]] };
|
|
51
56
|
/**
|
|
52
57
|
* Thinking/reasoning configuration.
|
|
53
58
|
* - Gemini 2.x models: use `budgetTokens` to set the thinking budget
|
|
@@ -212,6 +217,7 @@ interface CloudCodeAssistRequest {
|
|
|
212
217
|
toolConfig?: {
|
|
213
218
|
functionCallingConfig: {
|
|
214
219
|
mode: FunctionCallingConfigMode;
|
|
220
|
+
allowedFunctionNames?: string[];
|
|
215
221
|
};
|
|
216
222
|
};
|
|
217
223
|
};
|
|
@@ -745,11 +751,19 @@ export function buildRequest(
|
|
|
745
751
|
const convertedTools = convertTools(context.tools, model);
|
|
746
752
|
request.tools = isAntigravity ? normalizeAntigravityTools(convertedTools) : convertedTools;
|
|
747
753
|
if (options.toolChoice) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
754
|
+
const choice = options.toolChoice;
|
|
755
|
+
if (typeof choice === "string") {
|
|
756
|
+
request.toolConfig = {
|
|
757
|
+
functionCallingConfig: { mode: mapToolChoice(choice) },
|
|
758
|
+
};
|
|
759
|
+
} else {
|
|
760
|
+
request.toolConfig = {
|
|
761
|
+
functionCallingConfig: {
|
|
762
|
+
mode: "ANY",
|
|
763
|
+
allowedFunctionNames: [...choice.allowedFunctionNames],
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
}
|
|
753
767
|
}
|
|
754
768
|
}
|
|
755
769
|
|
|
@@ -59,7 +59,12 @@ export type GoogleThinkingLevel = "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LO
|
|
|
59
59
|
* `google-gemini-cli` uses a different transport and request shape — do not extend this for it.
|
|
60
60
|
*/
|
|
61
61
|
export interface GoogleSharedStreamOptions extends StreamOptions {
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Tool selection mode. String forms map directly to Gemini
|
|
64
|
+
* `FunctionCallingConfigMode`. The object form forces a single named tool
|
|
65
|
+
* — `mode: "ANY"` is wire-required when `allowedFunctionNames` is set.
|
|
66
|
+
*/
|
|
67
|
+
toolChoice?: "auto" | "none" | "any" | { mode: "ANY"; allowedFunctionNames: [string, ...string[]] };
|
|
63
68
|
thinking?: {
|
|
64
69
|
enabled: boolean;
|
|
65
70
|
budgetTokens?: number;
|
|
@@ -690,11 +695,21 @@ export function buildGoogleGenerateContentParams<T extends "google-generative-ai
|
|
|
690
695
|
};
|
|
691
696
|
|
|
692
697
|
if (context.tools && context.tools.length > 0 && options.toolChoice) {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
+
const choice = options.toolChoice;
|
|
699
|
+
if (typeof choice === "string") {
|
|
700
|
+
config.toolConfig = {
|
|
701
|
+
functionCallingConfig: { mode: mapToolChoice(choice) },
|
|
702
|
+
};
|
|
703
|
+
} else {
|
|
704
|
+
// Named-tool routing — `mode: "ANY"` plus an explicit allow-list. The
|
|
705
|
+
// caller is responsible for ensuring the names exist in `context.tools`.
|
|
706
|
+
config.toolConfig = {
|
|
707
|
+
functionCallingConfig: {
|
|
708
|
+
mode: "ANY",
|
|
709
|
+
allowedFunctionNames: [...choice.allowedFunctionNames],
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
}
|
|
698
713
|
} else {
|
|
699
714
|
config.toolConfig = undefined;
|
|
700
715
|
}
|
|
@@ -49,11 +49,11 @@ import {
|
|
|
49
49
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
50
50
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
51
51
|
import {
|
|
52
|
+
getOpenAIStreamFirstEventTimeoutMs,
|
|
52
53
|
getOpenAIStreamIdleTimeoutMs,
|
|
53
|
-
getStreamFirstEventTimeoutMs,
|
|
54
54
|
iterateWithIdleTimeout,
|
|
55
55
|
} from "../utils/idle-iterator";
|
|
56
|
-
import { parseStreamingJson } from "../utils/json-parse";
|
|
56
|
+
import { parseStreamingJson, parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
57
57
|
import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
|
|
58
58
|
import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
59
59
|
import { notifyRawSseEvent } from "../utils/sse-debug";
|
|
@@ -170,7 +170,7 @@ function createCodexWebSocketTimeoutMessage(reason: string, details: CodexWebSoc
|
|
|
170
170
|
|
|
171
171
|
type CodexTransport = "sse" | "websocket";
|
|
172
172
|
type CodexEventItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
|
|
173
|
-
type CodexOutputBlock = ThinkingContent | TextContent | (ToolCall & { partialJson: string });
|
|
173
|
+
type CodexOutputBlock = ThinkingContent | TextContent | (ToolCall & { partialJson: string; lastParseLen?: number });
|
|
174
174
|
|
|
175
175
|
export interface OpenAICodexWebSocketDebugStats {
|
|
176
176
|
fullContextRequests: number;
|
|
@@ -603,7 +603,7 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
|
|
|
603
603
|
: requestAbortController.signal;
|
|
604
604
|
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
|
|
605
605
|
const websocketIdleTimeoutMs = options?.streamIdleTimeoutMs ?? getCodexWebSocketIdleTimeoutMs();
|
|
606
|
-
const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ??
|
|
606
|
+
const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
607
607
|
const websocketFirstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getCodexWebSocketFirstEventTimeoutMs();
|
|
608
608
|
const wrapCodexSseStream = (
|
|
609
609
|
source: AsyncGenerator<Record<string, unknown>>,
|
|
@@ -1216,7 +1216,11 @@ function handleToolCallArgumentsDelta(
|
|
|
1216
1216
|
if (currentItem?.type !== "function_call" || currentBlock?.type !== "toolCall") return;
|
|
1217
1217
|
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
1218
1218
|
currentBlock.partialJson += delta;
|
|
1219
|
-
|
|
1219
|
+
const throttled = parseStreamingJsonThrottled(currentBlock.partialJson, currentBlock.lastParseLen ?? 0);
|
|
1220
|
+
if (throttled) {
|
|
1221
|
+
currentBlock.arguments = throttled.value;
|
|
1222
|
+
currentBlock.lastParseLen = throttled.parsedLen;
|
|
1223
|
+
}
|
|
1220
1224
|
stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), delta, partial: output });
|
|
1221
1225
|
}
|
|
1222
1226
|
|
|
@@ -1230,6 +1234,8 @@ function handleToolCallArgumentsDone(
|
|
|
1230
1234
|
if (typeof args === "string") {
|
|
1231
1235
|
currentBlock.partialJson = args;
|
|
1232
1236
|
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
|
|
1237
|
+
delete (currentBlock as { partialJson?: string }).partialJson;
|
|
1238
|
+
delete (currentBlock as { lastParseLen?: number }).lastParseLen;
|
|
1233
1239
|
}
|
|
1234
1240
|
}
|
|
1235
1241
|
|
|
@@ -1308,6 +1314,13 @@ function handleOutputItemDone(
|
|
|
1308
1314
|
name: item.name,
|
|
1309
1315
|
arguments: parseStreamingJson(item.arguments || "{}"),
|
|
1310
1316
|
};
|
|
1317
|
+
if (runtime.currentBlock?.type === "toolCall") {
|
|
1318
|
+
// Persist the authoritative final args on the stored block; the throttled
|
|
1319
|
+
// delta parser may have left currentBlock.arguments stale (often `{}`).
|
|
1320
|
+
runtime.currentBlock.arguments = toolCall.arguments;
|
|
1321
|
+
delete (runtime.currentBlock as { partialJson?: string }).partialJson;
|
|
1322
|
+
delete (runtime.currentBlock as { lastParseLen?: number }).lastParseLen;
|
|
1323
|
+
}
|
|
1311
1324
|
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1312
1325
|
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
|
|
1313
1326
|
return;
|
|
@@ -46,11 +46,11 @@ import {
|
|
|
46
46
|
rewriteCopilotError,
|
|
47
47
|
} from "../utils/http-inspector";
|
|
48
48
|
import {
|
|
49
|
+
getOpenAIStreamFirstEventTimeoutMs,
|
|
49
50
|
getOpenAIStreamIdleTimeoutMs,
|
|
50
|
-
getStreamFirstEventTimeoutMs,
|
|
51
51
|
iterateWithIdleTimeout,
|
|
52
52
|
} from "../utils/idle-iterator";
|
|
53
|
-
import { parseStreamingJson } from "../utils/json-parse";
|
|
53
|
+
import { parseStreamingJson, parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
54
54
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
55
55
|
import { getKimiCommonHeaders } from "../utils/oauth/kimi";
|
|
56
56
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
@@ -421,10 +421,10 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
421
421
|
|
|
422
422
|
try {
|
|
423
423
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
424
|
+
const idleTimeoutFallbackMs = getOpenAICompletionsStreamIdleTimeoutFallbackMs(model);
|
|
425
|
+
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(idleTimeoutFallbackMs);
|
|
426
|
+
const firstEventTimeoutMs =
|
|
427
|
+
options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
428
428
|
const requestTimeoutMs =
|
|
429
429
|
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
430
430
|
const {
|
|
@@ -539,7 +539,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
539
539
|
// so users don't see raw `<|...|>` tokens.
|
|
540
540
|
const stripDeepseekChatTemplateTokens =
|
|
541
541
|
/deepseek/i.test(model.id) && (model.provider === "nvidia" || model.provider === "deepseek");
|
|
542
|
-
type ToolCallStreamBlock = ToolCall & { partialArgs?: string; streamIndex?: number };
|
|
542
|
+
type ToolCallStreamBlock = ToolCall & { partialArgs?: string; streamIndex?: number; lastParseLen?: number };
|
|
543
543
|
type OpenAIStreamBlock = TextContent | ThinkingContent | ToolCallStreamBlock;
|
|
544
544
|
const pendingToolCallBlocks: ToolCallStreamBlock[] = [];
|
|
545
545
|
const toolCallBlockByIndex = new Map<number, ToolCallStreamBlock>();
|
|
@@ -554,6 +554,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
554
554
|
if (contentIndex < 0) return;
|
|
555
555
|
block.arguments = parseStreamingJson(block.partialArgs);
|
|
556
556
|
delete block.partialArgs;
|
|
557
|
+
delete block.lastParseLen;
|
|
557
558
|
if (block.streamIndex !== undefined) {
|
|
558
559
|
toolCallBlockByIndex.delete(block.streamIndex);
|
|
559
560
|
delete block.streamIndex;
|
|
@@ -848,7 +849,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
848
849
|
if (toolCall.function?.arguments) {
|
|
849
850
|
delta = toolCall.function.arguments;
|
|
850
851
|
block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments;
|
|
851
|
-
|
|
852
|
+
const throttled = parseStreamingJsonThrottled(block.partialArgs, block.lastParseLen ?? 0);
|
|
853
|
+
if (throttled) {
|
|
854
|
+
block.arguments = throttled.value;
|
|
855
|
+
block.lastParseLen = throttled.parsedLen;
|
|
856
|
+
}
|
|
852
857
|
}
|
|
853
858
|
stream.push({
|
|
854
859
|
type: "toolcall_delta",
|
|
@@ -1577,10 +1582,9 @@ export function convertMessages(
|
|
|
1577
1582
|
});
|
|
1578
1583
|
}
|
|
1579
1584
|
} else if (msg.role === "assistant") {
|
|
1580
|
-
// Some providers (e.g. Mistral) don't accept null content, use empty string instead
|
|
1581
1585
|
const assistantMsg: ChatCompletionAssistantMessageParam = {
|
|
1582
1586
|
role: "assistant",
|
|
1583
|
-
content:
|
|
1587
|
+
content: null,
|
|
1584
1588
|
};
|
|
1585
1589
|
|
|
1586
1590
|
const textBlocks = msg.content.filter(b => b.type === "text") as TextContent[];
|
|
@@ -1752,8 +1756,10 @@ export function convertMessages(
|
|
|
1752
1756
|
(assistantMsg as any).reasoning_details = reasoningDetails;
|
|
1753
1757
|
}
|
|
1754
1758
|
}
|
|
1755
|
-
//
|
|
1756
|
-
|
|
1759
|
+
// Some OpenAI-compatible backends concatenate assistant content as a
|
|
1760
|
+
// string even for tool-call replay. OpenAI accepts an empty string here;
|
|
1761
|
+
// null trips strict/proxy implementations before the tool result is read.
|
|
1762
|
+
if (assistantMsg.content === null && (hasReasoningField || assistantMsg.tool_calls)) {
|
|
1757
1763
|
assistantMsg.content = "";
|
|
1758
1764
|
}
|
|
1759
1765
|
// Skip assistant messages that have no content, no tool calls, and no reasoning payload.
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "../types";
|
|
31
31
|
import { normalizeResponsesToolCallId } from "../utils";
|
|
32
32
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
33
|
-
import { parseStreamingJson } from "../utils/json-parse";
|
|
33
|
+
import { parseStreamingJson, parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
34
34
|
import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
|
|
35
35
|
export const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES: ReadonlySet<string> = new Set([
|
|
36
36
|
"response.created",
|
|
@@ -401,7 +401,11 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
401
401
|
| ResponseFunctionToolCall
|
|
402
402
|
| ResponseCustomToolCall
|
|
403
403
|
| null = null;
|
|
404
|
-
let currentBlock:
|
|
404
|
+
let currentBlock:
|
|
405
|
+
| ThinkingContent
|
|
406
|
+
| TextContent
|
|
407
|
+
| (ToolCall & { partialJson: string; lastParseLen?: number })
|
|
408
|
+
| null = null;
|
|
405
409
|
const blocks = output.content;
|
|
406
410
|
const blockIndex = () => blocks.length - 1;
|
|
407
411
|
let sawFirstToken = false;
|
|
@@ -540,7 +544,11 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
540
544
|
} else if (event.type === "response.function_call_arguments.delta") {
|
|
541
545
|
if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
|
|
542
546
|
currentBlock.partialJson += event.delta;
|
|
543
|
-
|
|
547
|
+
const throttled = parseStreamingJsonThrottled(currentBlock.partialJson, currentBlock.lastParseLen ?? 0);
|
|
548
|
+
if (throttled) {
|
|
549
|
+
currentBlock.arguments = throttled.value;
|
|
550
|
+
currentBlock.lastParseLen = throttled.parsedLen;
|
|
551
|
+
}
|
|
544
552
|
stream.push({
|
|
545
553
|
type: "toolcall_delta",
|
|
546
554
|
contentIndex: blockIndex(),
|
|
@@ -552,6 +560,8 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
552
560
|
if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
|
|
553
561
|
currentBlock.partialJson = event.arguments;
|
|
554
562
|
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
|
|
563
|
+
delete (currentBlock as { partialJson?: string }).partialJson;
|
|
564
|
+
delete (currentBlock as { lastParseLen?: number }).lastParseLen;
|
|
555
565
|
}
|
|
556
566
|
} else if (event.type === "response.custom_tool_call_input.delta") {
|
|
557
567
|
if (currentItem?.type === "custom_tool_call" && currentBlock?.type === "toolCall") {
|
|
@@ -617,6 +627,15 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
617
627
|
name: item.name,
|
|
618
628
|
arguments: args,
|
|
619
629
|
};
|
|
630
|
+
if (currentBlock?.type === "toolCall") {
|
|
631
|
+
// Persist the authoritative final args on the stored block. The
|
|
632
|
+
// throttled delta parser may have skipped the last partial parse,
|
|
633
|
+
// leaving currentBlock.arguments stale (often `{}`); the emitted
|
|
634
|
+
// toolCall and the persisted block must agree.
|
|
635
|
+
currentBlock.arguments = args;
|
|
636
|
+
delete (currentBlock as { partialJson?: string }).partialJson;
|
|
637
|
+
delete (currentBlock as { lastParseLen?: number }).lastParseLen;
|
|
638
|
+
}
|
|
620
639
|
currentBlock = null;
|
|
621
640
|
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
|
|
622
641
|
} else if (item.type === "custom_tool_call") {
|
|
@@ -33,8 +33,8 @@ import { createAbortSourceTracker } from "../utils/abort";
|
|
|
33
33
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
34
34
|
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
|
|
35
35
|
import {
|
|
36
|
+
getOpenAIStreamFirstEventTimeoutMs,
|
|
36
37
|
getOpenAIStreamIdleTimeoutMs,
|
|
37
|
-
getStreamFirstEventTimeoutMs,
|
|
38
38
|
iterateWithIdleTimeout,
|
|
39
39
|
} from "../utils/idle-iterator";
|
|
40
40
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
@@ -228,7 +228,8 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
228
228
|
const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState);
|
|
229
229
|
const { params } = buildParams(model, context, options, providerSessionState, baseUrl);
|
|
230
230
|
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
|
|
231
|
-
const firstEventTimeoutMs =
|
|
231
|
+
const firstEventTimeoutMs =
|
|
232
|
+
options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
232
233
|
const requestTimeoutMs =
|
|
233
234
|
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
234
235
|
options?.onPayload?.(params);
|
|
@@ -418,7 +418,10 @@ export const streamGoogleGeminiCli = createLazyStream(
|
|
|
418
418
|
GOOGLE_GEMINI_CLI_LAZY_STREAM_LIMITS,
|
|
419
419
|
);
|
|
420
420
|
export const streamGoogleVertex = createLazyStream(loadGoogleVertexProviderModule);
|
|
421
|
-
export const streamOpenAICodexResponses = createLazyStream(
|
|
421
|
+
export const streamOpenAICodexResponses = createLazyStream(
|
|
422
|
+
loadOpenAICodexResponsesProviderModule,
|
|
423
|
+
PROVIDER_HANDLED_STREAM_TIMEOUTS,
|
|
424
|
+
);
|
|
422
425
|
export const streamOpenAICompletions = createLazyStream(
|
|
423
426
|
loadOpenAICompletionsProviderModule,
|
|
424
427
|
PROVIDER_HANDLED_STREAM_TIMEOUTS,
|
package/src/stream.ts
CHANGED
|
@@ -650,7 +650,7 @@ export function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["t
|
|
|
650
650
|
return undefined;
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
-
function mapGoogleToolChoice(
|
|
653
|
+
export function mapGoogleToolChoice(
|
|
654
654
|
choice?: ToolChoice,
|
|
655
655
|
): GoogleOptions["toolChoice"] | GoogleGeminiCliOptions["toolChoice"] | GoogleVertexOptions["toolChoice"] {
|
|
656
656
|
if (!choice) return undefined;
|
|
@@ -659,7 +659,16 @@ function mapGoogleToolChoice(
|
|
|
659
659
|
if (choice === "auto" || choice === "none" || choice === "any") return choice;
|
|
660
660
|
return undefined;
|
|
661
661
|
}
|
|
662
|
-
|
|
662
|
+
// Named-tool routing on Google: emit an `ANY`-mode allow-list of one entry,
|
|
663
|
+
// mirroring the Anthropic mapper that returns `{type: "tool", name}`.
|
|
664
|
+
if (choice.type === "tool") {
|
|
665
|
+
return choice.name ? { mode: "ANY", allowedFunctionNames: [choice.name] } : undefined;
|
|
666
|
+
}
|
|
667
|
+
if (choice.type === "function") {
|
|
668
|
+
const name = "function" in choice ? choice.function?.name : choice.name;
|
|
669
|
+
return name ? { mode: "ANY", allowedFunctionNames: [name] } : undefined;
|
|
670
|
+
}
|
|
671
|
+
return undefined;
|
|
663
672
|
}
|
|
664
673
|
|
|
665
674
|
function mapOpenAiToolChoice(choice?: ToolChoice): OpenAICompletionsOptions["toolChoice"] {
|
|
@@ -718,6 +727,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
718
727
|
initiatorOverride: options?.initiatorOverride,
|
|
719
728
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
720
729
|
metadata: options?.metadata,
|
|
730
|
+
taskBudget: options?.taskBudget,
|
|
721
731
|
sessionId: options?.sessionId,
|
|
722
732
|
promptCacheKey: options?.promptCacheKey,
|
|
723
733
|
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
package/src/types.ts
CHANGED
|
@@ -153,6 +153,12 @@ import type { Effort } from "./model-thinking";
|
|
|
153
153
|
/** Token budgets for each thinking level (token-based providers only) */
|
|
154
154
|
export type ThinkingBudgets = { [key in Effort]?: number };
|
|
155
155
|
|
|
156
|
+
export interface TokenTaskBudget {
|
|
157
|
+
type: "tokens";
|
|
158
|
+
total: number;
|
|
159
|
+
remaining?: number;
|
|
160
|
+
}
|
|
161
|
+
|
|
156
162
|
export type MessageAttribution = "user" | "agent";
|
|
157
163
|
|
|
158
164
|
export type ToolChoice =
|
|
@@ -319,6 +325,11 @@ export interface StreamOptions {
|
|
|
319
325
|
* For example, Anthropic uses `user_id` for abuse tracking and rate limiting.
|
|
320
326
|
*/
|
|
321
327
|
metadata?: Record<string, unknown>;
|
|
328
|
+
/**
|
|
329
|
+
* Advisory token budget for a full agentic loop. Anthropic encodes this as
|
|
330
|
+
* `output_config.task_budget` with the `task-budgets-2026-03-13` beta header.
|
|
331
|
+
*/
|
|
332
|
+
taskBudget?: TokenTaskBudget;
|
|
322
333
|
/**
|
|
323
334
|
* Optional session identifier for providers that support session-based
|
|
324
335
|
* routing, request affinity, or transport reuse. Providers may also use this
|
|
@@ -363,6 +374,11 @@ export interface StreamOptions {
|
|
|
363
374
|
* `0` to disable both layers for this request. After the first semantic
|
|
364
375
|
* event arrives, `streamIdleTimeoutMs` governs inter-event stalls. Falls
|
|
365
376
|
* back to `PI_STREAM_FIRST_EVENT_TIMEOUT_MS` and then to a 100s default.
|
|
377
|
+
* OpenAI-family transports additionally honor
|
|
378
|
+
* `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` as the most-specific override and
|
|
379
|
+
* floor the first-event budget at the resolved idle (per-call
|
|
380
|
+
* `streamIdleTimeoutMs` or `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS`) so slow local
|
|
381
|
+
* OpenAI-compatible servers are not undercut during prompt processing.
|
|
366
382
|
*
|
|
367
383
|
* Iterator-level honored by: every built-in provider (via the lazy-stream
|
|
368
384
|
* forwarder in `register-builtins`). SDK-request honored by:
|
|
@@ -58,6 +58,33 @@ export function getStreamFirstEventTimeoutMs(
|
|
|
58
58
|
return normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallback);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Returns the first-event timeout used for OpenAI-family streaming transports.
|
|
63
|
+
*
|
|
64
|
+
* Precedence: explicit `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` (including a
|
|
65
|
+
* `"0"` disable) wins outright. Otherwise the resolved idle (caller-supplied
|
|
66
|
+
* `idleTimeoutMs` — which itself already encompasses per-call
|
|
67
|
+
* `streamIdleTimeoutMs` or `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` resolved
|
|
68
|
+
* upstream) floors the first-event budget so slow local OpenAI-compatible
|
|
69
|
+
* servers are not undercut by a shorter `PI_STREAM_FIRST_EVENT_TIMEOUT_MS`
|
|
70
|
+
* or the global default during prompt processing.
|
|
71
|
+
*
|
|
72
|
+
* Returns `undefined` when an explicit env knob disables the watchdog.
|
|
73
|
+
*/
|
|
74
|
+
export function getOpenAIStreamFirstEventTimeoutMs(
|
|
75
|
+
idleTimeoutMs?: number,
|
|
76
|
+
fallbackMs: number = DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS,
|
|
77
|
+
): number | undefined {
|
|
78
|
+
const openAIFirstEventRaw = $env.PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS;
|
|
79
|
+
if (openAIFirstEventRaw !== undefined) {
|
|
80
|
+
return normalizeIdleTimeoutMs(openAIFirstEventRaw, fallbackMs);
|
|
81
|
+
}
|
|
82
|
+
const base = normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallbackMs);
|
|
83
|
+
if (base === undefined) return undefined;
|
|
84
|
+
if (idleTimeoutMs === undefined || idleTimeoutMs <= 0) return base;
|
|
85
|
+
return Math.max(base, idleTimeoutMs);
|
|
86
|
+
}
|
|
87
|
+
|
|
61
88
|
export interface IdleTimeoutIteratorOptions {
|
|
62
89
|
idleTimeoutMs?: number;
|
|
63
90
|
firstItemTimeoutMs?: number;
|
package/src/utils/json-parse.ts
CHANGED
|
@@ -146,3 +146,37 @@ export function parseStreamingJson<T = Record<string, unknown>>(partialJson: str
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Default minimum byte growth before `parseStreamingJsonThrottled` will
|
|
152
|
+
* re-parse a streaming tool-call argument buffer. Bounds the mid-stream
|
|
153
|
+
* partial-parse cost from quadratic to linear in N.
|
|
154
|
+
*/
|
|
155
|
+
export const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Throttled variant of {@link parseStreamingJson} for the per-delta hot path.
|
|
159
|
+
*
|
|
160
|
+
* Tool calls arrive as a long sequence of small deltas — calling
|
|
161
|
+
* `parseStreamingJson(buffer)` on every delta re-parses the entire buffer
|
|
162
|
+
* each time, giving O(N²) work in the total buffer length. Throttling skips
|
|
163
|
+
* the re-parse until at least `minGrowthBytes` of new content has arrived
|
|
164
|
+
* since the last successful parse, bounding mid-stream cost to O(N).
|
|
165
|
+
*
|
|
166
|
+
* Each provider tracks the last parsed length on its tool-call block, so the
|
|
167
|
+
* final `toolcall_end` parse (which providers already perform unconditionally)
|
|
168
|
+
* is the authoritative full parse — the throttle only delays mid-stream UI
|
|
169
|
+
* updates by at most `minGrowthBytes` of accumulated partial content.
|
|
170
|
+
*
|
|
171
|
+
* @returns the parsed object plus the new `parsedLen` to persist; or `null`
|
|
172
|
+
* when the buffer has not grown enough to warrant a re-parse.
|
|
173
|
+
*/
|
|
174
|
+
export function parseStreamingJsonThrottled<T = Record<string, unknown>>(
|
|
175
|
+
partialJson: string | undefined,
|
|
176
|
+
lastParsedLen: number,
|
|
177
|
+
minGrowthBytes: number = STREAMING_JSON_PARSE_MIN_GROWTH,
|
|
178
|
+
): { value: T; parsedLen: number } | null {
|
|
179
|
+
const len = partialJson?.length ?? 0;
|
|
180
|
+
if (len === 0 || (lastParsedLen > 0 && len - lastParsedLen < minGrowthBytes)) return null;
|
|
181
|
+
return { value: parseStreamingJson<T>(partialJson), parsedLen: len };
|
|
182
|
+
}
|
|
@@ -51,7 +51,7 @@ async function validateXiaomiApiKey(apiKey: string, signal?: AbortSignal): Promi
|
|
|
51
51
|
method: "POST",
|
|
52
52
|
headers: {
|
|
53
53
|
"Content-Type": "application/json",
|
|
54
|
-
|
|
54
|
+
Authorization: `Bearer ${apiKey}`,
|
|
55
55
|
},
|
|
56
56
|
body: JSON.stringify({
|
|
57
57
|
model: ep.model,
|