@oh-my-pi/pi-ai 15.7.3 → 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 +14 -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/types.d.ts +15 -0
- package/dist/types/utils/idle-iterator.d.ts +14 -0
- package/package.json +2 -2
- package/src/auth-gateway/server.ts +1 -0
- package/src/auth-gateway/types.ts +10 -1
- package/src/providers/anthropic-messages-server-schema.ts +13 -0
- package/src/providers/anthropic-messages-server.ts +3 -0
- package/src/providers/anthropic.ts +28 -4
- package/src/providers/azure-openai-responses.ts +3 -2
- package/src/providers/openai-codex-responses.ts +2 -2
- package/src/providers/openai-completions.ts +10 -9
- package/src/providers/openai-responses.ts +3 -2
- package/src/providers/register-builtins.ts +4 -1
- package/src/stream.ts +1 -0
- package/src/types.ts +16 -0
- package/src/utils/idle-iterator.ts +27 -0
- package/src/utils/oauth/xiaomi.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,23 @@
|
|
|
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
|
+
|
|
5
17
|
### Fixed
|
|
6
18
|
|
|
7
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))
|
|
8
22
|
|
|
9
23
|
## [15.7.3] - 2026-05-31
|
|
10
24
|
|
|
@@ -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>;
|
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;
|
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",
|
|
@@ -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). */
|
|
@@ -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,
|
|
@@ -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,
|
|
@@ -1779,8 +1794,14 @@ function createClient(
|
|
|
1779
1794
|
function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming): void {
|
|
1780
1795
|
const toolChoice = params.tool_choice;
|
|
1781
1796
|
if (!toolChoice) return;
|
|
1782
|
-
if (toolChoice.type
|
|
1783
|
-
|
|
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) {
|
|
1784
1805
|
delete params.output_config;
|
|
1785
1806
|
}
|
|
1786
1807
|
}
|
|
@@ -2107,7 +2128,7 @@ function buildParams(
|
|
|
2107
2128
|
if (effort) {
|
|
2108
2129
|
// SDK's OutputConfig.effort type is not yet widened to include the new "xhigh"
|
|
2109
2130
|
// level introduced with Claude Opus 4.7. Cast until the SDK catches up.
|
|
2110
|
-
params.
|
|
2131
|
+
getAnthropicOutputConfig(params).effort = effort;
|
|
2111
2132
|
}
|
|
2112
2133
|
} else {
|
|
2113
2134
|
params.thinking = {
|
|
@@ -2116,7 +2137,7 @@ function buildParams(
|
|
|
2116
2137
|
display: options.thinkingDisplay ?? "summarized",
|
|
2117
2138
|
} as typeof params.thinking;
|
|
2118
2139
|
if (mode === "anthropic-budget-effort" && effort) {
|
|
2119
|
-
params.
|
|
2140
|
+
getAnthropicOutputConfig(params).effort = effort;
|
|
2120
2141
|
}
|
|
2121
2142
|
}
|
|
2122
2143
|
} else if (options?.thinkingEnabled === false) {
|
|
@@ -2124,6 +2145,9 @@ function buildParams(
|
|
|
2124
2145
|
}
|
|
2125
2146
|
}
|
|
2126
2147
|
|
|
2148
|
+
if (options?.taskBudget) {
|
|
2149
|
+
getAnthropicOutputConfig(params).task_budget = options.taskBudget;
|
|
2150
|
+
}
|
|
2127
2151
|
const metadataUserId = resolveAnthropicMetadataUserId(options?.metadata?.user_id, isOAuthToken);
|
|
2128
2152
|
if (metadataUserId) {
|
|
2129
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 = {
|
|
@@ -49,8 +49,8 @@ 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
56
|
import { parseStreamingJson, parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
@@ -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>>,
|
|
@@ -46,8 +46,8 @@ 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
53
|
import { parseStreamingJson, parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
@@ -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 {
|
|
@@ -1582,10 +1582,9 @@ export function convertMessages(
|
|
|
1582
1582
|
});
|
|
1583
1583
|
}
|
|
1584
1584
|
} else if (msg.role === "assistant") {
|
|
1585
|
-
// Some providers (e.g. Mistral) don't accept null content, use empty string instead
|
|
1586
1585
|
const assistantMsg: ChatCompletionAssistantMessageParam = {
|
|
1587
1586
|
role: "assistant",
|
|
1588
|
-
content:
|
|
1587
|
+
content: null,
|
|
1589
1588
|
};
|
|
1590
1589
|
|
|
1591
1590
|
const textBlocks = msg.content.filter(b => b.type === "text") as TextContent[];
|
|
@@ -1757,8 +1756,10 @@ export function convertMessages(
|
|
|
1757
1756
|
(assistantMsg as any).reasoning_details = reasoningDetails;
|
|
1758
1757
|
}
|
|
1759
1758
|
}
|
|
1760
|
-
//
|
|
1761
|
-
|
|
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)) {
|
|
1762
1763
|
assistantMsg.content = "";
|
|
1763
1764
|
}
|
|
1764
1765
|
// Skip assistant messages that have no content, no tool calls, and no reasoning payload.
|
|
@@ -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
|
@@ -727,6 +727,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
727
727
|
initiatorOverride: options?.initiatorOverride,
|
|
728
728
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
729
729
|
metadata: options?.metadata,
|
|
730
|
+
taskBudget: options?.taskBudget,
|
|
730
731
|
sessionId: options?.sessionId,
|
|
731
732
|
promptCacheKey: options?.promptCacheKey,
|
|
732
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;
|
|
@@ -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,
|