mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 15:12:23 +03:00
6302a78657c0d55af2bb48eb908dde5e7e7aeb6d
1985 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6302a78657 |
fix(db): register SIGHUP handler and stop force-killing server on win32 stop paths (#8045) (#8148)
Windows console-window close delivers CTRL_CLOSE_EVENT, which Node/libuv maps to a JS-visible SIGHUP event. initGracefulShutdown() only listened for SIGTERM/SIGINT, so closing the window never ran cleanup() (WAL checkpoint + closeDbInstance()), leaving storage.sqlite's WAL un-checkpointed for the next launch. Separately, process.kill(pid, "SIGTERM") on win32 unconditionally force-terminates the target process instead of delivering an interceptable signal. The CLI's own stop paths (ServerSupervisor.stop() and runStopCommand()) sent it immediately on every stop, racing and beating the child's own async graceful shutdown before the WAL checkpoint could run. Fix: - src/lib/gracefulShutdown.ts: register a SIGHUP handler alongside SIGTERM/SIGINT. - src/shared/platform/windowsProcess.ts (new): stopProcessGracefully() skips the immediate SIGTERM on win32 (letting the target's own CTRL_C/CTRL_CLOSE handling run) and polls before escalating to SIGKILL; unchanged immediate SIGTERM behavior on POSIX. - bin/cli/runtime/processSupervisor.mjs and bin/cli/commands/stop.mjs: use stopProcessGracefully() instead of an unconditional process.kill(SIGTERM). Regression tests: tests/unit/graceful-shutdown-sighup-8045.test.ts (reuses the RED probe from the triage analysis) and tests/unit/windows-process-stop-8045.test.ts. |
||
|
|
1c116e0501 |
fix(cli): merge node bin dir into CLI healthcheck PATH for codex detection (#8036) (#8156)
checkRunnable() built the healthcheck spawn's minimalEnv.PATH from the caller's PATH only, never merging in this Node's own bin dir the way locateCommand's known-path search already does. npm-installed CLIs like codex are `#!/usr/bin/env node` shebang scripts, so when the server is launched with a minimal PATH (systemd/docker/PM2/Electron) lacking node's dir, the healthcheck spawn fails even though the binary was correctly located, and the tool shows as undetected. Extracted the merge into a new buildHealthcheckPath() helper (cliRuntimeHealthcheckPath.ts) to keep cliRuntime.ts within its frozen file-size ceiling. |
||
|
|
7a0fb27cf9 |
fix(db): stop closing the sql.js singleton in getDbInstance() probe/reopen (#7494) (#8153)
getDbInstance()'s probe-then-reopen pattern (written for per-open-handle drivers like better-sqlite3/node:sqlite) was calling .close() on a throwaway probe connection before opening the "real" connection right after. For sql.js, openSqliteDatabase()'s fallback path always returns the SAME module-global cached singleton for a given filePath, so closing "the probe" closed the ONLY connection that file would ever get until process restart — every subsequent query threw sql.js's raw "Database closed" string, matching the reported crash-loop on every boot once storage.sqlite already exists and both sync drivers are unavailable. Adds closeProbeIfSafe() (src/lib/db/core.ts) and uses it at every probe-close site in getDbInstance()/captureCriticalDbState() — it skips the close for sql.js-backed adapters and lets the same live adapter flow through, while still closing real per-handle drivers normally. Also makes sqljsAdapter.ts's gracefulClose() remove its 3 process-level listeners (beforeExit/SIGINT/SIGTERM) so a closed adapter's closure (raw sql.js Database + buffers) can actually be garbage collected instead of being pinned forever — addresses the compounding-OOM sub-finding as a consequence of the same defect. Regression test: tests/unit/db-sqljs-close-poison-7494.test.ts |
||
|
|
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> |
||
|
|
295189d4a8 |
fix(models): stop inventing chat capabilities for specialty surfaces (#8016) (#8022)
Co-authored-by: RaviTharuma <ravitharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
7c08c0afea |
chore(dashboard): reframe Kimi partnership as "Open Source Friends" (#8117)
* chore(dashboard): reframe Kimi partnership as "Open Source Friends" Kimi (Moonshot AI) asked to frame the collaboration under an "Open Source Friends" narrative instead of "Official Sponsor". Adopt "Supported by our Open Source Friends" — it keeps the backing/support signal and the friendship warmth — with Kimi as the founding friend, listed first. The substance is unchanged: affiliate links, the transparency note, first-in-list placement and the banner display window all stay exactly as they were. Only the label moves. - README: section header "Sponsors" -> "Supported by our Open Source Friends"; badge "Official Supporter" -> "Founding Friend"; thank-you and support-line copy reworded; official K3 banner (public/sponsors/kimi-k3-banner.png) added full-width at the top of the section, linked with aff=omniroute. - ProviderCard: badge/tooltip fallback strings -> founding-friend wording. - i18n: kimiSponsorBanner.title, kimiOfficialSupporterBadge and kimiOfficialSupporterTooltip updated across all 43 locales (en + 42 translations), dropping the sponsor framing in every language. - Test providerCardKimiPartnerAccent aligned to the new "Founding Friend" badge. * docs(readme): add Sponsors honor-roll for financial backers Credit the project's GitHub Sponsors just below the Open Source Friends section. The two public sponsors (Professor Igor Morais Vasconcelos, longtao) are named with avatar links; the one sponsor who chose private visibility on GitHub Sponsors is credited anonymously, without exposing their identity. * docs(readme): generalize private-sponsor credit to 'and others' |
||
|
|
e86e5bcc51 |
feat(media): Adobe Firefly image + video generation provider (#8006)
* feat(media): Adobe Firefly image + video generation provider
Add unofficial adobe-firefly media provider with full OpenAI-compatible
image and video generation: Nano Banana / GPT Image families, Sora 2,
Veo 3.1 (standard/fast/reference), and Kling 3.0.
Supports browser session cookies (auto IMS token exchange) or direct
IMS access tokens, async submit-and-poll against Firefly 3P endpoints,
aspect-ratio and resolution controls, and multi-account web-session UX.
Chat completions are intentionally rejected (media-only surface).
Includes unit coverage for registry wiring, payload builders, auth
resolution, and mocked generate happy-paths.
* fix(adobe-firefly): clio auth, discovery fallback, credits balance
Root-cause 401 invalid token against live firefly.adobe.com captures:
generate/discovery use x-api-key + IMS client_id clio-playground-web
(not projectx_webapp). Align headers/origin, dual cookie to IMS exchange
(clio first, Express fallback), BKS poll rewrite for /jobs/result.
Models: parse POST /v2/models/discovery + static fallback catalog from
adobe/get_models.txt; expand image/video registries.
Limits: GET firefly.adobe.io/v1/credits/balance (SunbreakWebUI1) with
total/remaining + free/plan detail quotas. Clarify cookie vs JWT UX.
Unit tests: 27/27 pass.
* fix(adobe-firefly): reject guest tokens from page-only cookies
Live repro with firefly.adobe.com Cookie export: IMS check with
guest_allowed=true returns account_type=guest (no AdobeID). That token
fails generate (401 invalid token) and credits/balance (403
ErrMismatchOauthToken). guest_allowed=false needs adobelogin.com IMS
session cookies which are not present in a page-only Cookie paste.
- Detect/reject guest JWTs; clear error tells user to paste Bearer JWT
- Prefer user JWT from HAR/mixed paste; improve credential extraction
- Update web-cookie + credential UX to recommend Authorization Bearer
Unit tests 29/29.
* fix(adobe-firefly): production auth, Limits, and 408 load handling
Live validation against firefly.adobe.com + packaged VibeProxy:
Auth / credentials
- Prefer IMS user JWT (Bearer from firefly-3p); reject guest tokens from
page-only cookies with an actionable error
- Extract JWT from Bearer, access_token=, IMS sessionStorage tokenValue,
and mixed HAR pastes; prefer non-guest tokens
- Strip JWT from Cookie header (undici Headers.append crash on mixed paste)
- Keep sherlockToken → x-arp-session-id + sanitized Cookie for generate
Limits
- credits/balance → Record quotas (firefly_total / free / plan) so
providerLimits caches them (arrays were ignored)
- Allowlist adobe-firefly + firefly in USAGE_SUPPORTED + APIKEY limits
- Live: 10000 plan credits parsed end-to-end after refresh
Generate
- Browser-shaped gpt-image body (size auto, no extra top-level size)
- Exponential 408 "system under load" retries (8 attempts) with clear
client message that 408 is Adobe capacity, not invalid token
- Live: generate returns proper 408 under load; balance/models stay 200
Tests: adobe-firefly unit suite 33/33 pass.
* fix(adobe-firefly): match live capture headers; add gpt-image-2
- Do not send firefly.adobe.com Cookie to firefly-3p (wrong-origin; soft 408)
- Lift sherlockToken only into x-arp-session-id
- Poll headers match status_check.txt (Bearer + accept, no x-api-key)
- Catalog gpt-image-2 alias → upstream modelVersion "2" (GPT Image 2)
- Shorter 408 retry budget so clients fail fast with clear message
- Unit suite 34/34
* fix(adobe-firefly): always send x-arp-session-id on generate (fixes 408)
Root cause of Bearer JWT → HTTP 408 colligo "system under load":
submit only set x-arp-session-id when sherlockToken was present in a
cookie paste. JWT-only credentials never sent the header, and Adobe
soft-blocks those requests with instant 408 (x-colligo-timeout:0.0).
A/B against a real user IMS token:
- det nonce + synthetic ARP → 200
- random nonce + synthetic ARP → 200
- det nonce without ARP → 408
Match adobe2api / GPT2Image-Pro:
- buildAdobeSubmitNonce = sha256(user_id + prompt[:256])
- buildAdobeArpSessionId = base64({sid, ftr}) synthetic session
- buildAdobeSubmitHeaders always sets both headers
Live adobeFireflyGenerateImage end-to-end: submit + poll → S3 presigned URL.
* fix(adobe-firefly): drop literal cred fallbacks + type-clean tests
Addresses pre-merge review feedback on #8006:
- Removes the `|| "literal"` fallback after resolvePublicCred() in
adobeFireflyApiKey()/adobeFireflyExpressClientId()/adobeFireflyBalanceApiKey()
(open-sse/services/adobeFireflyClient.ts). resolvePublicCred() already
always returns the decoded embedded default, so the literal fallback
was dead code that reproduced the exact env-or-literal anti-pattern
docs/security/PUBLIC_CREDS.md documents as BAD (Hard Rule #11).
- Replaces the 11 `@typescript-eslint/no-explicit-any` casts in
tests/unit/adobe-firefly.test.ts with concrete types
(Record<string, unknown>, Headers, Error-narrowing on the
assert.rejects predicate), matching the pattern already used
elsewhere in this suite. `no-explicit-any` is a hard ESLint error
under tests/ in this repo.
- Freezes file-size baseline entries for the new
open-sse/services/adobeFireflyClient.ts (1958 LOC, new-file cap 800,
mirrors the qoderCli.ts precedent for a legitimately large new
provider client), open-sse/config/imageRegistry.ts (800->821, new
adobe-firefly registry entry) and the +3 LOC growth in
src/lib/usage/providerLimits.ts (1000->1003).
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: artickc <artickc@users.noreply.github.com>
|
||
|
|
1b010f6c40 |
feat(sse): add HyperAgent (hyperagent.com) unofficial web provider (#7994)
* feat(sse): add HyperAgent (hyperagent.com) unofficial web provider
Reverse-engineered from live SPA captures (hyperagent/*.txt):
Chat:
- Cookie session auth (full Cookie header)
- New thread via GET /threads/new (or POST /api/threads)
- POST /api/threads/{id}/chat with SPA feature flags + content
- SSE parse of text/session_start/session_end/done events
- Multi-turn sticky threadId + sessionId cache (history prefix + last assistant)
Models:
- Hardcoded catalog from SPA pricing map
- Pretty display names (Claude Fable 5) while wire modelId stays fable etc.
- /v1/models exposes pretty name; chat uses modelId
Limits:
- GET /api/settings/billing/usage → creditBlocks initialUsd/remainingUsd/usedUsd
- USD Credits quota for Limits page
Tests: 15/15 unit/executor-hyperagent
* fix(sse): HyperAgent execution mode + fable-latest wire model (no plan mode)
* fix(sse): document HyperAgent env vars + regenerate golden snapshot
Addresses pre-merge review feedback on #7994:
- Documents HYPERAGENT_USAGE_URL in .env.example and ENVIRONMENT.md
(OMNIROUTE_DATA_DIR was already documented via the sibling PromptQL
provider) so check-env-doc-sync.test.ts passes.
- Regenerates the provider-translate-path golden snapshot to include
the new hyperagent/ha registry entries.
- Swaps the local toNumber() helper in usage/hyperagent.ts for the
canonical @/shared/utils/numeric import (#7879 no-restricted-syntax
rule landed on the release branch after this PR was opened).
- Freezes file-size baseline entries for the new
open-sse/executors/hyperagent.ts (937 LOC, new-file cap 800) and the
+3 LOC growth in src/lib/usage/providerLimits.ts (1000->1003), both
irreducible to this PR's own provider-registration wiring.
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>
|
||
|
|
ce3f2445a6 |
fix(security): namespace Notion thread cache per caller + validate client thread ids (IDOR)
Security-review follow-up to #7900. The notion-web thread-session cache was keyed only by Notion spaceId (space-, not user-scoped) and accepted arbitrary client-supplied thread ids, so two users of the same space could pin/read each other's thread. Now: (1) the cache key includes hashNotionCallerCookie(cookie) so each caller gets an isolated namespace, and (2) readClientThreadId rejects any value that is not a well-formed Notion UUID. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
79ec594c1f |
fix(embeddings): support secure multimodal inputs (#7978)
* fix(embeddings): support secure multimodal inputs Closes #7956 * fix(embeddings): translate multimodal inputs and harden URL/base64 bounds Reject oversize base64 before format validation to avoid Zod stack overflows, translate canonical items to Jina modality-keyed and Gemini embedContent contracts, and fetch HTTPS media server-side with DNS pinning before provider submission. Closes #7956 * fix(embeddings): pin DNS only for embedding media fetches Default remote-image fetch keeps the previous globalThis.fetch path so image-generation tests and callers stay mockable. Multimodal embeddings still opt into undici DNS pinning for URL media. * fix(embeddings): close 2 SSRF/DoS gaps in secure multimodal input (#7978) Closes two gaps in the multimodal embedding input hardening from #7956: 1. `createPinnedFetch()` (the connection-pinning mechanism that closes the DNS-rebinding TOCTOU window, GHSA-cmhj-wh2f-9cgx) had zero test coverage anywhere in the repo. Writing that test surfaced a real regression: its custom `connect.lookup` only implemented the single-address callback form `(err, address, family)`. Node's autoSelectFamily/Happy Eyeballs (on by default since Node 18) calls `lookup` with `{ all: true }` and requires the array form `(err, addresses[])` — the mismatch threw `ERR_INVALID_IP_ADDRESS` on every real pinned fetch, silently breaking all URL-sourced multimodal embedding requests in production. Fixed by branching on `options.all`. 2. The documented "16 MiB decoded per request" cap was enforced by the Zod schema only for base64-sourced items; URL-sourced items were excluded, and all up-to-32 items were fetched concurrently via `Promise.all` — allowing ~256 MiB in memory at once (16x the documented bound). Fixed by resolving items sequentially with a running byte budget shared across base64 and fetched-URL sources, rejecting once the aggregate is exhausted instead of after over-fetching. Adds tests/unit/remote-image-fetch-pin-dns-connection.test.ts (real loopback-server pinning tests) and a new aggregate-cap test in tests/unit/embeddings-multimodal-7956.test.ts; both were verified to fail against the pre-fix code before the corresponding fix was applied. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
0f6e440dfe |
fix(models): attach models.dev pricing to GET /v1/models entries (#8018) (#8025)
Co-authored-by: RaviTharuma <ravitharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
0a7a46f3da |
fix(capabilities): resolve models.dev specialty rows across provider keys (#8017) (#8023)
Co-authored-by: RaviTharuma <ravitharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
9020ed53f9 |
fix(grok-cli): require full auth.json on OAuth paste import (#7610) (#8027)
* fix(grok-cli): require full auth.json on OAuth paste import (#7610) The Grok Build paste path told operators to paste only the JWT "key" field, which creates connections with refresh_token=null that can never auto-refresh. Require the full ~/.grok/auth.json object (with refresh_token) in OAuthModal, and reject bare JWT pastes with a clear error. * fix(grok-cli): add behavioral test coverage for auth.json paste-import (#7610) Replace the source-regex-only test for the OAuth paste-import path with a real behavioral suite (bare JWT rejected, auth.json missing refresh_token rejected, multi-entry auth.json accepted, valid auth.json POSTed) using the existing grok-device-oauth-modal.test.tsx jsdom harness. Extract parseGrokCliPasteToken() into its own src/lib/oauth/utils/grokCliAuthJson.ts module so it is directly unit-testable and to keep OAuthModal.tsx's frozen file-size gate from growing (bump 1080->1100, justified in file-size-baseline.json, mirroring the existing extraction precedent on this file). Also fixes two pre-existing "JWT Token" label assertions that this PR's own tab rename ("Import auth.json") had left stale. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
23aa75ddd5 |
fix(grok-cli): sanitize function_call_output before Grok Build dispatch (#7611) (#8030)
* fix(grok-cli): sanitize function_call_output before Grok Build dispatch (#7611) Grok Build cli-chat-proxy rejects Responses bodies when tool-result outputs contain incomplete \u escapes or other malformed JSON text. Sanitize function_call_output.output values in GrokCliExecutor so large agent tool transcripts no longer fail intermittently with 400 body-parse errors. * fix(grok-cli): type test credentials instead of casting through any (#7611) tests/ has no-explicit-any as an ESLint error; the 3 `{ accessToken: "tok" } as any` casts passed to transformRequest() were the proven, non-drift cause of this PR's own "No new ESLint warnings" CI failure. Replace them with a single properly-typed ProviderCredentials literal (all fields on that type are optional, so no cast is needed). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
fb6ea295bf |
feat(compression): add Responses tool-output engine (#8010)
* Add Responses tool-output compression engine * fix: enable Codex Responses stacked steps * fix(compression): share Codex tokenizer and rebase UI * fix(compression): sync MCP engine selection * fix(compression): i18n parity for codex-responses mode + rebaseline The codex-responses compression engine already imports the shared countTextTokens/resolveTokenizerEncoding from tiktokenCounter.ts (no duplicate encoder) and CompressionSettingsTab.tsx already threads the new mode through the existing useTranslations()/labelKey pattern - both pre-existing on this branch tip after rebasing onto release/v3.8.49. What was missing after the rebase: the new compressionModeCodexResponses / compressionModeCodexResponsesDesc keys existed only in en.json. Filled en-fallback into all 42 locales via scripts/i18n/fill-missing-from-en.mjs and added real pt-BR/vi translations. Also rebaselined the three files whose own growth (new codex-responses mode wiring) crossed the frozen file-size caps: open-sse/mcp-server/schemas/tools.ts, open-sse/services/ compression/strategySelector.ts, and src/lib/db/compression.ts. 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: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
dc41a73ff7 |
fix(notion-web): reuse threadId across OpenAI multi-turn (no new chat each request) (#7900)
* fix(notion-web): reuse threadId across OpenAI multi-turn (no new chat each request) Root cause: every execute() minted a random threadId with createThread:true, so each OpenAI messages[] turn became a brand-new Notion AI chat. That broke multi-turn agent flows (tool result follow-ups looked like cold starts). - History-keyed in-memory session cache (spaceId + conversation prefix hash) - First user turn: createThread true + new UUID - Follow-up with prior turns: createThread false + same threadId - Optional client continuity: body.notion_thread_id / X-Notion-Thread-Id - Echo thread id on chat.completion (notion_thread_id + response header) - Also accept OpenAI content-parts arrays for message content - Unit tests: 34/34 (session lookup/store + createThread false on turn 2) * fix(notion-web): read X-Notion-Thread-Id from clientHeaders ExecuteInput exposes client request headers as clientHeaders, not headers. input.headers was always undefined so client-supplied thread pins were ignored. * fix(notion-web): prefer clientHeaders with defensive headers fallback * fix(notion-web): sticky threads on errors + partial follow-ups - Bind conversation root (first user) to a threadId *before* upstream call so temporarily-unavailable / empty replies never mint a new Notion chat on retry - Persist sticky map under DATA_DIR so multi-turn survives process restarts - Follow-ups use createThread:false, isPartialTranscript:true, and only the steps after the last assistant (full re-transcript was overloading Notion) - Detect in-band Notion error objects (subType temporarily-unavailable) and retry once with the same threadId - Keep custom-agent workflowId support and clientHeaders thread pin * refactor(notion-web): split thread-session/stream-parser/transcript-builder into services The merged notion-web.ts (1490 lines) and its test file (1000 lines) tripped the file-size gate (cap 800 for new/uncapped files). Extract three self-contained pieces into open-sse/services/, no behavior change: - notionThreadSessions.ts: sticky thread-session cache, disk persistence, conversation hashing, client thread-id pin (body/header) - notionStreamParser.ts: NDJSON runInferenceTranscript response parsing + in-band upstream error detection - notionTranscriptBuilder.ts: config/context/message-step transcript building Split the corresponding "Notion thread session continuity" describe block into tests/unit/executor-notion-web-thread-sessions.test.ts. All symbols previously reachable via the notion-web.ts namespace import stay reachable (re-exported) so existing test destructuring is unaffected. 44/44 tests pass. Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Artur <artur@local> 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> |
||
|
|
e09b5d2527 |
feat: provider tab account search + mirrored top pagination (#7937) (#7968)
* feat: provider tab account search + mirrored top pagination (#7937) Two client-side UI improvements to the provider connections/accounts list (all data already loaded in memory; PAGE_SIZE=50): - Mirror the pagination bar ABOVE the list (previously bottom-only) in both the flat/untagged branch and the tagged/grouped branch. - Add a case-insensitive substring account search input (id/tag/name/email) to the left of the status filter pills, searching across ALL accounts (not just the current page), resetting pagination to page 0 on change. - Add pagination to the tagged/grouped view, which previously had none. New pure helper `connectionsSearchFilter.ts` keeps the substring matcher testable and out of the already-large ConnectionsListPanel.tsx. Closes #7937 * i18n(vi): add providers.accountSearchPlaceholder for locale parity (#7937) |
||
|
|
042af3659e |
fix(pricing): clarify disabled automatic sync status (#7972)
* fix(pricing): clarify disabled automatic sync status * fix(i18n): add pricing auto-sync labels * fix(pricing): clarify disabled automatic sync status + sync new i18n keys to all locales (#7955) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
f879a394f4 |
feat(routing): add prompt-cache affinity (#8008)
* Add prompt cache locality routing * fix: preserve weighted cache-affinity routing * feat(routing): add cache-optimized combos * fix(routing): preserve normal ordering on cache misses * fix(routing): bind cache affinity to concrete accounts * feat(routing): add prompt-cache affinity + align combo-auto-config test with new defaults 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: JxnLexn <JxnLexn@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
9564028922 |
fix(resilience): cap exactCooldownMs against maxCooldownMs (#7940) (#7980)
Co-authored-by: Erick Kinnee <erick@ekinnee.dev> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
ee8e028aad |
fix(sse): bound forwarded response headers (#8041)
* fix(sse): bound forwarded response headers * docs(changelog): record forwarded response header fix |
||
|
|
1699933326 |
fix(chatcore): report string-reason client aborts as 499, not 502 (#7907) (#8011)
abort(reason) rejects the upstream fetch with the raw reason, which is
often a bare string ("request_signal_aborted", "Client disconnected: ...")
carrying no `name` or `status`. The chatCore catch block only recognized
`error.name === "AbortError"`, so those aborts fell through to the 502
provider-failure default and were surfaced as `FAILED 502 / Bad Gateway`
in the client response, request logs, and usage records.
Classify the caught error with the existing isLocalStreamLifecycleError
helper (expanded by #7908 to cover AbortError plus the known abort reason
strings) so every client-abort shape maps to `499 Request aborted`.
Status-normalization follow-up to #7908, which already excluded these
aborts from provider circuit-breaker and cooldown accounting.
Co-authored-by: xiaolong.835 <xiaolong.835@bytedance.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 |
||
|
|
6602af7478 |
feat: narrow mcp:connect scope + per-key HTTP tool-scope binding (#7895) (#7967)
* feat: narrow mcp:connect scope + per-key HTTP tool-scope binding (#7895) Adds MCP_CONNECT_SCOPE ("mcp:connect"), a narrow additive API-key scope (kept out of MANAGEMENT_API_KEY_SCOPES, same precedent as SELF_USAGE_SCOPE) that authorizes ONLY the /api/mcp/ LOCAL_ONLY route-guard carve-out -- remote MCP-only callers no longer need broad manage/admin scope just to reach the transport routes. Scoped strictly to /api/mcp/; every other LOCAL_ONLY bypass prefix still requires hasManageScope() unchanged. Also resolves the caller's real api_keys.scopes over HTTP/SSE (httpAuthContext.ts::resolveMcpCallerAuthInfo) and passes it to the MCP SDK's transport.handleRequest(req, { authInfo }), so extra.authInfo.scopes reaching tool calls reflects the Bearer key's own scopes instead of the OMNIROUTE_MCP_SCOPES env fallback -- scopeEnforcement.ts already prioritized authInfo, it was simply unfed over HTTP. Does not flip the OMNIROUTE_MCP_ENFORCE_SCOPES default; stdio is unaffected (no per-caller identity, stays on the meta/env fallback chain). Closes #7895 * test(mcp): register mcp-connect-scope test in stryker tap.testFiles (#7895) |
||
|
|
ff320cbfd5 |
fix(combo): exempt content_filter from empty-content detection (#7973)
## Problem When Gemini Flash returns a safety-filtered response (finish_reason: content_filter, empty content), isEmptyContentResponse() misclassifies it as a fake-success empty response and returns HTTP 502. This triggers the combo fallback chain and account cooldown escalation (5s → 10s → 20s → 40s), even though the response is a legitimate terminal state. ## Root cause errorClassifier.ts line 14: LEGIT_EMPTY_OPENAI_FINISH only exempts "length" and "tool_calls". The "content_filter" finish reason (mapped from Gemini's SAFETY/PROHIBITED_CONTENT) is not exempted, so safety-filtered responses are treated as empty content failures. ## Fix Add "content_filter" to LEGIT_EMPTY_OPENAI_FINISH so safety-filtered responses pass through as valid (though filtered) completions. ## Testing - 10/10 unit tests pass (empty-content-stopreason-3572.test.ts) including 2 new content_filter test cases - E2E: hot-patched OmniRoute v3.8.48 on X500, verified the previously failing prompt (4.6KB review) now returns valid content instead of empty-content 502 Signed-off-by: Minxi Hou <houminxi@gmail.com> |
||
|
|
0f342f41fe |
fix(providers): refresh duckduckgo-web catalog to current Duck.ai wire ids (#8000) (#8079)
duckduckgo-web returned 400 ERR_BAD_REQUEST on every request because the model
catalog advertised ids DuckDuckGo has retired from the free Duck.ai lineup
(gpt-4o-mini, gpt-5-mini, llama-4-scout, mistral-small-2501, o3-mini,
claude-3-5-haiku-20241022). duckchat/v1/chat validates `model` server-side and
rejects retired ids, and normalizeDuckDuckGoModel() defaulted to / passed through
gpt-4o-mini, so the retired id reached the wire verbatim.
Update all three id sources to the current free wire ids captured live from
duckchat/v1/models (2026-07-22): gpt-5.4-mini, gpt-5.4-nano, claude-haiku-4-5,
mistral-small-2603, tinfoil/gpt-oss-120b, tinfoil/gemma4-31b —
- executor: default gpt-4o-mini -> gpt-5.4-mini; legacy ids aliased to the
nearest current model via a lookup map; dropped the invalid gpt-5-mini
"minimal" reasoningEffort;
- freeModelCatalog.data.ts + providers/registry/duckduckgo-web: current ids.
Regression test duckduckgo-web-model-catalog-8000.test.ts asserts no retired id
ever reaches the wire and all three catalogs match the current set (RED on the
old default/passthrough + retired catalogs). Live 200 confirmation remains a
recommended VPS smoke per the plan-file.
|
||
|
|
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> |
||
|
|
91f4c35e9d | feat: copilot-m365-web tone-selected model variants (#7872) (#7997) | ||
|
|
effddc6a0e |
fix(build): split pure semver helpers into versionCompare so the Kimi client banner gate stops dragging child_process into the browser bundle
The KimiSponsorBanner (use client) version gate imported isNewer/normalizeVersion
from versionCheck.ts, whose top-level 'import { execFile } from child_process'
cannot be tree-shaken out of a client bundle — Turbopack next build failed with 33
'Module not found' errors (child_process, fs, net, dns, module). Move the pure
helpers to a dependency-free versionCompare.ts; versionCheck.ts re-exports them.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
||
|
|
898e2bfcaa |
fix(sse): replace spoofable .includes() PromptQL issuer check with hostname comparison (#8029) (#8042)
isDdnProjectPromptQlToken() (jwt.ts) and isLikelyDdnToken() (usage/promptql.ts) used
`iss.includes("auth.pro.hasura.io")`, which a spoofed issuer like
"https://auth.pro.hasura.io.evil.com/ddn/token" also satisfies
(js/incomplete-url-substring-sanitization, 2 open CodeQL high alerts).
Adds a shared issuerHostIsTrusted() helper in jwt.ts that parses `iss` with `new URL()`
and compares the hostname (exact match or trusted subdomain), and points both call
sites at it, de-duplicating the previously copy-pasted predicate.
|
||
|
|
5e234d503d |
fix(sse): bound Codex SSE peek read with per-read timeout (#8020) (#8043)
peekCodexSseTransientError() ran before chatCore's normal readiness/idle-timeout pipeline and read the first SSE chunk with a bare reader.read() — no timeout wrapper. A 200 text/event-stream body that never emitted a byte hung for ~15min (901399ms observed) before the platform killed the connection and surfaced a generic 502. Wrap the peek loop's read and the re-assembled passthrough body's pull() in readStreamChunkWithTimeout, bounded PER READ (not a total deadline) so a long-but-alive reasoning stream keeps resetting the window on every chunk it emits. On timeout the reader is cancelled and the request now fails fast with a 504 instead of hanging. New small module open-sse/executors/codex/bodyTimeout.ts holds the wrapping helpers to keep codex.ts within its frozen size baseline. |
||
|
|
2b6e856f64 |
fix(providers): migrate muse-spark-web from GraphQL to WebSocket protocol (#7528)
* 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) * feat: add protobuf+WS helpers and tests for muse-spark-web Co-Authored-By: Claude <noreply@anthropic.com> * fix: remove 50ms auto-close timer from wsChat, fix test mock to respond properly The 50ms setTimeout in wsChat sent a close signal before the server could respond. Tests now trigger a response event from the mock's send() and then close naturally. wsChat waits indefinitely (or until timeout) for real server data. Co-Authored-By: Claude <noreply@anthropic.com> * fix(provider): migrate muse-spark-web from GraphQL to WebSocket protocol Meta AI retired the persisted query (doc_id 29ae946c...) that OmniRoute used for message sending. The AttachmentInput type was removed from Meta's GraphQL schema, causing 502 errors on every request. Replace the old GraphQL POST approach with Meta's current protocol: 1. GraphQL warmup (doc_id e7f80258...) — init conversation 2. GraphQL mode switch (doc_id c32bbe99...) — set think_fast/think_hard 3. WebSocket (wss://gateway.meta.ai/ws/clippy) — protobuf-framed messaging All frame encoding uses inline protobuf helpers (no new deps). The existing continuation cache, model mapping, and response formatters are preserved. Fixes #7267 Co-Authored-By: Claude <noreply@anthropic.com> * fix: add warmup+mode-switch GraphQL calls and Buffer ESM import Also moves modelInfo extraction earlier so mode-switch can use it. Co-Authored-By: Claude <noreply@anthropic.com> * fix: share requestId between WS URL and prompt frame, add auth fallback - Pass requestId from wsChat into buildWsPromptFrame so both the WS URL and the prompt frame use the same identifier, matching Meta's protocol. - Add fallback to extract the ecto1:... authorization token from the apiKey cookie string when providerSpecificData.authorization is not set. This lets users paste both the cookie and auth token in OmniRouter's single input field (e.g. 'ecto_1_sess=...; ecto1:...'). Co-Authored-By: Claude <noreply@anthropic.com> * fix: address Gemini Code Review findings on PR #7528 - AbortSignal: graphqlPost now accepts and propagates signal to fetch, warmup and mode-switch calls pass the caller's signal. - GraphQL errors: parse response body for errors array on HTTP 200. - Abort listener leak: store handler reference and removeEventListener on settle, instead of relying solely on { once: true }. - Binary WS frames: decode Buffer/ArrayBuffer/Uint8Array to UTF-8. - Test: add test for GraphQL error-in-200 detection. Co-Authored-By: Claude <noreply@anthropic.com> * fix: narrow ProtoField value before BigInt in serializeProtoFields setBigUint64(0, BigInt(f.value)) failed tsc TS2345 because f.value's union includes Uint8Array. Wire type 1 always carries a numeric value; guard the Uint8Array case with a clear throw instead of coercing. Co-Authored-By: Claude <noreply@anthropic.com> * refactor: remove dead readTextResponse from muse-spark-web Unused since the WebSocket migration dropped body-streaming reads. The identically named live copy in blackbox-web.ts is untouched. Co-Authored-By: Claude <noreply@anthropic.com> * refactor: remove dead postMetaAiRequest from muse-spark-web Replaced by the WebSocket send path; no remaining call sites. Co-Authored-By: Claude <noreply@anthropic.com> * refactor: remove dead buildHttpErrorResult/buildParsedErrorResult Both were part of the retired GraphQL-POST error path; the WebSocket path builds errors via errorResult directly. No remaining call sites. Co-Authored-By: Claude <noreply@anthropic.com> * test: nest connectionId overrides into credentials Four tests passed connectionId at the top level of makeBaseInput, where the spread never reached credentials.connectionId that execute reads -- so they silently ran against the default conn-test-1 instead of their named ids. Add a withConnection helper and route them through it. Co-Authored-By: Claude <noreply@anthropic.com> * docs: document template fingerprint fields verified STATIC vs live capture Live WS captures from two independent meta.ai accounts confirm the 64-hex session token, actor numeric ID, locale, and app ID are app-level constants — identical in Meta's own client. No fingerprint randomization warranted. Co-Authored-By: Claude <noreply@anthropic.com> * fix: address code review — NaN uniqueMsgId, varint truncation, cache eviction, empty WS 502 - uniqueMessageId: use Math.random() decimal suffix instead of crypto.randomUUID().slice(0,4) which produced NaN ~80% of the time (UUID hex chars like 'a'-'f' break Number()). - encodeVarint: use BigInt arithmetic instead of >>> bitwise operators that truncated 41-bit Date.now() timestamps to 32 bits (lost minutes). - submittedMs: use ?? instead of || so valid zero timestamps are accepted. - Cache eviction: add evictContinuationIfNeeded on WS error path (was missing, letting stale conversation entries survive WS failures). - Empty WS response: return 502 instead of 200 when WS closes with no content, matching the old parseMetaAiResponseText behavior. Co-Authored-By: Claude <noreply@anthropic.com> * chore(7528): keep .mergify.yml at release tip (maintainer CI config lands via its own PRs, not this provider fix) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> |
||
|
|
159873719c |
feat(providers): add hailuo-web (MiniMax web) chat provider (#6673) (#7734)
Adds hailuo-web as a new free web-cookie chat provider targeting the
MiniMax consumer chat product at hailuo.ai (chat.minimax.io), distinct
from the existing paid API-key minimax/minimax-cn providers.
Ported from the g4f reference implementation
(g4f/Provider/needs_auth/mini_max/{HailuoAI,crypt}.py):
- MD5-chain request signing (generate_yy_header/get_body_to_yy)
- Custom event:/data: SSE parsing (send_result/message_result/close_chunk),
where message_result.content is a cumulative snapshot diffed into deltas
- Device-fingerprint query params, derived deterministically per-connection
from the token when the user hasn't captured the real browser values
New catalog entry, executor, registry entry, dispatch wiring, tests
(17 cases covering signing test vectors independently verified via
Python hashlib.md5, SSE parsing, streaming/non-streaming dispatch, and
401-terminal vs 429-transient error mapping), and a regenerated
provider-translate-path golden snapshot (purely additive diff).
|
||
|
|
287802cf86 |
fix: repair pre-existing red gates on the release/v3.8.49 tip (#8055)
* fix(dashboard): resolve Kimi banner casing collision + shrink frozen test file (release tip) - Rename src/app/(dashboard)/dashboard/kimiSponsorBanner.ts to kimiSponsorBannerGate.ts so it no longer differs from KimiSponsorBanner.tsx only by the first letter's case (breaks next build on case-insensitive filesystems). Updates the sole importer (KimiSponsorBanner.tsx) and the two tests that reference it. - Extract the 8 Kimi/Moonshot featured-ordering tests out of the frozen tests/unit/providers-page-utils.test.ts (grown 3 lines past its 1294 cap by #8039's rebrand-comment update) into a new sibling file tests/unit/providers-page-utils-kimi.test.ts. No assertions dropped; both files pass in full (24 + 8 = 32 tests). * fix(sse): register PromptQlExecutor in the executor registry (release tip) getExecutor("promptql") had no entry in open-sse/executors/index.ts, so it silently fell through to DefaultExecutor's provider fallback, which issues a raw fetch() and returns the bare upstream Response instead of the executor wrapper shape {response, url, headers, transformedBody}. The real PromptQlExecutor class (open-sse/executors/promptql.ts) already honors the contract correctly — it was just never wired into the registry. Fixes tests/unit/executor-web-cookie-sweep.test.ts "promptql executor returns wrapper shape". * fix(i18n): backfill 2220 missing pt-BR keys to restore en.json parity (release tip) pt-BR.json fell behind after #7935 restored +2220 keys into en.json and vi.json but left pt-BR.json unmodified. Translated all missing entries to Brazilian Portuguese, preserving ICU/interpolation placeholders and existing terminology, and merged them mirroring en.json's key order so the diff is additions-only (the small comma-only deletions are pure JSON reformatting from new sibling keys). * fix(providers): repair 4 pre-existing catalog/registry reds on release tip - providers-constants-split.test.ts: APIKEY_PROVIDERS grew 182->187 (PR #7887 added 5 free-tier providers: ainative/aion/sealion/routeway/nara). Verified no dup/loss (6-family partition sums exactly to 187) and updated the stale expected count + comment trail to match. - cline registry: added the missing minimax/minimax-m3 free OpenRouter entry (#3321) and fixed the neighbouring nemotron-3-ultra-550b-a55b entry, which carried a stray ":free" id suffix and an imprecise 1_000_000 contextLength instead of the 1_048_576 the test (and every sibling 1M-context entry in this catalog) expects. - promptqlModels.ts / registry/promptql/index.ts: PROMPTQL_FALLBACK_MODELS's minimax-m3 entry was missing supportsVision, and the registry mapping dropped it entirely (only id/name were passed through) — it was the sole minimax-m3 entry across the whole registry not flagged multimodal, despite every other provider (minimax, minimax-cn, ollama-cloud, trae, bazaarlink, clinepass, codebuddy-cn, opencode-zen/go, synthetic, huggingchat, lmarena) agreeing MiniMax-M3 supports vision. Added the field to the PromptQlModel type and threaded it through. - tests/snapshots/provider/translate-path.json: regenerated the golden via UPDATE_GOLDEN=1. Diffed old vs new — zero providers removed, 5 added (ainative/aion/nara/routeway/sealion, matching #7887), and the only changed entry (cline) reflects the already-merged #7914 ClinePass header protocol change (Cline/<version> User-Agent + X-Task-ID) that a prior narrow golden touch-up missed capturing. * fix(docs): repair docs-sync/env-sync/repo-contract gates (release tip) Six pre-existing reds on release/v3.8.49, all "repo drifted from its own documented contract": - check-docs-counts-sync: free-tier headline was stale (~1.4B/~2.0B) vs the live catalog (~1.53B steady / ~2.15B first month, 43 pools). Updated README.md and docs/reference/FREE_TIERS.md to the live numbers and added a v3.8.49 correction note explaining the pool-count delta (39->43, #7840). Also fixed a soft executors-count drift in ARCHITECTURE.md (84->86, 268->271 providers) while touching that line. - release-green-docs-drift-7253: docs/proxy-subscriptions.md referenced a fabricated migration filename (123_proxy_subscriptions.sql); the real file is 131_proxy_subscriptions.sql. Fixed all 3 occurrences. - check-env-doc-sync + issue-7793-env-doc-sync-repro: OMNIROUTE_DATA_DIR (DATA_DIR fallback alias read by open-sse/executors/promptql/threadSticky.ts) was undocumented. Added to .env.example and docs/reference/ENVIRONMENT.md. - check-db-rules: src/lib/db/proxySubscriptions.ts (#7299) is a db-internal split of proxies.ts (kept under the frozen file-size cap) whose one export is already re-exported via proxies.ts -> localDb.ts. Added it to INTENTIONALLY_INTERNAL with the same db-internal justification used for identical split modules (apiKeyColumnFallbacks, providerNodeSelect, webSessionDedup) rather than a redundant direct re-export from localDb.ts. - mcp-server-hollow-dist-deps: the sanity test expected better-sqlite3 among the MCP bundle's static top-level external imports. That's been stale since the pre-#7878 migration to a cascading SqliteAdapter driver factory (createRequire()-based lazy require, not a static import); better-sqlite3 already has its own native-asset copy guarantee in assembleStandalone.mjs, unrelated to this test's EXTRA_MODULE_ENTRIES concern. Updated the assertion to a still-genuinely-static external (zod) with a comment explaining the change. No production runtime behavior changed — docs, .env.example, and a checker allowlist/test-expectation only. * fix(dashboard): repair stale UI component-shape test assertions (release tip) Two pre-existing reds in the dashboard UI component-contract cluster were caused by test assertions that had gone stale after intentional, correct refactors — not by real defects in the components: - quota-pool-wizard-multi.test.ts: the step-3 preview assertion required the literal single-line substring "connectionIds.map((cid)". Prettier (100-char width, project config) legitimately breaks the connectionIds.map(...).filter(...) chain across lines because of the multi-line callback body, so the literal never matches. PoolWizard.tsx still builds previewByProvider correctly by mapping over connectionIds; updated the assertion to a regex that tolerates the line break. - v388-phase1-screen-fixes.test.ts: the shared Select placeholder-guard assertion required the literal "!children && placeholder". An earlier, intentional i18n commit changed the hardcoded "Select an option" default to a translated fallback (`placeholder ?? t("selectOption")`), which requires parens around the ?? expression for operator precedence. The guard behavior is unchanged (still gated on !children); updated the assertion to match the current, correct guard shape. Both fixes are read-only test-file changes; no production behavior changed. review-reviews-v3814-fixes.test.ts still has one pre-existing, unrelated red (LEDGER-4: minimax-m3 registry entries missing supportsVision) that requires editing the promptql provider registry/catalog — out of this cluster's scope, left untouched and reported separately. * fix(providers): reconcile cline catalog contradictions + deterministic golden (release tip) The first tip-green pass introduced 3 regressions caught by CI on sibling guard tests: - clinepass-provider + cline-catalog-models-3321 encoded OPPOSITE expectations of the same cline model list (minimax presence, nvidia :free suffix). Reference upstream (OpenRouter free lineup) confirms nvidia/nemotron-3-ultra-550b-a55b:free (with :free, 1M ctx) is correct, so restore that id and fix #3321's stale no-:free assertion; add minimax/minimax-m3 (the real #3321 gap) to clinepass-provider's list. - check-db-rules-classification froze INTENTIONALLY_INTERNAL at 35; proxySubscriptions was the intentional 36th entry — add it + bump the count. - provider-translate-path golden stored a LITERAL Cline/3.8.49: clineAuth resolves the version from APP_CONFIG.version (stable), but the golden sanitizer collapsed only process.env.npm_package_version (unset under `node`, set under `npm run`) — so the golden was shard-dependent. Resolve APP_VERSION from APP_CONFIG.version like clineAuth and regenerate; now Cline/<APP> normalizes identically in every shard. * fix(services): type execFile signal/killed in classifyError + ratchet dashboard baseline (release tip) Pre-existing base-red on the tip's Fast Quality Gates (dashboard-typecheck), missed in the first inventory: - src/lib/services/installers/utils.ts TS2339 — `err.signal` was read off a value typed as NodeJS.ErrnoException, which @types/node does not declare `signal`/`killed` on (those belong to execFile's ExecFileException). Widen classifyError's param to type both, and drop the now-redundant `(err as … { killed })` cast. - Ratchet config/quality/dashboard-typecheck-baseline.json down: 5 baselined errors were fixed by already-merged PRs but never ratcheted (OAuthModal TS2769 4→3 / TS2345 4→3, CliproxyModelMappingEditor TS2339, CompressionPreviewAccordion TS4104, MonacoEditor TS2307). Baseline now 254, matching live — gate exits 0. |
||
|
|
577bbf3e47 |
[defer] feat(combo): universal cooldown-aware retry & auto-strategy combo-ref guard (#7301)
* feat(combo): universal cooldown-aware retry & auto-strategy combo-ref guard
Two changes:
1. Universal cooldown-aware retry (combo.ts):
- Remove strategy==="quota-share" gate from comboCooldownWaitEnabled
- Enables all 18 combo strategies (priority, weighted, round-robin, etc.)
to wait out a short transient cooldown and retry the full set
- Non-quota-share strategies use shouldWaitForComboCooldown directly
with earliestRetryAfter and reason="rate_limit" (no per-model lockout)
- quota-share retains its existing per-target model lockout logic
2. Auto-strategy combo-ref guard (autoStrategy.ts):
- expandAutoComboCandidatePool now detects kind==="combo-ref" entries
- When present, returns eligibleTargets without expanding to ALL providers
- Fixes scenario where an "auto" combo with combo-ref delegates to a
sub-combo but pulls in every model from every active provider
* feat(combo): global comboTimeoutMs + aggregated error diagnostics
Adds two features to improve combo resilience and debuggability:
1. Global combo timeout (comboTimeoutMs)
- Configurable per-combo via DEFAULT_COMBO_CONFIG (default 0 = disabled)
- After each target completes, checks if total elapsed time exceeds limit
- When exceeded, stops trying further targets and returns 504 immediately
- Backward-compatible: 0 preserves legacy unlimited-iteration behavior
2. Aggregated error diagnostics
- comboErrors array accumulates per-model failure details (model, status, error)
- On combo timeout or all-models-exhausted, returns a message listing the
first (up to 5) model-level errors with their HTTP status codes
- Enables operators to see WHICH models failed and WHY without digging
through individual server logs
* test(combo): cover universal cooldown-aware retry, comboTimeoutMs, and combo-ref guard
Adds/updates automated coverage for this PR's production changes (PR Test
Policy requires tests alongside src/open-sse/electron/bin changes):
- Update the "preserves the first failure status" expectation in
combo-routing-engine.test.ts: the aggregated per-model error-diagnostics
suffix is new intended output, not a regression.
- Add two new tests for the global comboTimeoutMs feature: the combo stops
dispatching further targets and returns 504/COMBO_TIMEOUT with aggregated
diagnostics once the ceiling trips, and comboTimeoutMs=0 (default) never
trips it.
- Rewrite the non-quota-share (priority) cooldown-wait scenario in
combo-quota-share-cooldown-wait-timing.test.ts: comboCooldownWait is no
longer gated on strategy === "quota-share", so a priority combo now waits
out a short 429 and re-dispatches too (via shouldWaitForComboCooldown with
reason "rate_limit"), instead of propagating immediately as before. Also
adds the disabled-flag counterpart for parity with the quota-share suite.
- Add coverage for the #COMBO-REF guard in expandAutoComboCandidatePool:
a combo whose models array contains a kind:"combo-ref" entry must not be
expanded to every model of every active provider.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* test(combo): keep the new comboTimeoutMs tests free of no-explicit-any
The two new tests initially copied the neighbouring tests' `any`-typed
handleSingleModel params / json() casts. Those neighbours are pre-existing
violations frozen in config/quality/eslint-suppressions.json at a count of 261
for this file, so the 7 new occurrences pushed it to 268 and broke `npm run
lint` (no-explicit-any is an error in tests/ since #6218; new violations must
be fixed, not re-frozen).
Type the new tests properly instead: `unknown`/`string` params, a
ComboErrorPayload interface for the parsed body, and drop the unused
`relayOptions: null as any` (the sibling combo cooldown suites already omit
it). Back to exactly 261 — the suppression file is untouched.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(combo): gate the universal cooldown retry on the REAL lock reason, not a hardcoded "rate_limit"
The universal cooldown-aware retry kept the quota-share path on
resolveComboCooldownWaitDecision but gave every OTHER strategy a shortcut that
hardcoded `reason: "rate_limit"` and fed shouldWaitForComboCooldown the
earliestRetryAfter directly.
comboCooldownRetry.ts documents TWO deliberate barriers ("SECURITY —
quota_exhausted must be excluded"): (1) the reason allow-list, and (2) the
maxWaitMs ceiling, explicitly called the SECOND barrier. Hardcoding the reason
removed barrier 1 for 17 of the 18 strategies and left only the ceiling — which
does NOT cover a quota_exhausted lock whose wait lands under maxWaitMs. In that
case the combo waits, redispatches against a model that is locked until the
quota resets, and burns the retry budget for nothing.
The shortcut's premise ("non-quota-share combos have no per-connection model
lockout tracking") is also false: recordModelLockoutFailure in the target loop
is not gated on quota-share, so every strategy records model lockouts and the
real reason is always available.
Fix: one decision path for every strategy, always through
resolveComboCooldownWaitDecision, so the reason always crosses the allow-list.
The lock lookup is now keyed on each TARGET's own model (via a new third
`target` arg on lookupLock) — quota-share combos are single-model/multi-account
so this is identical to the previous orderedTargets[0] behavior, but
heterogeneous combos (priority, weighted, round-robin, …) carry a different
model per target and would otherwise miss every lock but the first.
Regression guard (tests/unit/serial/combo-quota-share-cooldown-wait-timing.test.ts):
a priority combo where modelLockout.errorCodes=[403] leaves the 403's
quota_exhausted lock as the only one in play while a 429 crystallizes the
status, and the resulting wait is short enough that the ceiling lets it through
— so only the allow-list can stop it. Verified failing-then-passing: with the
hardcoded reason the combo makes 6 dispatches (wait+redispatch x2); with the
real reason it makes 2 and propagates the 429.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* chore(quality): rebaseline file-size for PR #7301 own growth (combo +91, combo-routing-engine test +68)
---------
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
|
||
|
|
0844163966 |
[defer] fix embedded CLIProxyAPI config handling (#6877)
* fix embedded CLIProxyAPI config flag * preserve embedded CLIProxyAPI config * test(services): add fs-backed regression test for cliproxy resolveSpawnArgs (#6877) The existing cliproxy.test.ts only re-asserted string literals and never called the real resolveSpawnArgs() against a filesystem, so it could not have caught the -c/--config flag mismatch or the config.yaml clobbering bug this PR fixes. Add a test that imports the real function against a temp DATA_DIR and asserts: the spawn args always use --config (never -c), a missing config.yaml gets the default template, and an existing operator-customized config.yaml is left byte-identical. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
edbce2e35b |
feat: support Bun bundled SQLite runtime (#7878)
* feat: support Bun bundled SQLite runtime * fix: harden Bun SQLite backups and params * docs: keep Node as the only supported runtime; document bun:sqlite as best-effort compatibility path Co-authored-by: Arul Kumaran <arul@luracast.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> |
||
|
|
f6d6ad6047 |
feat(dashboard): Kimi sponsor banner, Kimi Coding preset, official logomarks and partner links (#8039)
Official Kimi (Moonshot AI) partnership rollout on the dashboard: a dismissible home-page sponsor banner (version-gated through v3.8.60), a one-click "Kimi Coding" combo preset (kimi-k3 primary via moonshot, fallback to kimi-coding/kimi-web), the official theme-aware logomark wired into ProviderIcon and the README sponsors card, and aff-tagged partner links (aff=omniroute) across the 3 visible Kimi provider cards' top-of-page header link. The moonshot provider's dashboard display name is rebranded to "Kimi" (id/alias/routing untouched — DB connections, combos and /dashboard/providers/moonshot still address it by id). Refs: Kimi partnership pilot. |
||
|
|
4012bac41d |
fix(i18n): preserve remaining Vietnamese localization (#7935)
* fix(i18n): preserve remaining Vietnamese localization * chore(quality): rebaseline file-size cap for 9 dashboard components (i18n wiring) Restoring the Vietnamese localization on 9 dashboard components (useTranslations wiring + t()/tc() call-site swaps for previously hardcoded strings) grows each file by a small, irreducible amount. Bumps the frozen file-size-baseline.json caps to match, with a justification entry per the project's own ratchet policy. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(i18n): wire weekday localization + add missing qwen CLI description Two gaps left by this PR's own new contract tests, caught while reconciling the branch against the release tip: - CostOverviewTab.tsx added formatWeekdayLabel() but never called it; the Weekly Usage Pattern chart still showed raw English day abbreviations regardless of locale. Now maps weeklyPattern rows through it before handing them to WeeklyPatternCard. - cliTools.toolDescriptions was missing an entry for "qwen" (a baseUrlSupport:"full" tool) in both en.json and vi.json, failing the PR's own cli-catalog-display-contract.test.ts. Covered by the PR's existing tests/unit/dashboard-localization-contract.test.ts and tests/unit/cli-catalog-display-contract.test.ts (both now pass). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * i18n(vi): backfill the 4 proxySubscription keys #7299 added to en.json #7299 (proxy subscriptions) merged while this branch was rebasing, adding settings.proxySubscriptionsTab and settings.proxySubscription.error.{LOCAL_CORE_ENDPOINT_INVALID, NEEDS_CORE_NOT_CONFIGURED,NO_USABLE_NODES} to en.json. This PR's own i18n-vi-completeness contract asserts full en↔vi key parity, so the merge of the current release tip surfaced them as missing. Adds the Vietnamese translations, keeping the SS/VMess/Trojan/VLESS/SOCKS5 technical terms verbatim. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com> |
||
|
|
eab59d4048 |
fix(translators): normalize TitleCase tool names for non-Anthropic models (#7926)
* fix(translators): normalize TitleCase tool names for non-Anthropic models * fix(translator): parse XML <invoke> blocks in OpenAI→Claude response translator * fix(gemini-web): mark gemini-3.1-flash-lite as toolCalling: false * fix(gemini-web): mark all models as toolCalling: false, add to no-tools combo * fix(web-providers): mark all web-cookie models as toolCalling: false * fix(nvidia): disable tool calling on models that can't handle it * fix(lint): drop explicit any annotation on extractXmlInvokeBlocks state param Removes the no-explicit-any violation introduced alongside the TitleCase tool-name normalization work so the merged branch stays lint-clean (state stays implicitly typed like its sibling helpers in this file). 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: 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 ( |
||
|
|
19cbe8ae14 |
fix(providers): route iflytek/sparkdesk to Spark's OpenAI-compatible host (#7942)
Both entries declare format: "openai" with authHeader: "bearer", but pointed at spark-api.xf-yun.com — Spark's WebSocket host, which authenticates with an HMAC-SHA256 signature over app_id/apiKey/apiSecret and rejects bearer tokens. The OpenAI-compatible HTTP API lives on spark-api-open.xf-yun.com/v1, so neither provider could complete a request as configured. sparkdesk also listed a "general" model; that is a WebSocket domain value and is not accepted by the HTTP endpoint, so it becomes "lite" (Spark Lite). The free-model catalog's sparkdesk row is updated to match, and a regression test locks both baseUrls, the removed "general" model id, and catalog/registry cross-reference. Reconstructed against release/v3.8.49 (folds in the same-PR follow-up "point sparkdesk free-catalog row at lite"). Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
a865fddb26 |
fix(perplexity-web): multi-step empty content + advanced-quota cooldown (#7930)
Perplexity's live multi-step/copilot streams can surface the advanced_models_quota_low upsell instead of any answer text when the account's weekly advanced-model budget is exhausted. Detect it and return HTTP 429 with reset_seconds/Retry-After (mapped to rate_limited_until) instead of a silent empty-content error. Also fixes plan-goal (thinking) extraction for live multi-step streams that deliver the plan as an RFC-6902 diff patch against plan_block instead of a materialized plan_block object — those goals were previously dropped. Reconstructed against release/v3.8.49: most of the original "empty content" fix in this PR was independently and differently addressed on release already (extractAnswerFromFinalText + longestMarkdownAnswer), so only the two non-overlapping pieces above are ported here. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
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> |
||
|
|
124557cc4b |
fix(combo): context-aware fallback ignores model_context_override (#7933)
* fix(combo): context-aware fallback ignores model_context_override filterTargetsByRequestCompatibility resolved a target's context capacity through the override-free getResolvedModelCapabilities, so a persisted model_context_override (Feature 5004) never reached the compatibility filter. A provider whose catalog maxInputTokens is a deliberately small client-facing hint (below the real window so coding agents auto-compact, #6191) is then dropped from the fallback pool for large-context requests even when an operator recorded its true larger capacity. With only that provider left after Claude quota is exhausted, the combo returns a hard 503 with no fallback. evaluateContextLimit now consults the raw override (getModelContextOverride, null when unset) before catalog limits, only when an override exists - so a genuinely-too-small maxInputTokens is still enforced for non-overridden models. Consistent with two other override-aware call sites in this file. Registry values (#6191 client hint) untouched. Tests: override rescues small-catalog target; without override too-small still dropped; existing #6191/#7039 tests pass (14/14). * fix(quality): extract context-fit evaluation to keep comboStructure.ts under cap The model_context_override fix grew open-sse/services/combo/comboStructure.ts past the 800-line file-size cap. Extract evaluateContextLimit() (the override-then-catalog context-fit check) into a new leaf, open-sse/services/combo/contextOverrideGate.ts, so comboStructure.ts only keeps the two call-site wires. No behavior change. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: tmone <25759142+tmone@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@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> |
||
|
|
a20771d6ac |
fix(auto): pool accounts by provider model (#7928)
* fix(auto): pool accounts by provider model * test(auto): update provider-family-combos to the #7928 Cartesian pool shape Since #7928 the auto-combo candidate pool is a connections × models Cartesian product, so createBuiltinAutoCombo("auto/<family>") now surfaces each backend's full family line-up rather than exactly one default model per connection. The #6453 invariant is unchanged and still asserted — which providers span the family and that unrelated providers (the connected openai/gpt-4o-mini, the connected glm on auto/zai) are excluded — but the two exact-count assertions are relaxed to the provider SET and the detectModelFamily() family check, matching the pattern the sibling auto/minimax case already uses. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Your Name <you@example.com> Co-authored-by: adrianaryaputra <adrian.arya@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> |
||
|
|
11b5ce1f0e |
feat(providers): add 5 free-tier providers (ainative, aion, sealion, routeway, nara) (#7887)
Five OpenAI-compatible free-tier aggregators OmniRoute did not cover yet, added
as a registry entry plus a canonical provider each.
ainative api.ainative.studio/api/v1 — 84-model public catalog, passthrough
aion api.aionlabs.ai/v1 — 5 models, public catalog w/ pricing
sealion api.sea-lion.ai/v1 — AI Singapore, pinned models (10 RPM)
routeway api.routeway.ai/v1 — 236-model catalog; browser UA pinned
because Cloudflare 1010s non-browser UAs
nara router.bynara.id/v1 — shared 5M/day pool, pinned free models
All five /models endpoints were probed live 2026-07-20 (200 for the public ones;
sealion/nara 401 without a key, as expected) and every pinned model id was
confirmed to exist upstream.
Free-catalog honesty: ainative ("~10M tok/mo claimed"), aion (20k tok/day),
sealion (10 RPM) and routeway (200 RPD) have no verifiable monthly TOKEN quota,
so they are recurring-uncapped — real access, never summed into the headline.
Only nara publishes a token figure (5M tokens/day shared = 150M/month), recorded
as one deduped pool. Net: 484 -> 514 models, 1.376B -> 1.526B tokens, the +150M
coming solely from nara.
|
||
|
|
f909b1d45e |
fix(resilience): don't cool down accounts or trip the breaker on client aborts (#7908)
* fix(resilience): don't cool down accounts or trip the breaker on client aborts When the caller drops the connection mid-stream, the in-flight request surfaces request_signal_aborted, "Client disconnected", or a DOM AbortError with no upstream status code. These shapes were counted as provider failures: the serving connection went into cooldown, the provider circuit breaker accrued failures, and healthy accounts ended up marked unavailable from client-side cancellations alone. Treat client aborts as local stream lifecycle events (#4602 policy): extend isLocalStreamLifecycleError() to recognize abort shapes and skip connection disable and breaker accounting for them. Genuine upstream failures (5xx/429/401) are still counted. Fixes #7907. * fix(resilience): guard the two remaining breaker-trip call sites against client aborts (#7907) PR #7908 correctly wired isLocalStreamLifecycleError() into shouldSkipConnDisable() and chatHelpers.ts's onStreamFailure, but two separate breaker._onFailure()-triggering call sites were purely status-code gated and never checked it, so a client-side abort (no upstream status, defaults to 502, error='request_signal_aborted') still tripped the whole-provider circuit breaker — the highest blast-radius of the 3 resilience mechanisms: - src/sse/handlers/chat.ts: the single-model, non-combo terminal-failure path called breaker._onFailure() directly, bypassing the isFailure option (which only applies inside breaker.execute()). Extracted the predicate into shouldTripProviderBreakerForResult() and added the missing isLocalStreamLifecycleError guard. - open-sse/services/combo/comboPredicates.ts::shouldRecordProviderBreakerFailure(), used by handleComboChat's executeTarget (open-sse/services/combo.ts), gained the same guard via a new optional `error` field. Added tests/unit/circuit-breaker-abort-provider-trip-7907.test.ts exercising both real predicates directly (not just the isolated isLocalStreamLifecycleError() helper) — confirmed red on the unfixed code (missing export) and green after the fix, alongside the existing #4602/#7908/combo-breaker-429 suites (24/24 pass, no regressions). file-size-baseline.json: +1 combo.ts (irreducible call-site wiring for the new `error` field) and +1 chatHelpers.ts (own growth from the PR's already-verified onStreamFailure guard, surfaced only now since fast-gates PR->release skip check:file-size). Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> Co-authored-by: insoln <insoln@ya.ru> * test(quality): register circuit-breaker abort tests in stryker tap.testFiles The two new tests (circuit-breaker-abort-provider-trip-7907, circuit-breaker-client-abort) import mutated modules (circuitBreaker.ts, comboPredicates.ts) but were not listed in stryker.conf.json tap.testFiles, tripping check:mutation-test-coverage --strict. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.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> |