76 lines
3.9 KiB
TypeScript
76 lines
3.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 { isAnthropicFableOrMythosModel } from "@oh-my-pi/pi-catalog/identity";
|
|
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"));
|
|
// Server-side fallback (opt-in): when the user enables it AND the
|
|
// resolved model is a Claude Fable/Mythos on Anthropic's messages
|
|
// API, inject the `fallbacks: [{ model: "claude-opus-4-8" }]` chain.
|
|
// The provider layer picks it up, sends the beta header, and honors
|
|
// the response signals. Every other model / API is untouched.
|
|
const serverSideFallbackEnabled =
|
|
settings.get("providers.anthropic.serverSideFallback") &&
|
|
model.api === "anthropic-messages" &&
|
|
model.provider === "anthropic" &&
|
|
isAnthropicFableOrMythosModel(model.id);
|
|
const fallbacks =
|
|
streamOptions?.fallbacks ?? (serverSideFallbackEnabled ? [{ model: "claude-opus-4-8" }] : undefined);
|
|
const merged: SimpleStreamOptions = {
|
|
...streamOptions,
|
|
openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
|
|
antigravityEndpointMode: streamOptions?.antigravityEndpointMode ?? antigravityEndpointMode,
|
|
textVerbosity: streamOptions?.textVerbosity ?? textVerbosity,
|
|
streamFirstEventTimeoutMs: streamOptions?.streamFirstEventTimeoutMs ?? streamFirstEventTimeoutMs,
|
|
streamIdleTimeoutMs: streamOptions?.streamIdleTimeoutMs ?? streamIdleTimeoutMs,
|
|
maxRetryDelayMs: streamOptions?.maxRetryDelayMs ?? settings.get("retry.maxDelayMs"),
|
|
maxInFlightRequests: validateProviderMaxInFlightRequests(
|
|
streamOptions?.maxInFlightRequests ?? settings.get("providers.maxInFlightRequests"),
|
|
),
|
|
loopGuard: {
|
|
enabled: settings.get("model.loopGuard.enabled"),
|
|
checkAssistantContent: settings.get("model.loopGuard.checkAssistantContent"),
|
|
...streamOptions?.loopGuard,
|
|
},
|
|
hideThinkingSummary: streamOptions?.hideThinkingSummary ?? settings.get("omitThinking"),
|
|
...(fallbacks !== undefined ? { fallbacks } : {}),
|
|
};
|
|
return base(model, context, merged);
|
|
};
|
|
}
|