mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
0bd7ceadf8dfd47aa48f89beb26185a9bc584385
849 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
052cab3d46 |
fix(providers): complete OpenCode Go effort alias exposure (#8610)
* fix(providers): complete OpenCode Go effort aliases * chore: number OpenCode Go changelog fragment |
||
|
|
ea678daad6 |
fix(auth): tag internal/loopback-origin failed logins in the audit log (#8606)
* fix(auth): tag internal/loopback-origin failed logins in the audit log Failed dashboard logins (`auth.login.failed`) are emitted only after a submitted, non-empty password fails verification, and the recorded IP is accurate. On a single-process deployment with no reverse proxy, a loopback / private source IP therefore means the attempt genuinely originated on the box or the LAN (someone browsing http://localhost and mistyping, or a browser autofill replaying a stale password) — but the Audit Log had no way to distinguish those from an external intrusion attempt, so they read as suspicious noise. Add `classifyIpScope()` to `ipUtils` (loopback / private / public / unknown, using bounded string-prefix checks — ReDoS-safe) and stamp `sourceScope` + `internalOrigin` onto the `auth.login.failed` audit metadata so the audit view can label internal-origin failures distinctly. No change to which events are written or to IP attribution. Closes #8336 * test(auth): assert the new origin tags on the failed-login audit event This PR tags failed logins with sourceScope/internalOrigin, but the pre-existing admin-audit-events assertion still deepEqual'd the old two-field metadata shape and broke. The request under test carries a public x-forwarded-for, so the expected tags are sourceScope: "public" and internalOrigin: false — the assertion stays strict, it just covers the fields this PR introduces. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
4d24c0c4de |
fix(api): prevent silent lost updates on concurrent settings writes (#7784) (#8587)
Add opt-in settingsRevision / If-Match optimistic concurrency so stale PATCH writers get 409 instead of silently clobbering map settings. |
||
|
|
644da2924b |
fix(sse): hide synthetic OpenAI startup reasoning (#8729)
* fix(reasoning): sanitize Kimi K3 think tags * ci: build patched OmniRoute image * fix(sse): hide synthetic OpenAI startup reasoning --------- Co-authored-by: rinseaid <rinseaid@rinseaid.net> |
||
|
|
1f3f43ea39 |
fix: hydration mismatch, duplicate React keys, and missing ponytail i18n (#8723)
- KimiSponsorBanner/RiskNoticeBanner: read the localStorage dismissal flag
via useSyncExternalStore (server snapshot = visible) instead of a
useState lazy initializer, so the first client render matches SSR
(which has no localStorage) instead of diverging on hydration.
- usage/analytics route: key the per-model aggregation map by model name
alone instead of `${provider}::${model}`, matching the table's one-row-
per-model display and eliminating duplicate `key={m.model}` rows when a
model is served through multiple provider connections/accounts.
- CommandPalette: look up existing section/subgroup by id across the whole
list instead of only comparing to the previous item, so sections whose
children interleave root items and groups (e.g. omni-proxy) don't produce
two subgroups sharing the same "_root" key.
- Add the missing `compressionOutputStyle.ponytail` label/description to
all 43 locale message files (present in the style catalog but never
added to any locale, causing a MISSING_MESSAGE crash).
Co-authored-by: Gillz <gillz@Gillzs-MacBook-Pro.local>
|
||
|
|
ba28e497fe |
fix(oauth): show GitLab Duo setup before authorize error (#8710)
* fix(oauth): show GitLab Duo setup before authorize error Surface the OAuth app registration and env-var recipe in the Add Connection modal before auto-starting authorize, and keep the same shared copy for catalog authHint and the authorize fallback (#8688). * fix(oauth): keep OAuthModal under file-size baseline for #8688 Extract waiting/error panels so the GitLab Duo setup step does not trip the Fast Quality Gates file-size ratchet, and update the retry Button regression guard for the extracted error step. |
||
|
|
10115000d8 |
perf(api): skip the full catalog build for quota-exclusive keys (#8771)
buildUnifiedModelsResponseCore builds the entire catalog first — every provider's models, the auto/* combos with their candidate scoring, the embedding/image/rerank/ audio/moderation/video/music registries, the OpenRouter catalog, custom models — and only at the very end, once the caller turns out to be scoped to a quota pool (allowedQuotas non-empty), discards all of it and returns the pool's qtSd/* combos instead. Measured on a 1 vCPU shared-quota host: ~1.2s of CPU per cold build to return 10 models, and 4.4-7.1s under contention. Claude Code's gateway model discovery aborts at 3s, so on a busy host the discovery silently falls back. Everything the quota path needs (`combos`, `timestamp`, `buildComboCatalogMetadata`) already exists before the expensive loops start, so resolve the key metadata there and return early. Extracts two helpers into ./catalogResponse so the short-circuit and the full build share one implementation instead of two copies that drift: - applyCatalogPostFilters — the chain that runs AFTER the API-key filter (configuredOnly, claude effort variants, no-thinking variants, cc-discovery mirrors, synced effort variants, dedupe) - finalizeCatalogResponse — enrichment + the codex `models: []` compatibility field + the response envelope That chain is not optional for the quota path: the cc-discovery mirrors are exactly what lets Claude Code see a quota pool's models, and a first draft of this change dropped them by returning before it. The regression is now pinned by a test that was verified to fail without the call. catalog.ts 1615 -> 1480 lines. TDD: tests/unit/quota-exclusive-catalog-short-circuit.test.ts uses the OpenRouter catalog fetch as the observable — it is part of the full build and reaches the network, so a request that performs it did the whole build. The quota request runs first, while OpenRouter's 24h cache is still cold, otherwise a cached second call would make the assertion vacuous. Red before (1 fetch), green after (0), and a normal key still fetches. Behaviour guards green: quota-exclusive-catalog-4806 (2), quota-key-models-route (18), cc-discovery-aliases-catalog (9), catalog-helpers-extraction (12), cc-compatible-model-catalog (1), instrumentation-warm-catalog-cache (3), catalog-pricing-surface-8018 (3), apikeypolicy-quota-only (6). typecheck, lint and the file-size gate clean. |
||
|
|
b59127ea95 |
Train 1D: merge via --admin on .113 validation
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass. |
||
|
|
cd9a631464 |
feat: Claude Code discovery aliases (surface non-Claude models in the /model picker) (#8666)
* feat(db): cc discovery alias gate storage + EXPOSE_CC_DISCOVERY_ALIASES flag
Adds the gate for claude/<provider>/<model> discovery-alias mirror ids on
the /v1/models catalog: a new runtime feature flag (env forces on and wins
over the dashboard DB override), per-provider and per-model "on"/"off"/null
overrides stored in key_value under the ccDiscoveryAliases namespace, and a
pure precedence resolver (model > provider > global). Catalog wiring is a
separate follow-up task; this only lands the gate + storage.
* feat(sse): synthesize claude/ discovery aliases for the model catalog
* feat(api): advertise cc discovery aliases on /v1/models behind the 3-level gate
* feat(sse): resolve claude/ discovery aliases on the request path
* fix(sse): import getComboByName from db/combos, not the localDb barrel
* fix(sse): cover custom-node prefixes and the Codex WS bridge in cc discovery alias resolution
* feat(dashboard): cc discovery alias toggles + flag-screen env warning
Adds the operator-facing UI/API layer for the Claude Code discovery-alias
gate (claude/<provider>/<model> mirror ids on /v1/models): REST endpoint
for provider/model overrides, a provider-detail card with 3-state
(inherit/on/off) toggles, an info button on the Claude Code tool card
linking to Feature Flags, and an env-source warning on the
EXPOSE_CC_DISCOVERY_ALIASES flag card when it's forced on via env.
* feat(api): cc discovery usage metrics
* fix(api): record cc alias metric in the production wrapper + atomic counter upsert
* docs: document cc discovery aliases (Claude Code guide + feature flag catalog)
* fix(sse): don't mirror built-in auto/* combos as discovery aliases (advertised-but-unroutable)
* i18n(vi): translate the discovery-alias strings instead of shipping placeholders
vi is the one locale with a strict "no internal missing markers" test, so the 17
__MISSING__ entries this branch added (the provider ccAlias panel, the info
button, the feature-flag description and the env warning) would have turned that
test red the moment the base itself was repaired. Translated, keeping every ICU
placeholder ({modelId}, {error}) and the literal claude/<provider>/<model> id
shape intact.
* chore(quality): raise the frozen caps this feature legitimately grows
catalog.ts 1615 -> 1639: the alias synthesis is wired into the catalog builder,
which is where the per-key-filtered list is assembled — the only place the mirror
entries can be appended after model hiding has been applied.
localDb.ts 808 -> 810: two re-export lines for the new ccDiscoveryAliases db
module, which is exactly what the "Adding a New DB Module" recipe prescribes.
* refactor(dashboard,api): keep the complexity ratchets flat
The feature added four cyclomatic violations and one cognitive one, which the
ratchets reject — the baseline only moves when a metric improves. Split the new
code instead:
- appendCcDiscoveryAliases: the four skip-guards become isMirrorableId().
- resolveCcDiscoveryAliasStripWith: alias parsing and gate resolution become
parseCcAliasTarget() and resolveGateFor(), replacing a chain of ternaries that
each re-tested isComboAlias.
- FeatureFlagCard: the env-precedence warning becomes its own component instead
of a conditional branch inside an already-large render.
- ProviderCcAliasSection: the loader moves to useCcAliasData(), and the override
list and add-row become ModelOverrideList / AddOverrideRow, bringing both
oversized functions back under the 80-line rule.
Behavior unchanged — the 74 discovery-alias tests pass untouched. Both ratchets
now sit exactly at baseline (2188 / 971).
|
||
|
|
6706d5ff7d |
fix(api): serve /v1/models stale-first and sanitize its error bodies (#8703)
* fix(api): serve the model catalog stale-first and sanitize its error body A client with a short discovery timeout (Claude Code allows 3s) hit a full catalog rebuild — 290 providers plus SQLite reads — every time the memoized entry expired, and got an empty model picker with no error. Serve an expired entry immediately and revalidate in the background, bounded by a staleness window so a permanently failing refresh cannot pin an old catalog forever. Only a cached 200 is eligible; a state change still drops the cache outright. The builder's catch block also returned the raw error message in the response body. Route it through the shared sanitizer (hard rule #12). * fix(api): reject a failed catalog refresh instead of resolving it stale catalogInFlight is shared with the cold path, so resolving the background refresh with the stale entry handed it to callers that had already aged past CATALOG_STALE_WHILE_REVALIDATE_MS — a stale 200 they were no longer entitled to, with a build failure disguised as success. The refresh now rejects; the stale path never awaits it (the rejection is pre-handled, so it can never surface as an unhandledRejection) and a cold-path caller that joins it gets the sanitized 500. A failed refresh still leaves the cached entry untouched. Also sanitize the core builder's own catch — that is the realistically reachable 500 for this endpoint, and it still returned the raw error message (hard rule #12); keep the cache-key format private to the module by having the two test hooks take the Request and derive the key themselves. * refactor(api): extract the model-catalog response cache into its own module The stale-while-revalidate work pushed catalog.ts from 1615 to 1745 lines, past its frozen size cap. Raising the cap on a file already flagged as too large is the wrong answer: the caching layer is a self-contained concern (coalescing, TTL memoization, staleness window, background refresh) that only needs a builder callback from the catalog module. catalogCache.ts now owns the maps, the cache key, the state-change invalidation, the header merge, the background refresh and the test hooks; catalog.ts keeps auth, the builder, and the error shape, and re-exports the hooks so the existing tests keep importing them from where they always did. Net effect: catalog.ts 1745 -> 1513, i.e. 102 lines below the cap it was frozen at, and the test-only surface no longer sits in the production catalog module. No behavior change — all 281 tests across every suite importing catalog.ts pass, including the #6408 one-builder-run guard. |
||
|
|
1f04333a19 |
merge: resolve conflicts for #7904 local corpus context (#8685)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com> |
||
|
|
d4b9ce6016 |
fix(kiro): harden auth flows, quota lookup, and model discovery (#8565)
* fix(kiro): fetch builder id quota without profile arn * fix(kiro): harden auth imports polling and model discovery * fix(kiro): preserve auth identity and OAuth polling semantics * docs(changelog): add Kiro auth and model discovery fix --------- Co-authored-by: Nguyễn Thanh Hà <nguyenha@Mac-mini-M4.local> Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> |
||
|
|
26d50f1913 |
feat(adobe-firefly): reference image attach + /v1/images/edits (follow-up #8006) (#8510)
* feat(adobe-firefly): reference image attach + /v1/images/edits (follow-up #8006)
Upload source images to Firefly storage (POST /v2/storage/image) and attach
them as referenceBlobs on generate-async, matching live firefly.adobe.com
captures (usage:general for nano multi-ref; usage:subject for gpt-image).
Also wire built-in adobe-firefly through OpenAI-compatible POST /v1/images/edits
(multipart or JSON data URLs, up to 4 refs) so Media edit-with-references
and Open WebUI image-edit hit the same path as image2image generate.
Unit suite: tests/unit/adobe-firefly.test.ts 41/41.
* test(api): add route-level coverage for Adobe Firefly /v1/images/edits + fix typecheck/file-size drift
Covers the referenceBlobs upload path, the 4-reference cap error, and the
credentials/rate-limit branches added to the /v1/images/edits route for
adobe-firefly (#8510). Also fixes a Buffer/BodyInit typecheck mismatch in
uploadAdobeFireflyImage and corrects the adobeFireflyClient.ts file-size
baseline entry to match the gate's actual LOC count (it counts the trailing
newline, so the frozen value is 2317, not 2316), plus a testFrozen entry for
adobe-firefly.test.ts's own +159 line growth from this PR. Moves the
handleAdobeFireflyImageGeneration re-export out of the middle of the import
block in imageGeneration.ts for readability.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(security): restore bounded JWT regex quantifiers dropped by edit-route refactor
The edits-route extraction (test commit
|
||
|
|
fae10668ba |
fix(api): expose responses-format models on all VS Code Ollama listing routes (#7587) (#8564)
isUsableChatModel() was copy-pasted into 5 vscode listing routes. PR #7012 widened only models/route.ts to accept api_format "responses"/"openai-responses" alongside "chat-completions"; the other 4 copies (token+raw api/tags and api/show) still rejected anything that wasn't literally "chat-completions", silently dropping Codex-discovery-synced GPT models (apiFormat "responses") from the Ollama-compatible /api/tags endpoint VS Code's "Ollama" provider import flow actually calls. Extracted the predicate into a single shared module (vscode/[token]/usableChatModel.ts) imported by all 5 routes so this one-fixed-four-left-behind drift cannot recur. Co-authored-by: ikelvingo <im.kelvinwong@gmail.com> |
||
|
|
a784c52d34 |
fix(api): warn (never reject) when a combo name shadows a real model id (#8530) (#8563)
POST /api/combos and PUT /api/combos/[id] had zero validation or observability when a combo name collided with a real model id, and sseModelService.getComboForModel() always resolves the combo first. That combo-first precedence is not a bug: #6940 documents a combo named after a bare model id (e.g. `gpt-5.5`) as the supported mechanism for per-model provider fallback, reusing the #3227/#3233 machinery and covered by tests/unit/responses-combo-resolution-3227.test.ts and tests/unit/combo-name-codex-responses-rewrite.test.ts. Hard-rejecting a colliding name (as #8530's literal acceptance criteria requested) would regress that documented workflow. Instead, both routes now attach a non-blocking `warning` field (`COMBO_NAME_SHADOWS_MODEL`) to the create/rename response when the name collides with a real model id, and a new boot-time scan (scanComboModelNameCollisionsAtBoot in src/instrumentation-node.ts) logs a startup warning enumerating existing collisions — so an operator who hits this by accident has a signal, while the #6940-sanctioned pattern keeps working exactly as before. New tests/unit/combo-model-name-collision-8530.test.ts proves both: the sanctioned shapes (create/rename to a colliding name) still return 201/200 with the warning attached, and non-colliding names get no warning field at all. Co-authored-by: ikelvingo <im.kelvinwong@gmail.com> |
||
|
|
7b3ef477f3 |
fix(providers): persist runtime-discovered Antigravity projectId to the connection (#8491) (#8562)
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com> |
||
|
|
61277813b2 |
fix(backend): compute AgentBridge diagnose DNS check per-agent instead of hard-coded Antigravity (#8466) (#8502)
getMitmStatus() hard-wired dnsConfigured to a single Antigravity hostname regex regardless of which agent was being diagnosed, and the diagnose route never accepted an agentId to check against. Add checkDNSEntryForAgent() reusing resolveHostsForAgent()'s existing per-target host resolution, thread an optional agentId through getMitmStatus(), and have the diagnose route parse ?agentId= and pass it through. Callers that omit agentId keep the legacy Antigravity-only behavior unchanged. Co-authored-by: ikelvingo <im.kelvinwong@gmail.com> |
||
|
|
3432579eb0 | fix(oauth): add devin-cli and agy entries to OAUTH_TEST_CONFIG (#8427) | ||
|
|
5d9ace6778 | feat(providers): map upstream reasoning-level metadata in openai-compatible discovery (#8347) (#8363) | ||
|
|
53a91b3df8 |
feat(api): quota-aware fallback routing for web-fetch providers (#8297) (#8335)
Mirror the search/route.ts pattern for /v1/web/fetch: skip rate-limited stubs instead of letting them short-circuit auto-select, walk the fixed-priority pool (fill-first) with a request-time fallback on retryable/quota upstream statuses (429 always; 402/403 for Firecrawl/Tavily/TinyFish quota-style tiers), and return a proper 429 (with Retry-After) when the whole pool is exhausted instead of a generic 400. Explicit-provider requests never silently fall back. |
||
|
|
544ae2d3da | fix(api): accept a missing status query-param on GET /api/plugins instead of rejecting null with Invalid status value (#8374) (#8399) | ||
|
|
9b7bd6e5d3 | fix(api): stop leaking the internal provider UUID in /v1/models and honor the configured prefix (#8327) (#8361) | ||
|
|
fa46d93941 | fix(api): accept the current compatible-provider connection id scheme in the models test route (#8326) (#8359) | ||
|
|
07067841b5 |
feat(combo): enforce provider and model family invariants (#8304)
* feat(combo): enforce provider and model family invariants Closes #8279 Co-Authored-By: Ravi Tharuma <ravitharuma@users.noreply.github.com> * fix(combo): complete invariant enforcement paths Map invariant failures to structured API errors, correct target diagnostics, and validate restored combos inside the existing migration transaction. Co-Authored-By: Ravi Tharuma <noreply@github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Ravi Tharuma <noreply@github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
8a2a3d48b0 |
perf(api): singleflight version lookups (#8278) (#8301)
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
08a21bcf27 |
fix(backend): add structure-aware chat admission (#8296)
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
e9f297021e |
fix(usage): correct token/request counting for 30D/90D/YTD/ALL ranges (#7300)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168)
* fix(ci): add the auto-enqueue pull_request_rule to the Mergify config (queue_conditions alone are eligibility-only) (#7179)
* fix(ci): migrate Mergify auto-enqueue to merge_protections_settings.auto_merge_conditions (rules-based path is EOL 2026-07-16) (#7216)
* fix(ci): drop Mergify batch settings (batching is a paid-tier feature; free plan queue is serial) (#7220)
* fix(ci): merge queue tolerates the advisory dast-smoke failure (its GH-hosted build hang dequeued every attempt) (#7225)
* fix(usage): correct token/request counting for 30D/90D/YTD/ALL ranges
Two bugs caused incorrect usage statistics for date ranges beyond the
raw data retention window:
1. Cutoff mismatch: the analytics route computed rawCutoffDate from
aggregation.rawDataRetentionDays (migration 046 seeds =7) while
cleanupUsageHistory rolls up and deletes at retention.usageHistory
(=30). The window [day-30, day-7) existed in usage_history but was
excluded from BOTH UNION legs — raw leg floored at day-7, aggregated
leg ended at day-7 — producing undercounted token sums for 30D,
90D, YTD, and ALL ranges.
Fix: use dbSettings.retention.usageHistory for the raw cutoff in
both route.ts and getRawDataCutoffDate() (aggregateHistory.ts),
matching the actual cleanup boundary.
2. Request undercount: COUNT(*) on the unified source counted each
daily_usage_summary row as 1, not total_requests. A day with 50
rolled-up requests counted as 1.
Fix: add a 'requests' column to both UNION legs (raw: 1, aggregated:
total_requests), change COUNT(*) to SUM(requests) in 6 query
functions, and change successfulRequests from
SUM(CASE WHEN success=1 THEN 1 ELSE 0 END) to
SUM(CASE WHEN success=1 THEN requests ELSE 0 END). Also set
agg leg latency_ms to NULL so AVG(latency_ms) is not skewed.
Tests: 33/33 source-level tests pass (db-usageanalytics-split.test.ts),
verifying 'requests' column presence and SUM(requests) usage in all
affected queries. DB-level integration test added to
usage-analytics.test.ts (requires node + better-sqlite3).
* fix(ci): green CI reds on #7300 — file-size ratchet, stale test fixture, shallow-checkout selfref test
- src/app/api/usage/analytics/route.ts: trim the new comment to keep the file
at the frozen file-size baseline (942 lines) after the retention.usageHistory
cutoff fix — no logic change.
- tests/unit/usage-analytics-route.test.ts: the pre-existing "does not
double-count raw and aggregated rows" test hardcoded a 30-day cutoff that
matched the OLD (buggy) aggregation.rawDataRetentionDays default. Now that
the raw/aggregated boundary correctly uses retention.usageHistory (365 days
by default, matching cleanupUsageHistory's actual rollup/delete boundary),
the fixture's synthetic "old" row was within the raw window and got
excluded from the aggregated leg. Read the real retention setting instead
of hardcoding 30 so the fixture reflects the corrected boundary. Same
assertions (still expects no double-counting, totalRequests=2,
totalTokens=185) — only the fixture dates change.
- tests/unit/check-test-masking-selfref-6634.test.ts: tolerate the shallow/
single-ref checkout used by GitHub-hosted Unit Tests runners (no local
origin/main ref) by fetching it on demand and skipping (never failing) when
unreachable offline. Matches the fix already applied on another branch
(2e42b8efc/#7174) for the same root cause, not yet on main.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* test(ci): make the #6634 selfref test checkout-independent (read the real file, no git ref)
The previous on-demand `git fetch origin main` + t.skip() fallback cleared the
shallow-checkout failure but tripped the PR Test Policy's test-masking gate
(a new .skip counts as a silenced assert — correctly so).
Drop the git dependency entirely instead: read the REAL current source of
tests/unit/check-test-masking.test.ts from disk (so the actual #6404 fixture
literals stay under test) and model the pre-#6404 state with an empty base,
which maximizes headTaut - baseTaut — the strictest input for the exclusion
this test asserts. No skip, no weakened assertion, same deepEqual guarantee.
Verified non-vacuous: neutralizing SELF_TEST_FIXTURE_RE in
scripts/check/check-test-masking.mjs makes this test fail; restoring it makes
it pass.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* test(ci): make the #6634 selfref guard hermetic — main's copy hard-fails every PR (#7341)
main's copy of this test still does git I/O inside a unit test:
const baseSrc = git(['show', 'origin/main:' + FILE]);
Runners check out a shallow single ref, so origin/main does not resolve and the
test dies with 'fatal: invalid object name origin/main'. Every PR into main
fails Unit Tests (7/8) on it — today that is #7313, #7315, #7316, #7334, #7336
and #7337, six PRs red on a defect none of them introduced. #7313 has no other
red at all.
release/v3.8.49 already carries a fix (
|
||
|
|
869d08ff8b |
Add Alibaba-family media model support (#8266)
* Add Alibaba-family media models * chore(quality): rebaseline imageRegistry+cognitive for #8266 media own-growth --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
2a865aaaa7 |
feat(settings): configurable model catalog cache TTL (#8219)
* feat(settings): configurable model catalog cache TTL Add modelCatalogCacheTtlMs to DatabaseSettings with default 1500ms. Extend cache-config API route to accept the new field. Replace hardcoded catalog cache TTL with dynamic settings value. Add 'Cache' settings tab at /dashboard/settings/cache with sidebar entry, i18n keys, header description, and legacy route redirect. * feat(combo): add model connection filter toggle to ModelSelectModal Adds a 'Show configured only' checkbox below the search bar in ModelSelectModal that filters each provider group's models through hasEligibleConnectionForModel. Toggle state persists in localStorage. - Import hasEligibleConnectionForModel from domain/connectionModelRules - showConfiguredOnly state + localStorage persistence - connectionFilteredGroups memo layered on filteredGroups - Renders both provider section and empty state from connectionFilteredGroups * test(combo): add connection filter toggle tests for ModelSelectModal Three test cases: (1) hide excluded models when toggle on, (2) show empty state when all models excluded, (3) drop provider group when all its models excluded. All 3 tests pass. * fix(settings): correct cache-config route import + add route/tab coverage The cache-config route imported get/update helpers from a nonexistent module (@/lib/localDb/databaseSettings) and called an undefined updateSettings() in PUT, crashing every request. Import the real databaseSettings module (matching the sibling database/route.ts convention) and call updateDatabaseSettings(); idempotencyWindowMs is routed through the flat @/lib/db/settings module instead, since that is where it is actually read at runtime (idempotencyLayer.ts, runtimeSettings.ts) — it was never part of the databaseSettings "cache" section type. Also fixes a dashboard-typecheck regression in ModelSelectModal.tsx: the new connection-filter toggle called hasEligibleConnectionForModel() with activeProviders entries typed too narrowly to include providerSpecificData, which real connection objects carry at runtime. Adds: - tests/unit/cache-config-route-8219.test.ts: GET/PUT resolve without crashing, modelCatalogCacheTtlMs and idempotencyWindowMs round-trip. - tests/unit/ui/cache-settings-tab-bounds-8219.test.tsx: CacheSettingsTab min/max TTL bounds (100ms/60000ms) gate the Save button and surface a validation message; in-bounds values PUT correctly. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): rebaseline sections.ts for #8219 own-growth --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
81ade9e122 |
feat(providers): add Typhoon (Thailand) and Inception Mercury diffusion LLM (#8170)
* feat(providers): add Typhoon and Inception Mercury API-key providers Two OpenAI-compatible API-key providers, each verified against a live endpoint smoke test with a negative control before registration: an unknown route answers 404 while /v1/chat/completions answers 401, which rules out gateways that reply identically to every path. - typhoon (SCB 10X, Thailand): first Thai-first provider in the catalog. /v1/models answers 200 unauthenticated and serves exactly one chat model, typhoon-v2.5-30b-a3b-instruct (128K ctx). The docs also list typhoon-v2.1-12b-instruct, but the live endpoint no longer serves it, so it is deliberately not registered. The typhoon-ocr* and typhoon-asr* entries are OCR and speech models, not chat, and are omitted. - inception (Inception Labs): first diffusion LLM (dLLM) in the catalog. mercury-2 has a 128K context, 50K max output, and supports tools, json_mode and structured outputs. The mercury-coder models advertised on the vendor blog are no longer served by the live endpoint and are therefore not registered. Both expose a working /v1/models catalog, so they are added to NAMED_OPENAI_STYLE_PROVIDERS for discovery and key validation. Free-tier metadata is claimed only where it is documented and durable: Typhoon issues a free API key rate-limited to 5 req/s and 200 req/m, and Inception grants 10M tokens on signup with no card, so both are registered with hasFree: true. Provider count moves from 280 to 282; README/AGENTS/CLAUDE counters were stale at 278 and are resynced against docs/reference/PROVIDER_REFERENCE.md. * fix(8170): union frontier-labs Inception+Writer, regen ref+golden * fix(8170): close inception object + regen ref/golden --------- Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
7ca821aeee |
feat(providers): add Sarvam AI, Writer Palmyra and PLaMo API-key providers (#8161)
* feat(providers): add Sarvam AI, Writer Palmyra and PLaMo API-key providers Three OpenAI-compatible API-key providers, each verified against a live endpoint smoke test before registration: - sarvam (India): /v1/models answers 200 unauthenticated and lists sarvam-105b (128K ctx) and sarvam-30b (64K ctx). The older sarvam-m is discontinued upstream and is deliberately not registered. - writer (Palmyra): api.writer.com exposes the OpenAI alias /v1/chat/completions alongside its native /v1/chat — confirmed with a negative control, since an unknown route answers 404 'endpoint not available via API gateway' while /v1/chat/completions answers 401. Registers palmyra-x5 (1M ctx) and palmyra-x4 (128K ctx); the medical/financial/creative/vision variants are deprecated upstream and are omitted. - plamo (Preferred Networks, Japan): only plamo-3.0-prime (262K ctx) is registered. plamo-3.0-prime-beta is discontinued on 2026-07-31 and plamo-2.2-prime on 2026-09-30, so neither is worth wiring up. Free-tier metadata is claimed only where it is documented and durable: Sarvam ships a permanent signup credit, while PLaMo's 10M-token grant is a campaign that expires on 2026-07-31 and Writer documents no free tier, so both are registered with hasFree: false. * regen golden+ref --------- Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
6ba2260178 |
feat(providers): add CLOVA Studio, InternLM and Ant Ling API-key providers (#8077)
* feat(providers): add CLOVA Studio, InternLM and Ant Ling API-key providers Adds three OpenAI-compatible frontier-lab providers, closing regional gaps in the catalog (Korea had none; the Shanghai AI Lab and Ant Group families were both missing). - clova-studio: Naver HyperCLOVA X (HCX-007 reasoning, HCX-005 multimodal) on the current clovastudio.stream.ntruss.com host. The legacy clovastudio.apigw.ntruss.com endpoint is being deprecated and is not used. - internlm: Shanghai AI Lab Intern-S1 family (intern-s1-pro is a 1T MoE). Ships a free monthly quota, so it is flagged hasFree. - ant-ling: Ant Group / inclusionAI Ling-2.6-1T and Ring-2.6-1T. All three endpoints were smoke-tested: each returns HTTP 401 on <baseUrl>/models (endpoint live, awaiting auth) and resolves against public DNS. All three are registered for live model discovery, so their catalogs refresh from upstream. Known limitation: the ant-ling model ids are best-effort from public docs and are NOT verified against a live /v1/models response, which requires an API key. This is recorded in the registry comment and in the provider authHint so it is visible to operators rather than silently assumed. Its baseUrl is likewise not published in the public docs and was found by smoke test. Two AI SUTRA was evaluated for this batch and deliberately excluded: its documented endpoint api.two.ai does not resolve in public DNS (ENOTFOUND against 1.1.1.1 and 8.8.8.8, with www.two.ai resolving as control). The translate-path golden snapshot is regenerated; the change is additive only (209 -> 212 keys, exactly the three new providers, none removed or altered). * feat(providers): verify ant-ling against official docs, add Ling-2.6-flash and free tier The ant-ling entry was added with model ids marked best-effort because they could not be checked without an API key. Ant Ling's own documentation turns out to publish enough to verify them, so the uncertainty is now resolved: - The quickstart sample uses base_url "https://api.ant-ling.com/v1" with model "Ling-2.6-1T", confirming both the endpoint and the exact casing. - The pricing page bills exactly three models over the API, so Ling-2.6-flash was missing from the catalog and is added. - Each account gets 500,000 free tokens per day (resets 02:00 UTC+8, no rollover), so the provider is flagged hasFree with a freeNote. The Ming family (Ming-Flash-Omni, Ming-Light) is deliberately left out: it is documented as open-source / Ling Studio only and does not appear on the pricing page, so it is not served over this chat-completions API. That reasoning is recorded in the registry so it is not re-litigated later. The authHint no longer claims the ids are unverified, and now points at the API console (https://chat.ant-ling.com/open) where keys are actually created. Same correction applied to the en, pt-BR and vi message catalogs. * docs: sync provider counts to 283 and regenerate the provider reference --------- Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
b640b69078 |
feat(codex): support reference image edits (#8122)
* feat(codex): support reference image edits * docs(changelog): add Codex edit fragment * fix(codex): harden image edit admission * fix(security): redact image error credentials * fix(security): close error redaction bypasses * feat(codex): support multiple image references * fix(codex): preserve reference candidate semantics * chore(quality): rebaseline image-generation-handler.test.ts for #8122 codex image edits own-growth Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
cc17b304ab |
fix(sse): Gemini TPM/RPD quota classification + combo cooldown-wait resilience (#8213)
* fix(sse): Gemini TPM classification, combo-cooldown-wait for auto/quota-share, and target-timeout floor
Gemini TPM/RPM 429s were misclassified as QUOTA_EXHAUSTED because
sanitizeErrorMessage() truncates to the first line, hiding Google's
metric name and retry hint on lines 2-3. Added a rawMessage field
(internal-only, never reaches the client) and classifyGeminiQuotaMetricFromText()
to classify from the untruncated text, reordered ahead of the generic
credits/daily-quota checks.
Widened comboCooldownWaitEnabled (wait out a short transient cooldown
instead of crystallizing a 429/503) from quota-share-only to also cover
auto-strategy combos, and raised the wait ceiling to 65s/130s-budget/90s-cap
to match Gemini's ~60s TPM/RPM windows.
The per-target timeout (DEFAULT_COMBO_TARGET_TIMEOUT_MS, 120s) was shorter
than the new 130s cooldown-wait budget, so a target could get cut off
mid-wait with a synthetic 524 instead of completing the retry. Added
resolveComboTargetTimeoutMsForCombo()/isComboCooldownWaitEligible() in
comboConfig.ts to raise the per-target floor to budgetMs+buffer only for
wait-eligible strategies (auto/quota-share), verified live: a 12-request
concurrent burst against a TPM-exhausted combo went from 2/12 succeeding
(10 x 524) to 12/12 succeeding with zero 503/524.
Also: liveGeminiShared.ts's sendAndValidate now fails fast on a 503
instead of retrying past it, and the health dashboard + request logger
surface TPM stats alongside RPM/RPD.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo-exhausted rejection logs now capture request body + attempted models
recordRejectedRequestUsage() (the fast path for combo requests that never
reach handleChatCore, e.g. all targets locked by resilience cooldown)
hardcoded provider: "-" and never passed a request body to saveCallLog(),
so /dashboard/logs entries for these failures were nearly useless for
debugging: no way to see the client's request or which models were tried.
- recordRejectedRequestUsage() now accepts requestBody and persists it
through the existing saveCallLog() artifact mechanism (same path
handleChatCore's own logging uses).
- Added summarizeComboAttemptedModels(), which reads the combo's own model
list (always available, unlike the response's combo-diagnostics headers —
a model-level resilience-lockout skip never touches the
exhaustedProviders/exhaustedConnections sets those headers are built
from) to populate a real "provider" value instead of "-".
- Wired both into the call site in src/sse/handlers/chat.ts.
NOTE: unrelated to the Gemini TPM/combo-cooldown-wait fix on this branch —
landed here per operator request, to be split into its own branch/PR.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* feat(sse): synthetic streaming keep-alive event + 5-minute Gemini cooldown-wait ceiling
Many clients enforce a first-SSE-byte timeout, which made it unsafe to wait
out a longer upstream rate-limit cooldown on a streaming request — the
client would abandon the connection before any bytes arrived. This landed
in two parts:
1. Synthetic startup "thinking" event (OpenAI chat/completions format):
the already-existing withEarlyStreamKeepalive wrapper (open-sse/utils/
earlyStreamKeepalive.ts, wired into /v1/chat/completions, /v1/messages,
/v1/responses since #2544) opens the SSE stream immediately once a
request runs past its threshold, but only ever sent empty/no-op
keepalive frames. Added a `startupFrame` option (defaults to
`keepaliveFrame` — zero behavior change unless a route opts in) so the
very first frame can carry real content instead. Wired
OPENAI_STARTUP_THINKING_FRAME (a reasoning_content delta: "OmniRoute:
got request, sending to provider") into /v1/chat/completions only —
Claude Messages and Responses API formats both require a preceding
envelope event (message_start / response.created) that a synthetic
pre-dispatch frame can't safely fabricate without risking a duplicate
envelope once the real stream arrives, so those two routes keep their
existing (safe, proven) keepalive frames unchanged.
2. Raised the "wait out a known cooldown, then retry" ceiling to 5 minutes
for both retry mechanisms, now that a client-side first-byte timeout is
no longer a risk on the (opted-in) route:
- comboCooldownWait (auto/quota-share combos, open-sse/services/combo.ts):
maxWaitMs hard clamp raised 90s -> 300s (src/lib/resilience/settings/
normalize.ts); defaults raised to maxWaitMs:90s/maxAttempts:5/
budgetMs:300s. comboConfig.ts's resolveComboTargetTimeoutMsForCombo
already derives the per-target timeout floor from budgetMs, so it
tracks the new ceiling with no further changes.
- waitForCooldown (direct, non-combo model requests, src/sse/handlers/
chat.ts): this mechanism had NO cumulative cap before — only a
per-wait cap (maxRetryWaitMs) and a retry count (maxRetries), so
maxRetries x maxRetryWaitMs could exceed 5 minutes with no ceiling.
Added a budgetMs field (mirrors comboCooldownWait) to
WaitForCooldownSettings/CooldownAwareRetrySettings, threaded a
requestRetryBudgetLeftMs tracker through chat.ts's requestAttemptLoop
(mirrors combo.ts's comboCooldownBudgetLeftMs), and made
getCooldownAwareRetryDecision refuse to wait once the cumulative
budget is exhausted even if the single wait is under maxRetryWaitMs.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): extend the synthetic keep-alive thinking event to /v1/responses
Live incident (OpenClaw, log id 1784407081908-cbc24f): a /v1/responses
request to gemini/gemma-4-31b-it took 56s to produce a first byte and the
client disconnected (499 request_signal_aborted) — the same client-first-byte-
timeout problem the previous commit fixed for /v1/chat/completions, but
/v1/responses only had the generic bare-comment keepalive (no content), so it
wasn't covered.
Added RESPONSES_STARTUP_THINKING_FRAME: a self-contained synthetic reasoning
item (response.output_item.added -> reasoning_summary_part.added ->
reasoning_summary_text.delta -> reasoning_summary_part.done), opened AND
closed within this one frame rather than left dangling — it never carries a
response_id, so it can't collide with the real upstream response's own
independent response.created lifecycle that follows. Mirrors the abbreviated
delta+part.done close pattern open-sse/utils/stream.ts's own
emitSyntheticResponsesReasoningSummary already uses for real mid-stream
reasoning content.
Wired into src/app/api/v1/responses/route.ts via the startupFrame option
added in the previous commit.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo cooldown-wait vars reset every setTry, crystallizing a bogus 503 instead of waiting
Live incident (log id 1784416706646-51): a request to the "default" combo
(strategy=auto, maxSetRetries=3) hit a real Gemini TPM 429 on both gemma-4
targets, correctly classified as a short 40s rate_limit lockout — then
crystallized a 503 "all upstream accounts are inactive" in 6.9s instead of
ever reaching the cooldown-aware wait.
Root cause: `lastError`/`earliestRetryAfter`/`lastStatus` were declared with
`let` INSIDE the `for (setTry...)` loop body, so they reset to null at the
start of every set-try. When both targets lock out on setTry 0, every
subsequent setTry (1..maxSetRetries) pre-skips both targets via the
isModelLocked check with no real dispatch — so on the FINAL setTry (the only
one whose values the post-loop decision reads, since it's gated behind
`if (setTry < maxSetRetries) continue`), lastStatus was null, hitting the
"!lastStatus" branch (ALL_ACCOUNTS_INACTIVE 503) and completely bypassing the
comboCooldownWaitEnabled / earliestRetryAfter wait logic — even though a
real 429 with a known ~40s retry-after WAS observed on setTry 0.
This bug predates today's Gemini TPM work (any combo with maxSetRetries > 0
whose targets all lock out on the first pass was affected) but was masked in
existing tests: the "auto strategy (2 models...)" regression test uses
maxSetRetries: 0, so it only ever runs ONE setTry iteration and never
exercises the reset-on-retry path. It also explains why the dedicated
12-concurrent-request burst test passed cleanly — with concurrent requests,
timing variance meant some request's FINAL setTry iteration still had a live
target to dispatch to, giving lastStatus/earliestRetryAfter fresh data. A
single isolated request has no such luck.
Fix: hoist lastError/earliestRetryAfter/lastStatus to just inside
dispatchWithCooldownRetry, before the setTry loop, so they persist across
set-tries (still reset fresh on each recursive dispatchWithCooldownRetry()
call after a wait, which is correct). recordedAttempts/fallbackCount/
exhaustedProviders etc. are intentionally left per-iteration (unrelated to
this bug).
New regression test in tests/unit/combo-quota-share-cooldown-wait.test.ts
reproduces the exact live scenario (2 targets, both lock out on setTry 0,
maxSetRetries: 3) — confirmed red (503) against the pre-fix code, green
(200, waits and retries) against the fix.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* test(sse): extend live Gemini workload to Responses API + add large-context TPM test
Two additions to the live Gemini test suite, both live-verified against the
dev instance:
1. sendAndValidate() (tests/integration/liveGeminiShared.ts) now accepts an
apiFormat: "chat" | "responses" parameter, building the Responses-API
request shape (input array, max_output_tokens) and parsing its SSE events
(response.output_text.delta / response.reasoning_summary_text.delta /
response.completed) via the new readResponsesSSEStream(). Wired into two
new tests in live-gemini-workload.test.ts ([30]/[31]), mirroring the
existing Chat Completions streaming coverage. Verified live: 24/25 + 5/5
payloads succeeded end-to-end through the new code path (the one failure
was a ~300s test-client fetch timeout unrelated to the Responses API code
itself — a separate, not-yet-addressed test-harness limitation).
2. genHugeContextMessage() builds a single message large enough (~4
chars/token estimate) to approach or exceed Gemini's free-tier TPM ceiling
(16000 input tokens/min for gemma-4) by itself. Every other prompt
generator in this file tops out around 1-2k tokens — nowhere near that
ceiling — so none of the existing workload tests ever exercised a REAL TPM
429, only RPM-style rate limiting. tests/integration/gemini-large-context-tpm.test.ts
sends two ~12-13k-token requests back-to-back (comfortably exceeding
16000/min together) to exercise the full path against production Gemini:
TPM classification, the comboCooldownWait retry, and the synthetic
keep-alive frame on a genuinely slow request.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): abandoned combo target dispatch now observes its own per-target timeout, fixing a permanent "pending" dashboard leak
Live incident (dashboard log id 1784418258231-14961a, reported as "an ongoing
request even though there's already a 200"): a combo target dispatch
abandoned by comboTargetTimeoutMs (open-sse/services/combo/targetTimeoutRunner.ts)
left a permanent phantom "pending" entry in the dashboard, even after the
overall combo request had already succeeded via a different retry.
Root cause: chatCore.ts's createStreamController — and everything downstream
that depends on it (withRateLimit's Promise.race against Bottleneck,
acquireAccountSemaphore) — only ever watches clientRawRequest.signal, which
is the ORIGINAL client's request signal (set once via buildClientRawRequest
and reused unchanged across every target dispatch in a combo). It has no
connection to targetTimeoutRunner.ts's OWN AbortController
(target.modelAbortSignal), which is what actually fires when
comboTargetTimeoutMs (300s) elapses. src/sse/handlers/chat.ts's
handleSingleModel bridge between combo.ts and handleSingleModelChat received
`target.modelAbortSignal` but silently dropped it — never forwarded it
anywhere. So when a target got abandoned (e.g. stuck inside a wedged
Bottleneck rate-limiter queue, see the WEDGED force-reset log line from the
same incident), its per-target timeout fired and let the COMBO move on and
retry successfully elsewhere — but the abandoned dispatch's own promise
chain never learned it had been superseded, so it hung forever waiting on a
signal that was never going to fire, and trackPendingRequest(false) (the
finalize call) never ran.
Fix: thread target.modelAbortSignal through as a new modelAbortSignal
runtimeOption, and merge it into clientRawRequest.signal (via the existing
mergeAbortSignals helper from open-sse/executors/base.ts) right before
dispatch, so an abandoned target's own promise chain now observes its abort
and can reach its cleanup path — new resolveDispatchClientRawRequest() makes
this mechanically testable in isolation.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
* fix(sse): combo cooldown-wait state recording, rate-limit wedge recovery, OpenAI-format SSE error frames
Five related fixes surfaced by live incidents (dashboard log ids 1784457764961-73,
1784465227489-a2cbc0, 1784504040241-6f8b9a) while validating the Gemini TPM/cooldown-wait
work on this branch against real OpenClaw traffic:
- combo.ts: the model-lockout bail-out branches in dispatchWithCooldownRetry never
recorded lastStatus, so once every target in a set hit an existing lockout the final
check crystallized a bogus ALL_ACCOUNTS_INACTIVE 503 instead of reaching the
cooldown-wait decision, even with a real 429 + short retry-after observed.
- combo.ts/combo/types.ts: the "all credentials cooling down" pre-dispatch rejection
(buildModelCooldownBody) nests its retry hint as error.retry_after/reset_seconds, not
the top-level retryAfter every other 429 shape uses — combo's extraction only read the
latter, so earliestRetryAfter stayed null for this shape even after lastStatus was fixed.
- rateLimitManager.ts: the wedge-recovery watchdog used disconnect(), which releases the
heartbeat timer but never rejects jobs already QUEUED on that instance — orphaned
dispatches hung until the outer ~300s per-target timeout, well past real clients'
patience. Switched to stop({ dropWaitingJobs: true }), safe because the wedge condition
already requires RUNNING===0 && EXECUTING===0.
- earlyStreamKeepalive.ts: the in-band error frame emitted after committing to a 200 SSE
stream was hardcoded to Anthropic's `event: error` convention for every route, including
the OpenAI-format ones (/v1/chat/completions, /v1/responses) where that framing is
either invisible or malformed to a plain data-line parser. Added per-route
OPENAI_CHAT_ERROR_FRAME / OPENAI_RESPONSES_ERROR_FRAME and wired them in.
- chatCore.ts: persisted a synthetic clientResponse error body even when the client had
already disconnected (AbortError) before that body was ever computed — misleading the
dashboard into showing "what the client received" for a response that was never sent.
Also: RequestLoggerDetail.tsx — Provider/Client Event Stream panes lost their collapse
toggle when StreamSection replaced the collapsible PayloadSection (
|
||
|
|
4ecca379fc |
fix(providers): fix Azure AI Foundry multi-model discovery and per-deployment connection testing (#8174) (#8206)
Co-authored-by: not-knope <185121404+not-knope@users.noreply.github.com> |
||
|
|
ed8755a6d0 | feat(github-models): refresh catalog and compatibility (#8225) | ||
|
|
2d4db0157e |
fix(providers): discover live AGY models (#8123)
Live AGY model discovery (isDiscoverableAgyModelId + filterUserCallableAntigravityModels) composing with the #8013 antigravity discovery rewrite. Reconstructed onto the current release tip (branch was ~977 commits behind); updated the test to the renamed version-cache API (seedAntigravityVersionCache -> seedAntigravityIde/CliVersionCache) after the fusion. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
888c872459 |
refactor(antigravity): align official clients and callable catalog (#8013)
* fix(antigravity): preserve protocol fidelity and fail closed * chore: add PR-numbered changelog fragment * test: split oversized Antigravity suites * refactor(antigravity): align official IDE and CLI identities * fix(antigravity): align catalog with callable models * test(antigravity): update 2 test files to renamed version-cache API (#8013 fix) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> Co-authored-by: backryun <backryun@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com> Co-authored-by: Probe Test <probe@example.com> |
||
|
|
c2acaf287b | refactor(compression): extract resolveHeadroomDetail to keep dispatchCompression under the complexity gate (#8058) | ||
|
|
066e9275c4 |
fix(models): drop generic catalog siblings of specialty surfaces (#8015) (#8021)
Final catalog dedupe pass drops a generic/untyped chat-like sibling row when a typed non-chat specialty row (audio/video/moderation/...) exists for the same public id — closing the #4424 follow-up (whisper-1, tts-1, omni-moderation-latest, elevenlabs/*, veo-free/*). Pure, I/O-free, order-preserving; also removes a stray raw NUL byte that was embedded in the dedupe key template literal (which made git render the file binary). Restores test coverage for relative-order preservation across distinct ids and for two distinct id-less entries never being grouped, keeping the suite's assertion count at parity with the pre-fix baseline. Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> |
||
|
|
e7f965c9cc |
fix(compression): persist Headroom minRows (set 5 and reload keeps 5) (#8058)
* fix(compression): persist Headroom minRows (set 5 and reload keeps 5) Fixes diegosouzapw/OmniRoute#8056. Headroom detail settings had a Save-looking form but EngineConfigPage only persisted aggressive/ultra via SETTINGS_SUBOBJECT, so minRows always reseeded to the schema default (8) after reload. - Add HeadroomConfig + DEFAULT_HEADROOM_CONFIG (minRows: 8) - Accept headroom in compressionSettingsUpdateSchema (minRows 2..10000) - Normalize/store headroom in get/updateCompressionSettings - Register headroom in EngineConfigPage SETTINGS_SUBOBJECT so Save works - Merge settings.headroom into stacked stepConfig for runtime apply - Thread minRows through preview API + EngineConfigPage preview payload - Tests: schema/DB round-trip, engine apply, stacked merge, UI Save→PUT 5 * chore(quality): rebaseline compression.ts + strategySelector.ts own-growth (#8056 headroom minRows) --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> |
||
|
|
98b1aa34b5 |
fix(sse): run compression pipeline per turn in Codex Responses WS bridge (#8052) (#8154)
The Codex Responses-over-WebSocket bridge bypassed the whole prompt-compression pipeline (and its analytics writes) that the HTTP/SSE path (chatCore.ts) runs on every request, via two gaps: 1. prepare() in codex-responses-ws/route.ts never called anything from open-sse/services/compression/* — it authenticated, injected memory, applied reasoning-routing, then went straight to executor.transformRequest(). 2. scripts/dev/responses-ws-proxy.mjs memoized the upstream connection in ensureUpstream() and only called the internal "prepare" action on the FIRST response.create of a WS session — every subsequent turn on a reused connection bypassed prepare() (and therefore compression) entirely. Fix: a new compression.ts module wires the core compression pipeline (settings resolution -> selectCompressionStrategy -> applyCompressionAsync -> compression_analytics/compression_engine_breakdown writes, reusing adaptBodyForCompression's existing Responses-API input[] adapter) into prepare(); responses-ws-proxy.mjs now re-runs prepare() (via a new shared runPrepare() helper) for every logical response.create turn on a reused connection, not just the first, without recreating the upstream socket. Regression test: tests/unit/responses-ws-proxy-compression-parity.test.ts proves the reused-connection bypass by execution (RED: 1 prepare call for 2 turns; GREEN after the fix: 2 prepare calls for 2 turns). |
||
|
|
813bea4184 |
feat(vnc-session): persistent noVNC browser login for web-cookie providers (#7892)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168) * feat(vnc-session): persistent noVNC browser login for web cookie/token providers ## Why (the headless-install problem) OmniRoute's web cookie/token providers (ChatGPT Web, Gemini Web, Claude Web, DeepSeek Web, …) need a live browser session, but the gateway normally runs **headless** — as a systemd service, inside Docker, or on a VPS with no display. There is no desktop for the operator to log into the provider in. Today the operator has to obtain the session cookie/token *out of band* (open a real browser elsewhere, export cookies, paste them into the connection row). That is fiddly, breaks on every provider UI change, and is a non-starter on a headless box where you can't open a browser at all. This PR adds an **on-demand interactive login**: OmniRoute boots a containerized browser that exposes a noVNC web UI at the host. The operator opens that URL in *their own* browser, logs in normally, and OmniRoute then harvests the resulting cookies / localStorage back into the provider's `provider_connections` row over the DevTools Protocol. No display required on the host — the headless server renders the login into a container and the human just drives it through a web page. ## How we ran into this - The shipped `dist/` bundle has **no App Router source**, so the only visible seam was `dist/server-ws.mjs`'s `http.createServer` monkeypatch. That seam is **dead**: Next's standalone `startServer` creates its own http server in a way that bypasses the override, so a route registered there never fires (debug logs confirmed: zero requests reached it). The real seam is the Next **App Router** (`src/app/api/...`), which lives in the dev tree, not `dist/`. - **Chromium ≥130 forces the remote-debugging port onto `127.0.0.1`** and ignores `--remote-debugging-address=0.0.0.0`. A plain published port can't reach it, so cookie harvest needs an in-container TCP bridge to republish the loopback CDP onto `0.0.0.0`. We shipped that bridge, but the cleaner default is **Firefox** (`jlesage/firefox`): its debugger binds `0.0.0.0` out of the box, so harvest works with no bridge at all. - The CDP harvester **hung forever** on the first tries: the message handler was defined but never attached to the socket, so every `send()` promise stayed pending. We replaced Playwright's `connectOverCDP` (which stalls through the bridge) with a **raw `ws` client** and wired the handler — now resolves. ## What New management API (scoped like the other admin endpoints via `requireManagementAuth`): | Method | Path | Purpose | | --- | --- | --- | | GET | `/api/vnc-session` | list active sessions + supported providers | | GET | `/api/vnc-session/:provider` | session state | | POST | `/api/vnc-session/:provider/start` | boot browser container → returns `vncUrl` | | POST | `/api/vnc-session/:provider/harvest` | persist cookies into the provider row | | POST | `/api/vnc-session/:provider/touch` | defer idle auto-stop | | DELETE | `/api/vnc-session/:provider` | stop + remove the container | ## Implementation - `src/lib/vncSession/manifest.ts` — provider → login URL + cookie/token map + config - `src/lib/vncSession/harvest.ts` — raw-CDP cookie/localStorage harvester (`ws`) - `src/lib/vncSession/service.ts` — docker lifecycle, port allocation, idle sweep, DB write - `src/app/api/vnc-session/**` — App Router routes - `src/lib/gracefulShutdown.ts` — tears down running login containers on exit ## Browser image choice Default is **`jlesage/firefox`** (0.0.0.0-friendly CDP, no bridge). The Chromium image + in-container bridge lives under `docker/vnc-browser/chromium`, selectable via `OMNIROUTE_VNC_IMAGE`. See `docker/vnc-browser/README.md`. ## Config (env) `OMNIROUTE_VNC_IMAGE`, `OMNIROUTE_VNC_CONTAINER_VNC_PORT`, `OMNIROUTE_VNC_CONTAINER_CDP_PORT`, `OMNIROUTE_VNC_PROFILE_DIR`, `OMNIROUTE_VNC_IDLE_MS`, `OMNIROUTE_VNC_MAX_MS`, `OMNIROUTE_VNC_MAX_SESSIONS`, `OMNIROUTE_DOCKER_BIN` — all documented in the docker README. ## Tests `tests/unit/vnc-session.test.ts` — manifest lookup + credential mapping (cookie / token / whole-jar). All passing via the Node test runner. ## Notes - Docker is the only external dependency; if the `docker` CLI is missing, `start` throws a clear error and shutdown is a no-op. - No secrets are returned by any endpoint — only session metadata + ports. Co-authored-by: Sora <138304505+Capslockb@users.noreply.github.com> Co-authored-by: Bernardo <138304505+Capslockb@users.noreply.github.com> * refactor(vnc-session): derive provider credentials from shared contract * fix(vnc-session): harden CDP harvesting and credential filtering * refactor(vnc-session): scope lifecycle to provider connections * fix(vnc-session): sanitize and scope management routes * fix(vnc-session): use canonical provider list in API * test(vnc-session): align coverage with canonical manifest * docs(vnc-session): align browser setup with current implementation * fix(security): loopback-gate /api/vnc-session (Hard Rule #15/#17) The new /api/vnc-session/* routes spawn Docker containers via child_process.spawn (src/lib/vncSession/service.ts) but were never registered in LOCAL_ONLY_API_PREFIXES or SPAWN_CAPABLE_PREFIXES, so they were reachable from non-loopback callers (any manage-scope API key or dashboard session over a tunnel) - the same CVE class (GHSA-fhh6-4qxv-rpqj) those constants exist to close. Register VNC_ROUTE_PREFIX (already exported but unused in manifest.ts) in both prefix lists, and add a regression test asserting isLocalOnlyPath()/isLocalOnlyBypassableByManageScope() correctly classify the new prefix. Co-authored-by: CAPSLOCKB <138304505+Capslockb@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> |
||
|
|
5dd3c76ad7 | feat: canonical numeric helpers + tier-1 (analytics) migration (#7879) (#7969) | ||
|
|
992fe98386 |
feat(compression): select model-aware tokenizers (#8009)
* Add model-aware tokenizer selection * fix: recognize cx Codex model prefix |
||
|
|
b861dd045a |
feat: browser login for Grok Build provider (#7013) (#7735)
* feat(oauth): add browser login for Grok Build provider (#7013) * feat(oauth): grok-build supports device_code AND browser-PKCE side-by-side (#7013) Reworks #7735 so the browser PKCE login is added ALONGSIDE the device_code flow (#7358) instead of replacing it; the OAuthModal lets the user pick either method. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
55549bfe5a |
feat(sse): add PromptQL playground provider (unofficial) (#7911)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168) * fix(ci): add the auto-enqueue pull_request_rule to the Mergify config (queue_conditions alone are eligibility-only) (#7179) * fix(ci): migrate Mergify auto-enqueue to merge_protections_settings.auto_merge_conditions (rules-based path is EOL 2026-07-16) (#7216) * fix(ci): drop Mergify batch settings (batching is a paid-tier feature; free plan queue is serial) (#7220) * fix(ci): merge queue tolerates the advisory dast-smoke failure (its GH-hosted build hang dequeued every attempt) (#7225) * test(ci): make the #6634 selfref guard hermetic — main's copy hard-fails every PR (#7341) main's copy of this test still does git I/O inside a unit test: const baseSrc = git(['show', 'origin/main:' + FILE]); Runners check out a shallow single ref, so origin/main does not resolve and the test dies with 'fatal: invalid object name origin/main'. Every PR into main fails Unit Tests (7/8) on it — today that is #7313, #7315, #7316, #7334, #7336 and #7337, six PRs red on a defect none of them introduced. #7313 has no other red at all. release/v3.8.49 already carries a fix ( |
||
|
|
f31f3c081e |
feat(proxy): operator-level proxy subscriptions (Karing-style) — hardened, ready for review (#7299)
* feat(proxy-subscriptions): src/lib/proxySubscription/parse.ts * feat(proxy-subscriptions): src/lib/proxySubscription/subscriptionService.ts * feat(proxy-subscriptions): src/lib/proxySubscription/index.ts * feat(proxy-subscriptions): src/lib/db/migrations/123_proxy_subscriptions.sql * feat(proxy-subscriptions): src/app/api/v1/management/proxy-subscriptions/route.ts * feat(proxy-subscriptions): src/app/api/v1/management/proxy-subscriptions/[id]/route.ts * feat(proxy-subscriptions): src/app/api/v1/management/proxy-subscriptions/[id]/refresh/route.ts * feat(proxy-subscriptions): src/app/api/v1/management/proxy-subscriptions/[id]/nodes/route.ts * feat(proxy-subscriptions): src/app/(dashboard)/dashboard/settings/components/proxy/SubscriptionTab.tsx * feat(proxy-subscriptions): tests/unit/proxySubscription.parse.test.ts * feat(proxy-subscriptions): tests/unit/proxySubscription.service.test.ts * feat(proxy-subscriptions): docs/proxy-subscriptions.md * feat(proxy-subscriptions): src/lib/db/proxies/types.ts * feat(proxy-subscriptions): src/lib/db/proxies/mappers.ts * feat(proxy-subscriptions): src/lib/db/proxies.ts * feat(proxy-subscriptions): src/app/(dashboard)/dashboard/settings/components/ProxyTab.tsx * i18n(proxy-subscriptions): add proxySubscriptionsTab key + use in ProxyTab * i18n(proxy-subscriptions): add proxySubscriptionsTab key + use in ProxyTab * i18n(proxy-subscriptions): add proxySubscriptionsTab key + use in ProxyTab * i18n(proxy-subscriptions): add proxySubscriptionsTab key + use in ProxyTab * test(proxy-subscriptions): extract isSubscriptionDue + unit tests * test(proxy-subscriptions): extract isSubscriptionDue + unit tests * test(proxy-subscriptions): extract isSubscriptionDue + unit tests * test(proxy-subscriptions): add global->rule switch re-bind integration test * feat(proxy-subscriptions): inline needs-local-core guidance in SubscriptionTab * i18n: add proxySubscriptionsTab to en (rebased on current main) * i18n: add proxySubscriptionsTab to zh-CN (rebased on current main) * i18n: add proxySubscriptionsTab to pt-BR (rebased on current main) * test(proxy-subscriptions): extract needsCore detection into pure module + unit tests * test(proxy-subscriptions): extract needsCore detection into pure module + unit tests * test(proxy-subscriptions): extract needsCore detection into pure module + unit tests * refactor(proxy-subscriptions): extract scopes.ts into pure module + unit tests (#65) * refactor(proxy-subscriptions): extract coreEndpoint.ts into pure module + unit tests (#65) * refactor(proxy-subscriptions): extract subscriptionService.ts into pure module + unit tests (#65) * refactor(proxy-subscriptions): extract proxySubscription.scopes.test.ts into pure module + unit tests (#65) * refactor(proxy-subscriptions): extract proxySubscription.coreEndpoint.test.ts into pure module + unit tests (#65) * security(proxy-subscriptions): fetchGuard.ts — SSRF guard + core scheme (#P0) * security(proxy-subscriptions): coreEndpoint.ts — SSRF guard + core scheme (#P0) * security(proxy-subscriptions): subscriptionService.ts — SSRF guard + core scheme (#P0) * security(proxy-subscriptions): proxySubscription.fetchGuard.test.ts — SSRF guard + core scheme (#P0) * security(proxy-subscriptions): proxySubscription.coreEndpoint.test.ts — SSRF guard + core scheme (#P0) * refactor(proxy-subscriptions): subscriptionService.ts — concurrency lock / resilience / url redaction (#P1) * refactor(proxy-subscriptions): url.ts — concurrency lock / resilience / url redaction (#P1) * refactor(proxy-subscriptions): index.ts — concurrency lock / resilience / url redaction (#P1) * refactor(proxy-subscriptions): route.ts — concurrency lock / resilience / url redaction (#P1) * refactor(proxy-subscriptions): route.ts — concurrency lock / resilience / url redaction (#P1) * refactor(proxy-subscriptions): proxySubscription.url.test.ts — concurrency lock / resilience / url redaction (#P1) * i18n(proxy-subscriptions): subscriptionService.ts — stable error codes + locale keys (#P2-6) * i18n(proxy-subscriptions): SubscriptionTab.tsx — stable error codes + locale keys (#P2-6) * i18n(proxy-subscriptions): en.json — stable error codes + locale keys (#P2-6) * i18n(proxy-subscriptions): zh-CN.json — stable error codes + locale keys (#P2-6) * i18n(proxy-subscriptions): pt-BR.json — stable error codes + locale keys (#P2-6) * i18n(proxy-subscriptions): en.json — normalize to LF line endings (#P2-6) * i18n(proxy-subscriptions): zh-CN.json — normalize to LF line endings (#P2-6) * i18n(proxy-subscriptions): pt-BR.json — normalize to LF line endings (#P2-6) * enhance(proxy-subscriptions): reject subscription fetch if ANY resolved DNS address is blocked (P3-1) * enhance(proxy-subscriptions): add withRetry() exponential-backoff helper (P3-2) * enhance(proxy-subscriptions): DNS multi-record guard + retry/backoff fetch + batch scope writes + observability (P3-1..P3-4) * enhance(proxy-subscriptions): batch addProxiesToScopePool() to drop N+1 writes (P3-3) * enhance(proxy-subscriptions): add last_error_at + consecutive_failures observability columns (P3-4) * enhance(proxy-subscriptions): surface consecutive failures + last error time in subscription cards (P3-4) * test(proxy-subscriptions): cover multi-record DNS SSRF (block if ANY address internal) (P3-1) * test(proxy-subscriptions): cover withRetry() first-success / retries / backoff / non-retryable stop (P3-2) * fix(proxy-subscriptions): resolve js-yaml import + missing backup/generation-bump imports Two bugs made the feature non-functional and its own test suite false: 1. parse.ts used `import yaml from "js-yaml"` (default import), but js-yaml@^5 is ESM-only with no default export — this threw a SyntaxError at module load, crashing every caller (index.ts re-exports parse.ts, so every API route hit this too). Switch to `import * as yaml`, matching how the rest of the codebase already imports js-yaml (hermes-agent.ts, openapiParser.ts, openapi/spec route.ts, guide-settings route.ts). 2. subscriptionService.ts's unapplySubscription() called backupDbFile() and bumpProxyRegistryGeneration() without importing either — backupDbFile exists but wasn't imported; bumpProxyRegistryGeneration was a private, non-exported function in db/proxies.ts. This threw a ReferenceError whenever a subscription with bound proxies was disabled/deleted (the normal path). Import backupDbFile from ../db/backup and export+import bumpProxyRegistryGeneration from ../db/proxies, matching the identical backup+bump pattern already used by deleteProxyById for the same proxy_assignments/proxy_registry mutation shape. Fixing both unmasked a third, previously-unreachable bug (both crashes happened before any test assertion could run): recomputeProxyEnabled() checked `proxy_subscriptions.enabled = 1` alone, which stays true across an unapply/disable cycle since unapplySubscription() never touches that column — the proxyEnabled flag would get stuck on `true` even after the subscription's proxies were fully detached. Changed the check to require an actually-bound proxy_assignments row for an enabled subscription, matching hasNonSubscriptionGlobalProxy()'s existing bound-check pattern. Traced all 3 production call sites (mode/rule switch, disable, delete) to confirm this doesn't change their outcome — only the previously-wrong "unapply in isolation" case. Also fixed the test file's own pre-existing bug: its provider_connections inserts omitted created_at/updated_at (NOT NULL, no default in the schema since 001_initial_schema.sql), which 0 assertions had ever reached before because the SyntaxError always crashed the file first. All 9 proxySubscription test files now run clean: 49/49 pass (previously 2 files crashed outright at import time, 0 assertions ever ran). Separately confirmed via testing against the pristine PR head: this PR has 3 more pre-existing gate failures unrelated to the above (file-size on db/proxies.ts, cognitive-complexity, complexity, changelog-integrity vs the current release tip) plus 3 pre-existing TS2345 errors in parse.ts (lines 228/233/263, unrelated to the yaml import). All are the PR's own scope/base-drift, out of scope for this fix — the PR has never had a real CI run (base=main), so no gate has ever surfaced them; they need the base retarget + a full CI pass called out in the plan file's own remaining mandatory items. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(db): renumber proxy-subscriptions migrations to avoid version collision release/v3.8.49 already ships 123_quota_auto_ping.sql and 124_generic_session_affinity_ttl.sql; this PR's 123/124 files collided, tripping migrationRunner's version-collision guard and failing all proxySubscription.service tests. Renumber to 127/128 (next free slots after 126_reasoning_routing_rules.sql). Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> * refactor(db): extract proxySubscriptions + registryGeneration to keep proxies.ts under file-size cap Moves addProxiesToScopePool to ./proxySubscriptions.ts and the registry-generation helpers to ./proxies/registryGeneration.ts (re-exported from proxies.ts), keeping the module under its frozen 1177-line cap after the operator-proxy-subscriptions feature. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(db): renumber proxy-subscriptions migrations past release collision Rebasing onto release/v3.8.49 surfaced a migration version collision: this branch's 127_proxy_subscriptions.sql and 128_proxy_subscriptions_meta.sql now collide with 127_usage_history_account_identity.sql and 128_auto_candidate_overrides.sql that landed on release since this branch last synced. Renumbered to 131/132 (next free prefixes after the current 130_remove_unregistered_qwen_data.sql) and updated the in-file header comments to match. No schema/behavior change. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> Co-authored-by: xier2012 <xier2012@users.noreply.github.com> |
||
|
|
2d1801985c |
feat(cline): align ClinePass catalog and request protocol (#7914)
* feat(cline): align catalogs and official protocol * fix(models): clean imports after final connection removal * fix(quality): extract Cline/ClinePass auth-header wiring to shrink default.ts open-sse/executors/default.ts grew to 894 lines against the frozen 890-line cap after adding the ClinePass official-protocol import plus two Object.assign header-merge blocks. Extract the merge logic into a new applyClineAuthHeaders() helper in src/shared/utils/clineAuth.ts so the executor's case "clinepass" / case "cline" branches shrink to a single call each, dropping default.ts back to 875 lines. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
3238df3204 |
perf: lazy provider init, P2C quota cache, structuredClone elimination, getSettings→getCachedSettings (batch 2) (#7893)
* perf: startup parallelization, stream TextEncoder lift, auth middleware bottlenecks
Startup (~100-300ms faster cold start):
- Parallelize 4 early imports via Promise.all() in registerNodejs()
- Parallelize 10 independent background services via Promise.allSettled()
- Each service has independent try/catch — no failure domino effect
Streaming pipeline (8 fewer TextEncoder GC allocations per SSE event):
- Lift new TextEncoder() from per-chunk inside buildClaudeStreamingResponse
to function scope alongside existing decoder singleton
Auth middleware bottlenecks (from PerfBottleneckAnalysis):
- Backoff decay loop: replace updateProviderConnection (full CRUD:
SELECT+encrypt+cache-invalidate+backup) with resetConnectionBackoff
(targeted UPDATE of backoff/error columns only)
- Dual .filter() for quota: replace two passes calling
isQuotaExhaustedForRequest per connection with a single for loop
partitioning into withQuota/exhaustedQuota
- Debug-log filter recomputation: capture connectionFilterStatus Map
during the filter pass; debug loop reads 6 string comparisons instead
of 6 function calls per connection
Supporting:
- Add resetConnectionBackoff to src/lib/db/providers.ts (patterned after
clearConnectionErrorIfUnchanged, no CAS check)
- Re-export resetConnectionBackoff from src/lib/localDb.ts
- Update integration-wiring.test.ts regex for parallelized dynamic import
* perf: P2C quota cache, lazy provider init, structuredClone elimination, getSettings→getCachedSettings
- **auth.ts: P2C quota re-evaluation cache** — quotaResults Map threaded
through selectPoolSubset → compareP2CConnections → getP2CConnectionScore.
Populated during filter + partition passes, eliminating redundant
evaluateQuotaLimitPolicy / isQuotaExhaustedForRequest calls when the
P2C comparator re-evaluates previously-scored connections.
- **constants.ts: lazy PROVIDERS via Proxy** — replaces eager
generateLegacyProviders() + loadProviderCredentials() at module load
with Proxy delegating to deferred init on first property access.
- **providerModels.ts: lazy PROVIDER_MODELS + PROVIDER_ID_TO_ALIAS** —
same Proxy pattern for both exports; generateModels()/generateAliasMap()
deferred until first read.
- **stream.ts: structuredClone → minimal object spread** — replaces
O(n) deep clone of SSE response chunks with targeted reconstruction
of only mutated fields (usage, delta.content, finish_reason).
- **progressTracker.ts: TextDecoder lift** — module-level decoder
instead of per-chunk new TextDecoder().
- **Route files: getSettings() → getCachedSettings()** — 13 API route
files converted from uncached per-request DB reads to TTL-cached
wrapper (5s default), eliminating redundant queries on every request.
- **settings.ts: re-export getCachedSettings** from readCache for
non-localDb consumers.
- **Remove settingsCache.ts** — dead file, no imports reference it.
TS compile: 0 errors. Auth tests: 225/225 pass. Services: 269/269 pass.
* perf: Phase 1 tangible wins — egressCache eviction, mmap_size PRAGMA, composite indexes, proxyFallback lazy import
- egressCache: lazy TTL eviction on getCachedEgressIp access (bounds memory
leak to distinct proxy URLs, typically <100)
- mmap_size: apply stored PRAGMA from key_value table (256MiB default) after
applyStoredDatabaseOptimizationSettings — setting was stored but never applied
- schemaColumns: add idx_uh_provider_model_timestamp (covers getModelLatencyStats)
and idx_pc_provider_auth_type (covers 6+ provider_connections queries)
- proxyFallback: convert static import to dynamic import() inside error handler
(defers 210ms module load from startup to first proxy-retry scenario)
* perf: add dedup expression index, unref() sweep timers
- Add COALESCE expression index idx_uh_dedup on usage_history
matching the exact dedup query pattern. Eliminates FULL TABLE
SCAN on every saveRequestUsage insert.
- Add composite idx_uh_provider_model_timestamp on usage_history.
- Add composite idx_pc_provider_auth_type on provider_connections.
- Add .unref() to setInterval in batchProcessor.ts (polling loop).
- Add .unref() to setInterval in runtimeHeartbeat.ts (heartbeat).
* perf: bump SQLite cache_size default from 16MB to 64MB
New installs now start with 64MB page cache (was 16MB). Existing
users' stored settings are unchanged. Reduces disk reads for the
typical ~250MB database by keeping ~25% of pages in memory.
Also resolved pre-existing merge conflict in webhooks.ts.
* docs: add Redis production config guide and proxy port clash investigation report
- docs/redis-production-config.md: comprehensive Redis tuning guide
covering client options, server config, Docker settings, scaling,
and monitoring for all three Redis workloads (rate limiting,
auth cache, quota store)
- docs/proxy-port-clash-report.md: investigation confirming proxy
subsystem has no port binding issues; real EADDRINUSE history
traced to process supervisor crash-loop restart race (#4425) and
live-dashboard port clash (#6324), both already fixed
* fix: address PR #7893 review — add Proxy traps, extract migrations to reduce providers.ts size
- Add set trap to PROVIDER_ID_TO_ALIAS Proxy (providerModels.ts)
- Add deleteProperty traps to all three lazy Proxies (PROVIDERS,
PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS)
- Extract autoMigrateLegacyEncryptedConnections and getGheCopilotHosts
from providers.ts (1129→1036 lines, -93) into providers/migrations.ts
- Both functions re-exported via providers.ts for backward compat
File-size ratchet resolved: src/lib/db/providers.ts now 1036 lines.
* fix: resolve merge conflict markers in 3 route/test files
- model-combo-mappings/route.ts: kept upstream version (Zod pagination
via validateBody + isValidationFailure), restored missing return
statement for GET handler
- playground/presets/route.ts: kept stashed version details (satisfies
type-narrowing + inlined Response) — functionally identical
- error-sanitization.test.ts: matches upstream exactly (no diff)
Test verification: same 7 pre-existing failures confirmed on upstream
baseline (
|