diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a283a411f..c61af02707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### 🐛 Fixed - **fix(intelligence): run pricing + models.dev sync from the live startup path** — like the Arena ELO sync (v3.8.24), the external **pricing sync** (`PRICING_SYNC_ENABLED`) and the **models.dev capability sync** (Settings → AI toggle) were only initialized from `server-init.ts`, which the Next standalone runtime never executes — and models.dev had no caller at all. Their toggles were inert in production. Both are now initialized from `instrumentation-node.ts` (self-gated, opt-in preserved, non-blocking, never fatal). (thanks @diegosouzapw) +- **test(proxy): guard the per-connection 'direct' bypass over a global proxy + clearer label** — the per-connection "Proxy Off" toggle (`proxyEnabled: false`) already overrides a configured **global** proxy (`resolveProxyForConnection` short-circuits to `level: "direct"` before the global step). Added an explicit regression test proving the bypass beats a global assignment (and round-trips on re-enable), and relabeled the UI to "Direct (bypass proxy)" so operators recognize it. Closes the verification gap in [#2996](https://github.com/diegosouzapw/OmniRoute/issues/2996). (thanks @diegosouzapw) - **feat(connections): per-connection "disable cooldown" opt-out** — a connection can now opt out of the transient cooldown (`providerSpecificData.disableCooling`, with a toggle in the Edit Connection modal). When set, a recoverable failure still records the error/backoff but does **not** take the connection out of rotation, so it stays eligible for selection — useful for a primary key you never want parked on a blip. Terminal states (banned / expired / credits_exhausted) still apply. ([#2997](https://github.com/diegosouzapw/OmniRoute/issues/2997) — thanks @diegosouzapw) - **fix(combo): restore sessionless combo stickiness + reasoning-aware readiness (504 / TPS regression after v3.8.14)** — #3399 (v3.8.16) replaced the ``-tag combo pinning with a server-side context-cache pin gated on a client `sessionId`. Clients that send no session id (most OpenAI-compatible tools) lost combo stickiness, so combos re-ran strategy selection every turn → upstream prompt-cache misses → cold high-reasoning starts (~78s) → intermittent `[504] Upstream request did not return response headers` + TPS collapse (only on combos). The pin now falls back to a stable per-conversation fingerprint (`extractSessionAffinityKey(body)`) when no session id is present — **only when `context_cache_protection` is on**, so #3399's anti-leak behaviour is preserved. Separately, the stream-readiness window now grants the +30s reasoning budget **unconditionally** for high-reasoning Codex GPT-5.x (small high-reasoning prompts were 504-ing at the 80s base regardless of stickiness). ([#3825](https://github.com/diegosouzapw/OmniRoute/issues/3825) — thanks @bypanghu) - **test(combo): cover the `skipProviderBreaker` consumer gate** — the producer was tested but the consumer (whether a failed combo target trips the whole-provider circuit breaker) was not; the breaker decision is now an exported pure predicate (`shouldRecordProviderBreakerFailure`, behaviour-identical) with direct tests asserting a `connection_cooldown` 503 does not trip the breaker while a plain 503 does. Closes another deferred test gap from [#2743](https://github.com/diegosouzapw/OmniRoute/issues/2743). (thanks @diegosouzapw) diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 21ebe6a8bd..b0e2ec8c2d 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -4179,9 +4179,9 @@ "proxySourceKey": "Key", "proxyConfiguredBySource": "Proxy ({source}): {host}", "proxyOn": "Proxy On", - "proxyOff": "Proxy Off", + "proxyOff": "Direct (bypass proxy)", "proxyEnabledTitle": "Proxy is enabled for this connection", - "proxyDisabledTitle": "Proxy is disabled for this connection", + "proxyDisabledTitle": "Direct connection — bypasses every proxy (including the global proxy) for this connection", "perKeyProxyOn": "Per-key", "perKeyProxyOff": "Connection", "perKeyProxyEnabledTitle": "Per-key proxy assignment enabled for this provider", diff --git a/tests/unit/proxy-registry.test.ts b/tests/unit/proxy-registry.test.ts index 4de351fcee..c2b747ca5d 100644 --- a/tests/unit/proxy-registry.test.ts +++ b/tests/unit/proxy-registry.test.ts @@ -417,6 +417,61 @@ test("connection proxy toggle gates account assignments and invalidates cached r assert.equal((enabledResolved.proxy as any).host, "pool-proxy.local"); }); +// #2996: Per-connection proxy 'direct' bypass. A connection with proxyEnabled:false +// must go DIRECT even when a GLOBAL proxy is configured. The existing toggle test +// (above) only proves the bypass beats an ACCOUNT-scoped assignment; this guards the +// exact scenario the issue requests — the per-connection bypass overriding a configured +// GLOBAL proxy (resolveProxyForConnection step 9, "global" registry level), and that it +// is a clean two-way toggle (re-enabling returns to the global proxy). +test("per-connection proxy 'direct' bypass overrides a configured GLOBAL proxy (#2996)", async () => { + await resetStorage(); + + const globalProxy = await proxiesDb.createProxy({ + name: "Global Bypass Proxy", + type: "http", + host: "global-bypass.local", + port: 8080, + }); + await proxiesDb.assignProxyToScope("global", null, globalProxy.id); + + // Use a provider name unique to this test so a registry/legacy provider-scoped + // assignment left over from another test in the shared process cannot resolve at + // step 6/8 and mask the GLOBAL precondition we are asserting (mirrors the hermetic + // unique-provider pattern used by the "connection proxy toggle gates" test above). + const connection = await providersDb.createProviderConnection({ + provider: "proxy-global-bypass-2996-provider", + authType: "apikey", + name: "Global Bypass Account", + apiKey: "sk-global-bypass", + }); + + // No per-connection assignment: the connection should inherit the GLOBAL proxy. + const globalResolved = await settingsDb.resolveProxyForConnection((connection as any).id); + assert.equal(globalResolved.level, "global"); + assert.ok(globalResolved.proxy, "expected the global proxy to be resolved before bypass"); + assert.equal((globalResolved.proxy as any).host, "global-bypass.local"); + + // Per-connection Proxy Off must override the configured GLOBAL proxy → direct. + const disabled = await providersDb.updateProviderConnection((connection as any).id, { + proxyEnabled: false, + }); + assert.equal((disabled as any).proxyEnabled, false); + + const disabledResolved = await settingsDb.resolveProxyForConnection((connection as any).id); + assert.equal(disabledResolved.level, "direct"); + assert.equal(disabledResolved.proxy, null); + + // Re-enabling restores the GLOBAL proxy — proves it is a clean toggle, not one-way. + const enabled = await providersDb.updateProviderConnection((connection as any).id, { + proxyEnabled: true, + }); + assert.equal((enabled as any).proxyEnabled, true); + + const enabledResolved = await settingsDb.resolveProxyForConnection((connection as any).id); + assert.equal(enabledResolved.level, "global"); + assert.equal((enabledResolved.proxy as any).host, "global-bypass.local"); +}); + test("provider connection proxy toggle fields round-trip as booleans", async () => { await resetStorage();