mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
Rebased onto the current release/v3.8.51 tip as part of a combined provider-retirement/provenance merge batch (Designer Web, Felo Web, Runtime, GPL-derived removal, Qwen Web already landed). Large conflict set (this is the biggest PR in the batch — the common ChatGPT Web provider touches chat, images, count-tokens, session leases, and combos). Conflicts resolved: - `open-sse/config/providers/registry/chatgpt-web/*`, `open-sse/executors/chatgpt-web*`, `open-sse/handlers/imageGeneration/providers/chatgptWeb.ts`, and their tests: kept deleted, matching the PR's stated scope. - `open-sse/config/providers/registry/minimax/web/index.ts`, `open-sse/handlers/imageGeneration/providers/geminiWeb.ts`, `open-sse/executors/gemini-web.ts`'s stale image-mode branch: base-drift collisions against already-merged sibling retirements (#11691, #11708) — kept deleted / dropped the dead code, since this PR's own branch forked before those merged. - `src/shared/constants/reservedProviderPrefixes.ts`, `open-sse/executors/index.ts`, `executorProxy.ts`, `virtualFactory.ts`, `autoStrategy.ts`, `src/lib/db/providers.ts`, `src/sse/handlers/chat.ts`: combined the Designer + Runtime (Felo/Qwen) + common-ChatGPT-Web retirement guard calls at each shared chokepoint — compute-once-then-OR pattern, consistent with prior combinations in this batch. - `src/sse/services/model.ts` / `src/sse/handlers/chatHelpers.ts`: adopted this PR's new `getModelInfoOrRetirementResponse()` central wrapper (a real improvement over ad-hoc try/catch), and extended it to also catch the Designer + Runtime retirement errors it didn't originally cover, so the consolidation doesn't regress the other two mechanisms. - `src/app/api/v1/images/edits/route.ts`: this PR moved the retirement check earlier (before `enforceApiKeyPolicy`) but left the old later call+catch block in place from base drift — removed the now-redundant duplicate `resolveImageRouteModel()` call and merged the Designer catch into the earlier one. - `open-sse/config/imageRegistry.ts`, `tests/snapshots/executors/executor-map.json` (`keyCount` recomputed to 133), `tests/snapshots/provider/translate-path.json`: same "both sides inserted a different retired provider at the same slot" pattern — resolved by dropping both. - `tests/unit/chatcore-executor-proxy.test.ts`, `provider-node-reserved-prefix.test.ts`, `combo-auto-candidate-expansion.test.ts`, `messages-count-tokens-route.test.ts`, `virtual-auto-combo.test.ts`: split into independent per-mechanism test blocks (established pattern); `virtual-auto-combo.test.ts`'s old "includes cookie web-session providers" positive-inclusion test (which used chatgpt-web as its example) was retired along with the provider and replaced by this PR's negative-exclusion test for the same slot. - `docs/architecture/ARCHITECTURE.md`, `CODEBASE_DOCUMENTATION.md` (+ 4 i18n mirrors), `README.md`, `FREE-TIERS-GUIDE.md`, `docs/diagrams/free-tier-budget.svg`, `docs/screenshots/free-tier-budget-card.svg`, `docs/reference/PROVIDER_REFERENCE.md`: recomputed every stale count from the real merged state — 104 executors (`countFiles` gate logic), 351 providers (regenerated via `gen:provider-reference`), 152/351 `hasFree` entries, 445/438/7 free-tier catalog rows, 13 ToS-avoid providers, budget-card regenerated via its real generator script. One doc conflict (`oauth/` module list) needed picking HEAD's side specifically — theirs still listed the already-removed `raycast` module instead of the real `openference`. - `config/quality/test-masking-allowlist.json`: additive merge of the PR's 17 `_deletedWithReplacement` entries alongside the batch's existing ones (one real duplicate-key mistake in my first pass, caught and fixed via a `object_pairs_hook` duplicate-key check before finalizing). Also fixed two real, unrelated-to-my-merge issues surfaced by the focused suite: - `tests/unit/resolve-web-provider-host.test.ts`: the PR's own test had a typo — it asserted `perplexity-web`'s resolved host as `"perplexity.ai"`, but the provider's registered `website` is `"https://www.perplexity.ai"` and the resolver returns the URL's `host` verbatim (no www-stripping), so the correct value is `"www.perplexity.ai"` (consistent with the same test's own `url` assertion). - `tests/unit/hard-session-lease-bypass-inventory.test.ts`: this golden call-site inventory was already stale on the pristine post-#11713 tip (confirmed via a throwaway probe worktree) — `src/lib/db/providers.ts`'s 3 connection-fallback sites and a third `src/app/api/providers/route.ts` site were never added to the golden list by the earlier-merged #11698/#11720 PRs. Updated it to the real current inventory (dated inline comments explain each delta and which PR introduced it), plus this PR's own legitimate deltas (image-edits duplicate-call removal, `ChatGptWebExecutor.execute()` site removed). Focused suite green (433/433 across executor-proxy, reserved-prefix, hard-session-lease-bypass-inventory, resolve-web-provider-host, retirement/runtime-block/source-retirement/management-retirement/image-handler-retirement, migration-168, combo-auto-candidate-expansion, virtual-auto-combo, executor-map-golden and siblings), plus `typecheck:core`, `check-file-size`, and `check-changelog-integrity` clean. Thanks for the thorough provenance-hold retirement work — appreciated.
198 lines
11 KiB
Markdown
198 lines
11 KiB
Markdown
---
|
|
title: Account-Ban / Banned-Keyword Detection
|
|
---
|
|
|
|
# Account-Ban / Banned-Keyword Detection
|
|
|
|
OmniRoute scans upstream error responses for signals that indicate a provider
|
|
**account is permanently dead** (suspended / deactivated / ToS-banned) and, when
|
|
matched, moves that connection into a **terminal `banned` state** so it is no
|
|
longer selected for requests. This is what the **Security → Banned Keywords**
|
|
settings card configures ("Additional keywords that trigger permanent account
|
|
ban detection. Built-in keywords always apply.").
|
|
|
|
This page documents the built-in list, the detection flow, its scope, how to add
|
|
custom keywords safely, and how to recover a flagged connection. The terminal
|
|
state itself is part of the resilience model — see
|
|
[RESILIENCE_GUIDE](../architecture/RESILIENCE_GUIDE.md) ("Terminal states").
|
|
|
|
**Source of truth:** `open-sse/services/accountFallback.ts`
|
|
(`ACCOUNT_DEACTIVATED_SIGNALS`, `getMergedBannedSignals()`, `isAccountDeactivated()`).
|
|
|
|
## Built-in keywords
|
|
|
|
These 8 substrings always apply (case-insensitive), regardless of any custom list:
|
|
|
|
```
|
|
account_deactivated
|
|
account has been deactivated
|
|
account has been disabled
|
|
your account has been suspended
|
|
this account is deactivated
|
|
verify your account to continue (Antigravity / Google Cloud Code)
|
|
this service has been disabled in this account for violation (Antigravity)
|
|
this service has been disabled in this account (Antigravity)
|
|
```
|
|
|
|
> This list evolves as providers change their ban wording. The authoritative
|
|
> copy is `ACCOUNT_DEACTIVATED_SIGNALS` in `open-sse/services/accountFallback.ts`;
|
|
> treat the block above as a snapshot.
|
|
|
|
Two adjacent, **separate** signal tables live in the same file and are _not_ part
|
|
of banned-keyword detection:
|
|
|
|
- `CREDITS_EXHAUSTED_SIGNALS` — billing/quota depleted (`insufficient_quota`,
|
|
`credit_balance_too_low`, `payment required`, …) → terminal `credits_exhausted`.
|
|
- `OAUTH_INVALID_TOKEN_SIGNALS` — **non-terminal**; a token refresh can recover.
|
|
|
|
Note: common transient phrases like **`rate limit`** / `429` are handled by the
|
|
rate-limit / connection-cooldown path and are **not** ban signals.
|
|
|
|
## Detection flow
|
|
|
|
```
|
|
upstream error response
|
|
→ body stringified + lowercased
|
|
→ isAccountDeactivated(body): getMergedBannedSignals().some(sig => body.includes(sig)) [substring match]
|
|
→ match?
|
|
→ connection testStatus = "banned" (permanent — 1-year cooldown, never auto-recovers)
|
|
→ if setting `autoDisableBannedAccounts` is on and `autoDisableBannedScope`
|
|
includes this connection (`all`, or `subscription` for OAuth/cookie/session)
|
|
→ also isActive = false. Prepaid API keys stay active when scope is
|
|
`subscription`.
|
|
→ connection is skipped during account selection (combo QUOTA_BLOCKING statuses)
|
|
```
|
|
|
|
- The match is a **case-insensitive substring** search on the response **body**
|
|
(`isAccountDeactivated`, `accountFallback.ts`).
|
|
- The permanent `banned` terminalization fires on a banned-signal body at **any
|
|
HTTP status** (via `markAccountUnavailable` → `checkFallbackError`). The
|
|
narrower **`deactivated`** label (`isActive=false` when the connection has no
|
|
spare API keys) is written by the inline `chatCore.ts` path on **HTTP 401 / 403**
|
|
(classified via `classifyProviderError` → `ACCOUNT_DEACTIVATED`). Note the
|
|
`markAccountUnavailable()` path writes a _different_ terminal status —
|
|
**`expired`** — for the same `ACCOUNT_DEACTIVATED` signal (via
|
|
`resolveTerminalConnectionStatus`), so the same ban can surface as either
|
|
`deactivated` or `expired` depending on which path handled the response. (The
|
|
older code comment says "when a 401 body contains these strings" — that
|
|
understates the current behavior.)
|
|
- A `banned` connection is excluded from selection everywhere terminal statuses
|
|
are filtered (`isTerminalConnectionStatus`, combo `QUOTA_BLOCKING_CONNECTION_STATUSES`).
|
|
|
|
## Scope — which providers are scanned
|
|
|
|
**All providers.** The check runs in the generic error-handling pipeline that
|
|
every failed upstream request flows through — it is **not** gated to
|
|
OAuth/subscription scrapers. The resulting terminal state is per **connection**,
|
|
not per provider.
|
|
|
|
That said, the built-in _strings_ are oriented toward subscription/OAuth
|
|
providers with real ban risk (ChatGPT Web Codex, Claude Web, Codex, Muse Spark,
|
|
Antigravity). An API-key provider will only trip the detector if its error body
|
|
literally contains one of the substrings.
|
|
|
|
`autoDisableBannedScope` (`all` | `subscription`, default `all`) controls whether
|
|
a match also flips `isActive=false`. `subscription` means login-style seats
|
|
(paid subscriptions and free accounts, including web-cookie sessions). It still
|
|
records `testStatus=banned` for prepaid API keys but leaves them in the routing
|
|
pool. The durable design is a per-provider and per-account override; the global
|
|
enum is the first cut.
|
|
|
|
## Custom banned keywords
|
|
|
|
Add or remove keywords in **Security → Banned Keywords** (persisted as the global
|
|
`customBannedSignals` setting via `PATCH /api/settings`). They are **added to**
|
|
the built-in list — never a replacement — and hot-reload on save (and at startup)
|
|
via `setCustomBannedSignals()`. Each keyword is capped at 200 characters; there is
|
|
no array-length limit.
|
|
|
|
**⚠ False-positive risk — choose specific phrases.** Detection is a raw substring
|
|
match on the whole response body, and a match is **permanent** (1-year cooldown,
|
|
manual recovery). A broad keyword can ban a perfectly healthy connection:
|
|
|
|
- **Bad:** `quota`, `limit`, `error`, `denied` — appear in many transient errors.
|
|
- **Good:** full ban sentences, e.g. `your account has been suspended for`,
|
|
`account permanently banned`, `violation of our terms`.
|
|
|
|
Prefer the longest unambiguous phrase the provider returns on a real ban. When in
|
|
doubt, watch the connection's `lastError` first, then add the exact wording.
|
|
|
|
## Recovering a flagged connection
|
|
|
|
Terminal `banned` / `deactivated` states **never auto-recover** (they are excluded
|
|
from the proactive-recovery tick — only `unavailable` cooldowns recover on their
|
|
own). An operator must clear them explicitly:
|
|
|
|
1. **Re-test the connection** — the dashboard **Test** action
|
|
(`POST /api/providers/{id}/test`); a successful probe resets `testStatus` to
|
|
`active` and clears the error fields.
|
|
2. **Re-authenticate / edit credentials** — for OAuth providers, re-run the login
|
|
/ refresh flow; provider create/import routes set `isActive = true`.
|
|
3. **Re-enable the connection** — if auto-disable set `isActive = false`
|
|
(scope `all`, or `subscription` for an OAuth/cookie/session connection),
|
|
toggle it back on after fixing the account.
|
|
|
|
There is no separate "clear ban flag" button — recovery is re-test, re-auth, or
|
|
re-enable, matching the general terminal-state rule in
|
|
[RESILIENCE_GUIDE](../architecture/RESILIENCE_GUIDE.md).
|
|
|
|
## Probe isolation (model test-all)
|
|
|
|
A **probe-origin failure** (model test-all / health-check dispatches executed
|
|
inside `runAsProbe`) never removes a connection from the pool (#9817): it is
|
|
**recorded for visibility** (`last_error`, `last_error_type`, `error_code`,
|
|
`last_error_at`) but skips **every** routing mutation — cooldowns, terminal
|
|
status (`banned` / `deactivated` / `credits_exhausted`), per-model lockouts,
|
|
the provider circuit breaker, the 5-minute quota cache, OAuth token refresh
|
|
and auto-disable. Only a real request-path failure deactivates. The recorded
|
|
error is what makes a flagged account visible in the dashboard while it stays
|
|
serving traffic.
|
|
|
|
The single decision point is `shouldIsolateProbeFailures()`
|
|
(`src/shared/utils/probeOrigin.ts`), consulted by **every** site that could
|
|
mutate routing state from a probe-origin failure:
|
|
|
|
- `markAccountUnavailable` (`auth.ts`) — record-only (`lastError` raw text,
|
|
`lastErrorType`, `errorCode`, `lastErrorAt`; deliberately **no**
|
|
`backoffLevel`, which would trigger the selection-time auto-decay and wipe
|
|
the record)
|
|
- `maybeAutoDisableBannedAccount` — no auto-disable
|
|
- `chatCore` — FORBIDDEN, ACCOUNT_DEACTIVATED, QUOTA_EXHAUSTED (record-only,
|
|
no terminal `credits_exhausted`), GEO_BLOCKED (no 24h exclusion),
|
|
MODEL_NOT_FOUND (no `lockModel`), the codex 429 account-rotation failover
|
|
(no `markCodexScopeRateLimited`, no persisted `rate_limited_until`, no
|
|
session-affinity clear), `persistCodexQuotaState` (no quota-state write,
|
|
no cache invalidation), `recordKeyHealthStatus` (key-health rotator
|
|
untouched)
|
|
- OAuth refresh — both the proactive refresh in the executor base
|
|
(`base.ts` `execute()`, no refresh-token rotation consumed) and the
|
|
reactive 401/403 path in `chatCore` (no `expired` deactivation)
|
|
- `chat.ts` — provider circuit breaker and the 5-minute quota cache
|
|
(`markAccountExhaustedFrom429`) never degraded
|
|
|
|
The recorded error is what makes a flagged account visible in the dashboard
|
|
while it stays serving traffic. Note: the probe record stores the **raw**
|
|
(unsliced) error text, unlike the real path's `slice(0,100)` truncation.
|
|
|
|
Operators who use test-all as a maintenance tool can restore the historical
|
|
behavior (probe counts as a real generation) via either:
|
|
|
|
- the `probeCanDisable` setting (`POST /api/settings` with
|
|
`{"probeCanDisable": true}`, or a direct `key_value` DB edit), or
|
|
- feature flag **`PROBE_CAN_DISABLE=true`** (env or DB override; wins over the
|
|
setting).
|
|
|
|
Fail-safe: if the flag or settings lookup throws, isolation stays ON.
|
|
|
|
## Source files
|
|
|
|
| Concern | File |
|
|
| --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
| Signal tables + match | `open-sse/services/accountFallback.ts` |
|
|
| Terminalization / persistence | `src/sse/services/auth.ts` (`markAccountUnavailable`, `resolveTerminalConnectionStatus`, `clearAccountError`) |
|
|
| Auto-disable scope | `src/shared/utils/autoDisableBanned.ts`, `src/sse/services/autoDisableBannedAccount.ts` |
|
|
| Inline classification | `open-sse/handlers/chatCore.ts`, `open-sse/services/errorClassifier.ts` |
|
|
| Terminal-state recovery exclusion | `src/lib/quota/connectionRecovery.ts` |
|
|
| Custom-keyword runtime load | `src/lib/config/runtimeSettings.ts` (`setCustomBannedSignals`) |
|
|
| Settings UI | `src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx` |
|