feat(settings): expose stream recovery feature flags (#4586)

Integrated into release/v3.8.34
This commit is contained in:
Randi
2026-06-22 16:04:30 -04:00
committed by GitHub
parent def39ea292
commit bd1fd79543
4 changed files with 134 additions and 14 deletions

View File

@@ -1,4 +1,5 @@
import { DEFAULT_API_LIMITS, PROVIDER_PROFILES } from "@omniroute/open-sse/config/constants";
import { resolveFeatureFlag } from "@/shared/utils/featureFlags";
type JsonRecord = Record<string, unknown>;
type AuthCategory = "oauth" | "apikey";
@@ -105,7 +106,7 @@ export interface StreamRecoverySettings {
* open-sse/config/constants.ts) so an early cutoff can be retried before any byte
* reaches the client. OFF by default because holding the window adds up to
* STREAM_RECOVERY.HOLDBACK_MS of time-to-first-token latency on every stream.
* Default seeds from the STREAM_RECOVERY_ENABLED env var.
* Default seeds from the STREAM_RECOVERY_ENABLED feature flag / env var.
*/
enabled: boolean;
/**
@@ -113,8 +114,8 @@ export interface StreamRecoverySettings {
* bytes already reached the client, re-request with the partial text as an assistant
* prefill and stitch the missing suffix (plain-text OpenAI-compatible streams only;
* never with a tool call in flight). OFF by default because the recovered tail arrives
* as one burst rather than token-by-token. Default seeds from
* STREAM_RECOVERY_MIDSTREAM_ENABLED.
* as one burst rather than token-by-token. Default seeds from the
* STREAM_RECOVERY_MIDSTREAM_ENABLED feature flag / env var.
*/
continueMidStream: boolean;
}
@@ -168,6 +169,40 @@ function toBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
function parseFeatureFlagBoolean(value: string, fallback: boolean): boolean {
const normalized = value.trim().toLowerCase();
if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
return true;
}
if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") {
return false;
}
return fallback;
}
function resolveBooleanFeatureFlag(key: string, fallback: boolean): boolean {
try {
return parseFeatureFlagBoolean(resolveFeatureFlag(key), fallback);
} catch (error) {
const envValue = process.env[key];
if (typeof envValue === "string" && envValue.trim() !== "") {
return parseFeatureFlagBoolean(envValue, fallback);
}
console.error(
`[resilience] Failed to resolve ${key}, falling back to ${String(fallback)}:`,
error instanceof Error ? error.message : error
);
return fallback;
}
}
function resolveStreamRecoveryDefaults(): StreamRecoverySettings {
return {
enabled: resolveBooleanFeatureFlag("STREAM_RECOVERY_ENABLED", false),
continueMidStream: resolveBooleanFeatureFlag("STREAM_RECOVERY_MIDSTREAM_ENABLED", false),
};
}
export const DEFAULT_REQUEST_QUEUE_MAX_WAIT_MS = (() => {
const parsed = Number(process.env.RATE_LIMIT_MAX_WAIT_MS || "120000");
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 120000;
@@ -500,6 +535,7 @@ function normalizeStreamRecoverySettings(
function buildLegacyFallback(settings: JsonRecord): ResilienceSettings {
const profiles = asRecord(settings.providerProfiles);
const defaults = asRecord(settings.rateLimitDefaults);
const streamRecoveryDefaults = resolveStreamRecoveryDefaults();
const oauthLegacy = asRecord(profiles.oauth);
const apikeyLegacy = asRecord(profiles.apikey);
@@ -583,7 +619,7 @@ function buildLegacyFallback(settings: JsonRecord): ResilienceSettings {
},
providerCooldown: DEFAULT_RESILIENCE_SETTINGS.providerCooldown,
quotaPreflight: DEFAULT_RESILIENCE_SETTINGS.quotaPreflight,
streamRecovery: DEFAULT_RESILIENCE_SETTINGS.streamRecovery,
streamRecovery: streamRecoveryDefaults,
};
}
@@ -670,10 +706,7 @@ export function mergeResilienceSettings(
current.providerCooldown
),
quotaPreflight: normalizeQuotaPreflightSettings(updates.quotaPreflight, current.quotaPreflight),
streamRecovery: normalizeStreamRecoverySettings(
updates.streamRecovery,
current.streamRecovery
),
streamRecovery: normalizeStreamRecoverySettings(updates.streamRecovery, current.streamRecovery),
};
}

View File

@@ -222,7 +222,7 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [
warningLevel: "info",
},
// ──────────────── Runtime (10) ────────────────
// ──────────────── Runtime (12) ────────────────
{
key: "OMNIROUTE_MCP_ENFORCE_SCOPES",
label: "MCP Enforce Scopes",
@@ -313,6 +313,30 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [
requiresRestart: false,
warningLevel: "caution",
},
{
key: "STREAM_RECOVERY_ENABLED",
label: "Stream Recovery",
description:
"Enable transparent early retry for truncated upstream SSE streams before any response bytes reach the client.",
descriptionI18nKey: "featureFlagStreamRecoveryEnabledDescription",
category: "runtime",
defaultValue: "false",
type: "boolean",
requiresRestart: false,
warningLevel: "caution",
},
{
key: "STREAM_RECOVERY_MIDSTREAM_ENABLED",
label: "Mid-Stream Continuation",
description:
"Allow stream recovery to re-request and stitch a response after bytes have already reached the client.",
descriptionI18nKey: "featureFlagStreamRecoveryMidstreamEnabledDescription",
category: "runtime",
defaultValue: "false",
type: "boolean",
requiresRestart: false,
warningLevel: "danger",
},
{
key: "MODEL_CATALOG_INCLUDE_NAMES",
label: "Model Catalog Names",