From a15d4f9f04441cf631950258715f39f29c3c76c8 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 28 May 2026 23:53:07 -0500 Subject: [PATCH] fix(openai): proxy websocket connections under bun (#29832) --- packages/opencode/src/plugin/openai/ws.ts | 9 ++- packages/opencode/src/util/proxy-env.ts | 72 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/src/util/proxy-env.ts diff --git a/packages/opencode/src/plugin/openai/ws.ts b/packages/opencode/src/plugin/openai/ws.ts index 381c15651a..7ff8d7bb83 100644 --- a/packages/opencode/src/plugin/openai/ws.ts +++ b/packages/opencode/src/plugin/openai/ws.ts @@ -4,6 +4,7 @@ import WebSocket from "ws" import { ProviderError } from "@/provider/error" import { errorMessage } from "@/util/error" +import { ProxyEnv } from "@/util/proxy-env" export const PROTOCOL_HEADER = "responses_websockets=2026-02-06" @@ -72,7 +73,13 @@ export function connectResponsesWebSocket(options: ConnectResponsesWebSocketOpti } delete headers["content-length"] - const socket = new WebSocket(options.url, { headers }) + // Bun does not apply HTTP(S)_PROXY to WebSockets unless the proxy is supplied explicitly. + const proxy = + typeof Bun === "undefined" + ? undefined + : ProxyEnv.getProxyForUrl(options.url.replace(/^wss:/, "https:").replace(/^ws:/, "http:")) + const connect = { headers, ...(proxy ? { proxy } : {}) } + const socket = new WebSocket(options.url, connect) const timeout = options.timeout ? setTimeout(() => { cleanup() diff --git a/packages/opencode/src/util/proxy-env.ts b/packages/opencode/src/util/proxy-env.ts new file mode 100644 index 0000000000..6682b3ca13 --- /dev/null +++ b/packages/opencode/src/util/proxy-env.ts @@ -0,0 +1,72 @@ +/* + * Adapted from proxy-from-env: https://github.com/Rob--W/proxy-from-env + * + * The MIT License + * + * Copyright (C) 2016-2018 Rob Wu + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +const DEFAULT_PORTS: Record = { + ftp: 21, + gopher: 70, + http: 80, + https: 443, + ws: 80, + wss: 443, +} + +export function getProxyForUrl(input: string | URL) { + const url = typeof input === "string" ? (URL.canParse(input) ? new URL(input) : undefined) : input + if (!url) return + + const protocol = url.protocol.split(":", 1)[0] + const hostname = url.host.replace(/:\d*$/, "") + const port = Number.parseInt(url.port) || DEFAULT_PORTS[protocol] || 0 + if (!shouldProxy(hostname, port)) return + + const proxy = env(`${protocol}_proxy`) || env("all_proxy") + if (!proxy) return + return proxy.includes("://") ? proxy : `${protocol}://${proxy}` +} + +function shouldProxy(hostname: string, port: number) { + const noProxy = env("no_proxy").toLowerCase() + if (!noProxy) return true + if (noProxy === "*") return false + + return noProxy.split(/[,\s]/).every((proxy) => { + if (!proxy) return true + + const parsed = proxy.match(/^(.+):(\d+)$/) + const proxyHostname = parsed ? parsed[1] : proxy + const proxyPort = parsed ? Number.parseInt(parsed[2]) : 0 + if (proxyPort && proxyPort !== port) return true + + if (!/^[.*]/.test(proxyHostname)) return hostname !== proxyHostname + return !hostname.endsWith(proxyHostname.startsWith("*") ? proxyHostname.slice(1) : proxyHostname) + }) +} + +function env(key: string) { + return process.env[key.toLowerCase()] || process.env[key.toUpperCase()] || "" +} + +export * as ProxyEnv from "./proxy-env"