A Next.js standalone build writes a synthetic .build/next/package.json
({"type":"commonjs"}) that lacks a "name" field. The resolveProjectRoot()
walk-up was stopping at this marker instead of continuing to the real repo
root, making PROJECT_ROOT point at .build/next where no .git exists, which
caused the source-mode validation to report "Not a git repository."
Fix: only accept a package.json as a project-root marker when its parsed
content has a non-empty "name" field. Keep .git as a hard marker.
Add isValidPackageMarker() helper for testability.
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Acquire admission once after API-key policy, preserve lazy raw-request snapshots, and bind lease settlement to JSON, SSE, abort, deadline, and failure lifecycles. Expose a low-cardinality health summary and preserve non-SSE Ollama errors unchanged.
The local vi.mock("next-intl") predates the #7935 global polyfill and returns a
useTranslations without .rich, crashing t.rich() in ProviderParamFilterSection:199.
The global polyfill (backed by the real createTranslator) now covers this file;
assertions only check DOM/fetch, never translated text.
Co-authored-by: Max <maxmad64@gmail.com>
* fix(dashboard): add per-connection autoSync toggle handler
* fix(dashboard): render per-connection autoSync toggle in ConnectionRow
* fix(dashboard): wire canAutoSync into ConnectionsListPanel
* fix(dashboard): wire per-connection autoSync toggle into provider page
* fix(dashboard): make master autoSync toggle all-on with fan-out
* docs(dashboard): add changelog fragment for per-connection autoSync
* fix(dashboard): correct disable toast and assert fan-out classification
* test(dashboard): pin fan-out classification branches symmetrically
* docs(dashboard): fill changelog fragment with PR number
* fix(dashboard): port autoSync i18n keys to vi and pt-BR locales
* fix(dashboard): localize autoSync keys across all 43 locales
---------
Co-authored-by: Max <maxmad64@gmail.com>
* feat(providers): filter detail connections server-side
Filter provider detail requests at the database boundary while preserving
the full per-provider connection set needed by search, pagination, and bulk
actions. Alias-backed provider pages keep their existing aggregate behavior.
Co-authored-by: RobertsXML <RobertsXML@proton.me>
Inspired-by: https://github.com/decolua/9router/pull/2998
* chore(changelog): fragment for #9247
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: RobertsXML <RobertsXML@proton.me>
* fix(db): persist account egress IP into proxy_logs
The account egress IP (outbound IP the upstream saw, resolved via proxyEgress.ts
echo-IP probe with 5-min cache) was computed and surfaced in the proxy_logs
console and ring buffer, but never persisted: proxy_logs.egress_ip did not
exist, so the value was lost on restart and real traffic could not be
attributed to the node/IP active at that instant.
- migration 134 adds proxy_logs.egress_ip (nullable, backward-compatible)
- schemaColumns.ensureProxyLogsColumns() idempotent reconciler
- proxyLogger self-heals the schema in loadFromDb(), persists egress_ip on
INSERT, and matches it in search
Follows the session_tag (#8249) migration + schemaColumns reconciler pattern;
base SCHEMA_SQL untouched.
* docs(changelog): add 9291 fragment for proxy_logs egress_ip
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Root cause: a cold GET /v1/models catalog rebuild froze the entire server 41-54s.
node --prof profiling found a systemic missing-memoization pattern — a per-model
function rescanning a static or synced data structure with Object.entries()/
Object.keys() (or hitting SQLite) on every call instead of once per rebuild. Fixed
6 instances of the same pattern, found by iteratively re-profiling the full catalog
sweep after each fix (plus a whitebox review pass) until no further hotspot of this
shape remained:
1. getModelsDevPricing() (modelsDevSync.ts) — re-ran a synchronous SQLite query and
re-JSON.parse'd ~180 blobs on every call (up to ~6091x instead of once per
request). Memoized via the existing modelCatalogCacheVersion invalidation signal
(same pattern as getCachedRawProviderConnections/getCachedProviderNodes in
db/readCache.ts). Dominant cost of the original 41-54s freeze.
2. findInsensitive() (modelMetadataRegistry.ts, resolveCatalogPricing) — rebuilt a
full Object.entries() scan on every case-insensitive lookup miss, twice per
model. Replaced with a lowercase-key index built once per distinct pricing
object and cached by identity (WeakMap). Warns once at index-build time on a
case-insensitive key collision instead of silently discarding the second value.
3. getSyncedCapability() (modelsDevSync.ts) — ran a per-model SQLite SELECT on cold
cache instead of self-warming the whole-table cache; no caller in the
/v1/models build path ever primed it, so a cold rebuild ran one SQLite
round-trip per model per call site. Now self-warms via the existing bulk
getSyncedCapabilities() on first miss. Measured as the dominant remaining cost
after fixes 1-2 (~70% of a full catalog sweep).
4. getCanonicalModelSpecId() (shared/constants/modelSpecs.ts) — up to 3 separate
linear scans over the static MODEL_SPECS table per call (exact ci, alias ci,
prefix). Replaced with a lazy, lowercase-key index built once (MODEL_SPECS never
changes at runtime); prefix-match iteration order preserved exactly so
resolution outcomes are unchanged.
5. getStaticSpecCanonicalModelId() (modelCapabilities.ts) — duplicated the same
exact+alias scan as (4) in a second, separate rescan. Now reuses the shared
index via a new exported helper (findModelSpecIdByExactOrAlias) instead of
maintaining a second cache over the same static table.
reverseModelsDevProviders() (modelCapabilities.ts) — rescanned
Object.entries(MODELS_DEV_PROVIDER_MAP) (also static) on every call; memoized
by provider key. Result is frozen (readonly) since it is now shared across
calls instead of freshly allocated each time.
6. resolveModelAlias() (shared/constants/modelSpecs.ts) — rescanned
Object.entries(MODEL_SPECS) unconditionally once per model (verified 1:1 call
ratio, no short-circuit). Case-sensitive exact match (Array.includes(), no
.toLowerCase()) — uses a dedicated exact-match index, deliberately not the
case-insensitive alias index from fix 4/5 (would silently broaden matches).
Measured on a 1940-pair real-catalog sample (static PROVIDER_MODELS registry):
cold sweep 828ms -> 356ms after fixes 3-5 on top of 1-2, extrapolating to roughly
1s on the real ~6091-model catalog, down from the original 41-54s freeze.
Complementary to the stale-serve fix in #8801 (upstream) — neither alone
eliminates the freeze.
Tests: call-count regression guards for every fix (DB prepare / Object.entries /
Object.keys call counts staying constant instead of scaling with iteration count),
plus correctness coverage for case-insensitive/case-sensitive resolution. All
pre-existing consumer suites re-verified passing (96 tests total across 19 files).
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(token-refresh): exempt transient errors from exponential backoff
A refresh that failed on a network timeout was treated exactly like one
that failed on a revoked token: the streak incremented and the circuit
backed off exponentially, up to four hours. A brief upstream blip could
therefore park a healthy account for the rest of the day.
Transient failures now take a flat two-minute retry window instead of
advancing the streak. Classification checks structured signals first
(err.name for AbortError/TimeoutError, then err.code and err.cause.code)
and only falls back to matching the message text, so it does not depend
on upstream wording. Everything else keeps the existing exponential path.
Two properties worth preserving on sight:
- A transient failure never shortens a longer permanent backoff. The
new window is only adopted when the existing one is not already
further out.
- testStatus is preserved on both paths, so a connection whose access
token is still valid keeps serving requests while its refresh
retries.
Only a successful refresh clears the circuit. A successful request does
not, because requests do not refresh tokens.
* chore(quality): rebaseline file-size for tokenHealthCheck.ts
src/lib/tokenHealthCheck.ts lands at 1021 lines, above the 1000 cap. The
file consolidates token-refresh health checking that was previously split
across auth.ts and tokenRefresh.ts, and the refresh circuit state machine
does not divide cleanly, so splitting it to satisfy the cap would cost
more than it buys.
Scoped to this file only. Baseline entries for files this branch does not
touch are left at their upstream values.
Image and video payloads vary by provider and base64 encoding adds substantial overhead. Exempt media routes from OmniRoute's global request-body cap so provider-specific validation determines whether a request is too large. Keep finite body limits for non-media routes and cover both header and streamed-body admission paths.