Skip to content

[Feature] SOCKS5 proxy support for outbound provider calls - and fail fast on unsupported proxy schemes #2894

Description

@nordz0r

Area

Proxy and routing

What are you trying to accomplish?

Two related needs when reaching model upstreams from restricted networks:

  1. Route all provider traffic through an outbound HTTP or SOCKS5 proxy, including SOCKS5 endpoints exposed by local tunnel clients (sing-box, v2ray/Xray, ssh -D, Tor), which is the most common listen mode for those tools.
  2. Route individual providers through different proxies — and others directly. Example (real): Google Antigravity must exit through a US proxy, xAI through a residential/EU proxy, and zai/GLM should go direct from the local network, because a foreign exit adds latency and triggers regional risk controls on a Chinese upstream. One global proxy for everything cannot express this — exactly the split Omniroute has: a common default proxy plus a per-provider override with a "direct / default / custom" choice.

What prevents this today?

  1. config.proxy supports HTTP(S) proxy URLs only. Bun's fetch (the bundled runtime) does not implement SOCKS — socks5:// is not negotiated (upstream Add SOCKS support oven-sh/bun#16812 open), and worse, Bun silently treats the unknown scheme as HTTP instead of failing (Bun treating unknown proxy protocol as HTTP instead of fail oven-sh/bun#11343). The config layer accepts any string without scheme validation, so proxy: "socks5://127.0.0.1:1080" is stored, mirrored into HTTP_PROXY/HTTPS_PROXY by applyProxyEnv(), and only produces confusing network-level errors at request time. A user with only a SOCKS5 endpoint must spin up an extra HTTP listener (privoxy/glider/sing-box mixed port) as a workaround.
  2. proxy exists only at the top level of OcxConfig (src/types/config.ts:506). OcxProviderConfig (src/types/provider.ts:138) has no proxy field of any kind, so per-provider egress is impossible: either everything goes through the single proxy (regional upstreams suffer), or nothing does (restricted upstreams are unreachable).

What should OpenCodex do?

A. Per-provider proxy with a global default (Omniroute-style two-level model):

  • Global proxy/noProxy keep working exactly as today and become the default for every provider.
  • Each provider gains proxy + noProxy with three effective states:
    • unset → inherit the global default;
    • explicit value → use this provider's own proxy (fully overriding the global for that provider);
    • null / "" → force direct egress, exempting the provider from the global proxy.
  • Effective proxy resolution logged observably: ocx inspect (or ocx provider test <name>) should show which proxy each provider resolves to (global / custom / direct), and ocx doctor should probe each configured proxy.
  • Apply symmetrically to OAuth-backed providers (Antigravity, xAI, ...) — token refresh and quota probes must use the same per-provider egress as model calls, or the account looks healthy while model calls fail.

B. SOCKS5 support:

  • Accept socks5:// (local DNS) and ideally socks5h:// (remote DNS — normally what users behind restrictive networks want) in both the global and per-provider fields; RFC 1928 handshake + RFC 1929 user/pass auth.
  • At minimum, fail fast: validate the scheme at config-set/apply time and reject socks5*:// with a clear message ("SOCKS proxies are not supported yet — use the HTTP inbound of your tunnel client") instead of the current silent misrouting.

Example usage or interface

# Global default stays as-is (all providers inherit it):
ocx config set proxy "http://user:pass@proxy.example.com:8080"

# Per-provider overrides (new):
ocx provider edit google-antigravity --proxy "socks5://127.0.0.1:1080"
ocx provider edit xai                --proxy "http://residential.example.com:3128"
ocx provider edit zai                --proxy ""          # force DIRECT, exempt from the global proxy
// ~/.opencodex/config.json
{
  "proxy": "http://user:pass@proxy.example.com:8080",
  "providers": {
    "google-antigravity": {
      "baseUrl": "https://daily-cloudcode-pa.googleapis.com",
      "adapter": "google-antigravity",
      "proxy": "socks5://127.0.0.1:1080"          // inherits user/pass/noProxy only from itself
    },
    "zai": {
      "baseUrl": "https://api.z.ai/api/coding/paas/v4",
      "adapter": "openai-chat",
      "proxy": null                                 // explicit direct: ignores the global proxy
    }
  }
}

