Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -1495,6 +1495,20 @@ APP_LOG_TO_FILE=true
# healthy-result cache window under high concurrency.
# PROXY_HEALTH_UNHEALTHY_CACHE_TTL_MS=2000

# Background proxy health scheduler (src/lib/proxyHealth/scheduler.ts).
# Periodically probes every registered proxy and (optionally) removes dead ones.
# Set "false" to disable the scheduler entirely. Default: enabled.
# PROXY_HEALTH_ENABLED=true
# Sweep interval in ms (minimum 60000). Default: 600000 (10min).
# PROXY_HEALTH_INTERVAL_MS=600000
# Reachability probe target for the scheduler and the auto-test endpoint.
# Point it at an internal/self-hosted URL to avoid the public default.
# PROXY_HEALTH_TEST_URL=https://httpbin.org/ip
# Set "true" to let the scheduler auto-remove proxies after repeated failures.
# PROXY_AUTO_REMOVE=false
# Consecutive failures before an auto-remove fires. Default: 3.
# PROXY_AUTO_REMOVE_AFTER=3

# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
# directly when proxy reachability pre-checks fail. Default: false.
# Also configurable from Dashboard > Settings > Feature Flags.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
- **feat(xai):** surface Grok usage on the quota dashboard via local usage-history aggregation. (thanks @DevEstacion)
- **feat(services):** add **Mux** (`coder/mux`) as a managed embedded service — install/start/stop/restart/logs lifecycle + dashboard tab, loopback-only API, `127.0.0.1`-bound with the auth token passed via env (never argv). Ported from upstream 9router#1802. (thanks @Ansh7473)
- **feat(services):** promote **Bifrost** (`@maximhq/bifrost`) to a supervised embedded service ([#5670](https://github.com/diegosouzapw/OmniRoute/issues/5670)) — full install/start/stop/restart/update/status/auto-start lifecycle + dashboard tab, loopback-only API (hard rule #17). When a supervised Bifrost is running and `BIFROST_BASE_URL` is unset, the relay route auto-selects it as the routing backend (`getBifrostRoutingConfig()`); an explicit `BIFROST_BASE_URL` always takes precedence.
- **feat(proxy):** proxy-registry batch management ([#5918](https://github.com/diegosouzapw/OmniRoute/pull/5918)) — `POST /api/settings/proxies/batch-delete` (delete many proxies in one request) and `POST /api/settings/proxies/auto-test` (reachability-test proxies with optional auto-remove of dead ones), plus a background health scheduler (`src/lib/proxyHealth/scheduler.ts`) that periodically probes registered proxies and can auto-remove persistently-dead ones. Configurable via `PROXY_HEALTH_ENABLED` / `PROXY_HEALTH_INTERVAL_MS` / `PROXY_HEALTH_TEST_URL` / `PROXY_AUTO_REMOVE` / `PROXY_AUTO_REMOVE_AFTER` (the probe target is now operator-configurable instead of a hardcoded endpoint). Dashboard gains batch-select checkboxes, a "Test All" action, and colored proxy health indicators. Regression guard: `tests/unit/proxy-batch-routes-5918.test.ts`. (thanks @oyi77)

### 🔧 Bug Fixes

- **providers (alias resolution followed only one hop):** `resolveProviderAlias()` did a single-hop lookup, so a genuine two-hop alias chain (`oc` → `opencode` → `opencode-zen`, where the parent OpenCode provider registers `alias: "oc"` and a manual override maps `opencode` → the `opencode-zen` free tier) resolved `oc/<model>` to the intermediate `opencode` instead of the final provider. Resolution now follows the chain transitively, guarded by both a depth limit and a seen-set so cycles can't loop. Regression guard: `tests/unit/provider-alias-transitive-5918.test.ts`. ([#5918](https://github.com/diegosouzapw/OmniRoute/pull/5918) — thanks @oyi77)

- fix(providers): strip orphan tool_result blocks on the Antigravity MITM path before forwarding to Claude ([#6026](https://github.com/diegosouzapw/OmniRoute/issues/6026))

- **dashboard ("Update now" → Internal Server Error):** clicking **Update now** on the dashboard home could crash the page with a blank "Internal Server Error" screen (`Minified React error #31`). The handler POSTs the loopback-only `/api/system/version` auto-update endpoint and, on a non-OK JSON response (e.g. a `403` when the dashboard is reached through a reverse proxy / non-loopback origin), passed the raw error envelope object `{ error: { code, message, correlation_id } }` straight to `notify.error()`, which rendered the object as a React child and threw #31. The update-error path now funnels the body through `extractApiErrorMessage()` (the same safe extractor added in #5340), so a readable string always reaches the toast. Regression guard: `tests/unit/ui/home-update-error-render-5991.test.ts`. ([#5991](https://github.com/diegosouzapw/OmniRoute/issues/5991))
Expand Down
3 changes: 2 additions & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@
"src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx": 974,
"src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 898,
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012,
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1089,
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1117,
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1183,
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629,
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924,
Expand Down Expand Up @@ -342,6 +342,7 @@
"_rebaseline_2026_06_13_2743d_skipbreaker": "Re-baseline #2743 gap-d (testar consumer do skipProviderBreaker): combo.ts 5131→5162 (+31). Crescimento = extração do boolean inline da decisão de circuit-breaker para o predicado puro EXPORTADO shouldRecordProviderBreakerFailure() (byte-idêntico) + JSDoc, para torná-lo unit-testável sem o harness completo de combo. Shrink estrutural segue com #3501.",
"_rebaseline_2026_06_13_v3825_prettier_reconcile": "Reconciliação tardia: o prettier do pre-commit reformatou 3 arquivos DEPOIS da medição de file-size dos PRs, inflando linhas além do baseline setado — OAuthModal.tsx 956→960 e providers.ts 3146→3147 (#3324), combo.ts 5162→5164 (#2743d). Bumps de reformatação automática (sem lógica nova). LIÇÃO: medir file-size pós-commit (pós-prettier), não antes.",
"_rebaseline_2026_06_14_3826_release_drift": "Re-baseline release/v3.8.25 drift already documented from #3809 owner changes: ProxyRegistryManager.tsx 1072→1089, sidebarVisibility.ts 990→1006, schemas.ts 2519→2522. This PR does not touch those source files; updating the frozen values restores Fast Quality Gates on the current release branch.",
"_rebaseline_2026_07_03_5918_proxy_batch": "PR #5918 own growth: ProxyRegistryManager.tsx 1089→1117 (+28 = wiring the new batch-select/Test-All proxy management components — checkboxes, batch actions bar, health cells). Cohesive UI wiring for the batch-delete/auto-test feature; the reusable pieces already live in separate leaf components (ProxyBatchActions/ProxyCheckboxCell/ProxyHealthCell/useProxyBatchOperations). Legitimate feature growth, not a quality regression.",
"_rebaseline_2026_06_14_3825_combo_stickiness": "Re-baseline #3825 (sessionless combo stickiness + reasoning-aware readiness): combo.ts 5164→5198 (+34, pós-prettier). Crescimento = deriveComboSessionKey() + effectiveSessionId threading nos sites de read/write do pin server-side. streamReadinessPolicy.ts não-frozen (sob cap). Lógica coesa no handler de combo; não-extraível.",
"_rebaseline_2026_06_14_r3_3835_kiro_pricing": "PR #3835 own growth: pricing.ts 1470→1508 (+38 = missing Kiro pricing rows, claude-sonnet-4.6 etc., pure data). Also carries inherited release/v3.8.25 drift not yet frozen by prior r2 merges: RequestLoggerV2.tsx 1276→1282 (#3820 resizable log table) and combo.ts 5198→5203 (#3811 round-robin replay-response fix). This PR does not touch RequestLoggerV2/combo.ts source; updating the frozen values restores Fast Quality Gates on the current release branch.",
"_rebaseline_2026_06_14_r3_3849_transient_hide": "PR #3849 own growth: providerPageHelpers.ts 939→955 (+16 = expanded JSDoc on the auto-hide policy + transient-failure guard in evaluateTestAllEntry). Cohesive helper logic; not extractable.",
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,11 @@ Anthropic-compatible provider instead.
| `PROXY_FAST_FAIL_TIMEOUT_MS` | `2000` | `src/lib/proxyHealth.ts` | Fast-fail health check timeout. |
| `PROXY_HEALTH_CACHE_TTL_MS` | `30000` | `src/lib/proxyHealth.ts` | Health check result cache TTL. |
| `PROXY_HEALTH_UNHEALTHY_CACHE_TTL_MS` | `2000` | `src/lib/proxyHealth.ts` | Cache TTL for failed proxy health probes. Keep this shorter than `PROXY_HEALTH_CACHE_TTL_MS` so transient proxy timeouts under high concurrency retry quickly without disabling fast-fail for truly dead proxies. |
| `PROXY_HEALTH_ENABLED` | `true` | `src/lib/proxyHealth/scheduler.ts` | Set `false` to disable the background proxy health scheduler that periodically probes registered proxies. |
| `PROXY_HEALTH_INTERVAL_MS` | `600000` | `src/lib/proxyHealth/scheduler.ts` | Background health-scheduler sweep interval in ms (minimum `60000`). |
| `PROXY_HEALTH_TEST_URL` | `https://httpbin.org/ip` | `src/lib/proxyHealth/scheduler.ts` | Reachability probe target used by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Point it at an internal/self-hosted URL to avoid the public default. |
| `PROXY_AUTO_REMOVE` | `false` | `src/lib/proxyHealth/scheduler.ts` | Set `true` to let the scheduler auto-remove proxies after repeated consecutive failures. |
| `PROXY_AUTO_REMOVE_AFTER` | `3` | `src/lib/proxyHealth/scheduler.ts` | Consecutive failures before the scheduler auto-removes a proxy (when `PROXY_AUTO_REMOVE=true`). |
| `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | `false` | `src/shared/constants/featureFlagDefinitions.ts` | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Effective precedence is Feature Flags DB override > env var > default. |
| `RATE_LIMIT_MAX_WAIT_MS` | `120000` (2 min) | `open-sse/services/rateLimitManager.ts` | Max time to wait on a 429 before failing the request. |
| `RATE_LIMIT_AUTO_ENABLE` | _(unset)_ | `open-sse/services/rateLimitManager.ts` | Force the auto-enable rate limit safety net on/off regardless of the persisted Dashboard setting. Accepts `true`/`1`/`on` to force on, `false`/`0`/`off` to force off. |
Expand Down
19 changes: 13 additions & 6 deletions open-sse/services/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ for (const [id, alias] of Object.entries(PROVIDER_ID_TO_ALIAS)) {
// or backward-compatible slug changes, not a single provider's display name.
// opencode/ → opencode-zen (the main free/open tier; opencode-go is a separate paid tier)
ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen";

// Manual aliases for external compatibility not covered by PROVIDER_ID_TO_ALIAS.
// OpenCode's Zen provider now uses the "opencode" slug, but OmniRoute registers
// it as "opencode-zen". This alias ensures `opencode/<model>` resolves correctly.
ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen";
// xiaomi/ is the user-visible prefix for MiMo models; register it so
// parseModel("xiaomi/mimo-v2-flash") resolves provider = "xiaomi-mimo" instead
// of falling through to the identity fallback ("xiaomi").
Expand Down Expand Up @@ -147,7 +142,19 @@ interface ProviderConnectionLike {
*/
export function resolveProviderAlias(aliasOrId: string | null | undefined): string | null {
if (typeof aliasOrId !== "string") return null;
return ALIAS_TO_PROVIDER_ID[aliasOrId] || aliasOrId;
// Follow the alias chain transitively so intermediate aliases
// (e.g. "oc" -> "opencode" -> "opencode-zen") resolve to the final target.
// Guarded against infinite loops with both a depth limit and a seen-set.
let current = aliasOrId;
const seen = new Set<string>();
for (let i = 0; i < 10; i++) {
const next = ALIAS_TO_PROVIDER_ID[current];
if (!next || next === current) return current;
if (seen.has(next)) return next;
seen.add(next);
current = next;
}
return current;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { useTranslations } from "next-intl";
import { Button } from "@/shared/components";

interface ProxyBatchActionsProps {
selectedCount: number;
batchDeleting: boolean;
autoTesting: boolean;
onBatchDelete: () => void;
onAutoTestAll: () => void;
}

export function ProxyBatchActions({
selectedCount,
batchDeleting,
autoTesting,
onBatchDelete,
onAutoTestAll,
}: ProxyBatchActionsProps) {
const t = useTranslations("proxyRegistry");

return (
<>
{selectedCount > 0 && (
<>
<span className="text-xs text-text-muted">
{t("batchSelectedCount", { count: selectedCount })}
</span>
<Button
size="sm"
variant="secondary"
icon="delete"
onClick={onBatchDelete}
loading={batchDeleting}
className="!text-red-400 !border-red-500/30"
data-testid="proxy-registry-batch-delete"
>
{t("batchDeleteSelected", { count: selectedCount })}
</Button>
</>
)}
<Button
size="sm"
variant="secondary"
icon="network_check"
onClick={onAutoTestAll}
loading={autoTesting}
data-testid="proxy-registry-test-all"
>
{t("testAll")}
</Button>
</>
);
}
Loading
Loading