Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"_rebaseline_2026_08_08_v3850_base_drift_batch_9757": "Base drift on release/v3.8.50, not own growth: the 08-06..08-08 merge batches grew 12 already-frozen (or newly-landed) files without carrying their rebaselines — the dedicated rebaseline PR #9616 was closed as 'superseded' but its file-size entries never actually reached the base, and later merges (#8894 combos page, #9539 EditConnectionModal, #8895 models route, #9294/#9293 catalog, #9541 db/core, #8970 tokenHealthCheck, #8925 mcp schemas+server, #8890 accountFallback, #9467 chat.ts, #8931 openai-to-kiro, ProxyRegistryManager) kept growing them. All 12 values re-measured on THIS branch's tree (= pure tip + this PR's 1-line chat.ts fix, which adds zero lines). This PR's own source changes (chat.ts identifier restore, stream.ts format carve-out) do not grow any frozen file past these values.",
"_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.",
"_rebaseline_2026_08_08_9183_reasoning_cache_index_sync": "Extracted fix(responses-api): sync reasoning-cache write index with the fixed read side (from the originally-authored #9183) — chatCore.ts's write side cached every response under a hardcoded messageIndex:0, and translator/index.ts's plain-turn (non-tool-call) cache-key lookup ALSO still hardcoded messageIndex 0 at its call site (a second, previously-undiscovered instance of the same hardcoding bug, found while re-verifying this fix against the current upstream tip — the two never agreed once a conversation went past its first assistant turn, so DeepSeek/Xiaomi-mimo plain-turn reasoning replay silently missed the cache). Own growth: open-sse/handlers/chatCore.ts 5034->5042 (+8, computing messageIndex from the incoming request's message count at both the streaming and non-streaming cache-write call sites) — irreducible call-site wiring. Covered by tests/unit/reasoning-cache.test.ts (new end-to-end write/read regression test, rebaselined below) and tests/unit/translator-helper-branches.test.ts fixture updates. Other #9183 sub-fixes (output_index collision prevention, reasoning-content-alias generalization) were originally assumed already superseded by upstream's own independent fix — a live incident 2026-08-08 disproved that for the message-vs-tool-call collision case specifically (fixed separately in #9822); not re-extracted here since this PR's own scope is the narrower messageIndex sync only.",
"_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
"_rebaseline_2026_08_09_9342_network_error_guard": "PR #9342 own growth during the 2026-08-09 rebase: open-sse/services/accountFallback.ts 1978->2008 (+30 = the isQueueTimeout short-circuit plus a per-provider network-error dedup window in recordProviderFailure, keeping one VPN blip from the same provider's combo targets counting once per target). Covered by tests/unit/breaker-network-error-guard.test.ts. (chat.ts stays base-red: upstream tip is already 1918 > frozen 1904, this PR only adds +12 on top; not re-bumped per the no-inherit-ratchet rule.)", "_rebaseline_2026_08_09_9296_adobe_media_capabilities": "PR #9296 (artickc, fix/adobe-firefly-model-capabilities) own growth: src/app/api/v1/models/catalog.ts 1590->1597 (+7). The image and video catalog serializers now expose the already-normalized Adobe Firefly discovery capability data (media_capabilities, plus the existing video modality/size fields) at their only response-emission chokepoints. The discovery parser and capability normalization remain in open-sse/services/adobeFireflyModels.ts; extracting these seven serialization fields would obscure the catalog contract. Covered by tests/unit/adobe-firefly.test.ts and tests/unit/image-upscale.test.ts.", "_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent\u2019s conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR\u2019s own mandatory pre-merge checklist, not yet addressed) \u2014 unrelated to this file-size ratchet, tracked separately by /fix-prs.",
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() \u2014 when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) \u2014 new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline \u2014 not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
Expand Down
32 changes: 31 additions & 1 deletion open-sse/services/accountFallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ const CONNECTION_FAILURE_DEDUP_MS = 5000;
const MAX_CONNECTION_FAILURE_DEDUP_ENTRIES = 10_000;
const lastConnectionFailure = new Map<string, number>();

// Per-provider network-error dedup: several combo targets on the SAME provider can
// fail the same single network event (a VPN blip) in the same request. Without this,
// each target counts once and one transient blip opens the whole-provider breaker
// while the provider is healthy. A genuinely dead proxy persists ACROSS requests
// (past the window) and still accumulates to its threshold.
const NETWORK_ERROR_DEDUP_MS = 10_000;
const MAX_NETWORK_ERROR_DEDUP_ENTRIES = 1000;
const lastNetworkErrorByProvider = new Map<string, number>();