Effective-resolution view (illustrative):

$ ocx provider test google-antigravity
  egress: socks5://127.0.0.1:1080 (provider override) — handshake ok, exit IP 45.xx.xx.xx
$ ocx provider test zai
  egress: direct (provider override: null) — exit IP 91.xx.xx.xx

Alternatives or workarounds

  • Run an HTTP listener next to the SOCKS5 endpoint (sing-box mixed inbound / privoxy / glider) and point the global proxy at it — works, but adds a moving part per machine and still cannot split providers onto different exits.
  • Multiple OpenCodex instances with different global proxy values and split catalogs — heavy, doubles the service/dashboard footprint, and clients must know which port to call.
  • Transparent proxy (TUN mode / firewall rules) routing by process or destination — OS-level, brittle, and cannot distinguish providers that share one upstream domain.

Additional context

Checks

  • I searched existing issues and documentation.
  • This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
  • I removed secrets and personal data.

Activity

  1. github-actions commented on Aug 29, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  2. lidge-jun commented on Aug 29, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 55 / 80

    설명

    제한된 네트워크에서 업스트림으로 나갈 때 (1) HTTP만이 아니라 SOCKS5(sing-box, ssh -D, Tor 등)로 전체 provider 트래픽을 보내고, (2) 프로바이더마다 다른 프록시/다이렉트를 고르고 싶다는 기능 요청입니다. 오늘 config.proxy는 HTTP(S) URL만 가정하고, Bun fetch는 SOCKS를 구현하지 않으며(미구현 이슈 oven-sh/bun#16812), 모르는 스킴을 HTTP로 조용히 취급해 실패가 늦어집니다.

    현재 dev에서 src/config.ts의 proxy→HTTP(S)_PROXY 미러링과 src/cli/doctor.ts의 config.proxy 진단이 이 표면입니다. SOCKS5 문자열을 검색해도 제품 경로에 정식 지원은 없습니다. Omniroute식 "기본 프록시 + 프로바이더별 override(direct/default/custom)"는 account-pool 라벨과도 맞닿지만, 지금은 라우팅 전략(#2050 combo, #2875 kiro quota)과 다른 전송 계층 요구입니다.

    구현 난이도가 큽니다. Bun 런타임이 SOCKS를 안 하면 undici/socks-proxy-agent 같은 우회 fetch 스택을 프로바이더별로 갈라야 하고, 네이티브 fetch에 의존하는 경로가 많으면 구멍 mid-flight가 생깁니다. "미지원 스킴은 즉시 실패"는 상대적으로 싸고 지금 당장 가치가 있습니다. doctor가 socks5://를 보고 HTTP로 오인하지 않게 하는 가드가 첫 단계로 적합합니다.

    types/config 분할 캠페인과 겹칩니다. config.proxy 스키마·프로바이더별 override를 넣으면 types.ts/config.ts 대형 PR과 충돌하기 쉽습니다. 분할이 끝나기 전에 큰 스키마 확장 PR이 오면 close-don't-rebase를 권하는 기존 방침이 적용될 수 있습니다.

    경로 config.proxy / src/config.ts - HTTP(S)만 env로 미러. socks5는 Bun이 협상하지 않아 연결이 이상하게 실패하거나 오인됩니다.
    경로 src/cli/doctor.ts config.proxy - 스킴 검증을 강화해 SOCKS/unknown을 명시적으로 거부·경고하는 편이 안전합니다. 전체 SOCKS 구현보다 우선 가능합니다.
    경로 프로바이더별 프록시 - 스키마·GUI·풀 라우팅까지 범위가 커집니다. Omniroute 패리티를 한 PR에 넣지 말고 단계를 나누어야 합니다.
    경로 런타임 - Bun SOCKS 미지원이 막히면 Node undici 전환 또는 외부 forward proxy(HTTP로 SOCKS에 다시 붙는 local relay) 문서화가 현실적 대안입니다.

    메인테이너의 판단이 필요한 지점

    • 단기: 미지원 스킴 fail-fast + 문서만 할지, SOCKS 지원을 정식 로드맵에 올릴지
    • 프로바이더별 프록시 override를 config 분할 이후로 미룰지
    • Bun 업스트림 지원을 기다릴지, 우회 HTTP 클라이언트를 도입할지
    • 보안/유출 리뷰가 필요한 전송 변경이므로 non-author 승인 거버넌스 적용 여부

    너의 추천
    enhancement로 유지하되, 먼저 "SOCKS/unknown 스킴 fail-fast + doctor 경고"만 작은 PR로 받고, 실제 SOCKS 전송과 per-provider override는 config 분할 이후 설계 이슈로 분리하세요. 지금 큰 구현 PR이 오면 분할 캠페인에 무효화될 가능성이 있어 close-don't-rebase 대상이 될 수 있다고 명시합니다.

    이 댓글은 grok-bot이 작성했습니다

  3. franz101 commented on Aug 29, 2026

    @franz101
  4. Ingwannu commented on Aug 29, 2026

    @Ingwannu
    Owner

    Thanks, this materially improves the upstream path. I checked oven-sh/bun#40461: it is currently open, unmerged, and blocked on review at head 9a66e8508862b55f97851a2726824d083d2d7d12, so it is not available in the Bun 1.3.14 runtime OpenCodex currently pins.

    The near-term order therefore stays the same: first reject unsupported or unknown proxy schemes at config/apply time and surface a precise doctor message instead of letting Bun silently treat SOCKS as HTTP. Once the upstream PR is merged into a Bun release we can actually pin and test, we should re-evaluate native socks5 and socks5h support before introducing a second fetch stack. Per-provider direct/default/custom egress remains a separate transport-design slice because it must cover model calls, OAuth refresh, quota probes, discovery, and sidecars consistently.

  5. lidge-jun commented on Sep 20, 2026

    @lidge-jun
    Owner

    Post-main scope consolidation at 7c625fc (2.60.0): the global SOCKS5 HTTP/SSE transport is delivered through #4986 and its content-coding / lifecycle follow-ups (#5070, #5126, #5127). This is not a remaining request to implement global SOCKS5 from scratch.

    Keep this issue open for its distinct per-provider egress requirement: an explicit inherit/direct/proxy decision, correct noProxy and protocol handling, and the same decision for inference, provider discovery, quota and OAuth refresh without leaking proxy credentials. #3901 is the existing HTTP(S)-override implementation candidate, not proof that every SOCKS/control-plane case is delivered. #5087 covers the separate prerequisite that a proxy must actually apply to the destination before direct-route DNS pinning can be relaxed.

    Use those existing PRs rather than opening another competing egress implementation. Preserve unsupported transport cases as explicit refusals; a missing effective proxy must not silently change the admitted direct destination.

  6. lidge-jun commented on Sep 24, 2026

    @lidge-jun
    Owner

    Closing as delivered. The global SOCKS5 transport shipped through #4986 and its follow-ups (#5070, #5126, #5127), and the remaining per-provider egress requirement recorded in the 2026-09-20 scope note landed later that day in #5289: providers.<name>.proxy accepts absent (inherit), "direct"/null, http(s):// and socks5(h)://, and providers.<name>.noProxy applies to whichever route resolved. The authority is src/lib/provider-egress.ts on dev. If a specific provider or transport still ignores its per-provider route, please open a new issue with that route and a redacted config.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions