Files
oh-my-pi/packages/coding-agent/src/session/settings-stream-fn.ts
T
roboomp 2c8b723083 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
2026-06-30 07:07:41 +00:00

60 lines
2.9 KiB
TypeScript

/**
* Settings-aware stream wrapper shared by the main agent (sdk.ts) and the
* advisor agent (AgentSession.#buildAdvisorRuntime).
*
* 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
* OpenRouter never saw `providers.openrouterVariant`, breaking sticky routing
* and OpenRouter response-cache hits across advisor calls.
*/
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`).
*
* Caller-supplied `streamOptions` always win — the helper only fills holes.
*/
export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn = streamSimple): StreamFn {
return (model, context, streamOptions) => {
const openrouterRoutingPreset = settings.get("providers.openrouterVariant");
const openrouterVariant =
openrouterRoutingPreset && openrouterRoutingPreset !== "default" ? openrouterRoutingPreset : undefined;
const antigravityEndpointMode = settings.get("providers.antigravityEndpoint");
const textVerbosity =
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"),
),
loopGuard: {
enabled: settings.get("model.loopGuard.enabled"),
checkAssistantContent: settings.get("model.loopGuard.checkAssistantContent"),
...streamOptions?.loopGuard,
},
};
return base(model, context, merged);
};
}