package providers import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" "github.com/google/uuid" "github.com/gorilla/websocket" ) type CodexProvider struct { base *HTTPProvider sessionMu sync.Mutex sessions map[string]*codexExecutionSession } type codexPromptCacheEntry struct { ID string Expire time.Time } type codexExecutionSession struct { mu sync.Mutex reqMu sync.Mutex conn *websocket.Conn wsURL string } const ( codexResponsesWebsocketBetaHeaderValue = "responses_websockets=2026-02-06" codexResponsesWebsocketHandshakeTO = 30 * time.Second codexResponsesWebsocketIdleTimeout = 5 * time.Minute ) var codexPromptCacheStore = struct { mu sync.Mutex items map[string]codexPromptCacheEntry }{items: map[string]codexPromptCacheEntry{}} func NewCodexProvider(providerName, apiKey, apiBase, defaultModel string, supportsResponsesCompact bool, authMode string, timeout time.Duration, oauth *oauthManager) *CodexProvider { return &CodexProvider{ base: NewHTTPProvider(providerName, apiKey, apiBase, defaultModel, supportsResponsesCompact, authMode, timeout, oauth), } } func (p *CodexProvider) GetDefaultModel() string { if p == nil || p.base == nil { return "" } return p.base.GetDefaultModel() } func (p *CodexProvider) SupportsResponsesCompact() bool { return p != nil && p.base != nil && p.base.SupportsResponsesCompact() } func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) { if p == nil || p.base == nil { return nil, fmt.Errorf("provider not configured") } body, statusCode, contentType, err := p.postWebsocketStream(ctx, endpointFor(p.codexCompatBase(), "/responses"), p.requestBody(messages, tools, model, options, false, true), options, nil) if err != nil { body, statusCode, contentType, err = p.postJSONStream(ctx, endpointFor(p.codexCompatBase(), "/responses"), p.requestBody(messages, tools, model, options, false, false), nil) } if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("API error (status %d, content-type %q): %s", statusCode, contentType, previewResponseBody(body)) } if !json.Valid(body) { return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", statusCode, contentType, previewResponseBody(body)) } return parseResponsesAPIResponse(body) } func (p *CodexProvider) ChatStream(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, onDelta func(string)) (*LLMResponse, error) { if p == nil || p.base == nil { return nil, fmt.Errorf("provider not configured") } if onDelta == nil { onDelta = func(string) {} } body, statusCode, contentType, err := p.postWebsocketStream(ctx, endpointFor(p.codexCompatBase(), "/responses"), p.requestBody(messages, tools, model, options, true, true), options, onDelta) if err != nil { body, statusCode, contentType, err = p.postJSONStream(ctx, endpointFor(p.codexCompatBase(), "/responses"), p.requestBody(messages, tools, model, options, true, false), func(event string) { var obj map[string]interface{} if err := json.Unmarshal([]byte(event), &obj); err != nil { return } if d := strings.TrimSpace(fmt.Sprintf("%v", obj["delta"])); d != "" { onDelta(d) return } if delta, ok := obj["delta"].(map[string]interface{}); ok { if txt := strings.TrimSpace(fmt.Sprintf("%v", delta["text"])); txt != "" { onDelta(txt) } } }) } if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("API error (status %d, content-type %q): %s", statusCode, contentType, previewResponseBody(body)) } if !json.Valid(body) { return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", statusCode, contentType, previewResponseBody(body)) } return parseResponsesAPIResponse(body) } func (p *CodexProvider) BuildSummaryViaResponsesCompact(ctx context.Context, model string, existingSummary string, messages []Message, maxSummaryChars int) (string, error) { if !p.SupportsResponsesCompact() { return "", fmt.Errorf("responses compact is not enabled for this provider") } input := make([]map[string]interface{}, 0, len(messages)+1) if strings.TrimSpace(existingSummary) != "" { input = append(input, responsesMessageItem("system", "Existing summary:\n"+strings.TrimSpace(existingSummary), "input_text")) } pendingCalls := map[string]struct{}{} for _, msg := range messages { input = append(input, toResponsesInputItemsWithState(msg, pendingCalls)...) } if len(input) == 0 { return strings.TrimSpace(existingSummary), nil } compactReq := map[string]interface{}{"model": model, "input": input} compactBody, statusCode, contentType, err := p.base.postJSON(ctx, endpointFor(p.codexCompatBase(), "/responses/compact"), compactReq) if err != nil { return "", fmt.Errorf("responses compact request failed: %w", err) } if statusCode != http.StatusOK { return "", fmt.Errorf("responses compact request failed (status %d, content-type %q): %s", statusCode, contentType, previewResponseBody(compactBody)) } if !json.Valid(compactBody) { return "", fmt.Errorf("responses compact request failed (status %d, content-type %q): non-JSON response: %s", statusCode, contentType, previewResponseBody(compactBody)) } var compactResp struct { Output interface{} `json:"output"` CompactedInput interface{} `json:"compacted_input"` Compacted interface{} `json:"compacted"` } if err := json.Unmarshal(compactBody, &compactResp); err != nil { return "", fmt.Errorf("responses compact request failed: invalid JSON: %w", err) } compactPayload := compactResp.Output if compactPayload == nil { compactPayload = compactResp.CompactedInput } if compactPayload == nil { compactPayload = compactResp.Compacted } payloadBytes, err := json.Marshal(compactPayload) if err != nil { return "", fmt.Errorf("failed to serialize compact output: %w", err) } compactedPayload := strings.TrimSpace(string(payloadBytes)) if compactedPayload == "" || compactedPayload == "null" { return "", fmt.Errorf("empty compact output") } if len(compactedPayload) > 12000 { compactedPayload = compactedPayload[:12000] + "..." } summaryPrompt := fmt.Sprintf( "Compacted conversation JSON:\n%s\n\nReturn a concise markdown summary with sections: Key Facts, Decisions, Open Items, Next Steps.", compactedPayload, ) resp, err := p.Chat(ctx, []Message{{Role: "user", Content: summaryPrompt}}, nil, model, nil) if err != nil { return "", fmt.Errorf("responses summary request failed: %w", err) } summary := strings.TrimSpace(resp.Content) if summary == "" { return "", fmt.Errorf("empty summary after responses compact") } if maxSummaryChars > 0 && len(summary) > maxSummaryChars { summary = summary[:maxSummaryChars] } return summary, nil } func (p *CodexProvider) requestBody(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, stream bool, preservePreviousResponseID bool) map[string]interface{} { input := make([]map[string]interface{}, 0, len(messages)) pendingCalls := map[string]struct{}{} for _, msg := range messages { input = append(input, toResponsesInputItemsWithState(msg, pendingCalls)...) } requestBody := map[string]interface{}{ "model": model, "input": input, } responseTools := buildResponsesTools(tools, options) if len(responseTools) > 0 { requestBody["tools"] = responseTools requestBody["tool_choice"] = "auto" if tc, ok := rawOption(options, "tool_choice"); ok { requestBody["tool_choice"] = tc } if tc, ok := rawOption(options, "responses_tool_choice"); ok { requestBody["tool_choice"] = tc } } if maxTokens, ok := int64FromOption(options, "max_tokens"); ok { requestBody["max_output_tokens"] = maxTokens } if temperature, ok := float64FromOption(options, "temperature"); ok { requestBody["temperature"] = temperature } if include, ok := stringSliceOption(options, "responses_include"); ok && len(include) > 0 { requestBody["include"] = include } if metadata, ok := mapOption(options, "responses_metadata"); ok && len(metadata) > 0 { requestBody["metadata"] = metadata } if prevID, ok := stringOption(options, "responses_previous_response_id"); ok && prevID != "" { requestBody["previous_response_id"] = prevID } if stream { if streamOpts, ok := mapOption(options, "responses_stream_options"); ok && len(streamOpts) > 0 { requestBody["stream_options"] = streamOpts } } return normalizeCodexRequestBody(requestBody, preservePreviousResponseID) } func (p *CodexProvider) codexCompatBase() string { if p == nil || p.base == nil { return codexCompatBaseURL } base := strings.ToLower(strings.TrimSpace(p.base.apiBase)) if strings.Contains(base, "chatgpt.com/backend-api/codex") { return normalizeAPIBase(p.base.apiBase) } if base != "" && !strings.Contains(base, "api.openai.com") { return normalizeAPIBase(p.base.apiBase) } return codexCompatBaseURL } func normalizeCodexRequestBody(requestBody map[string]interface{}, preservePreviousResponseID bool) map[string]interface{} { if requestBody == nil { requestBody = map[string]interface{}{} } requestBody["stream"] = true requestBody["store"] = false requestBody["parallel_tool_calls"] = true if _, ok := requestBody["instructions"]; !ok { requestBody["instructions"] = "" } include := appendCodexInclude(nil, requestBody["include"]) requestBody["include"] = include delete(requestBody, "max_output_tokens") delete(requestBody, "max_completion_tokens") delete(requestBody, "temperature") delete(requestBody, "top_p") delete(requestBody, "truncation") delete(requestBody, "user") if !preservePreviousResponseID { delete(requestBody, "previous_response_id") } delete(requestBody, "prompt_cache_retention") delete(requestBody, "safety_identifier") if input, ok := requestBody["input"].([]map[string]interface{}); ok { for _, item := range input { if strings.EqualFold(strings.TrimSpace(fmt.Sprintf("%v", item["role"])), "system") { item["role"] = "developer" } } requestBody["input"] = input } return requestBody } func appendCodexInclude(dst []string, raw interface{}) []string { seen := map[string]struct{}{} out := make([]string, 0, 2) appendOne := func(v string) { v = strings.TrimSpace(v) if v == "" { return } if _, ok := seen[v]; ok { return } seen[v] = struct{}{} out = append(out, v) } for _, v := range dst { appendOne(v) } switch vals := raw.(type) { case []string: for _, v := range vals { appendOne(v) } case []interface{}: for _, v := range vals { appendOne(fmt.Sprintf("%v", v)) } case string: appendOne(vals) } appendOne("reasoning.encrypted_content") return out } func (p *CodexProvider) postJSONStream(ctx context.Context, endpoint string, payload map[string]interface{}, onEvent func(string)) ([]byte, int, string, error) { attempts, err := p.base.authAttempts(ctx) if err != nil { return nil, 0, "", err } var lastBody []byte var lastStatus int var lastType string for _, attempt := range attempts { attemptPayload := codexPayloadForAttempt(payload, attempt) jsonData, err := json.Marshal(attemptPayload) if err != nil { return nil, 0, "", fmt.Errorf("failed to marshal request: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(jsonData)) if err != nil { return nil, 0, "", fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "text/event-stream") applyAttemptAuth(req, attempt) applyAttemptProviderHeaders(req, attempt, p.base, true) applyCodexCacheHeaders(req, attemptPayload) body, status, ctype, quotaHit, err := p.doStreamAttempt(req, attempt, onEvent) if err != nil { return nil, 0, "", err } if !quotaHit { p.base.markAttemptSuccess(attempt) return body, status, ctype, nil } lastBody, lastStatus, lastType = body, status, ctype if attempt.kind == "oauth" && attempt.session != nil && p.base.oauth != nil { reason, _ := classifyOAuthFailure(status, body) p.base.oauth.markExhausted(attempt.session, reason) recordProviderOAuthError(p.base.providerName, attempt.session, reason) } if attempt.kind == "api_key" { reason, _ := classifyOAuthFailure(status, body) p.base.markAPIKeyFailure(reason) } } return lastBody, lastStatus, lastType, nil } func codexPayloadForAttempt(payload map[string]interface{}, attempt authAttempt) map[string]interface{} { if payload == nil { return nil } out := cloneCodexMap(payload) cacheKey, hasCacheKey := out["prompt_cache_key"] if hasCacheKey && strings.TrimSpace(fmt.Sprintf("%v", cacheKey)) != "" { return out } if userCacheKey := codexPromptCacheKeyForUser(out); userCacheKey != "" { out["prompt_cache_key"] = userCacheKey return out } if attempt.kind == "api_key" { token := strings.TrimSpace(attempt.token) if token != "" { out["prompt_cache_key"] = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+token)).String() } } return out } func codexPromptCacheKeyForUser(payload map[string]interface{}) string { metadata := mapFromAny(payload["metadata"]) userID := strings.TrimSpace(asString(metadata["user_id"])) model := strings.TrimSpace(asString(payload["model"])) if userID == "" || model == "" { return "" } key := model + "-" + userID now := time.Now() codexPromptCacheStore.mu.Lock() defer codexPromptCacheStore.mu.Unlock() if entry, ok := codexPromptCacheStore.items[key]; ok && entry.ID != "" && entry.Expire.After(now) { return entry.ID } entry := codexPromptCacheEntry{ ID: uuid.New().String(), Expire: now.Add(time.Hour), } codexPromptCacheStore.items[key] = entry return entry.ID } func cloneCodexMap(src map[string]interface{}) map[string]interface{} { if src == nil { return nil } out := make(map[string]interface{}, len(src)) for k, v := range src { out[k] = cloneCodexValue(v) } return out } func cloneCodexValue(v interface{}) interface{} { switch typed := v.(type) { case map[string]interface{}: return cloneCodexMap(typed) case []map[string]interface{}: out := make([]map[string]interface{}, len(typed)) for i := range typed { out[i] = cloneCodexMap(typed[i]) } return out case []interface{}: out := make([]interface{}, len(typed)) for i := range typed { out[i] = cloneCodexValue(typed[i]) } return out default: return v } } func applyCodexCacheHeaders(req *http.Request, payload map[string]interface{}) { if req == nil || payload == nil { return } key := strings.TrimSpace(fmt.Sprintf("%v", payload["prompt_cache_key"])) if key == "" { return } req.Header.Set("Conversation_id", key) req.Header.Set("Session_id", key) } func (p *CodexProvider) doStreamAttempt(req *http.Request, attempt authAttempt, onEvent func(string)) ([]byte, int, string, bool, error) { client, err := p.base.httpClientForAttempt(attempt) if err != nil { return nil, 0, "", false, err } resp, err := client.Do(req) if err != nil { return nil, 0, "", false, fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() ctype := strings.TrimSpace(resp.Header.Get("Content-Type")) if !strings.Contains(strings.ToLower(ctype), "text/event-stream") { body, readErr := io.ReadAll(resp.Body) if readErr != nil { return nil, resp.StatusCode, ctype, false, fmt.Errorf("failed to read response: %w", readErr) } return body, resp.StatusCode, ctype, shouldRetryOAuthQuota(resp.StatusCode, body), nil } scanner := bufio.NewScanner(resp.Body) scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024) var dataLines []string var finalJSON []byte completed := false for scanner.Scan() { line := scanner.Text() if strings.TrimSpace(line) == "" { if len(dataLines) == 0 { continue } payload := strings.Join(dataLines, "\n") dataLines = dataLines[:0] if strings.TrimSpace(payload) == "[DONE]" { continue } if onEvent != nil { onEvent(payload) } var obj map[string]interface{} if err := json.Unmarshal([]byte(payload), &obj); err == nil { if typ := strings.TrimSpace(fmt.Sprintf("%v", obj["type"])); typ == "response.completed" { completed = true if respObj, ok := obj["response"]; ok { if b, err := json.Marshal(respObj); err == nil { finalJSON = b } } } } continue } if strings.HasPrefix(line, "data:") { dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:"))) } } if err := scanner.Err(); err != nil { return nil, resp.StatusCode, ctype, false, fmt.Errorf("failed to read stream: %w", err) } if resp.StatusCode >= 200 && resp.StatusCode < 300 && !completed { return nil, resp.StatusCode, ctype, false, fmt.Errorf("stream error: stream disconnected before completion: stream closed before response.completed") } if len(finalJSON) == 0 { finalJSON = []byte("{}") } return finalJSON, resp.StatusCode, ctype, false, nil } func (p *CodexProvider) postWebsocketStream(ctx context.Context, endpoint string, payload map[string]interface{}, options map[string]interface{}, onDelta func(string)) ([]byte, int, string, error) { attempts, err := p.base.authAttempts(ctx) if err != nil { return nil, 0, "", err } var lastBody []byte var lastStatus int var lastType string var lastErr error for _, attempt := range attempts { body, status, ctype, err := p.doWebsocketAttempt(ctx, endpoint, payload, attempt, options, onDelta) if err == nil { p.base.markAttemptSuccess(attempt) return body, status, ctype, nil } lastBody, lastStatus, lastType = body, status, ctype p.handleAttemptFailure(attempt, status, body) lastErr = err } if lastErr == nil { lastErr = fmt.Errorf("websocket unavailable") } return lastBody, lastStatus, lastType, lastErr } func (p *CodexProvider) handleAttemptFailure(attempt authAttempt, status int, body []byte) { if reason, detail, disabled := classifyCodexPermanentDisable(status, body); disabled { if attempt.kind == "oauth" && attempt.session != nil && p.base != nil && p.base.oauth != nil { p.base.oauth.disableSession(attempt.session, reason, detail) recordProviderOAuthError(p.base.providerName, attempt.session, reason) } if attempt.kind == "api_key" && p.base != nil { p.base.markAPIKeyFailure(reason) } return } reason, retry := classifyOAuthFailure(status, body) if !retry { return } if attempt.kind == "oauth" && attempt.session != nil && p.base != nil && p.base.oauth != nil { p.base.oauth.markExhausted(attempt.session, reason) recordProviderOAuthError(p.base.providerName, attempt.session, reason) } if attempt.kind == "api_key" && p.base != nil { p.base.markAPIKeyFailure(reason) } } func classifyCodexPermanentDisable(status int, body []byte) (oauthFailureReason, string, bool) { if status != http.StatusUnauthorized && status != http.StatusPaymentRequired { return "", "", false } lower := strings.ToLower(strings.TrimSpace(string(body))) switch { case strings.Contains(lower, "token_revoked"), strings.Contains(lower, "invalidated oauth token"): return oauthFailureRevoked, "oauth token revoked", true case strings.Contains(lower, "deactivated_workspace"): return oauthFailureDisabled, "workspace deactivated", true default: return "", "", false } } func (p *CodexProvider) doWebsocketAttempt(ctx context.Context, endpoint string, payload map[string]interface{}, attempt authAttempt, options map[string]interface{}, onDelta func(string)) ([]byte, int, string, error) { wsURL, err := buildCodexResponsesWebsocketURL(endpoint) if err != nil { return nil, 0, "", err } attemptPayload := codexPayloadForAttempt(payload, attempt) wsBody, err := json.Marshal(buildCodexWebsocketRequestBody(attemptPayload)) if err != nil { return nil, 0, "", fmt.Errorf("failed to marshal websocket request: %w", err) } headers := applyCodexWebsocketHeaders(http.Header{}, attempt, options) applyCodexCacheHeadersToHeader(headers, attemptPayload) session := p.getExecutionSession(codexExecutionSessionID(options)) if session != nil { session.reqMu.Lock() defer session.reqMu.Unlock() } conn, status, ctype, cleanup, err := p.prepareWebsocketConn(ctx, session, wsURL, headers, attempt) if err != nil { return nil, status, ctype, err } if cleanup != nil { defer cleanup() } if err := conn.WriteMessage(websocket.TextMessage, wsBody); err != nil { if session != nil { p.invalidateExecutionSession(session, conn) conn, status, ctype, cleanup, err = p.prepareWebsocketConn(ctx, session, wsURL, headers, attempt) if err != nil { return nil, status, ctype, err } if cleanup != nil { defer cleanup() } if err := conn.WriteMessage(websocket.TextMessage, wsBody); err != nil { p.invalidateExecutionSession(session, conn) return nil, 0, "", err } } else { return nil, 0, "", err } } for { msgType, msg, err := conn.ReadMessage() if err != nil { p.invalidateExecutionSession(session, conn) return nil, http.StatusOK, "application/json", err } if msgType != websocket.TextMessage { continue } msg = bytes.TrimSpace(msg) if len(msg) == 0 { continue } if wsErr, status, _, ok := parseCodexWebsocketError(msg); ok { p.invalidateExecutionSession(session, conn) return msg, status, "application/json", wsErr } msg = normalizeCodexWebsocketCompletion(msg) var event map[string]interface{} if err := json.Unmarshal(msg, &event); err != nil { continue } switch strings.TrimSpace(fmt.Sprintf("%v", event["type"])) { case "response.output_text.delta": if d := strings.TrimSpace(fmt.Sprintf("%v", event["delta"])); d != "" { if onDelta != nil { onDelta(d) } } case "response.completed": if respObj, ok := event["response"]; ok { b, _ := json.Marshal(respObj) return b, http.StatusOK, "application/json", nil } return msg, http.StatusOK, "application/json", nil } } } func codexExecutionSessionID(options map[string]interface{}) string { if value, ok := stringOption(options, "codex_execution_session"); ok { return strings.TrimSpace(value) } return "" } func (p *CodexProvider) getExecutionSession(id string) *codexExecutionSession { id = strings.TrimSpace(id) if p == nil || id == "" { return nil } p.sessionMu.Lock() defer p.sessionMu.Unlock() if p.sessions == nil { p.sessions = map[string]*codexExecutionSession{} } if sess, ok := p.sessions[id]; ok && sess != nil { return sess } sess := &codexExecutionSession{} p.sessions[id] = sess return sess } func (p *CodexProvider) prepareWebsocketConn(ctx context.Context, session *codexExecutionSession, wsURL string, headers http.Header, attempt authAttempt) (*websocket.Conn, int, string, func(), error) { if session == nil { conn, status, ctype, err := p.dialWebsocket(ctx, wsURL, headers, attempt) if err != nil { return nil, status, ctype, nil, err } return conn, status, ctype, func() { _ = conn.Close() }, nil } session.mu.Lock() defer session.mu.Unlock() if session.conn != nil && session.wsURL == wsURL { return session.conn, http.StatusOK, "application/json", nil, nil } if session.conn != nil { _ = session.conn.Close() session.conn = nil } conn, status, ctype, err := p.dialWebsocket(ctx, wsURL, headers, attempt) if err != nil { return nil, status, ctype, nil, err } session.conn = conn session.wsURL = wsURL return conn, status, ctype, nil, nil } func (p *CodexProvider) invalidateExecutionSession(session *codexExecutionSession, conn *websocket.Conn) { if session == nil || conn == nil { return } session.mu.Lock() defer session.mu.Unlock() if session.conn == conn { _ = session.conn.Close() session.conn = nil session.wsURL = "" } } func (p *CodexProvider) CloseExecutionSession(sessionID string) { if p == nil { return } sessionID = strings.TrimSpace(sessionID) if sessionID == "" { return } p.sessionMu.Lock() session := p.sessions[sessionID] delete(p.sessions, sessionID) p.sessionMu.Unlock() if session == nil { return } session.mu.Lock() conn := session.conn session.conn = nil session.wsURL = "" session.mu.Unlock() if conn != nil { _ = conn.Close() } } func (p *CodexProvider) dialWebsocket(ctx context.Context, wsURL string, headers http.Header, attempt authAttempt) (*websocket.Conn, int, string, error) { conn, resp, err := p.websocketDialer(attempt).DialContext(ctx, wsURL, headers) if err != nil { status := 0 ctype := "" if resp != nil { status = resp.StatusCode ctype = strings.TrimSpace(resp.Header.Get("Content-Type")) } if resp != nil && resp.Body != nil { _, _ = io.ReadAll(resp.Body) _ = resp.Body.Close() } return nil, status, ctype, err } _ = conn.SetReadDeadline(time.Now().Add(codexResponsesWebsocketIdleTimeout)) conn.EnableWriteCompression(false) return conn, http.StatusOK, "application/json", nil } func (p *CodexProvider) websocketDialer(attempt authAttempt) *websocket.Dialer { dialer := &websocket.Dialer{ HandshakeTimeout: codexResponsesWebsocketHandshakeTO, EnableCompression: true, Proxy: http.ProxyFromEnvironment, } proxyRaw := "" if attempt.session != nil { proxyRaw = strings.TrimSpace(attempt.session.NetworkProxy) } if proxyRaw == "" { return dialer } parsed, err := url.Parse(proxyRaw) if err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") { dialer.Proxy = http.ProxyURL(parsed) return dialer } dialContext, err := proxyDialContext(proxyRaw) if err == nil { dialer.Proxy = nil dialer.NetDialContext = dialContext } return dialer } func buildCodexWebsocketRequestBody(body map[string]interface{}) map[string]interface{} { if body == nil { return nil } out := cloneCodexMap(body) out["type"] = "response.create" return out } 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" } return parsed.String(), nil } func applyCodexWebsocketHeaders(headers http.Header, attempt authAttempt, options map[string]interface{}) http.Header { if headers == nil { headers = http.Header{} } if token := strings.TrimSpace(attempt.token); token != "" { headers.Set("Authorization", "Bearer "+token) } headers.Set("x-codex-beta-features", "") headers.Set("x-codex-turn-state", codexHeaderOption(options, "codex_turn_state", "turn_state")) headers.Set("x-codex-turn-metadata", codexHeaderOption(options, "codex_turn_metadata", "turn_metadata")) headers.Set("x-responsesapi-include-timing-metrics", "") headers.Set("Version", codexClientVersion) betaHeader := strings.TrimSpace(headers.Get("OpenAI-Beta")) if betaHeader == "" || !strings.Contains(betaHeader, "responses_websockets=") { betaHeader = codexResponsesWebsocketBetaHeaderValue } headers.Set("OpenAI-Beta", betaHeader) if strings.TrimSpace(headers.Get("Session_id")) == "" { headers.Set("Session_id", randomSessionID()) } headers.Set("User-Agent", codexCompatUserAgent) if attempt.kind != "api_key" { headers.Set("Originator", "codex_cli_rs") if attempt.session != nil { accountID := firstNonEmpty( strings.TrimSpace(attempt.session.AccountID), strings.TrimSpace(asString(attempt.session.Token["account_id"])), strings.TrimSpace(asString(attempt.session.Token["account-id"])), ) if accountID != "" { headers.Set("Chatgpt-Account-Id", accountID) } } } return headers } func codexHeaderOption(options map[string]interface{}, directKey, streamKey string) string { if value, ok := stringOption(options, directKey); ok { return strings.TrimSpace(value) } streamOpts, ok := mapOption(options, "responses_stream_options") if !ok { return "" } value := strings.TrimSpace(asString(streamOpts[streamKey])) return value } func applyCodexCacheHeadersToHeader(headers http.Header, payload map[string]interface{}) { if headers == nil || payload == nil { return } key := strings.TrimSpace(fmt.Sprintf("%v", payload["prompt_cache_key"])) if key == "" { return } headers.Set("Conversation_id", key) headers.Set("Session_id", key) } func normalizeCodexWebsocketCompletion(payload []byte) []byte { root := mustJSONMap(payload) if strings.TrimSpace(asString(root["type"])) == "response.done" { updated, err := json.Marshal(map[string]interface{}{ "type": "response.completed", "response": root["response"], }) if err == nil { return updated } } return payload } func mustJSONMap(payload []byte) map[string]interface{} { var out map[string]interface{} _ = json.Unmarshal(payload, &out) return out } func parseCodexWebsocketError(payload []byte) (error, int, http.Header, bool) { root := mustJSONMap(payload) if strings.TrimSpace(asString(root["type"])) != "error" { return nil, 0, nil, false } status := intValue(root["status"]) if status == 0 { status = intValue(root["status_code"]) } if status <= 0 { status = http.StatusBadGateway } headers := parseCodexWebsocketErrorHeaders(root["headers"]) errNode := root["error"] if errMap := mapFromAny(errNode); len(errMap) > 0 { msg := strings.TrimSpace(asString(errMap["message"])) if msg == "" { msg = http.StatusText(status) } return fmt.Errorf("codex websocket upstream error (%d): %s", status, msg), status, headers, true } if msg := strings.TrimSpace(asString(errNode)); msg != "" { return fmt.Errorf("codex websocket upstream error (%d): %s", status, msg), status, headers, true } return fmt.Errorf("codex websocket upstream error (%d)", status), status, headers, true } func parseCodexWebsocketErrorHeaders(raw interface{}) http.Header { headersMap := mapFromAny(raw) if len(headersMap) == 0 { return nil } headers := make(http.Header) for key, value := range headersMap { name := strings.TrimSpace(key) if name == "" { continue } switch typed := value.(type) { case string: if v := strings.TrimSpace(typed); v != "" { headers.Set(name, v) } case float64, bool, int, int64: headers.Set(name, strings.TrimSpace(fmt.Sprintf("%v", typed))) } } if len(headers) == 0 { return nil } return headers }