Repository navigation
feat(proxy): support custom proxy routing for api keys and bypass loopback in fetch dispatcher - #873
Conversation
…pback in fetch dispatcher - configure Undici EnvHttpProxyAgent with default loopback bypass (localhost, 127.0.0.1, ::1) and support targetProxyUrl dispatching - add proxyId field and PATCH /auth/api-keys/:id/proxy endpoint to allow per-key proxy routing - resolve direct, global, auto, and proxy UUID assignments through ProxyPool and inject into upstream adapters - expose proxy selector in web UI for adding and managing API keys
SsuJojo
left a comment
There was a problem hiding this comment.
Request Changes
Solid feature overall (loopback bypass via EnvHttpProxyAgent, per-key proxy routing, dispatcher caching, adapter cache invalidation on proxyId all look right, and tests are good), but there is one behavioral bug:
🐛 null proxyId is treated as direct, but the UI sends null for "Global Default"
In KeyRow (web/src/components/ApiKeyManager.tsx), selecting Global Default calls onProxyChange(entry.id, null), and ApiKeyPool.add stores proxyId: input.proxyId ?? null. However ProxyPool.resolveProxyUrlById treats null as direct connection:
if (proxyId === undefined || proxyId === "global") return undefined; // global
if (proxyId === null || proxyId === "direct") return null; // directThis conflicts with account assignments, where unset = "global" (see ProxyPool.getAssignment). Net effect: any API key added/edited through the UI with "Global Default" selected will silently bypass the global proxy and connect directly — the opposite of the label.
Suggested fix (pick one)
- Map UI "global" →
undefined(and storeundefined/omit the field), treating only the literal"direct"as direct; or - Invert
resolveProxyUrlByIdsemantics fornull→ global and havesetProxy/addstore"global"explicitly.
Minor
src/routes/api-keys.tsPATCH/proxyaccepts any string ≤64 chars without validating"global" | "direct" | "auto" | <known proxy uuid>; invalid values silently degrade to global. Consider validating (the accountassignendpoint already does).- The
proxyId?.startsWith("http")fallback duplicated inadapter-factory.tsandembeddings.tsis undocumented magic; prefer routing it throughProxyPoolor removing it. createRuntimeUpstreamRouternow accepts either anAdapterFactoryor aProxyPoolin the same positional arg — consider two distinct params/overload for clarity.
Happy to approve once the null/global semantics are fixed. Thanks!
…xy assignments
- treat null/undefined/global proxyId as global default (undefined dispatcher) in ProxyPool and createAdapterForEntry, reserving direct connection (null dispatcher) strictly for 'direct'
- normalize 'global' proxyId to null in ApiKeyPool
- validate proxyId on POST /auth/api-keys and PATCH /auth/api-keys/:id/proxy against configured ProxyPool
- remove undocumented startsWith('http') fallback in adapter-factory and embeddings routes
- add function overloads to createRuntimeUpstreamRouter for cleaner proxyPool injection
|
Thanks for the review @SsuJojo! All feedback addressed in 35561b6:
|
SsuJojo
left a comment
There was a problem hiding this comment.
Fix verified in 35561b6:
- resolveProxyUrlById now treats null/undefined/"global" as global default, reserving "direct" for direct connection — semantics now consistent with account assignments.
- normalizeProxyId normalizes "global" to null at add/setProxy/persisted-reload.
- proxyId validated against ["global","direct","auto"] + registered proxy UUIDs (400 on unknown) on both POST and PATCH.
- Undocumented startsWith("http") fallback removed from adapter-factory and embeddings.
- createRuntimeUpstreamRouter now has proper TypeScript overloads; call site in index.ts updated.
- Regression tests added for all of the above; CI green.
Thanks for the quick turnaround! Approving.
Summary
This PR enables per-API-Key proxy routing configuration across the proxy pipeline and bypasses loopback addresses in the global fetch dispatcher. Previously, Undici's global proxy agent intercepted local loopback addresses (e.g.
127.0.0.1,localhost) whenever an external proxy was configured or detected, causing local models (like Ollama or local backends) to fail when offline or when the proxy rejected loopback traffic. Furthermore, third-party API keys can now route through dedicated proxies or bypass them (global,direct,auto, or specific proxy node UUID).Changes
src/proxy/fetch-dispatcher.ts):EnvHttpProxyAgentwith default loopback bypass (localhost,127.0.0.1,::1) merged withNO_PROXY/no_proxy.getFetchDispatcher(targetProxyUrl)andwithFetchDispatcher(init, dispatcherOrProxyUrl).src/auth/api-key-pool.ts,src/proxy/proxy-pool.ts,src/routes/api-keys.ts):ApiKeyEntrywith optionalproxyId?: string | nullfield and persistence.PATCH /auth/api-keys/:id/proxyendpoint andsetProxymethod.resolveProxyUrlByIdinProxyPoolto handledirect(returnsnull),global(returnsundefined),auto(round-robin), and proxy UUID lookup.src/proxy/adapter-factory.ts,src/proxy/upstream-router.ts,src/proxy/upstream-router-bootstrap.ts, upstream implementations):OpenAIUpstream,AnthropicUpstream,GeminiUpstream,ResponsesUpstream,AntigravityUpstream) now receive injected custom dispatchers.proxyIdfor hot-swapping proxy routes on key updates.shared/hooks/use-api-keys.ts,web/src/components/ApiKeyManager.tsx):CHANGELOG.md):[Unreleased]with Added and Fixed entries.Test Plan
npx tsc --noEmitnpm run build:webnpx vitest run tests/unit/proxy/fetch-dispatcher.test.ts tests/unit/proxy/fetch-dispatcher-loopback.test.ts tests/unit/auth/api-key-pool.test.ts tests/unit/proxy/proxy-pool-resolve.test.ts tests/unit/proxy/adapter-factory.test.ts tests/unit/routes/api-keys.test.tsnpm run test:unit(285 test files, 3005 tests passed)