function pruneConnectionFailureDedupeEntries(): void {
while (lastConnectionFailure.size > MAX_CONNECTION_FAILURE_DEDUP_ENTRIES) {
const oldestKey = lastConnectionFailure.keys().next().value;
Expand Down Expand Up @@ -974,9 +983,30 @@ export function recordProviderFailure(
provider: string | null | undefined,
log?: { warn?: (...args: unknown[]) => void },
connectionId?: string | null,
profile?: ProviderBreakerProfile | null
profile?: ProviderBreakerProfile | null,
opts?: { isQueueTimeout?: boolean; isNetworkError?: boolean }
): void {
if (!provider) return;
// OmniRoute's own rate-limit queue timeout is backpressure we applied, not a
// provider failure — the provider never saw the request, so it must not count
// toward the provider breaker.
if (opts?.isQueueTimeout) return;

// Network-layer errors (proxy_unreachable) get a separate SAME-PROVIDER dedup, so a
// single transient network event is not counted once per combo target (see the
// declaration). A dead proxy persists across requests and still accumulates.
if (opts?.isNetworkError) {
const now = Date.now();
const last = lastNetworkErrorByProvider.get(provider);
if (last && now - last < NETWORK_ERROR_DEDUP_MS) return;
lastNetworkErrorByProvider.delete(provider);
lastNetworkErrorByProvider.set(provider, now);
while (lastNetworkErrorByProvider.size > MAX_NETWORK_ERROR_DEDUP_ENTRIES) {
const oldestKey = lastNetworkErrorByProvider.keys().next().value;
if (typeof oldestKey !== "string") break;
lastNetworkErrorByProvider.delete(oldestKey);
}
}

// Deduplicate rapid-fire failures from the same connection
if (connectionId) {
Expand Down
8 changes: 7 additions & 1 deletion open-sse/services/combo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1865,7 +1865,13 @@ export async function handleComboChat({
isProxyUnreachable: structuredError?.code === "proxy_unreachable",
})
) {
recordProviderFailure(provider, log, targetWithConnection.connectionId, profile);
const isQueueTimeout =
errorText.includes("RATE_LIMIT_QUEUE_TIMEOUT") ||
errorText.includes("RATE_LIMIT_QUEUE_WEDGED");
recordProviderFailure(provider, log, targetWithConnection.connectionId, profile, {
isQueueTimeout,
isNetworkError: structuredError?.code === "proxy_unreachable",
});
}

const quotaExhausted = await isQuotaExhaustionResponse(
Expand Down
14 changes: 13 additions & 1 deletion src/sse/handlers/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1386,10 +1386,22 @@ async function handleSingleModelChat(
}

const breakerFailureStatus = Number(lastStatus ?? credentials?.lastErrorCode);
// lastError is a string here — check for the proxy_unreachable tag embedded by
// tagProxyUnreachable (proxyFetch.ts) and OmniRoute's own queue timeouts. Both mean
// we never reached the provider, so they must not trip the provider breaker.
const isNetworkError =
typeof lastError === "string" &&
(lastError.includes("proxy_unreachable") || lastError.includes("PROXY_UNREACHABLE"));
const isQueueTimeout =
typeof lastError === "string" &&
(lastError.includes("RATE_LIMIT_QUEUE_TIMEOUT") ||
lastError.includes("RATE_LIMIT_QUEUE_WEDGED"));
if (
!forceLiveComboTest &&
credentials?.allRateLimited &&
isProviderBreakerFailureStatus(breakerFailureStatus)
isProviderBreakerFailureStatus(breakerFailureStatus) &&
!isNetworkError &&
!isQueueTimeout
) {
breaker._onFailure();
}
Expand Down
6 changes: 6 additions & 0 deletions src/sse/handlers/chatPredicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export function shouldTripProviderBreakerForResult(
!isRequestScopedUpstreamFailure({ code: result.errorCode, type: result.errorType }) &&
!(result.response && getTrustedLocalRateLimitResponse(result.response)) &&
!isLocalStreamLifecycleError(result.error) &&
// Network-layer errors (ECONNREFUSED, ETIMEDOUT) never reached the provider —
// the provider may be healthy, only the network path is broken. OmniRoute's own
// rate-limit queue timeouts are backpressure we applied, not a provider failure.
result.errorCode !== "proxy_unreachable" &&
result.errorCode !== "RATE_LIMIT_QUEUE_TIMEOUT" &&
result.errorCode !== "RATE_LIMIT_QUEUE_WEDGED" &&
PROVIDER_BREAKER_FAILURE_STATUSES.has(Number(result.status))
);
}
Expand Down
Loading
Loading