Skip to content

fix(proxy): fail closed for OAuth usage account proxies - #3051

Merged
diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.8from
terence71-glitch:fix/claude-provider-limits-fail-closed
Jun 1, 2026
Merged

diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.8from
terence71-glitch:fix/claude-provider-limits-fail-closed

Conversation

@terence71-glitch

Copy link
Copy Markdown
Contributor

Problem

Provider-limits / usage sync resolves the selected connection proxy before fetching usage for OAuth connections. However, if that proxied usage fetch fails with a network/proxy error, the code currently retries the same usage fetch without any proxy:

result = await fetchUsageWithContext(null);

That direct retry is unsafe for account-isolated setups. If a connection has an explicit account/key proxy configured, provider-limits traffic is account-bound traffic and should not silently fall back to the OmniRoute host IP when the account proxy is unavailable.

This was first identified through Claude/Anthropic hardening, where risky endpoints include:

  • https://api.anthropic.com/api/oauth/usage
  • https://api.anthropic.com/api/claude_cli/bootstrap
  • legacy usage fallback endpoints under https://api.anthropic.com/v1/...

But the isolation rule is broader than Claude:

An explicit account/key proxy means traffic for that OAuth connection should either use that account proxy or fail closed.

Solution

Add a narrow fail-closed policy in src/lib/usage/providerLimits.ts for OAuth connections whose resolved proxy is account-scoped:

function isAccountScopedProxyResolution(proxyInfo: unknown): boolean {
  if (!isRecord(proxyInfo)) return false;
  if (!proxyInfo.proxy) return false;
  return proxyInfo.level === "key" || proxyInfo.level === "account";
}

function shouldFailClosedForProviderLimitsProxy(
  connection: ProviderConnectionLike,
  proxyInfo: unknown
): boolean {
  return connection.authType === "oauth" && isAccountScopedProxyResolution(proxyInfo);
}

When this policy applies:

  • thrown proxy/network failures are re-thrown instead of retried directly
  • usage responses that contain proxy/network failure messages are converted into a 503 failure instead of retried directly

This keeps the change scoped to explicit account isolation:

OAuth + key/account proxy + proxy works   -> usage sync works through proxy
OAuth + key/account proxy + proxy fails   -> fail closed; no direct retry
OAuth + provider/global proxy + failure   -> preserve existing direct retry behavior
No account/key proxy                      -> preserve existing behavior

Tests

Added tests/unit/provider-limits-proxy-fail-closed.test.ts.

Coverage:

  1. Claude provider limits fail closed when an account proxy is unreachable

    • creates a Claude OAuth connection
    • assigns an unreachable key/account proxy
    • calls fetchAndPersistProviderLimits()
    • verifies the call rejects
    • verifies no direct Anthropic usage fetch is attempted
  2. non-Claude OAuth provider limits fail closed when an account proxy is unreachable

    • creates a GitHub OAuth connection
    • assigns an unreachable key/account proxy
    • calls fetchAndPersistProviderLimits()
    • verifies the call rejects
    • verifies no direct usage fetch is attempted
  3. Claude provider limits preserve direct retry for non-account proxy failures

    • creates a Claude OAuth connection
    • assigns an unreachable provider-level proxy
    • calls fetchAndPersistProviderLimits()
    • verifies the existing direct retry path still succeeds

Targeted test command:

node --import tsx/esm --test tests/unit/provider-limits-proxy-fail-closed.test.ts

Result:

tests 3
pass 3
fail 0

Typecheck command:

npm run typecheck:core

Result:

passed
```or manual validation that reviewers should know about.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a fail-closed mechanism for provider limits when using account-scoped proxies with OAuth connections. Specifically, it adds helper functions to detect account-scoped proxy resolutions and prevent falling back to direct retries without a proxy if a proxy-related network failure occurs. Additionally, comprehensive unit tests have been added under tests/unit/ to verify this behavior for both Claude and non-Claude OAuth providers, while ensuring that non-account proxy failures still preserve the existing direct retry behavior. No review comments were provided, and the changes look solid and conform to the repository style guides.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@diegosouzapw
diegosouzapw merged commit 5350b5e into cheaper-inference:release/v3.8.8 Jun 1, 2026
2 checks passed
diegosouzapw added a commit that referenced this pull request Jun 2, 2026
…add contributor hall

Audited all 687 commits / 60 release/v3.8.8 PRs since v3.8.7 against the CHANGELOG:

- Added 5 missing Fixed entries: #3052 (heap-pressure auto-calibration),
  #3051/#3048 (proxy fail-closed + registry assignments, @terence71-glitch),
  #3049/#3046 (session-pool fingerprint rotation + claude-web cf_clearance, @oyi77).
- Credited previously-uncredited contributors: @branben (#2958 scope fix, #2959
  Notion context source) and @JxnLexn (per-API-key stream default mode).
- Added an Added entry for the per-API-key stream default mode feature.
- Added the "🏆 Contributors" hall (24 contributors), matching the v3.8.6 format.

Maintainer fix-PRs (#2966–#3030) are intentionally referenced by their original
issue numbers in the body rather than the fix-PR number; @diegosouzapw is in the hall.
@diegosouzapw diegosouzapw mentioned this pull request Jun 2, 2026
diegosouzapw added a commit that referenced this pull request Jul 17, 2026
* fix(providers): honor configured proxy on Grok Build egress

The grok-cli executor reaches Grok Build over raw `https.request()` (forced
IPv4, to dodge Cloudflare blocking on the direct path) rather than the
process-wide patched `fetch()` that every other executor uses. `https.request()`
never consults the proxy AsyncLocalStorage context, so the proxy the caller
already pinned upstream in chatHelpers.ts (`runWithProxyContext`) was silently
ignored on BOTH grok-cli paths: chat inference (`nativePost`) and OAuth token
refresh (`nativeHttpsPost`, POST https://auth.x.ai/oauth2/token).

User-visible effect: an operator who assigns a proxy to a Grok Build connection
(or provider/global scope) still egresses on the host's real IP — an IP leak
that defeats account-isolation/anonymity setups, and breaks Grok Build entirely
for operators who must egress through a proxy.

Fix is delta-only: `resolveGrokRequestDispatch()` reads the already-resolved
proxy via the shared `resolveProxyForRequest()` and returns either an
HttpsProxyAgent bound to it, or — when no proxy is configured — the existing
forced-IPv4 direct options, unchanged. Only HTTP/HTTPS CONNECT proxies are
supported on this path; an explicitly configured proxy of another kind (SOCKS5)
fails closed rather than silently leaking direct, matching the fail-closed
convention for OAuth/account proxies (#3051). The proxy URL is never logged, so
proxy credentials cannot leak into logs.

Regression test: tests/unit/grok-cli-proxy-selection.test.ts (RED before the fix
— `resolveGrokRequestDispatch` did not exist and both request builders hardcoded
`family: 4` with no agent; GREEN after).

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Inspired-by: decolua/9router#2343

* chore(changelog): fragment for #7244

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…add contributor hall

Audited all 687 commits / 60 release/v3.8.8 PRs since v3.8.7 against the CHANGELOG:

- Added 5 missing Fixed entries: cheaper-inference#3052 (heap-pressure auto-calibration),
  cheaper-inference#3051/cheaper-inference#3048 (proxy fail-closed + registry assignments, @terence71-glitch),
  cheaper-inference#3049/cheaper-inference#3046 (session-pool fingerprint rotation + claude-web cf_clearance, @oyi77).
- Credited previously-uncredited contributors: @branben (cheaper-inference#2958 scope fix, cheaper-inference#2959
  Notion context source) and @JxnLexn (per-API-key stream default mode).
- Added an Added entry for the per-API-key stream default mode feature.
- Added the "🏆 Contributors" hall (24 contributors), matching the v3.8.6 format.

Maintainer fix-PRs (cheaper-inference#2966–cheaper-inference#3030) are intentionally referenced by their original
issue numbers in the body rather than the fix-PR number; @diegosouzapw is in the hall.
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…inference#7244)

* fix(providers): honor configured proxy on Grok Build egress

The grok-cli executor reaches Grok Build over raw `https.request()` (forced
IPv4, to dodge Cloudflare blocking on the direct path) rather than the
process-wide patched `fetch()` that every other executor uses. `https.request()`
never consults the proxy AsyncLocalStorage context, so the proxy the caller
already pinned upstream in chatHelpers.ts (`runWithProxyContext`) was silently
ignored on BOTH grok-cli paths: chat inference (`nativePost`) and OAuth token
refresh (`nativeHttpsPost`, POST https://auth.x.ai/oauth2/token).

User-visible effect: an operator who assigns a proxy to a Grok Build connection
(or provider/global scope) still egresses on the host's real IP — an IP leak
that defeats account-isolation/anonymity setups, and breaks Grok Build entirely
for operators who must egress through a proxy.

Fix is delta-only: `resolveGrokRequestDispatch()` reads the already-resolved
proxy via the shared `resolveProxyForRequest()` and returns either an
HttpsProxyAgent bound to it, or — when no proxy is configured — the existing
forced-IPv4 direct options, unchanged. Only HTTP/HTTPS CONNECT proxies are
supported on this path; an explicitly configured proxy of another kind (SOCKS5)
fails closed rather than silently leaking direct, matching the fail-closed
convention for OAuth/account proxies (cheaper-inference#3051). The proxy URL is never logged, so
proxy credentials cannot leak into logs.

Regression test: tests/unit/grok-cli-proxy-selection.test.ts (RED before the fix
— `resolveGrokRequestDispatch` did not exist and both request builders hardcoded
`family: 4` with no agent; GREEN after).

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Inspired-by: decolua/9router#2343

* chore(changelog): fragment for cheaper-inference#7244

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…add contributor hall

Audited all 687 commits / 60 release/v3.8.8 PRs since v3.8.7 against the CHANGELOG:

- Added 5 missing Fixed entries: cheaper-inference#3052 (heap-pressure auto-calibration),
  cheaper-inference#3051/cheaper-inference#3048 (proxy fail-closed + registry assignments, @terence71-glitch),
  cheaper-inference#3049/cheaper-inference#3046 (session-pool fingerprint rotation + claude-web cf_clearance, @oyi77).
- Credited previously-uncredited contributors: @branben (cheaper-inference#2958 scope fix, cheaper-inference#2959
  Notion context source) and @JxnLexn (per-API-key stream default mode).
- Added an Added entry for the per-API-key stream default mode feature.
- Added the "🏆 Contributors" hall (24 contributors), matching the v3.8.6 format.

Maintainer fix-PRs (cheaper-inference#2966–cheaper-inference#3030) are intentionally referenced by their original
issue numbers in the body rather than the fix-PR number; @diegosouzapw is in the hall.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…add contributor hall

Audited all 687 commits / 60 release/v3.8.8 PRs since v3.8.7 against the CHANGELOG:

- Added 5 missing Fixed entries: cheaper-inference#3052 (heap-pressure auto-calibration),
  cheaper-inference#3051/cheaper-inference#3048 (proxy fail-closed + registry assignments, @terence71-glitch),
  cheaper-inference#3049/cheaper-inference#3046 (session-pool fingerprint rotation + claude-web cf_clearance, @oyi77).
- Credited previously-uncredited contributors: @branben (cheaper-inference#2958 scope fix, cheaper-inference#2959
  Notion context source) and @JxnLexn (per-API-key stream default mode).
- Added an Added entry for the per-API-key stream default mode feature.
- Added the "🏆 Contributors" hall (24 contributors), matching the v3.8.6 format.

Maintainer fix-PRs (cheaper-inference#2966–cheaper-inference#3030) are intentionally referenced by their original
issue numbers in the body rather than the fix-PR number; @diegosouzapw is in the hall.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…inference#7244)

* fix(providers): honor configured proxy on Grok Build egress

The grok-cli executor reaches Grok Build over raw `https.request()` (forced
IPv4, to dodge Cloudflare blocking on the direct path) rather than the
process-wide patched `fetch()` that every other executor uses. `https.request()`
never consults the proxy AsyncLocalStorage context, so the proxy the caller
already pinned upstream in chatHelpers.ts (`runWithProxyContext`) was silently
ignored on BOTH grok-cli paths: chat inference (`nativePost`) and OAuth token
refresh (`nativeHttpsPost`, POST https://auth.x.ai/oauth2/token).

User-visible effect: an operator who assigns a proxy to a Grok Build connection
(or provider/global scope) still egresses on the host's real IP — an IP leak
that defeats account-isolation/anonymity setups, and breaks Grok Build entirely
for operators who must egress through a proxy.

Fix is delta-only: `resolveGrokRequestDispatch()` reads the already-resolved
proxy via the shared `resolveProxyForRequest()` and returns either an
HttpsProxyAgent bound to it, or — when no proxy is configured — the existing
forced-IPv4 direct options, unchanged. Only HTTP/HTTPS CONNECT proxies are
supported on this path; an explicitly configured proxy of another kind (SOCKS5)
fails closed rather than silently leaking direct, matching the fail-closed
convention for OAuth/account proxies (cheaper-inference#3051). The proxy URL is never logged, so
proxy credentials cannot leak into logs.

Regression test: tests/unit/grok-cli-proxy-selection.test.ts (RED before the fix
— `resolveGrokRequestDispatch` did not exist and both request builders hardcoded
`family: 4` with no agent; GREEN after).

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Inspired-by: decolua/9router#2343

* chore(changelog): fragment for cheaper-inference#7244

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants