Repository navigation
fix(egress-cascade, vision, nvidia, kimi, tls): 9 cherry-picked fixes - #14061
alvinveroy wants to merge 31 commits into
Conversation
…ode background identity headers + staged resilience work # Conflicts: # open-sse/executors/base.ts # open-sse/handlers/embeddings.ts # open-sse/services/combo.ts # open-sse/services/opencodeQuotaFetcher.ts # src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx # src/i18n/messages/in.json # src/lib/usage/callLogArtifacts.ts # src/shared/constants/visionModels.ts # src/shared/utils/httpClientAbortGuard.mjs # tests/unit/call-log-cap.test.ts # tests/unit/httpClientAbortGuard.test.mjs
…10, with a first-byte watchdog Root cause of the 2026-09-03/04 outage: with ENABLE_TLS_FINGERPRINT=true every direct inference dispatch rode the wreq-js (browser-JA3) transport, whose responses resolved but streamed ZERO bytes for 90-600s on opencode-go / command-code / bailian (agents aborted -> 499 storms; the then-live 110s headers-wait default turned the hangs into 504s). Plain node fetch streamed the same requests in seconds and control-plane calls were unaffected — the stall is transport+streaming-specific, not DNS/CF slow-walking. - wreq is no longer eager by default: plain direct fetch is the fast path; a Cloudflare fingerprint rejection (403 + 1010 / browser_signature_banned) arms a 1h per-provider cache and retries once via wreq (replay-safe requests only). TLS_FINGERPRINT_PROVIDERS keeps its legacy eager semantics, now guarded. - Every wreq response (direct, ladder retry, proxied overlay) carries a first-byte watchdog (TLS_FINGERPRINT_FIRST_BYTE_WATCHDOG_MS, default 10s): no first byte -> cancel and fall back to the direct dispatcher instead of hanging the caller. Verified live: fingerprint off + ladder armed-on-demand restores TTFB 3-6s on opencode-go (was 90-600s of zero bytes), zero 1010 recurrence in the window.
… watchdog 2026-09-05 wedge incident: opencode h2 stream resets (ERR_HTTP2_STREAM_ERROR, UND_ERR_SOCKET) fanned out through every combo target (8 fallbacks / 18 decisions per request), then a client abort during combo cleanup left the event loop wedged — listener accepting, nothing served, nothing logged, for hours. The existing supervisor only restarts on process exit. - Bounded transport abort (both dispatchers): consecutive transport-class network failures share the same egress — after DEFAULT_TRANSPORT_ABORT_BOUND (3), abort the combo with an explicit 502 instead of burning the remaining fan-out. Non-transport failures reset the counter. New predicates isTransportClassNetworkError / shouldAbortComboForTransportFailures in comboPredicates.ts. - Self-probe wedge watchdog (scripts/dev/self-probe-watchdog.mjs, wired in standalone-server-ws.mjs after the listener accepts): loopback probe of /api/status on the same event loop — a wedged loop cannot answer it. N consecutive connect/timeout failures (any HTTP response counts as alive, incl. 401) exit(1) so launchd KeepAlive / systemd / Docker relaunch. Config: OMNIROUTE_SELF_PROBE(_INTERVAL_MS|_TIMEOUT_MS|_THRESHOLD), default 60s/15s/3; OMNIROUTE_SELF_PROBE=off disables. - file-size rebaseline for the touched frozen files with justification.
…in TLS watchdog; stryker tap registration - dashboard/combos/page.tsx: defer the post-hydration localStorage correction one microtask so setShowUsageGuide is not a synchronous setState inside the effect body (react-hooks/set-state-in-effect base red on release/v3.8.51 from cheaper-inference#12448); eslint:json gate green. - proxyFetch.ts withTlsFirstByteWatchdog: the watchdog race consumed the first body chunk from the reader but never re-emitted it — the first SSE frame was silently dropped. The rest stream now re-emits the captured chunk before continuing the original stream. - stryker.conf.json: register 10281-combo-reasoning-probe + transport-bounded-abort in tap.testFiles (they cover mutated modules; --strict gate). - callLogArtifacts.ts / earlier dashboard stray: restore pristine remote state for files untouched by this branch (resolves unused-var lint).
# Conflicts: # config/quality/file-size-baseline.json # open-sse/services/combo.ts # src/app/(dashboard)/dashboard/combos/page.tsx # src/sse/handlers/chat.ts
scripts/dev/self-probe-watchdog.mjs is runtime-imported by dist/server-ws.mjs but had no entry in APP_STAGING_ALLOWED_EXACT_PATHS or the EXTRA_MODULE_ENTRIES that drives assembleStandalone, so the prepublish prune deleted it and the published bundle boots ERR_MODULE_NOT_FOUND. Add the bare entry, the standalone copy, and the closure required-path.
…ding fetch-start cap
merge content:
- 175 commits from origin/release/v3.8.51 (i18n EU locales, TLS watchdog
module via guardTlsFirstByte, dashboard session auth, semantic-cache
signature fix, opencode-zen RR collision fix, ...)
- shared/tlsFirstByteWatchdog.ts supersedes the local withTlsFirstByteWatchdog
helper in proxyFetch.ts; the adaptive arming ladder (providerFingerprintArmed
/ armProviderFingerprint / looksLikeFingerprintRejection) is preserved
- proxyFetch.ts streams.ts conflicts resolved to upstream; the 1k-token-fix
for usageOnlyChunk and the (streamBufferBytes -> highWaterMark) rename
land together
fix(kimi):
- cheaper-inference#13376: kimi-coding + kimi-coding-apikey 504 at exactly 110s on every
large cache-miss prefill (8h storm 09-12; measured 116s for a 556k-token
cache-miss in non-streaming mode). Same buffered-gateway shape as
opencode-go / command-code, so the registry cap is raised to 600s.
- test/unit/kimi-fetch-start-cap-13376.test.ts pins the projection so a
regression of cheaper-inference#11526's projection gap (registry override dropped at
generateLegacyProviders() time) cannot recur.
Kimi Coding (kimi-for-coding, -highspeed, k3) accepts ONLY temperature 1
or no temperature at all; any other value is rejected with
'[400] invalid temperature: only 1 is allowed for this model'.
Agent clients (the OMP memory-consolidation turn, and most low-temperature
agent frameworks) send temperature: 0.1 by default, so any request that
round-robins onto a Kimi slot 400'd. Observed 29 failures on 2026-09-12 and
66 on 2026-09-09, all carrying exactly {messages, model, temperature: 0.1,
tools: 7} via combo yano-free.
KimiExecutor had no temperature handling at all, unlike its sibling
MoonshotExecutor (moonshot.ts stripFixedTemperature/stripFixedSamplingParams)
which solves the same class of constraint for the same vendor.
stripNonUnitTemperature drops the key only when it is present and !== 1, so
an explicit temperature: 1 is preserved. Applied in both
normalizeOpenAIRequest and normalizeAnthropicRequest (before the latter's
three return points) since the constraint is wire-format independent.
Tests: 6 new cases in executor-kimi.test.ts (both wires, 1 preserved,
absent untouched, thinking-disabled Anthropic early return).
Verified: in 0.1/0.7/0/2 -> dropped (upstream 200); in 1 -> preserved (200).
… survive NVIDIA's GET /v1/models is a PARTIAL view of what the credential can route. It returns the media/infra buckets (flux, tacotron2, parakeet, whisper, embeddings, rerankers) plus only the two nvidia/* chat models, while the static registry also documents the third-party vendors the same connection fronts (moonshotai/, deepseek-ai/, meta/, poolside/, openai/). providerUsesAuthoritativeLiveCatalog() defaults to true, so once a model-sync refreshed NVIDIA's live list every static-only route was rejected BEFORE dispatch: Model 'moonshotai/kimi-k3' is not available in the active live catalog for provider 'nvidia' Observed 159x on 2026-09-12, which also made the nvidia combo target unusable (nvidia/moonshotai/kimi-k3 failed 150x across all 5 combos). Note the entry already declared passthroughModels: true (cheaper-inference#6773) for exactly this multiplexed shape — the authoritative gate was the missing half of that contract. Set liveCatalogAuthoritative: false, matching command-code (partial routing catalog), oneminai, and perplexity-agent. A model the credential genuinely cannot serve still fails upstream with its own 4xx, and passthroughModels keeps that failure scoped to the single model. Tests: tests/unit/nvidia-partial-catalog-routing.test.ts (4 cases) pins the opt-out, the passthroughModels pairing, the retained static vendor models, and that full-discovery providers stay authoritative.
Conflict resolutions: - resilience/settings.ts + ratelimit-admission-control-6593.test.ts: keep our 120000 queue-wait default (deliberate; upstream narrowed to 30000). - codebuddy-cn/index.ts, oauth/constants/oauth.ts, combos/controlCenter.ts: take upstream, which fixes the client/server bundle boundary at the root (providerHeaderProfiles.ts, providerAlias.ts) instead of per-file patches; drops the now-orphaned codebuddy-cn/userAgent.ts leaf. - executors/base.ts: union (applyProviderRequestDefaults + requireCompatibleBaseUrl). - .env.example: union (keep RATE_LIMIT_EXECUTION_MAX_WAIT_MS docs). - providerModelsConfig.ts: union (opencode identity headers + xai-oauth discovery). Preserved fork-only invariants: kimi fetchStartTimeoutCapMs=600000, NEXT_BUILD_CPUS build knob, opencode background identity headers.
DeepSeek-V4.1-Flash is natively multimodal (official announcement news260910: "native visual understanding" / "native multimodal support"; HF card pipeline_tag image-text-to-text, DeepSeek-ViT encoder, MMMU-Pro 56.5 / DocVQA 95.6 / RefCOCO 86.0). DeepSeek retired V4-Flash and V4-Flash-Vision-Exp INTO V4.1-Flash: both legacy ids now route there, as does deepseek-v4-pro until V4.1-Pro ships. OmniRoute modelled none of this, so image requests aimed at deepseek-v4.1-flash were intercepted by the Vision Bridge and rerouted to opencode-go/glm-5.3-flash. Live-verified before this change: with visionBridgeRerouteTextOnly=false the image reached the REAL target (served by deepseek/deepseek-v4.1-flash and deepseek-v4.1-flash, not the bridge) and answered a 4-quadrant colour prompt correctly through both the command-code and opencode-go gateways. Scoped to the 4.1 revision only. Plain deepseek-v4-flash is NOT marked vision: other providers ship the genuinely text-only V4-Flash under that id (DeepSeek own multimodal comparison rows are absent for V4-Flash and V4-Pro), and VISION_MODEL_ID_FRAGMENTS is a global substring match, so marking it would recreate the cheaper-inference#4071 false-positive the list warns about.
Repaired two defects in markAccountUnavailable's egress-bucketed branch
(src/sse/services/auth.ts:2905-2940 and open-sse/config/constants.ts:231).
Defect 1 — false cascade on per-account quotas
The egress-bucketed branch fired on either QUOTA_EXHAUSTED or
RATE_LIMIT_EXCEEDED. For opencode-go the upstream body is per-workspace
("Monthly usage limit reached. Resets in 9 days." URL wrk_XX/go). Yet
applyEgressIpLockout then expands to every sibling connection sharing
the egress IP, producing 8-account lockouts pinned to one workspace's
reset date. The IP-bucketed assumption (referenced in cheaper-inference#9611) holds only
for the opencode FREE tier; per-account monthly caps encode their own
reset hint and are already covered by the connection-scoped rule.
Narrowed the gate to RATE_LIMIT_EXCEEDED only.
Defect 2 — synthetic label overwrote the real upstream error
updateProviderConnection unconditionally set last_error to
"Shared egress IP quota exhausted (<provider>)". The original
upstream body was lost. Operators had no visibility into the actual
reset date even when it was right there in the response.
Now last_error keeps the truncated upstream body (1 KiB cap) with a
"[cascaded to siblings on shared egress IP - original]" tag, and
lastErrorType is set to a new QUOTA_CASCADE value (added to
RateLimitReason) so the UI and DB queries can distinguish IP-cascade
from single-account exhaustion.
Operational effect:
- opencode-go accounts cool per the upstream's own "Resets in N days."
hint, on their own schedule. Sibling fanout is gone.
- last_error shows the real upstream error text + the cascade tag.
- Genuine IP-bucketed rate limits (no reset hint) continue to use the
IP cascade with the default 2-minute COOLDOWN_MS.rateLimit.
- opencode, opencode-zen, opencode-cli (all in OPENCODE_RULE_FAMILY)
benefit from the same fix.
Verified:
- Live probe before: every 429 returned by opencode-go presented as
"Shared egress IP quota exhausted (opencode-go)" in last_error and
in call_logs, with all 8 connections locked from a single monthly
429 on one workspace.
- Last-resort fallback preserved: an empty errorText still emits the
synthetic label, so behavior under novel upstream shapes is
bounded (not undefined).
- The other egress-bucketed branch (cheaper-inference#10880 agentrouter 403 path) is
untouched; its connection-scope semantics were already correct.
Co-authored-by: yano-ai <ops@yano.ai>
Pulls 74 upstream commits onto our working branch base (commit c44c887 = release-v3.8.51-deploy). Picks up: Critical fixes that affect us: - 65dcb1d feat(resilience): stop retrying sibling connections after IP-bucketed 429 (overlaps with our bc3f3c5 egress-cascade fix - the two changes coexist cleanly because we modify different gates inside markAccountUnavailable) - e7872e5 fix(db): reclaim freed pages incrementally instead of blocking VACUUM (would address the auto_vacuum drift we investigated earlier) - c40a5f0 fix(build): externalize @modelcontextprotocol/sdk to heal MCP initialize 500 on standalone builds (we ship tgz builds -- relevant) - 176d632 feat(api): add per-key allowAutoCombos to gate the built-in auto/* combos Plus 70 other commits (security/sse/auth/provider/scheduler/combo/dashboard). Conflict resolution: - tests/unit/error-classifier.test.ts: took upstream version (ours == base on every line except for one verification test for fingerprint-rejection throttling. The throttling code itself is still in errorClassifier.ts; upstream's existing 16 fingerprint tests cover the behavior). Verified clean by: git merge-tree --write-tree --name-only perf-upgrade-20260917 origin/release/v3.8.51 which reported exactly one CONFLICT line before the actual merge. Source files modified by our prior commits MERGED CLEANLY: src/sse/services/auth.ts (egress-cascade patch) open-sse/config/constants.ts (QUOTA_CASCADE enum) open-sse/config/providerErrorRules.ts (auto-merged intact)
…ision fragment Pre-merge cleanup for the fork-to-upstream translation of this cherry-pick bundle: - open-sse/services/qoderCli.ts: remove the stray `import "server-only";` (package isn't a dependency and isn't used anywhere else) — it crashed any direct Node import (e.g. tests/unit/embeddings-input-clamp.test.ts) with ERR_MODULE_NOT_FOUND. - .env.example: drop the duplicated RATE_LIMIT_EXECUTION_MAX_WAIT_MS block re-added alongside the existing one. - src/shared/constants/visionModels.ts: revert the undocumented glm-5.3-flash -> glm-5.3 widening (now also matched glm-5.3-high/low), keeping the fragment scoped to the vision-capable flash variant. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…ation Post-merge cleanup found while running the mandatory pre-push gates: - open-sse/services/combo/decisionTrace.ts: the PR's own head commit carried a duplicate `detail` field (interface + object literal) and a duplicate "persisted_cooldown" entry in COMBO_SKIP_REASONS, breaking typecheck:core (TS2300/TS1117). Also extracted parseFuturePersistedCooldownResetMs() out of summarizeSkippedTargetsWithRetry() to fix a cognitive-complexity ratchet regression the new function introduced. - open-sse/utils/cursorAgentCliVersion.ts: same stray `import "server-only";` defect as qoderCli.ts (not a dependency, not used elsewhere), breaking tests/unit/cursor-agent-cli-version.test.ts with ERR_MODULE_NOT_FOUND. - src/shared/utils/httpClientAbortGuard.mjs: my merge resolution had dropped the release branch's COMBO_ABORT_REASONS message-text check as "redundant" with this PR's broader AbortError/TimeoutError name check — wrong: a raw `new Error(reason)` with no relabelled `.name` only matches the message check. Restored it; both httpClientAbortGuard.test.mjs cases pass again. - tests/unit/opencode-background-identity-headers.test.ts: two assertions expected a bare UUID / 16-hex hash for x-opencode-request and the seedless x-opencode-session, but opencodeHeaders.ts's applyCliDefaults() always renders those in the canonical `msg_`/`ses_` OpenCode id shape (OPENCODE_REQUEST_PATTERN/OPENCODE_SESSION_PATTERN) — realigned the assertions with that documented, correct behavior. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
ee99c2a to
c449e8e
Compare
dashboard/combos/page.tsx (a client component) imported resolveCanonicalProviderModel from open-sse/services/model.ts. That module is the server model hub: its inference helpers reach the DB read cache through dynamic imports (@/lib/db/readCache, @/lib/db/models), so webpack followed the client graph into usage.ts -> usage/qoder.ts -> qoderCli.ts and failed the client build with "Can't resolve 'child_process'". Move the pure alias-resolution trio (PROVIDER_MODEL_ALIASES, resolveProviderModelAlias, resolveCanonicalProviderModel) into a client-safe moduleAliases.ts. model.ts imports them back and re-exports resolveCanonicalProviderModel, so every existing server caller is unchanged and there is still exactly one implementation. Only the combos page import path changes. Verified: check:open-sse-typecheck 0 errors; check:dashboard-typecheck within frozen baseline.
- Latest from origin/release/v3.8.51: a58000c fix(security): pre-request hooks in isolated realm (GHSA-9p9m-h9rj-rhhg) cheaper-inference#14916 - Routing bug of interest: 96f6aee fix(routing): keep request-scoped SSE errors out of cooldowns cheaper-inference#14585 — Codex SSE errors/refusals no longer poison account cooldowns or affect other accounts - 07d2a8d fix(routing): serve sync tier from stored pricing snapshot cheaper-inference#14706 - 638d898 fix(sse): persisted connection cooldown wins over transient rate-limit signal cheaper-inference#14708 - 9c96184 fix(combo): skip connection cooldown on request-level parameter validation 400 cheaper-inference#14438 Resolved conflict on src/app/(dashboard)/dashboard/combos/page.tsx by taking upstream's import (resolveProviderAlias from providerAlias.ts), which matches upstream's downstream usage at the calling site.
…st object Every /v1/chat/completions, /v1/messages and /v1/responses request 500'd with "Cannot read private member #state from an object whose class did not declare it" after 8c05ec4 wired withDeadlineSignal() into the routes. Next.js App Router route handlers receive a Proxy-wrapped Request. `request instanceof Request` holds for it and reads forward to the target, but the object identity is the Proxy, so the native undici constructor's private-brand read on the input (`input.#state`) is unreachable. Constructing the deadline wrapper as `new Request(request, { signal, headers })` therefore threw on every request. Rebuild from primitives instead (url/method/headers/body): the wrapper is equivalent, and the body stream is handed over unread because this runs before admission and parsing. Covered by a regression test that wraps a Request in a proxy shaped like Next's (getters forwarded, identity still the Proxy) — it fails on the previous shape with the brand error and passes after. Verified S/E on the installed daemon: opencode-go/glm-5.3-flash and command-code/z-ai/glm-5.3-flash both return 200, #state error delta 0.
providerModelsConfig.ts registers an "opencode" entry in PROVIDER_MODELS_CONFIG but hardcodedModelsConfigIds.ts was not updated to match, failing the lockstep test in discovery-class.test.ts. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
# Conflicts: # open-sse/translator/index.ts # open-sse/translator/request/openai-responses/toResponses.ts # open-sse/utils/earlyStreamKeepalive.ts # open-sse/utils/proxyFetch.ts # tests/unit/early-stream-keepalive.test.ts # tests/unit/glm-1214-same-format-user-turn.test.ts # tests/unit/opencode-reasoning-presence-responses.test.ts
…odule proxyFetch.ts grew past its frozen file-size ceiling after the adaptive TLS-fingerprint ladder was added. Move the three pure arm/detect helpers (providerFingerprintArmed, armProviderFingerprint, looksLikeFingerprintRejection) into tlsFingerprintLadder.ts — no behavior change, reduces the ceiling overage. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
Re-homed to |
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…base.ts, proxyFetch.ts Own growth of the egress-cascade narrowing, the requestDefaults wiring and the adaptive TLS-fingerprint-ladder retry; the ladder helpers already live in a leaf. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…n + v3.8.52 cycle) into yano-patches
… golden, guard predicates, env docs - providers: register `typesafe` (alias `jev`) in the canonical catalog (src/shared/constants/providers/apikey/inference-hosts.ts) so the provider-consistency gate passes. The registry id + alias also land in the reserved-prefix set: 412 -> 414 (test constant updated, measured). - golden: add the `typesafe` translate-path block to tests/snapshots/provider/translate-path.json (CI unit shards parity with the registry; Linux user-agent pins preserved). - guard: keep isRecoverableUpstreamTimeoutError and isClientAbortError disjoint (cheaper-inference#12861). AbortError absorption now requires one of our known reasons/messages — an AbortError from an unknown subsystem surfaces again. TimeoutError stays broad, and DIRECT_RESPONSE_START_TIMEOUT remains covered through isRecoverableUpstreamTimeoutError (cheaper-inference#12164 breadth retained where it was earned). - tls: the cheaper-inference#12656 watchdog tests now engage wreq through the documented eager allowlist (TLS_FINGERPRINT_PROVIDERS); the adaptive ladder is reactive-by-default by design. - docs: declare OMNIROUTE_EGRESS_IP_LOCK_PROVIDERS in .env.example and docs/reference/ENVIRONMENT.md (env-doc sync gate).
|
Refreshed onto the latest |
…58 -> 359) - PROVIDER_REFERENCE.md regenerated (gen:provider-reference). - README/AGENTS/llm.txt/package.json + 5 diagrams + 2 public SVGs: 358 -> 359. - docs/i18n/*/llm.txt: synced via scripts/i18n/sync-llm-mirrors.mjs (66 locales). Keeps check:docs-all (docs-counts, docs-sync) green.
…ation - providers-constants-split: APIKEY_PROVIDER_COUNT 241 -> 242 (typesafe). - pack-artifact-policy: dist/self-probe-watchdog.mjs is a required root runtime path in this fork (closure fix), so it belongs in the expected missing-paths list.
|
Hi @alvinveroy — closing this pull request to clear the open queue. The branch |
|
Hi @alvinveroy — reopening this pull request. The close was a mistake, and it is back in the merge queue. The branch |
|
Thanks @alvinveroy. This is an omnibus of 9 fork cherry-picks across 137 files, ~1170 commits behind, touching protected agent docs (AGENTS.md, llm.txt and mirrors) with stale provider counts and changing resilience defaults. It can't be reviewed or merged as one PR. The egress-cascade fix in |
Summary
This PR contributes 9 fork-only fixes that have been running in production
against
release-v3.8.51-deployfor 1–4 weeks. Each one is independentlyuseful to upstream; the largest user-visible improvement is the
egress-cascadefix which corrects a regression in how OmniRoute handledper-account opencode-go quota exhaustion.
All 9 commits are cherry-picked — the PR diff is small (5 files, 89
insertions) because most of the work is local configuration, not source.
Highlights
bc3f3c56d fix(egress-cascade): scope to IP rate limits; preserve upstream errorUser-visible regression. The egress-bucketed branch in
markAccountUnavailablefired on eitherQUOTA_EXHAUSTEDorRATE_LIMIT_EXCEEDED, then expanded to every sibling connection sharingthe egress IP. For opencode-go, the upstream body is per-workspace
(
Monthly usage limit reached. Resets in N days.URLwrk_XX/go), butthe IP cascade locked all 8 workspaces at once for one workspace's reset
date. The IP-bucketed assumption (#9611) holds only for the opencode free
tier; per-account monthly caps already encode their own reset hint and
are covered by the connection-scoped rule. Narrowed the gate to
RATE_LIMIT_EXCEEDEDonly — the IP cascade now fires only for genuinelyIP-bucketed rates.
Also:
updateProviderConnectionwas overwritinglast_errorwith asynthetic label (
Shared egress IP quota exhausted (<provider>)),hiding the upstream body. Now
last_errorkeeps the truncated upstreambody (1 KiB cap) with a
[cascaded to siblings on shared egress IP — original]tag, andlastErrorTypeis set to a newRateLimitReason.QUOTA_CASCADEvalue so UI and DB queries can distinguish IP-cascade vs single-account
exhaustion.
Live probe before/after on the same bundle (3-minute window):
72a71148f feat(vision): mark DeepSeek-V4.1-Flash vision-capableUpstream data drift. DeepSeek's
deepseek-flashAPI id (their publicmultimodal V4.1-Flash) was not registered as vision-capable. OmniRoute's
vision routing fell through to a generic fallback for image requests
targeting it. Live-proven via a 4-color quadrant test (red/blue/green/yellow):
with the fix, image content reaches
deepseek-v4.1-flashdirectly;without it, the image never left OmniRoute (Vision Bridge intercepted
and rerouted to a different provider).
Scope is deliberately tight:
VISION_MODEL_ID_FRAGMENTSgets a singleentry (
deepseek-v4.1-flash), and the registry entry fordeepseek/deepseek-flashaliases it. The plaindeepseek-v4-flashisnot touched — other providers ship the genuinely text-only V4-Flash
under that id and the global substring match would create #4071 false
positives.
Other cherry-picks (smaller scope, longer history)
c44c88796fix(nvidia): mark the live catalog non-authoritative so static routes survive25a05dcccfix(kimi): drop non-1 temperature on Kimi Coding modelsb65d936e8fix(build): include self-probe-watchdog in pack closuresa955c0a98fix(lint): defuse set-state-in-effect base red; fix first-chunk loss in TLS watchdog; stryker tap registration550557425fix(resilience): bounded transport abort in combos + self-probe wedge watchdogffa72a4eafix(tls): adaptive fingerprint ladder — wreq-js only on Cloudflare 1010, with a first-byte watchdogf258d156bwip(snapshot): integration state 2026-09-04 — capMs follow-up + opencode background identity headers + staged resilience workMost of these were developed on
release-v3.8.51-deploy(the fork-onlydeploy-tracking branch), not
release/v3.8.51. They were merged intorelease/v3.8.51-yano-patchesvia5e10e2fcf merge: origin/release/v3.8.51 (120 commits) into release-v3.8.51-deployplus the 74-commit
72a6e8511 merge: upstream 74 commitsfor parity withupstream's HEAD. The 9 non-merge commits above are what's actually novel
relative to
origin/release/v3.8.51HEAD.Testing
bc3f3c56d: live-verified on a 3-minute window of production traffic.Per-account cooldown timestamps now match the upstream's "Resets in N
days" body. Cascade expansion (8 simultaneous locks) is gone.
72a71148f: live-proven with a deterministic 4-color quadrant image.Image content reaches
deepseek-v4.1-flashdirectly; correct colorsreturned.
release-v3.8.51-deploybranch for 1–4 weeks without regression.Each patch was merged in locally with
git merge-tree --write-treesimulation before the actual merge — most conflicted at zero files; the
one conflict (a test file in the most recent upstream merge) was resolved
by taking upstream's version, with no functional content loss.
Risk
bc3f3c56dchanges behavior for ALL egress-bucketed providers(opencode, opencode-zen, opencode-cli). The change is conservative
(narrowing, not expanding) — the existing IP cascade still fires for
RATE_LIMIT_EXCEEDED, which is the documented IP-bucketed signal.72a71148fadds a single string toVISION_MODEL_ID_FRAGMENTS.Risk is limited to a new provider id being matched; the global
substring match has been stable in this codebase.
Checklist
call_logsafter deploymentorigin/release/v3.8.51HEAD viagit merge-tree --write-tree --name-only(zero conflicts)