Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
7.3 kB · 240 lines
Go
at commit e8e39526
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241package executor
import ( "bytes" "context" "errors" "fmt" "net" "net/http" "net/url" "strings" "time"
"github.com/gorilla/websocket" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" "github.com/tidwall/sjson" "golang.org/x/net/proxy")
const ( codexResponsesWebsocketBetaHeaderValue = "responses_websockets=2026-02-06" codexResponsesWebsocketIdleTimeout = 5 * time.Minute codexResponsesWebsocketHandshakeTO = 30 * time.Second)
func (e *CodexWebsocketsExecutor) dialCodexWebsocket(ctx context.Context, auth *cliproxyauth.Auth, wsURL string, headers http.Header) (*websocket.Conn, *websocketConnectionCloser, *http.Response, error) { dialer := newProxyAwareWebsocketDialer(e.cfg, auth) dialer.HandshakeTimeout = codexResponsesWebsocketHandshakeTO dialer.EnableCompression = true if ctx == nil { ctx = context.Background() } conn, resp, err := dialer.DialContext(ctx, wsURL, headers) closer := newWebsocketConnectionCloser(conn) if conn != nil { // Avoid gorilla/websocket flate tail validation issues on some upstreams/Go versions. // Negotiating permessage-deflate is fine; we just don't compress outbound messages. conn.EnableWriteCompression(false) } return conn, closer, resp, err}
func writeCodexWebsocketMessage(sess *codexWebsocketSession, conn *websocket.Conn, payload []byte) error { if sess != nil { return sess.writeMessage(conn, websocket.TextMessage, payload) } if conn == nil { return fmt.Errorf("codex websockets executor: websocket conn is nil") } return conn.WriteMessage(websocket.TextMessage, payload)}
func mapCodexWebsocketWriteError(sess *codexWebsocketSession, conn *websocket.Conn, err error) error { if err == nil || sess == nil || conn == nil { return err } upstreamErr := sess.upstreamDisconnectError(conn) var closeErr *websocket.CloseError if !errors.As(upstreamErr, &closeErr) || closeErr.Code != websocket.CloseMessageTooBig { return err } return mapCodexWebsocketReadError(upstreamErr)}
func shouldRetryCodexWebsocketSend(err error) bool { if err == nil { return false } var requestErr cliproxyexecutor.RequestScopedError return !errors.As(err, &requestErr) || !requestErr.IsRequestScoped()}
type codexWebsocketMessageTooBigError struct { statusErr}
func (codexWebsocketMessageTooBigError) IsRequestScoped() bool { return true}
func mapCodexWebsocketReadError(err error) error { if err == nil { return nil } var closeErr *websocket.CloseError if errors.As(err, &closeErr) && closeErr.Code == websocket.CloseMessageTooBig { return codexWebsocketMessageTooBigError{statusErr: statusErr{ code: http.StatusRequestEntityTooLarge, msg: `{"error":{"message":"upstream websocket message too big","type":"invalid_request_error","code":"message_too_big"}}`, }} } return err}
func normalizeCodexWebsocketParallelToolCalls(body []byte, headers http.Header) []byte { if !isCodexResponsesLiteRequest(body, headers) { return body } body = helps.SetBoolIfDifferent(body, "parallel_tool_calls", false) return body}
func buildCodexWebsocketRequestBody(body []byte) []byte { if len(body) == 0 { return nil }
// Match codex-rs websocket v2 semantics: every request is `response.create`. // Incremental follow-up turns continue on the same websocket using // `previous_response_id` + incremental `input`, not `response.append`. body = helps.SanitizeCodexInputItemIDs(body) wsReqBody, errSet := sjson.SetBytes(bytes.Clone(body), "type", "response.create") if errSet == nil && len(wsReqBody) > 0 { return wsReqBody } fallback := bytes.Clone(body) fallback, _ = sjson.SetBytes(fallback, "type", "response.create") return fallback}
func readCodexWebsocketMessage(ctx context.Context, sess *codexWebsocketSession, conn *websocket.Conn, readCh chan codexWebsocketRead) (int, []byte, error) { if sess == nil { if conn == nil { return 0, nil, fmt.Errorf("codex websockets executor: websocket conn is nil") } _ = conn.SetReadDeadline(time.Now().Add(codexResponsesWebsocketIdleTimeout)) msgType, payload, errRead := conn.ReadMessage() return msgType, payload, errRead } if conn == nil { return 0, nil, fmt.Errorf("codex websockets executor: websocket conn is nil") } if readCh == nil { return 0, nil, fmt.Errorf("codex websockets executor: session read channel is nil") } for { select { case <-ctx.Done(): return 0, nil, ctx.Err() case ev, ok := <-readCh: if !ok { return 0, nil, fmt.Errorf("codex websockets executor: session read channel closed") } if ev.conn != conn { continue } if ev.err != nil { return 0, nil, ev.err } return ev.msgType, ev.payload, nil } }}
func newProxyAwareWebsocketDialer(cfg *config.Config, auth *cliproxyauth.Auth) *websocket.Dialer { dialer := &websocket.Dialer{ Proxy: http.ProxyFromEnvironment, HandshakeTimeout: codexResponsesWebsocketHandshakeTO, EnableCompression: true, NetDialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, }
proxyURL := "" if auth != nil { proxyURL = strings.TrimSpace(auth.ProxyURL) } if proxyURL == "" && cfg != nil { proxyURL = strings.TrimSpace(cfg.ProxyURL) } if proxyURL == "" { return dialer }
setting, errParse := proxyutil.Parse(proxyURL) if errParse != nil { log.Errorf("codex websockets executor: %v", errParse) return dialer }
switch setting.Mode { case proxyutil.ModeDirect: dialer.Proxy = nil return dialer case proxyutil.ModeProxy: default: return dialer }
switch setting.URL.Scheme { case "socks5", "socks5h": var proxyAuth *proxy.Auth if setting.URL.User != nil { username := setting.URL.User.Username() password, _ := setting.URL.User.Password() proxyAuth = &proxy.Auth{User: username, Password: password} } socksDialer, errSOCKS5 := proxy.SOCKS5("tcp", setting.URL.Host, proxyAuth, proxy.Direct) if errSOCKS5 != nil { log.Errorf("codex websockets executor: create SOCKS5 dialer failed: %v", errSOCKS5) return dialer } dialer.Proxy = nil dialer.NetDialContext = func(_ context.Context, network, addr string) (net.Conn, error) { return socksDialer.Dial(network, addr) } case "http", "https": dialer.Proxy = http.ProxyURL(setting.URL) default: log.Errorf("codex websockets executor: unsupported proxy scheme: %s", setting.URL.Scheme) }
return dialer}
func buildCodexResponsesWebsocketURL(httpURL string) (string, error) { parsed, err := url.Parse(strings.TrimSpace(httpURL)) if err != nil { return "", err } switch strings.ToLower(parsed.Scheme) { case "http": parsed.Scheme = "ws" case "https": parsed.Scheme = "wss" default: return "", fmt.Errorf("codex websockets executor: unsupported responses websocket URL scheme %q", parsed.Scheme) } if strings.TrimSpace(parsed.Host) == "" { return "", fmt.Errorf("codex websockets executor: responses websocket URL host is empty") } return parsed.String(), nil}