fix(coding-agent): forwarded stream timeout settings
Forwarded persisted provider stream timeout settings into model requests so slow local LLM streams can widen or disable first-event and idle watchdogs without environment variables. Fixes #3878
This commit is contained in:
@@ -207,8 +207,8 @@ OAuth host chain: `KIMI_CODE_OAUTH_HOST` → `KIMI_OAUTH_HOST` → `https://auth
|
||||
| `PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS` | Positive integer override (default 300000) |
|
||||
| `PI_CODEX_WEBSOCKET_RETRY_BUDGET` | Non-negative integer override (default 5) |
|
||||
| `PI_CODEX_WEBSOCKET_RETRY_DELAY_MS` | Positive integer base backoff override (default 500) |
|
||||
| `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` | Positive integer OpenAI first-event timeout override |
|
||||
| `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` | Positive integer OpenAI stream idle timeout override |
|
||||
| `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` | Positive integer OpenAI first-event timeout override; `0` disables. `omp config set providers.streamFirstEventTimeoutSeconds <seconds>` provides the persisted config equivalent |
|
||||
| `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` | Positive integer OpenAI stream idle timeout override; `0` disables. `omp config set providers.streamIdleTimeoutSeconds <seconds>` provides the persisted config equivalent |
|
||||
|
||||
### Cursor provider debug
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed slow local LLM streams by forwarding persisted stream timeout settings (`providers.streamFirstEventTimeoutSeconds`, `providers.streamIdleTimeoutSeconds`) into model requests, so users can widen or disable watchdogs without environment variables. ([#3878](https://github.com/can1357/oh-my-pi/issues/3878))
|
||||
- Improved reliability of DuckDuckGo web searches by updating browser request headers and parameters
|
||||
- Fixed an issue where CJK (Chinese, Japanese, Korean) history could become unrenderable during repeated context compactions.
|
||||
- Fixed a memory exhaustion bug in the TUI when using `/resume` on large previous sessions.
|
||||
|
||||
@@ -144,7 +144,7 @@ export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
|
||||
"Developer",
|
||||
],
|
||||
tasks: ["Modes", "Subagents", "Isolation", "Commands & Skills"],
|
||||
providers: ["Services", "Fireworks", "Tiny Model", "Protocol", "Privacy"],
|
||||
providers: ["Services", "Fireworks", "Tiny Model", "Protocol", "Timeouts", "Privacy"],
|
||||
};
|
||||
|
||||
/** Status line segment identifiers */
|
||||
@@ -4554,6 +4554,44 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
|
||||
"providers.streamFirstEventTimeoutSeconds": {
|
||||
type: "number",
|
||||
default: -1,
|
||||
ui: {
|
||||
tab: "providers",
|
||||
group: "Timeouts",
|
||||
label: "Stream First Event Timeout",
|
||||
description:
|
||||
"Seconds to wait for the first model stream event; -1 uses provider/env defaults, 0 disables the watchdog",
|
||||
options: [
|
||||
{ value: "-1", label: "Auto", description: "Use provider defaults and PI_* timeout env vars" },
|
||||
{ value: "0", label: "Off", description: "Disable first-event timeout" },
|
||||
{ value: "300", label: "5 minutes" },
|
||||
{ value: "600", label: "10 minutes" },
|
||||
{ value: "1800", label: "30 minutes" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"providers.streamIdleTimeoutSeconds": {
|
||||
type: "number",
|
||||
default: -1,
|
||||
ui: {
|
||||
tab: "providers",
|
||||
group: "Timeouts",
|
||||
label: "Stream Idle Timeout",
|
||||
description:
|
||||
"Seconds a model stream may stay silent between events; -1 uses provider/env defaults, 0 disables the watchdog",
|
||||
options: [
|
||||
{ value: "-1", label: "Auto", description: "Use provider defaults and PI_* timeout env vars" },
|
||||
{ value: "0", label: "Off", description: "Disable idle timeout" },
|
||||
{ value: "300", label: "5 minutes" },
|
||||
{ value: "600", label: "10 minutes" },
|
||||
{ value: "1800", label: "30 minutes" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
"providers.openrouterVariant": {
|
||||
type: "enum",
|
||||
values: ["default", "nitro", "floor", "online", "exacto"] as const,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Settings-aware stream wrapper shared by the main agent (sdk.ts) and the
|
||||
* advisor agent (AgentSession.#buildAdvisorRuntime).
|
||||
*
|
||||
* Reads OpenRouter / Antigravity routing variants, Responses-family text
|
||||
* verbosity, per-provider in-flight caps, and the loop guard out of `Settings`
|
||||
* verbosity, stream watchdog budgets, per-provider in-flight caps, and the loop
|
||||
* guard out of `Settings`
|
||||
* per request, layering them onto whatever options the caller passed. Before
|
||||
* this helper existed, advisor turns called bare `streamSimple` while the main
|
||||
* turn went through an inline closure that read these settings — so an advisor on
|
||||
@@ -14,6 +14,12 @@ import type { StreamFn } from "@oh-my-pi/pi-agent-core";
|
||||
import { type SimpleStreamOptions, streamSimple } from "@oh-my-pi/pi-ai";
|
||||
import { type Settings, validateProviderMaxInFlightRequests } from "../config/settings";
|
||||
|
||||
function timeoutSecondsToMs(value: number): number | undefined {
|
||||
if (!Number.isFinite(value) || value < 0) return undefined;
|
||||
if (value === 0) return 0;
|
||||
return Math.max(1, Math.trunc(value * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link StreamFn} that reads provider routing/guard settings from
|
||||
* `settings` per call and forwards to `base` (defaults to `streamSimple`).
|
||||
@@ -30,11 +36,15 @@ export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn =
|
||||
model.api === "openai-codex-responses" || model.api === "openai-responses"
|
||||
? settings.get("textVerbosity")
|
||||
: undefined;
|
||||
const streamFirstEventTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamFirstEventTimeoutSeconds"));
|
||||
const streamIdleTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamIdleTimeoutSeconds"));
|
||||
const merged: SimpleStreamOptions = {
|
||||
...streamOptions,
|
||||
openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
|
||||
antigravityEndpointMode: streamOptions?.antigravityEndpointMode ?? antigravityEndpointMode,
|
||||
textVerbosity: streamOptions?.textVerbosity ?? textVerbosity,
|
||||
streamFirstEventTimeoutMs: streamOptions?.streamFirstEventTimeoutMs ?? streamFirstEventTimeoutMs,
|
||||
streamIdleTimeoutMs: streamOptions?.streamIdleTimeoutMs ?? streamIdleTimeoutMs,
|
||||
maxInFlightRequests: validateProviderMaxInFlightRequests(
|
||||
streamOptions?.maxInFlightRequests ?? settings.get("providers.maxInFlightRequests"),
|
||||
),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Contract: `createSettingsAwareStreamFn` layers session provider settings
|
||||
* (`providers.openrouterVariant`, `providers.antigravityEndpoint`,
|
||||
* `providers.maxInFlightRequests`, `model.loopGuard.*`, `textVerbosity` for
|
||||
* Responses-family requests) onto every call while letting caller-supplied
|
||||
* `providers.stream*TimeoutSeconds`, `providers.maxInFlightRequests`,
|
||||
* `model.loopGuard.*`, `textVerbosity` for Responses-family requests)
|
||||
* options win — the same wiring the main agent and the advisor agent share so
|
||||
* OpenRouter sticky-routing / response caching behaves the same on advisor turns
|
||||
* (can1357/oh-my-pi#3639).
|
||||
@@ -65,6 +65,26 @@ describe("createSettingsAwareStreamFn", () => {
|
||||
expect(calls[2]?.options?.textVerbosity).toBe("medium");
|
||||
});
|
||||
|
||||
it("forwards configured stream watchdog budgets while preserving caller overrides", () => {
|
||||
const settings = Settings.isolated({
|
||||
"providers.streamFirstEventTimeoutSeconds": 600,
|
||||
"providers.streamIdleTimeoutSeconds": 300,
|
||||
});
|
||||
const { fn: base, calls } = captureBase();
|
||||
const wrapped = createSettingsAwareStreamFn(settings, base);
|
||||
|
||||
wrapped(stubModel, stubContext, undefined);
|
||||
wrapped(stubModel, stubContext, {
|
||||
streamFirstEventTimeoutMs: 15_000,
|
||||
streamIdleTimeoutMs: 10_000,
|
||||
});
|
||||
|
||||
expect(calls[0]?.options?.streamFirstEventTimeoutMs).toBe(600_000);
|
||||
expect(calls[0]?.options?.streamIdleTimeoutMs).toBe(300_000);
|
||||
expect(calls[1]?.options?.streamFirstEventTimeoutMs).toBe(15_000);
|
||||
expect(calls[1]?.options?.streamIdleTimeoutMs).toBe(10_000);
|
||||
});
|
||||
|
||||
it("treats the default openrouterVariant as absent so the base call carries no variant", () => {
|
||||
const settings = Settings.isolated({ "providers.openrouterVariant": "default" });
|
||||
const { fn: base, calls } = captureBase();
|
||||
|
||||
Reference in New Issue
Block a user