Add projects

This commit is contained in:
Alois 2026-08-24 00:10:41 +02:00
commit 8b607dd700
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
1802 changed files with 503346 additions and 2 deletions

View file

@ -0,0 +1,215 @@
package live
import (
"context"
"io"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/clienterror"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
log "github.com/sirupsen/logrus"
)
// HandleTranslation reports that the Codex OAuth upstream has no translation session capability.
func (h *Handler) HandleTranslation(c *gin.Context) {
writeCapabilityNotSupported(c, "Realtime translation sessions")
}
// HandleTranscriptionSession reports that the Codex OAuth upstream has no transcription-only capability.
func (h *Handler) HandleTranscriptionSession(c *gin.Context) {
writeCapabilityNotSupported(c, "Realtime transcription-only sessions")
}
// HandleSIPControl reports that the Codex OAuth upstream has no SIP dialog capability.
func (h *Handler) HandleSIPControl(c *gin.Context) {
action := "control"
if c != nil && c.Request != nil && c.Request.URL != nil {
parts := strings.Split(strings.Trim(c.Request.URL.Path, "/"), "/")
if len(parts) > 0 && strings.TrimSpace(parts[len(parts)-1]) != "" {
action = parts[len(parts)-1]
}
}
writeCapabilityNotSupported(c, "Realtime SIP "+action)
}
// HandleHangup forwards hangup for a locally created WebRTC call using its pinned OAuth credential.
func (h *Handler) HandleHangup(c *gin.Context) {
if h == nil || h.authManager == nil || h.sessions == nil {
writeRealtimeError(c, http.StatusServiceUnavailable, "Codex live session service unavailable", "server_error", "realtime_session_unavailable")
return
}
callID := strings.TrimSpace(c.Param("call_id"))
if !callIDPattern.MatchString(callID) {
writeRealtimeError(c, http.StatusBadRequest, "Invalid Realtime call ID", "invalid_request_error", "invalid_call_id")
return
}
session, ok := h.sessions.peek(callID)
if !ok {
writeRealtimeError(c, http.StatusNotFound, "Realtime call not found", "invalid_request_error", "realtime_call_not_found")
return
}
if ownerPrincipal, ownerProvider := requestOwner(c); session.ownerPrincipal != "" && (ownerPrincipal != session.ownerPrincipal || ownerProvider != session.ownerProvider) {
writeRealtimeError(c, http.StatusForbidden, "Realtime call belongs to another API principal", "invalid_request_error", "realtime_call_scope_mismatch")
return
}
ctx := context.WithValue(c.Request.Context(), "gin", c)
var activeSelection *auth.HomeDispatchSelection
var temporarySelection bool
var selected *auth.Auth
if session.homeSelection != nil && session.homeSelection.Active() {
activeSelection = session.homeSelection
selected = activeSelection.CloneAuth()
} else {
selectionOpts := coreexecutor.Options{
Headers: liveSelectionHeaders(c),
Metadata: map[string]any{
coreexecutor.PinnedAuthMetadataKey: session.authID,
coreexecutor.ExecutionSessionMetadataKey: callID,
},
}
selection, selectedAuth, errSelect := h.selectOAuth(ctx, session.model, selectionOpts)
if errSelect != nil {
writeSelectionError(c, errSelect)
return
}
activeSelection = selection
selected = selectedAuth
temporarySelection = selection != nil
}
var selectionRelease func()
if activeSelection != nil {
attemptCtx, releaseAttempt, errAttempt := activeSelection.AttemptContext(ctx)
if errAttempt != nil {
if temporarySelection {
activeSelection.End("attempt_bind_failed")
}
writeRealtimeError(c, http.StatusServiceUnavailable, errAttempt.Error(), "server_error", "realtime_upstream_unavailable")
return
}
ctx = attemptCtx
selectionRelease = releaseAttempt
}
defer func() {
if selectionRelease != nil {
selectionRelease()
}
if temporarySelection && activeSelection != nil {
activeSelection.End("request_closed")
}
}()
if selected == nil {
writeRealtimeError(c, http.StatusServiceUnavailable, "Codex auth unavailable", "server_error", "codex_auth_unavailable")
return
}
logging.SetGinCPATraceID(c, selected.EnsureIndex())
body, errRead := readBody(c.Request.Body)
if errRead != nil {
writeRealtimeError(c, http.StatusBadRequest, errRead.Error(), "invalid_request_error", "invalid_request")
return
}
upstreamURL := h.realtimeHTTPBaseURL() + "/realtime/calls/" + url.PathEscape(callID) + "/hangup"
baseHeaders := protocolHeaders(c.Request.Header)
if contentType := strings.TrimSpace(c.GetHeader("Content-Type")); contentType != "" {
baseHeaders.Set("Content-Type", contentType)
}
runtimeConfig := h.currentConfig()
performRequest := func(current *auth.Auth) (*http.Response, error) {
headers := baseHeaders.Clone()
setAccountHeader(headers, current)
request, errRequest := h.authManager.NewHttpRequest(ctx, current, http.MethodPost, upstreamURL, body, headers)
if errRequest != nil {
return nil, errRequest
}
authType, authValue := current.AccountInfo()
helps.RecordAPIRequest(ctx, runtimeConfig, helps.UpstreamRequestLog{
URL: upstreamURL,
Method: http.MethodPost,
Headers: headersForLogging(request.Header),
Body: body,
Provider: "codex",
AuthID: current.ID,
AuthLabel: current.Label,
AuthType: authType,
AuthValue: authValue,
})
return h.authManager.HttpRequest(ctx, current, request)
}
response, errRequest := performRequest(selected)
if errRequest != nil {
helps.RecordAPIResponseError(ctx, runtimeConfig, errRequest)
writeRealtimeError(c, clienterror.HTTPStatusFromErrorOr(errRequest, http.StatusBadGateway), errRequest.Error(), "api_error", "realtime_upstream_unavailable")
return
}
if activeSelection != nil && response.StatusCode == http.StatusUnauthorized {
h.authManager.ReportHomeUnauthorized(ctx, selected, "codex", session.model)
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 1<<20))
if errClose := response.Body.Close(); errClose != nil {
log.Errorf("codex realtime hangup: close unauthorized response body error: %v", errClose)
}
refreshed, didRefresh, errRefresh := h.authManager.RefreshHomeSelectionAfterUnauthorized(ctx, activeSelection, selected)
if errRefresh != nil {
writeSelectionError(c, errRefresh)
return
}
if !didRefresh || refreshed == nil {
writeRealtimeError(c, http.StatusUnauthorized, "Codex credential unauthorized", "authentication_error", "realtime_upstream_unauthorized")
return
}
selected = refreshed
logging.SetGinCPATraceID(c, selected.EnsureIndex())
response, errRequest = performRequest(selected)
if errRequest != nil {
helps.RecordAPIResponseError(ctx, runtimeConfig, errRequest)
writeRealtimeError(c, clienterror.HTTPStatusFromErrorOr(errRequest, http.StatusBadGateway), errRequest.Error(), "api_error", "realtime_upstream_unavailable")
return
}
if response.StatusCode == http.StatusUnauthorized {
h.authManager.ReportHomeUnauthorized(ctx, selected, "codex", session.model)
}
}
defer func() {
if errClose := response.Body.Close(); errClose != nil {
log.Errorf("codex realtime hangup: close response body error: %v", errClose)
}
}()
responseBody, errResponse := readLimitedBody(response.Body)
if errResponse != nil {
helps.RecordAPIResponseError(ctx, runtimeConfig, errResponse)
writeRealtimeError(c, http.StatusBadGateway, "Failed to read Realtime hangup response", "api_error", "realtime_upstream_unavailable")
return
}
helps.RecordAPIResponseMetadata(ctx, runtimeConfig, response.StatusCode, callResponseHeaders(response.Header))
helps.AppendAPIResponseChunk(ctx, runtimeConfig, responseBody)
if response.StatusCode >= http.StatusOK && response.StatusCode < http.StatusMultipleChoices {
if selectionRelease != nil {
selectionRelease()
selectionRelease = nil
}
h.sessions.complete(session, "client_hangup")
}
if contentType := response.Header.Get("Content-Type"); contentType != "" {
c.Header("Content-Type", contentType)
}
copyRealtimeHandshakeHeaders(c.Writer.Header(), response.Header)
c.Status(response.StatusCode)
if _, errWrite := c.Writer.Write(responseBody); errWrite != nil {
log.WithError(errWrite).Warn("codex realtime hangup: write response body failed")
}
}
func (h *Handler) realtimeHTTPBaseURL() string {
return strings.TrimRight(websocketHTTPURL(h.sidebandAPIBaseURL), "/")
}
func writeCapabilityNotSupported(c *gin.Context, capability string) {
writeRealtimeError(c, http.StatusNotImplemented, capability+" are not supported by the ChatGPT/Codex OAuth upstream", "not_supported_error", "realtime_capability_not_supported")
}

View file

@ -0,0 +1,97 @@
package live
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func TestHandleHangupForwardsPinnedOAuthCall(t *testing.T) {
gin.SetMode(gin.TestMode)
manager := auth.NewManager(nil, nil, nil)
executor := &captureExecutor{
statusCode: http.StatusOK,
responseBody: io.NopCloser(strings.NewReader(`{"status":"ok"}`)),
}
manager.RegisterExecutor(executor)
registerCredential(t, manager, &auth.Auth{
ID: "codex-oauth",
Provider: "codex",
Status: auth.StatusActive,
Metadata: map[string]any{"access_token": "oauth-token"},
})
handler := NewHandler(manager, nil)
handler.sessions.put("call-123", liveSession{
authID: "codex-oauth",
model: defaultLiveModel,
ownerPrincipal: "owner-key",
ownerProvider: "static",
})
router := gin.New()
router.POST("/v1/realtime/calls/:call_id/hangup", func(c *gin.Context) {
c.Set("userApiKey", "owner-key")
c.Set("accessProvider", "static")
c.Next()
}, handler.HandleHangup)
request := httptest.NewRequest(http.MethodPost, "/v1/realtime/calls/call-123/hangup", nil)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
}
if executor.request == nil || executor.request.URL.String() != "https://api.openai.com/v1/realtime/calls/call-123/hangup" {
t.Fatalf("upstream request = %#v", executor.request)
}
if _, ok := handler.sessions.peek("call-123"); ok {
t.Fatal("successful hangup retained session")
}
}
func TestHandleHangupRejectsDifferentAPIPrincipal(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := NewHandler(auth.NewManager(nil, nil, nil), nil)
handler.sessions.put("call-123", liveSession{
authID: "codex-oauth",
model: defaultLiveModel,
ownerPrincipal: "owner-key",
ownerProvider: "static",
})
router := gin.New()
router.POST("/v1/realtime/calls/:call_id/hangup", func(c *gin.Context) {
c.Set("userApiKey", "other-key")
c.Set("accessProvider", "static")
c.Next()
}, handler.HandleHangup)
request := httptest.NewRequest(http.MethodPost, "/v1/realtime/calls/call-123/hangup", nil)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusForbidden, recorder.Body.String())
}
}
func TestUnsupportedRealtimeCapabilitiesUseStandardError(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := NewHandler(nil, nil)
router := gin.New()
router.POST("/v1/realtime/transcription_sessions", handler.HandleTranscriptionSession)
router.POST("/v1/realtime/calls/:call_id/accept", handler.HandleSIPControl)
for _, path := range []string{"/v1/realtime/transcription_sessions", "/v1/realtime/calls/call-123/accept"} {
request := httptest.NewRequest(http.MethodPost, path, nil)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotImplemented {
t.Errorf("%s status = %d, want %d", path, recorder.Code, http.StatusNotImplemented)
}
if !strings.Contains(recorder.Body.String(), `"type":"not_supported_error"`) || !strings.Contains(recorder.Body.String(), `"code":"realtime_capability_not_supported"`) {
t.Errorf("%s body = %s", path, recorder.Body.String())
}
}
}

View file

@ -0,0 +1,419 @@
package live
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
)
const (
ClientSecretSessionContextKey = "codexLiveClientSecretSession"
ClientSecretPrincipalContextKey = "codexLiveClientSecretPrincipal"
clientSecretPrefix = "ek_"
clientSecretDefaultLifetime = 10 * time.Minute
clientSecretMinimumLifetime = 10 * time.Second
clientSecretMaximumLifetime = 2 * time.Hour
clientSecretMaxBodySize = 64 << 10
clientSecretMaxEntries = 1024
clientSecretMaxEntriesPerIssuer = 64
)
var (
errInvalidClientSecret = errors.New("Realtime client secret is invalid or expired")
errClientSecretCapacity = errors.New("Realtime client secret capacity exhausted")
errUnsupportedSessionType = errors.New("Realtime session type is not supported")
)
// ClientSecretAuthorization contains the local session configuration associated with an ephemeral key.
type ClientSecretAuthorization struct {
Principal string
IssuerPrincipal string
IssuerProvider string
Session json.RawMessage
}
type clientSecretEntry struct {
authorization ClientSecretAuthorization
expiresAt time.Time
}
type clientSecretStore struct {
mu sync.Mutex
entries map[string]clientSecretEntry
now func() time.Time
}
type clientSecretCreateRequest struct {
Session json.RawMessage `json:"session"`
ExpiresAfter *struct {
Anchor string `json:"anchor"`
Seconds int64 `json:"seconds"`
} `json:"expires_after,omitempty"`
}
type clientSecretCreateResponse struct {
Value string `json:"value"`
ExpiresAt int64 `json:"expires_at"`
Session json.RawMessage `json:"session"`
}
func newClientSecretStore() *clientSecretStore {
return &clientSecretStore{
entries: make(map[string]clientSecretEntry),
now: time.Now,
}
}
func (s *clientSecretStore) create(session json.RawMessage, lifetime time.Duration, issuerPrincipal, issuerProvider string) (string, ClientSecretAuthorization, time.Time, error) {
if s == nil {
return "", ClientSecretAuthorization{}, time.Time{}, errors.New("Realtime client secret store unavailable")
}
token, errToken := randomRealtimeID(clientSecretPrefix, 32)
if errToken != nil {
return "", ClientSecretAuthorization{}, time.Time{}, errToken
}
sessionID, errSessionID := randomRealtimeID("sess_", 18)
if errSessionID != nil {
return "", ClientSecretAuthorization{}, time.Time{}, errSessionID
}
authorization := ClientSecretAuthorization{
Principal: sessionID,
IssuerPrincipal: strings.TrimSpace(issuerPrincipal),
IssuerProvider: strings.TrimSpace(issuerProvider),
Session: append(json.RawMessage(nil), session...),
}
now := s.currentTime()
expiresAt := now.Add(lifetime)
s.mu.Lock()
s.removeExpiredLocked(now)
if len(s.entries) >= clientSecretMaxEntries {
s.mu.Unlock()
return "", ClientSecretAuthorization{}, time.Time{}, errClientSecretCapacity
}
if authorization.IssuerPrincipal != "" {
issuerEntries := 0
for _, entry := range s.entries {
if entry.authorization.IssuerPrincipal == authorization.IssuerPrincipal && entry.authorization.IssuerProvider == authorization.IssuerProvider {
issuerEntries++
}
}
if issuerEntries >= clientSecretMaxEntriesPerIssuer {
s.mu.Unlock()
return "", ClientSecretAuthorization{}, time.Time{}, errClientSecretCapacity
}
}
s.entries[token] = clientSecretEntry{authorization: authorization, expiresAt: expiresAt}
s.mu.Unlock()
return token, authorization, expiresAt, nil
}
func (s *clientSecretStore) authenticate(token string) (ClientSecretAuthorization, error) {
if s == nil || !strings.HasPrefix(token, clientSecretPrefix) {
return ClientSecretAuthorization{}, errInvalidClientSecret
}
now := s.currentTime()
s.mu.Lock()
entry, ok := s.entries[token]
if !ok || !entry.expiresAt.After(now) {
delete(s.entries, token)
s.mu.Unlock()
return ClientSecretAuthorization{}, errInvalidClientSecret
}
s.mu.Unlock()
entry.authorization.Session = append(json.RawMessage(nil), entry.authorization.Session...)
return entry.authorization, nil
}
func (s *clientSecretStore) close() {
if s == nil {
return
}
s.mu.Lock()
clear(s.entries)
s.mu.Unlock()
}
func (s *clientSecretStore) currentTime() time.Time {
if s != nil && s.now != nil {
return s.now()
}
return time.Now()
}
func (s *clientSecretStore) removeExpiredLocked(now time.Time) {
for token, entry := range s.entries {
if !entry.expiresAt.After(now) {
delete(s.entries, token)
}
}
}
func readClientSecretBody(body io.Reader) ([]byte, error) {
if body == nil {
return nil, nil
}
payload, errRead := io.ReadAll(io.LimitReader(body, clientSecretMaxBodySize+1))
if errRead != nil {
return nil, fmt.Errorf("failed to read Realtime client secret request: %w", errRead)
}
if len(payload) > clientSecretMaxBodySize {
return nil, errBodyTooLarge
}
return payload, nil
}
func randomRealtimeID(prefix string, size int) (string, error) {
payload := make([]byte, size)
if _, errRead := rand.Read(payload); errRead != nil {
return "", fmt.Errorf("generate Realtime identifier: %w", errRead)
}
return prefix + base64.RawURLEncoding.EncodeToString(payload), nil
}
// AuthenticateClientSecret validates a local ephemeral key when the request carries one.
func (h *Handler) AuthenticateClientSecret(request *http.Request) (ClientSecretAuthorization, bool, error) {
token := bearerToken(request)
if !strings.HasPrefix(token, clientSecretPrefix) {
return ClientSecretAuthorization{}, false, nil
}
if h == nil || h.clientSecrets == nil {
return ClientSecretAuthorization{}, true, errInvalidClientSecret
}
authorization, errAuthenticate := h.clientSecrets.authenticate(token)
return authorization, true, errAuthenticate
}
func bearerToken(request *http.Request) string {
if request == nil {
return ""
}
authorization := strings.TrimSpace(request.Header.Get("Authorization"))
const bearerPrefix = "Bearer "
if len(authorization) < len(bearerPrefix) || !strings.EqualFold(authorization[:len(bearerPrefix)], bearerPrefix) {
return ""
}
return strings.TrimSpace(authorization[len(bearerPrefix):])
}
// CreateClientSecret creates a short-lived credential scoped to this proxy.
func (h *Handler) CreateClientSecret(c *gin.Context) {
if h == nil || h.clientSecrets == nil {
writeRealtimeError(c, http.StatusServiceUnavailable, "Realtime client secret service unavailable", "server_error", "realtime_client_secret_unavailable")
return
}
body, errRead := readClientSecretBody(c.Request.Body)
if errRead != nil {
status := http.StatusBadRequest
if errors.Is(errRead, errBodyTooLarge) {
status = http.StatusRequestEntityTooLarge
}
writeRealtimeError(c, status, errRead.Error(), "invalid_request_error", "invalid_request")
return
}
var request clientSecretCreateRequest
if len(strings.TrimSpace(string(body))) > 0 {
if errUnmarshal := json.Unmarshal(body, &request); errUnmarshal != nil {
writeRealtimeError(c, http.StatusBadRequest, "Invalid Realtime client secret request", "invalid_request_error", "invalid_request")
return
}
}
h.createClientSecret(c, request.Session, request.ExpiresAfter, false)
}
// CreateLegacySession implements the deprecated Realtime session credential endpoint.
func (h *Handler) CreateLegacySession(c *gin.Context) {
if h == nil || h.clientSecrets == nil {
writeRealtimeError(c, http.StatusServiceUnavailable, "Realtime client secret service unavailable", "server_error", "realtime_client_secret_unavailable")
return
}
body, errRead := readClientSecretBody(c.Request.Body)
if errRead != nil {
status := http.StatusBadRequest
if errors.Is(errRead, errBodyTooLarge) {
status = http.StatusRequestEntityTooLarge
}
writeRealtimeError(c, status, errRead.Error(), "invalid_request_error", "invalid_request")
return
}
h.createClientSecret(c, json.RawMessage(body), nil, true)
}
func (h *Handler) createClientSecret(c *gin.Context, session json.RawMessage, expiresAfter *struct {
Anchor string `json:"anchor"`
Seconds int64 `json:"seconds"`
}, legacy bool) {
lifetime, errLifetime := clientSecretLifetime(expiresAfter)
if errLifetime != nil {
writeRealtimeError(c, http.StatusBadRequest, errLifetime.Error(), "invalid_request_error", "invalid_expires_after")
return
}
clientSession, upstreamSession, errSession := normalizeClientSecretSession(session)
if errSession != nil {
if errors.Is(errSession, errUnsupportedSessionType) {
writeRealtimeError(c, http.StatusNotImplemented, errSession.Error(), "not_supported_error", "realtime_capability_not_supported")
return
}
writeRealtimeError(c, http.StatusBadRequest, errSession.Error(), "invalid_request_error", "invalid_session")
return
}
issuerPrincipal, _ := c.Get("userApiKey")
issuerProvider, _ := c.Get("accessProvider")
issuerPrincipalValue, _ := issuerPrincipal.(string)
issuerProviderValue, _ := issuerProvider.(string)
token, authorization, expiresAt, errCreate := h.clientSecrets.create(upstreamSession, lifetime, issuerPrincipalValue, issuerProviderValue)
if errCreate != nil {
if errors.Is(errCreate, errClientSecretCapacity) {
c.Header("Retry-After", "1")
writeRealtimeError(c, http.StatusTooManyRequests, errCreate.Error(), "rate_limit_error", "realtime_client_secret_capacity_exhausted")
return
}
writeRealtimeError(c, http.StatusInternalServerError, "Failed to create Realtime client secret", "server_error", "realtime_client_secret_failed")
return
}
responseSession, errResponse := realtimeSessionResponse(clientSession, authorization.Principal, expiresAt)
if errResponse != nil {
writeRealtimeError(c, http.StatusInternalServerError, "Failed to encode Realtime session", "server_error", "realtime_session_failed")
return
}
c.Header("Cache-Control", "no-store")
if legacy {
var response map[string]any
if errUnmarshal := json.Unmarshal(responseSession, &response); errUnmarshal != nil {
writeRealtimeError(c, http.StatusInternalServerError, "Failed to encode Realtime session", "server_error", "realtime_session_failed")
return
}
response["client_secret"] = gin.H{"value": token, "expires_at": expiresAt.Unix()}
c.JSON(http.StatusOK, response)
return
}
c.JSON(http.StatusOK, clientSecretCreateResponse{
Value: token,
ExpiresAt: expiresAt.Unix(),
Session: responseSession,
})
}
func clientSecretLifetime(expiresAfter *struct {
Anchor string `json:"anchor"`
Seconds int64 `json:"seconds"`
}) (time.Duration, error) {
if expiresAfter == nil {
return clientSecretDefaultLifetime, nil
}
if expiresAfter.Anchor != "" && expiresAfter.Anchor != "created_at" {
return 0, errors.New("expires_after.anchor must be created_at")
}
minimumSeconds := int64(clientSecretMinimumLifetime / time.Second)
maximumSeconds := int64(clientSecretMaximumLifetime / time.Second)
if expiresAfter.Seconds < minimumSeconds || expiresAfter.Seconds > maximumSeconds {
return 0, fmt.Errorf("expires_after.seconds must be between %d and %d", minimumSeconds, maximumSeconds)
}
return time.Duration(expiresAfter.Seconds) * time.Second, nil
}
func normalizeClientSecretSession(session json.RawMessage) (json.RawMessage, json.RawMessage, error) {
trimmedSession := strings.TrimSpace(string(session))
if trimmedSession == "" || trimmedSession == "null" {
session = json.RawMessage(`{"type":"realtime","model":"gpt-realtime"}`)
}
var clientSession map[string]any
if errUnmarshal := json.Unmarshal(session, &clientSession); errUnmarshal != nil || clientSession == nil {
return nil, nil, errors.New("session must be a valid JSON object")
}
sessionType, _ := clientSession["type"].(string)
if strings.TrimSpace(sessionType) == "" {
sessionType = "realtime"
clientSession["type"] = sessionType
}
if sessionType != "realtime" {
return nil, nil, fmt.Errorf("%w by the Codex OAuth upstream: %q", errUnsupportedSessionType, sessionType)
}
model, _ := clientSession["model"].(string)
if strings.TrimSpace(model) == "" {
model = "gpt-realtime"
clientSession["model"] = model
}
clientEncoded, errMarshal := json.Marshal(clientSession)
if errMarshal != nil {
return nil, nil, fmt.Errorf("encode Realtime session: %w", errMarshal)
}
clientSession["model"] = codexRealtimeModel(model)
upstreamEncoded, errMarshal := json.Marshal(clientSession)
if errMarshal != nil {
return nil, nil, fmt.Errorf("encode Codex Realtime session: %w", errMarshal)
}
return clientEncoded, upstreamEncoded, nil
}
func realtimeSessionResponse(session json.RawMessage, sessionID string, expiresAt time.Time) (json.RawMessage, error) {
var response map[string]any
if errUnmarshal := json.Unmarshal(session, &response); errUnmarshal != nil {
return nil, errUnmarshal
}
response["id"] = sessionID
response["object"] = "realtime.session"
response["expires_at"] = expiresAt.Unix()
return json.Marshal(response)
}
func codexRealtimeModel(model string) string {
trimmed := strings.TrimSpace(model)
lower := strings.ToLower(trimmed)
if lower == "" || lower == "gpt-realtime" || strings.HasPrefix(lower, "gpt-realtime-") || strings.Contains(lower, "realtime-preview") {
return defaultLiveModel
}
return trimmed
}
func liveSelectionHeaders(c *gin.Context) http.Header {
if c == nil || c.Request == nil {
return make(http.Header)
}
headers := c.Request.Header.Clone()
if _, ok := c.Get(ClientSecretPrincipalContextKey); ok {
headers.Del("Authorization")
headers.Del("Proxy-Authorization")
}
return headers
}
func requestOwner(c *gin.Context) (string, string) {
if c == nil {
return "", ""
}
principalValue, _ := c.Get("userApiKey")
providerValue, _ := c.Get("accessProvider")
principal, _ := principalValue.(string)
provider, _ := providerValue.(string)
return strings.TrimSpace(principal), strings.TrimSpace(provider)
}
func clientSecretSession(c *gin.Context) json.RawMessage {
if c == nil {
return nil
}
value, ok := c.Get(ClientSecretSessionContextKey)
if !ok {
return nil
}
session, _ := value.(json.RawMessage)
return append(json.RawMessage(nil), session...)
}
func writeRealtimeError(c *gin.Context, status int, message, errorType, code string) {
c.JSON(status, gin.H{"error": gin.H{
"message": message,
"type": errorType,
"param": nil,
"code": code,
}})
}

View file

@ -0,0 +1,262 @@
package live
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func TestCreateClientSecretMapsStandardRealtimeModel(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := &Handler{clientSecrets: newClientSecretStore()}
router := gin.New()
router.POST("/v1/realtime/client_secrets", func(c *gin.Context) {
c.Set("userApiKey", "issuer-key")
c.Set("accessProvider", "static")
c.Next()
}, handler.CreateClientSecret)
request := httptest.NewRequest(http.MethodPost, "/v1/realtime/client_secrets", strings.NewReader(`{
"session":{"type":"realtime","model":"gpt-realtime","instructions":"help"},
"expires_after":{"anchor":"created_at","seconds":60}
}`))
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response struct {
Value string `json:"value"`
ExpiresAt int64 `json:"expires_at"`
Session struct {
ID string `json:"id"`
Object string `json:"object"`
Type string `json:"type"`
Model string `json:"model"`
Instructions string `json:"instructions"`
} `json:"session"`
}
if errUnmarshal := json.Unmarshal(recorder.Body.Bytes(), &response); errUnmarshal != nil {
t.Fatalf("unmarshal response: %v", errUnmarshal)
}
if !strings.HasPrefix(response.Value, clientSecretPrefix) {
t.Fatalf("client secret = %q", response.Value)
}
if response.ExpiresAt <= time.Now().Unix() {
t.Fatalf("expires_at = %d", response.ExpiresAt)
}
if response.Session.ID == "" || response.Session.Object != "realtime.session" || response.Session.Type != "realtime" {
t.Fatalf("session = %+v", response.Session)
}
if response.Session.Model != "gpt-realtime" || response.Session.Instructions != "help" {
t.Fatalf("client session = %+v", response.Session)
}
authRequest := httptest.NewRequest(http.MethodPost, "/v1/realtime/calls", nil)
authRequest.Header.Set("Authorization", "Bearer "+response.Value)
authorization, matched, errAuthenticate := handler.AuthenticateClientSecret(authRequest)
if errAuthenticate != nil || !matched {
t.Fatalf("AuthenticateClientSecret() matched=%t error=%v", matched, errAuthenticate)
}
if authorization.Principal != response.Session.ID {
t.Fatalf("principal = %q, want %q", authorization.Principal, response.Session.ID)
}
if authorization.IssuerPrincipal != "issuer-key" || authorization.IssuerProvider != "static" {
t.Fatalf("issuer = %q/%q", authorization.IssuerProvider, authorization.IssuerPrincipal)
}
if got := modelFromJSON(authorization.Session); got != defaultLiveModel {
t.Fatalf("upstream session model = %q, want %q", got, defaultLiveModel)
}
}
func TestStandardRealtimeCallMapsModelAndLocation(t *testing.T) {
gin.SetMode(gin.TestMode)
manager := auth.NewManager(nil, nil, nil)
executor := &captureExecutor{responseBody: io.NopCloser(strings.NewReader("v=0\r\n"))}
manager.RegisterExecutor(executor)
if _, errRegister := manager.Register(context.Background(), &auth.Auth{
ID: "codex-oauth",
Provider: "codex",
Status: auth.StatusActive,
Metadata: map[string]any{"access_token": "oauth-token"},
}); errRegister != nil {
t.Fatalf("register auth: %v", errRegister)
}
handler := NewHandler(manager, nil)
router := gin.New()
router.POST("/v1/realtime/calls", handler.Handle)
const boundary = "standard-realtime-boundary"
body := multipartBody(boundary, "v=0\r\n", `{"type":"realtime","model":"gpt-realtime"}`)
request := httptest.NewRequest(http.MethodPost, "/v1/realtime/calls", strings.NewReader(body))
request.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusCreated, recorder.Body.String())
}
if recorder.Header().Get("Location") != "/v1/realtime/calls/call-123" {
t.Fatalf("Location = %q", recorder.Header().Get("Location"))
}
if got := modelFromJSON(executor.body); got != defaultLiveModel {
t.Fatalf("upstream model = %q, want %q; body=%s", got, defaultLiveModel, executor.body)
}
}
func TestClientSecretStoreRejectsExpiredToken(t *testing.T) {
store := newClientSecretStore()
now := time.Unix(1700000000, 0)
store.now = func() time.Time { return now }
token, _, _, errCreate := store.create(json.RawMessage(`{"type":"realtime","model":"gpt-live-1-codex"}`), time.Minute, "issuer", "test")
if errCreate != nil {
t.Fatalf("create() error = %v", errCreate)
}
if _, errAuthenticate := store.authenticate(token); errAuthenticate != nil {
t.Fatalf("authenticate() error = %v", errAuthenticate)
}
now = now.Add(time.Minute)
if _, errAuthenticate := store.authenticate(token); errAuthenticate == nil {
t.Fatal("authenticate() accepted expired token")
}
}
func TestNormalizeClientSecretSessionHandlesWhitespaceNullAndRejectsArrays(t *testing.T) {
clientSession, upstreamSession, errNormalize := normalizeClientSecretSession(json.RawMessage(" null \n"))
if errNormalize != nil {
t.Fatalf("normalize whitespace null: %v", errNormalize)
}
if modelFromJSON(clientSession) != "gpt-realtime" || modelFromJSON(upstreamSession) != defaultLiveModel {
t.Fatalf("client=%s upstream=%s", clientSession, upstreamSession)
}
if _, _, errNormalize = normalizeClientSecretSession(json.RawMessage(`[]`)); errNormalize == nil {
t.Fatal("normalize accepted an array session")
}
}
func TestReadClientSecretBodyRejectsOversizedSession(t *testing.T) {
_, errRead := readClientSecretBody(bytes.NewReader(make([]byte, clientSecretMaxBodySize+1)))
if !errors.Is(errRead, errBodyTooLarge) {
t.Fatalf("readClientSecretBody() error = %v", errRead)
}
}
func TestCreateClientSecretRejectsUnsupportedSessionType(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := &Handler{clientSecrets: newClientSecretStore()}
router := gin.New()
router.POST("/v1/realtime/client_secrets", handler.CreateClientSecret)
request := httptest.NewRequest(http.MethodPost, "/v1/realtime/client_secrets", strings.NewReader(`{"session":{"type":"transcription","model":"gpt-4o-transcribe"}}`))
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotImplemented {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusNotImplemented, recorder.Body.String())
}
if !strings.Contains(recorder.Body.String(), "realtime_capability_not_supported") {
t.Fatalf("body = %s", recorder.Body.String())
}
}
func TestLiveSelectionHeadersRemoveLocalClientSecret(t *testing.T) {
gin.SetMode(gin.TestMode)
ginContext, _ := gin.CreateTestContext(httptest.NewRecorder())
ginContext.Request = httptest.NewRequest(http.MethodPost, "/v1/realtime/calls", nil)
ginContext.Request.Header.Set("Authorization", "Bearer ek_secret")
ginContext.Request.Header.Set("OpenAI-Safety-Identifier", "safe-user")
ginContext.Set(ClientSecretPrincipalContextKey, "sess_123")
headers := liveSelectionHeaders(ginContext)
if headers.Get("Authorization") != "" {
t.Fatalf("Authorization leaked: %q", headers.Get("Authorization"))
}
if headers.Get("OpenAI-Safety-Identifier") != "safe-user" {
t.Fatalf("safety identifier = %q", headers.Get("OpenAI-Safety-Identifier"))
}
}
func TestSidebandRejectsClientSecretScopeMismatch(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := NewHandler(auth.NewManager(nil, nil, nil), nil)
handler.sessions.put("call-123", liveSession{
authID: "codex-oauth",
model: defaultLiveModel,
clientSecretPrincipal: "sess_expected",
})
router := gin.New()
router.GET("/v1/realtime/calls/:call_id", func(c *gin.Context) {
c.Set(ClientSecretPrincipalContextKey, "sess_other")
c.Next()
}, handler.HandleSideband)
request := httptest.NewRequest(http.MethodGet, "/v1/realtime/calls/call-123", nil)
request.Header.Set("Connection", "Upgrade")
request.Header.Set("Upgrade", "websocket")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusForbidden, recorder.Body.String())
}
claimed, claim := handler.sessions.claim("call-123")
if claim != sessionClaimAcquired {
t.Fatalf("session claim = %v", claim)
}
handler.sessions.release(claimed)
}
func TestSidebandRejectsStandardPrincipalScopeMismatch(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := NewHandler(auth.NewManager(nil, nil, nil), nil)
handler.sessions.put("call-123", liveSession{
authID: "codex-oauth",
model: defaultLiveModel,
ownerPrincipal: "owner-key",
ownerProvider: "static",
})
router := gin.New()
router.GET("/v1/realtime/calls/:call_id", func(c *gin.Context) {
c.Set("userApiKey", "other-key")
c.Set("accessProvider", "static")
c.Next()
}, handler.HandleSideband)
request := httptest.NewRequest(http.MethodGet, "/v1/realtime/calls/call-123", nil)
request.Header.Set("Connection", "Upgrade")
request.Header.Set("Upgrade", "websocket")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusForbidden, recorder.Body.String())
}
}
func TestApplyClientSecretCallSession(t *testing.T) {
session := json.RawMessage(`{"type":"realtime","model":"gpt-live-1-codex","instructions":"help"}`)
body, contentType, model, errApply := applyClientSecretCallSession([]byte("v=0\r\n"), "application/sdp", defaultLiveModel, session)
if errApply != nil {
t.Fatalf("applyClientSecretCallSession() error = %v", errApply)
}
if contentType != "application/json" || model != defaultLiveModel {
t.Fatalf("contentType=%q model=%q", contentType, model)
}
var payload struct {
SDP string `json:"sdp"`
Session struct {
Instructions string `json:"instructions"`
} `json:"session"`
}
if errUnmarshal := json.Unmarshal(body, &payload); errUnmarshal != nil {
t.Fatalf("unmarshal body: %v", errUnmarshal)
}
if payload.SDP != "v=0\r\n" || payload.Session.Instructions != "help" {
t.Fatalf("payload = %+v", payload)
}
}

View file

@ -0,0 +1,840 @@
// Package live forwards Codex realtime WebRTC session bootstrap requests.
package live
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"path/filepath"
"reflect"
"strings"
"sync"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/clienterror"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
log "github.com/sirupsen/logrus"
)
const (
upstreamCallURL = "https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas"
defaultLiveModel = "gpt-live-1-codex"
maxBodySize = 16 << 20
)
var liveProtocolHeaders = []string{
"OpenAI-Alpha",
"X-Session-Id",
"Session-Id",
"Thread-Id",
"Originator",
"OpenAI-Safety-Identifier",
"OpenAI-Organization",
"OpenAI-Project",
"X-Oai-Attestation",
}
// Handler forwards Codex live session requests through the shared auth scheduler.
type Handler struct {
authManager *auth.Manager
cfg *config.Config
sessions *sessionStore
clientSecrets *clientSecretStore
sidebandAPIBaseURL string
mediaRelayMu sync.RWMutex
mediaRelay mediaRelayFactory
mediaRelayErr error
mediaRelayConfig config.CodexLiveMediaRelayConfig
mediaRelayConfigured bool
mediaLimiter *mediaSessionLimiter
}
// NewHandler creates a Codex live session handler.
func NewHandler(authManager *auth.Manager, cfg *config.Config) *Handler {
handler := &Handler{
authManager: authManager,
cfg: cfg,
sessions: newSessionStore(),
clientSecrets: newClientSecretStore(),
sidebandAPIBaseURL: defaultSidebandAPIBaseURL,
}
if errUpdate := handler.UpdateConfig(cfg); errUpdate != nil {
log.WithError(errUpdate).Error("failed to configure Codex Live media relay")
}
return handler
}
// UpdateConfig atomically applies Codex Live media relay settings to new sessions.
func (h *Handler) UpdateConfig(cfg *config.Config) error {
if h == nil {
return nil
}
var relayConfig config.CodexLiveMediaRelayConfig
if cfg != nil {
relayConfig = cfg.Codex.LiveMediaRelay
}
h.mediaRelayMu.Lock()
previousConfig := h.mediaRelayConfig
previouslyConfigured := h.mediaRelayConfigured
h.cfg = cfg
if previouslyConfigured && reflect.DeepEqual(previousConfig, relayConfig) {
currentErr := h.mediaRelayErr
h.mediaRelayMu.Unlock()
return currentErr
}
if h.mediaLimiter == nil {
h.mediaLimiter = &mediaSessionLimiter{}
}
var relay mediaRelayFactory
var relayErr error
if relayConfig.Enabled {
relay, relayErr = newPionMediaRelayWithLimiter(relayConfig, h.mediaLimiter)
}
h.mediaRelay = relay
h.mediaRelayErr = relayErr
h.mediaRelayConfig = relayConfig
h.mediaRelayConfigured = true
h.mediaRelayMu.Unlock()
if relayErr == nil && (previouslyConfigured || relayConfig.Enabled) {
message := "codex live media relay configured"
if previouslyConfigured {
message = "codex live media relay configuration reloaded; changes apply to new sessions"
}
log.WithFields(liveMediaConfigLogFields(relayConfig)).Info(message)
}
return relayErr
}
func liveMediaConfigLogFields(relayConfig config.CodexLiveMediaRelayConfig) log.Fields {
publicIP := strings.TrimSpace(relayConfig.PublicIP)
if publicIP == "" {
publicIP = "auto"
}
return log.Fields{
"enabled": relayConfig.Enabled,
"max_sessions": relayConfig.EffectiveMaxSessions(),
"disable_private_remote_ips": relayConfig.DisablePrivateRemoteIPs,
"public_ip": publicIP,
"udp_port_min": relayConfig.UDPPortMin,
"udp_port_max": relayConfig.UDPPortMax,
"ice_server_count": len(relayConfig.ICEServers),
}
}
func (h *Handler) currentRuntime() (*config.Config, mediaRelayFactory, error) {
if h == nil {
return nil, nil, nil
}
h.mediaRelayMu.RLock()
cfg := h.cfg
relay := h.mediaRelay
relayErr := h.mediaRelayErr
h.mediaRelayMu.RUnlock()
return cfg, relay, relayErr
}
func (h *Handler) currentConfig() *config.Config {
if h == nil {
return nil
}
h.mediaRelayMu.RLock()
cfg := h.cfg
h.mediaRelayMu.RUnlock()
return cfg
}
func (h *Handler) currentMediaRelay() (mediaRelayFactory, error) {
if h == nil {
return nil, nil
}
h.mediaRelayMu.RLock()
relay := h.mediaRelay
relayErr := h.mediaRelayErr
h.mediaRelayMu.RUnlock()
return relay, relayErr
}
// Close releases all active Codex live sessions.
func (h *Handler) Close() {
if h == nil {
return
}
if h.sessions != nil {
h.sessions.closeAll("server_stopped")
}
if h.clientSecrets != nil {
h.clientSecrets.close()
}
}
// Handle forwards a WebRTC SDP bootstrap request to the Codex realtime calls endpoint.
func (h *Handler) Handle(c *gin.Context) {
if h == nil || h.authManager == nil {
writeLiveError(c, http.StatusServiceUnavailable, "Codex auth manager unavailable")
return
}
body, errRead := readBody(c.Request.Body)
if errRead != nil {
status := clienterror.HTTPStatusFromErrorOr(errRead, http.StatusBadRequest)
if errors.Is(errRead, errBodyTooLarge) {
status = http.StatusRequestEntityTooLarge
}
writeLiveError(c, status, errRead.Error())
return
}
upstreamBody, upstreamContentType, model, errPayload := prepareCallRequest(body, c.GetHeader("Content-Type"))
if errPayload == nil {
upstreamBody, upstreamContentType, model, errPayload = applyClientSecretCallSession(upstreamBody, upstreamContentType, model, clientSecretSession(c))
}
if errPayload == nil {
upstreamBody, model, errPayload = rewriteCallRequestModel(upstreamBody, upstreamContentType, model)
}
if errPayload != nil {
writeLiveError(c, http.StatusBadRequest, errPayload.Error())
return
}
runtimeConfig, mediaRelay, mediaRelayErr := h.currentRuntime()
if mediaRelayErr != nil {
writeLiveError(c, http.StatusServiceUnavailable, mediaRelayErr.Error())
return
}
var mediaSession mediaRelaySession
mediaRetained := false
ctx := context.WithValue(c.Request.Context(), "gin", c)
selectionOpts := coreexecutor.Options{
Headers: liveSelectionHeaders(c),
OriginalRequest: body,
}
selection, selected, errSelect := h.selectOAuth(ctx, model, selectionOpts)
if errSelect != nil {
writeSelectionError(c, errSelect)
return
}
if selected == nil {
if selection != nil {
selection.End("missing_auth")
}
writeLiveError(c, http.StatusServiceUnavailable, "Codex auth unavailable")
return
}
if selection != nil {
attemptCtx, releaseAttempt, errAttempt := selection.AttemptContext(ctx)
if errAttempt != nil {
selection.End("attempt_bind_failed")
writeLiveError(c, http.StatusServiceUnavailable, errAttempt.Error())
return
}
ctx = attemptCtx
defer releaseAttempt()
}
selectedIndex := selected.EnsureIndex()
logging.SetGinCPATraceID(c, selectedIndex)
if selection != nil {
defer func() {
if selection.Active() && !selection.Retained() {
selection.End("request_closed")
}
}()
}
if mediaRelay != nil {
clientOffer, errSDP := callRequestSDP(upstreamBody, upstreamContentType)
if errSDP != nil {
writeLiveError(c, http.StatusBadRequest, errSDP.Error())
return
}
var upstreamOffer string
mediaSession, upstreamOffer, errSDP = mediaRelay.NewSession(ctx, clientOffer, mediaSessionRoute{
proxyURL: proxyURLForAuth(runtimeConfig, selected),
credential: mediaCredentialName(selected, selectedIndex),
authIndex: selectedIndex,
})
if errSDP != nil {
writeLiveError(c, clienterror.HTTPStatusFromErrorOr(errSDP, http.StatusBadGateway), errSDP.Error())
return
}
defer func() {
if !mediaRetained {
if errClose := mediaSession.CloseWithReason("request_not_retained"); errClose != nil {
log.WithError(errClose).Debug("codex live media: close unretained session")
}
}
}()
upstreamBody, upstreamContentType, errSDP = replaceCallRequestSDP(upstreamBody, upstreamContentType, upstreamOffer)
if errSDP != nil {
writeLiveError(c, http.StatusBadRequest, errSDP.Error())
return
}
}
baseHeaders := protocolHeaders(c.Request.Header)
baseHeaders.Set("Content-Type", upstreamContentType)
performRequest := func(current *auth.Auth) (*http.Response, error) {
headers := baseHeaders.Clone()
setAccountHeader(headers, current)
req, errRequest := h.authManager.NewHttpRequest(ctx, current, http.MethodPost, upstreamCallURL, upstreamBody, headers)
if errRequest != nil {
return nil, errRequest
}
authType, authValue := current.AccountInfo()
helps.RecordAPIRequest(ctx, runtimeConfig, helps.UpstreamRequestLog{
URL: upstreamCallURL,
Method: http.MethodPost,
Headers: headersForLogging(req.Header),
Body: upstreamBody,
Provider: "codex",
AuthID: current.ID,
AuthLabel: current.Label,
AuthType: authType,
AuthValue: authValue,
})
return h.authManager.HttpRequest(ctx, current, req)
}
if errContext := ctx.Err(); errContext != nil {
if selection != nil {
selection.End("attempt_canceled")
}
writeLiveError(c, clienterror.HTTPStatusFromErrorOr(errContext, http.StatusRequestTimeout), errContext.Error())
return
}
resp, errRequest := performRequest(selected)
if errRequest != nil {
if selection != nil {
selection.End("request_failed")
}
helps.RecordAPIResponseError(ctx, runtimeConfig, errRequest)
writeLiveError(c, clienterror.HTTPStatusFromErrorOr(errRequest, http.StatusBadGateway), errRequest.Error())
return
}
if selection != nil && resp.StatusCode == http.StatusUnauthorized {
h.authManager.ReportHomeUnauthorized(ctx, selected, "codex", model)
helps.RecordAPIResponseMetadata(ctx, runtimeConfig, resp.StatusCode, callResponseHeaders(resp.Header))
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<20))
if errClose := resp.Body.Close(); errClose != nil {
log.Errorf("codex live: close unauthorized response body error: %v", errClose)
}
refreshed, didRefresh, errRefresh := h.authManager.RefreshHomeSelectionAfterUnauthorized(ctx, selection, selected)
if errRefresh != nil {
selection.End("refresh_failed")
writeSelectionError(c, errRefresh)
return
}
if !didRefresh || refreshed == nil {
selection.End("refresh_unavailable")
writeLiveError(c, http.StatusUnauthorized, "Codex credential unauthorized")
return
}
selected = refreshed
logging.SetGinCPATraceID(c, selected.EnsureIndex())
resp, errRequest = performRequest(selected)
if errRequest != nil {
selection.End("retry_failed")
helps.RecordAPIResponseError(ctx, runtimeConfig, errRequest)
writeLiveError(c, clienterror.HTTPStatusFromErrorOr(errRequest, http.StatusBadGateway), errRequest.Error())
return
}
if resp.StatusCode == http.StatusUnauthorized {
h.authManager.ReportHomeUnauthorized(ctx, selected, "codex", model)
}
}
var closeResponseOnce sync.Once
var closeResponseErr error
closeResponseBody := func() error {
closeResponseOnce.Do(func() {
closeResponseErr = resp.Body.Close()
if closeResponseErr != nil {
log.Errorf("codex live: close response body error: %v", closeResponseErr)
}
})
return closeResponseErr
}
defer func() { _ = closeResponseBody() }()
if selection != nil {
if errBind := selection.Bind(closeResponseBody); errBind != nil {
selection.End("response_bind_failed")
writeLiveError(c, http.StatusServiceUnavailable, errBind.Error())
return
}
}
responseHeaders := callResponseHeaders(resp.Header)
helps.RecordAPIResponseMetadata(ctx, runtimeConfig, resp.StatusCode, responseHeaders)
responseBody, errResponse := readLimitedBody(resp.Body)
if errResponse != nil {
helps.RecordAPIResponseError(ctx, runtimeConfig, errResponse)
message := "Failed to read Codex live response"
status := clienterror.HTTPStatusFromErrorOr(errResponse, http.StatusBadGateway)
if errors.Is(errResponse, errBodyTooLarge) {
message = "Codex live response body too large"
status = http.StatusBadGateway
}
writeLiveError(c, status, message)
return
}
helps.AppendAPIResponseChunk(ctx, runtimeConfig, responseBody)
responseBodyToWrite := responseBody
success := resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices
callID := ""
if success {
callID = callIDFromLocation(resp.Header.Get("Location"))
if callID == "" && mediaSession != nil {
writeLiveError(c, http.StatusBadGateway, "Codex live response is missing a valid call ID")
return
}
if mediaSession != nil {
mediaSession.SetCallID(callID)
}
if callID != "" && strings.HasPrefix(c.Request.URL.Path, "/v1/realtime") {
responseHeaders.Set("Location", "/v1/realtime/calls/"+callID)
}
}
if success && mediaSession != nil {
upstreamAnswer, errSDP := callResponseSDP(responseBody, resp.Header.Get("Content-Type"))
if errSDP != nil {
writeLiveError(c, http.StatusBadGateway, errSDP.Error())
return
}
downstreamAnswer, errAnswer := mediaSession.AcceptUpstreamAnswer(ctx, upstreamAnswer)
if errAnswer != nil {
writeLiveError(c, clienterror.HTTPStatusFromErrorOr(errAnswer, http.StatusBadGateway), errAnswer.Error())
return
}
responseBodyToWrite = []byte(downstreamAnswer)
responseHeaders.Set("Content-Type", "application/sdp")
}
var storedSession liveSession
sessionStored := false
if success && h.sessions != nil {
if callID != "" {
session := liveSession{authID: selected.ID, model: model, media: mediaSession}
session.ownerPrincipal, session.ownerProvider = requestOwner(c)
if principal, ok := c.Get(ClientSecretPrincipalContextKey); ok {
session.clientSecretPrincipal, _ = principal.(string)
}
if selection != nil {
if mediaSession != nil {
if errBind := selection.Bind(func() error {
return mediaSession.CloseWithReason("home_selection_closed")
}); errBind != nil {
selection.End("media_bind_failed")
writeLiveError(c, http.StatusServiceUnavailable, errBind.Error())
return
}
}
if errBind := selection.Bind(func() error {
// End outside the resource closer to avoid waiting on the closer itself.
go selection.End("session_drained")
return nil
}); errBind != nil {
selection.End("session_drain_bind_failed")
writeLiveError(c, http.StatusServiceUnavailable, errBind.Error())
return
}
selection.Retain()
session.homeSelection = selection
}
storedSession = h.sessions.put(callID, session)
sessionStored = storedSession.callID != ""
if mediaSession != nil {
mediaSession.SetCloseHandler(func(reason string) {
h.sessions.complete(storedSession, reason)
})
mediaRetained = true
}
}
}
writeResponseHeaders(c.Writer.Header(), responseHeaders)
c.Status(resp.StatusCode)
if _, errWrite := c.Writer.Write(responseBodyToWrite); errWrite != nil {
if sessionStored {
h.sessions.complete(storedSession, "response_write_failed")
}
helps.RecordAPIResponseError(ctx, runtimeConfig, errWrite)
log.WithError(errWrite).Warn("codex live: write response body failed")
}
}
func mediaCredentialName(selected *auth.Auth, authIndex string) string {
if selected == nil {
return strings.TrimSpace(authIndex)
}
if label := strings.TrimSpace(selected.Label); label != "" {
return label
}
if fileName := strings.TrimSpace(selected.FileName); fileName != "" {
if baseName := strings.TrimSpace(filepath.Base(fileName)); baseName != "" && baseName != "." {
return baseName
}
}
return strings.TrimSpace(authIndex)
}
func (h *Handler) selectOAuth(ctx context.Context, model string, opts coreexecutor.Options) (*auth.HomeDispatchSelection, *auth.Auth, error) {
var selection *auth.HomeDispatchSelection
var selected *auth.Auth
var errSelect error
if h.authManager.HomeEnabled() {
selection, errSelect = h.authManager.SelectHomeAuthByKind(ctx, "codex", model, auth.AuthKindOAuth, opts)
if selection != nil {
selected = selection.CloneAuth()
}
} else {
selected, errSelect = h.authManager.SelectAuthByKind(ctx, "codex", "", auth.AuthKindOAuth, opts)
}
if errSelect != nil && selection != nil {
selection.End("selection_failed")
}
return selection, selected, errSelect
}
var errBodyTooLarge = errors.New("Codex live request body too large")
func readBody(body io.Reader) ([]byte, error) {
payload, errRead := readLimitedBody(body)
if errRead != nil {
if errors.Is(errRead, errBodyTooLarge) {
return nil, errRead
}
return nil, fmt.Errorf("failed to read Codex live request: %w", errRead)
}
return payload, nil
}
func readLimitedBody(body io.Reader) ([]byte, error) {
if body == nil {
return nil, nil
}
payload, errRead := io.ReadAll(io.LimitReader(body, maxBodySize+1))
if errRead != nil {
return nil, errRead
}
if len(payload) > maxBodySize {
return nil, errBodyTooLarge
}
return payload, nil
}
func prepareCallRequest(body []byte, contentType string) ([]byte, string, string, error) {
mediaType, params, errMediaType := mime.ParseMediaType(contentType)
if errMediaType == nil && strings.EqualFold(mediaType, "multipart/form-data") {
return multipartCallRequest(body, strings.TrimSpace(params["boundary"]))
}
model := modelFromJSON(body)
if model == "" {
model = defaultLiveModel
}
if strings.TrimSpace(contentType) == "" {
contentType = "application/json"
}
return body, contentType, model, nil
}
func applyClientSecretCallSession(body []byte, contentType, model string, session json.RawMessage) ([]byte, string, string, error) {
if len(session) == 0 {
return body, contentType, model, nil
}
mediaType, _, errMediaType := mime.ParseMediaType(contentType)
if errMediaType == nil && (strings.EqualFold(mediaType, "application/sdp") || strings.EqualFold(mediaType, "text/plain")) {
encoded, errEncode := encodeCallRequest(string(body), session)
if errEncode != nil {
return nil, "", "", errEncode
}
return encoded, "application/json", modelFromJSON(session), nil
}
if errMediaType != nil || !strings.EqualFold(mediaType, "application/json") {
return nil, "", "", errors.New("Realtime client secrets require an SDP or JSON call request")
}
var payload map[string]json.RawMessage
if errUnmarshal := json.Unmarshal(body, &payload); errUnmarshal != nil {
return nil, "", "", fmt.Errorf("failed to decode Realtime call request: %w", errUnmarshal)
}
payload["session"] = append(json.RawMessage(nil), session...)
encoded, errMarshal := json.Marshal(payload)
if errMarshal != nil {
return nil, "", "", fmt.Errorf("failed to encode Realtime call request: %w", errMarshal)
}
return encoded, "application/json", modelFromJSON(session), nil
}
func rewriteCallRequestModel(body []byte, contentType, model string) ([]byte, string, error) {
upstreamModel := codexRealtimeModel(model)
mediaType, _, errMediaType := mime.ParseMediaType(contentType)
if errMediaType != nil || !strings.EqualFold(mediaType, "application/json") || len(bytes.TrimSpace(body)) == 0 {
return body, upstreamModel, nil
}
var payload map[string]json.RawMessage
if errUnmarshal := json.Unmarshal(body, &payload); errUnmarshal != nil {
return nil, "", fmt.Errorf("failed to decode Realtime call request: %w", errUnmarshal)
}
changed := false
if sessionJSON, ok := payload["session"]; ok && len(sessionJSON) > 0 {
var session map[string]json.RawMessage
if errUnmarshal := json.Unmarshal(sessionJSON, &session); errUnmarshal != nil {
return nil, "", fmt.Errorf("failed to decode Realtime session: %w", errUnmarshal)
}
encodedModel, errMarshal := json.Marshal(upstreamModel)
if errMarshal != nil {
return nil, "", fmt.Errorf("failed to encode Realtime model: %w", errMarshal)
}
session["model"] = encodedModel
encodedSession, errMarshal := json.Marshal(session)
if errMarshal != nil {
return nil, "", fmt.Errorf("failed to encode Realtime session: %w", errMarshal)
}
payload["session"] = encodedSession
changed = true
} else if _, ok := payload["model"]; ok {
encodedModel, errMarshal := json.Marshal(upstreamModel)
if errMarshal != nil {
return nil, "", fmt.Errorf("failed to encode Realtime model: %w", errMarshal)
}
payload["model"] = encodedModel
changed = true
}
if !changed {
return body, upstreamModel, nil
}
encoded, errMarshal := json.Marshal(payload)
if errMarshal != nil {
return nil, "", fmt.Errorf("failed to encode Realtime call request: %w", errMarshal)
}
return encoded, upstreamModel, nil
}
func multipartCallRequest(body []byte, boundary string) ([]byte, string, string, error) {
if boundary == "" {
return nil, "", "", errors.New("Codex live multipart boundary is missing")
}
reader := multipart.NewReader(bytes.NewReader(body), boundary)
var sdp *string
var session json.RawMessage
model := ""
for {
part, errPart := reader.NextPart()
if errors.Is(errPart, io.EOF) {
break
}
if errPart != nil {
return nil, "", "", fmt.Errorf("failed to parse Codex live multipart body: %w", errPart)
}
partBody, errRead := io.ReadAll(part)
errClose := part.Close()
if errRead != nil {
return nil, "", "", fmt.Errorf("failed to read Codex live multipart field: %w", errRead)
}
if errClose != nil {
return nil, "", "", fmt.Errorf("failed to close Codex live multipart field: %w", errClose)
}
switch part.FormName() {
case "sdp":
value := string(partBody)
sdp = &value
case "session":
if !json.Valid(partBody) {
return nil, "", "", errors.New("Codex live session field must contain valid JSON")
}
session = append(json.RawMessage(nil), partBody...)
model = modelFromJSON(partBody)
}
}
if sdp == nil {
return nil, "", "", errors.New("Codex live multipart body requires an sdp field")
}
if model == "" {
model = defaultLiveModel
}
encoded, errEncode := encodeCallRequest(*sdp, session)
if errEncode != nil {
return nil, "", "", errEncode
}
return encoded, "application/json", model, nil
}
func encodeCallRequest(sdp string, session json.RawMessage) ([]byte, error) {
payload := struct {
SDP string `json:"sdp"`
Session json.RawMessage `json:"session,omitempty"`
}{
SDP: sdp,
Session: session,
}
encoded, errMarshal := json.Marshal(payload)
if errMarshal != nil {
return nil, fmt.Errorf("failed to encode Codex live request: %w", errMarshal)
}
return encoded, nil
}
func callRequestSDP(body []byte, contentType string) (string, error) {
mediaType, _, errMediaType := mime.ParseMediaType(contentType)
if errMediaType == nil && (strings.EqualFold(mediaType, "application/sdp") || strings.EqualFold(mediaType, "text/plain")) {
if strings.TrimSpace(string(body)) == "" {
return "", errors.New("Codex live call request requires an SDP offer")
}
return string(body), nil
}
if errMediaType != nil || !strings.EqualFold(mediaType, "application/json") {
return "", errors.New("Codex live media relay requires an SDP or JSON call request")
}
var payload struct {
SDP string `json:"sdp"`
}
if errUnmarshal := json.Unmarshal(body, &payload); errUnmarshal != nil {
return "", fmt.Errorf("failed to decode Codex live call request: %w", errUnmarshal)
}
if strings.TrimSpace(payload.SDP) == "" {
return "", errors.New("Codex live call request requires an SDP offer")
}
return payload.SDP, nil
}
func replaceCallRequestSDP(body []byte, contentType, sdp string) ([]byte, string, error) {
mediaType, _, errMediaType := mime.ParseMediaType(contentType)
if errMediaType == nil && (strings.EqualFold(mediaType, "application/sdp") || strings.EqualFold(mediaType, "text/plain")) {
encoded, errEncode := encodeCallRequest(sdp, nil)
if errEncode != nil {
return nil, "", errEncode
}
return encoded, "application/json", nil
}
if errMediaType != nil || !strings.EqualFold(mediaType, "application/json") {
return nil, "", errors.New("Codex live media relay requires an SDP or JSON call request")
}
var payload map[string]json.RawMessage
if errUnmarshal := json.Unmarshal(body, &payload); errUnmarshal != nil {
return nil, "", fmt.Errorf("failed to decode Codex live call request: %w", errUnmarshal)
}
encodedSDP, errMarshal := json.Marshal(sdp)
if errMarshal != nil {
return nil, "", fmt.Errorf("failed to encode Codex live SDP offer: %w", errMarshal)
}
payload["sdp"] = encodedSDP
encoded, errMarshal := json.Marshal(payload)
if errMarshal != nil {
return nil, "", fmt.Errorf("failed to encode Codex live call request: %w", errMarshal)
}
return encoded, "application/json", nil
}
func callResponseSDP(body []byte, contentType string) (string, error) {
mediaType, _, errMediaType := mime.ParseMediaType(contentType)
if errMediaType == nil && strings.EqualFold(mediaType, "application/json") {
var payload struct {
SDP string `json:"sdp"`
}
if errUnmarshal := json.Unmarshal(body, &payload); errUnmarshal != nil {
return "", fmt.Errorf("failed to decode Codex live response: %w", errUnmarshal)
}
if strings.TrimSpace(payload.SDP) == "" {
return "", errors.New("Codex live response requires an SDP answer")
}
return payload.SDP, nil
}
if strings.TrimSpace(string(body)) == "" {
return "", errors.New("Codex live response requires an SDP answer")
}
return string(body), nil
}
func modelFromJSON(body []byte) string {
var payload struct {
Model string `json:"model"`
Session struct {
Model string `json:"model"`
} `json:"session"`
}
if errUnmarshal := json.Unmarshal(body, &payload); errUnmarshal != nil {
return ""
}
if model := strings.TrimSpace(payload.Session.Model); model != "" {
return model
}
return strings.TrimSpace(payload.Model)
}
func protocolHeaders(source http.Header) http.Header {
headers := make(http.Header)
for _, name := range liveProtocolHeaders {
for _, value := range source.Values(name) {
headers.Add(name, value)
}
}
return headers
}
func setAccountHeader(headers http.Header, selected *auth.Auth) {
if selected == nil {
return
}
if accountID, ok := selected.Metadata["account_id"].(string); ok && strings.TrimSpace(accountID) != "" {
headers.Set("Chatgpt-Account-Id", accountID)
}
}
func headersForLogging(source http.Header) http.Header {
headers := source.Clone()
if headers.Get("X-Oai-Attestation") != "" {
headers.Set("X-Oai-Attestation", "[REDACTED]")
}
return headers
}
func callResponseHeaders(source http.Header) http.Header {
headers := make(http.Header)
for _, name := range []string{"Content-Type", "Location", "Retry-After", "X-Request-Id", "OpenAI-Request-Id"} {
for _, value := range source.Values(name) {
headers.Add(name, value)
}
}
return headers
}
func writeResponseHeaders(destination, source http.Header) {
for name, values := range source {
for _, value := range values {
destination.Add(name, value)
}
}
}
func writeLiveError(c *gin.Context, status int, message string) {
if c != nil && c.Request != nil && c.Request.URL != nil && strings.HasPrefix(c.Request.URL.Path, "/v1/realtime") {
errorType := "api_error"
if status >= http.StatusBadRequest && status < http.StatusInternalServerError {
errorType = "invalid_request_error"
}
if status == http.StatusUnauthorized {
errorType = "authentication_error"
}
writeRealtimeError(c, status, message, errorType, "realtime_request_failed")
return
}
c.JSON(status, gin.H{"error": message})
}
func writeSelectionError(c *gin.Context, err error) {
status := clienterror.HTTPStatusFromErrorOr(err, http.StatusServiceUnavailable)
for _, value := range auth.SafeResponseHeaders(err).Values("Retry-After") {
c.Writer.Header().Add("Retry-After", value)
}
writeLiveError(c, status, err.Error())
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,887 @@
package live
import (
"context"
"errors"
"fmt"
"io"
"net"
"strings"
"sync"
"github.com/google/uuid"
"github.com/pion/interceptor"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus"
"golang.org/x/net/proxy"
)
const (
realtimeDataChannelLabel = "oai-events"
mediaDataQueueSize = 64
mediaDataMessageMaxSize = 256 << 10
mediaDataBufferedMaxSize = 1 << 20
)
var opusCodec = webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1",
}
type mediaRelaySession interface {
AcceptUpstreamAnswer(context.Context, string) (string, error)
SetCallID(string)
SetCloseHandler(func(string))
Close() error
CloseWithReason(string) error
}
type mediaRelayFactory interface {
NewSession(context.Context, string, mediaSessionRoute) (mediaRelaySession, string, error)
}
type mediaSessionRoute struct {
proxyURL string
credential string
authIndex string
}
type pionMediaRelay struct {
downstreamAPI *webrtc.API
upstreamAPI *webrtc.API
proxyUpstreamAPI *webrtc.API
configuration webrtc.Configuration
limiter *mediaSessionLimiter
}
type mediaSessionLimiter struct {
mu sync.Mutex
limit int
active int
}
type pionMediaSession struct {
downstream *webrtc.PeerConnection
upstream *webrtc.PeerConnection
bridge *dataChannelBridge
done chan struct{}
closeOnce sync.Once
closeErr error
failureOnce sync.Once
handlerMu sync.Mutex
onClose func(string)
failureReason string
handlerCalled bool
mediaSessionID string
callID string
releaseSlot func()
proxyDialer proxy.ContextDialer
proxyScheme string
credential string
authIndex string
forwardingLogOnce sync.Once
localOffer string
tunnelsMu sync.Mutex
tunnels []*tcpCandidateTunnel
}
type dataChannelMessage struct {
data []byte
isString bool
}
type dataChannelPipe struct {
name string
done <-chan struct{}
queue chan dataChannelMessage
ready chan struct{}
readyOnce sync.Once
writable chan struct{}
destination *webrtc.DataChannel
mu sync.RWMutex
onError func(error)
}
type dataChannelBridge struct {
done <-chan struct{}
downToUp *dataChannelPipe
upToDown *dataChannelPipe
closeOnce sync.Once
downstreamMu sync.Mutex
downstream *webrtc.DataChannel
upstreamMu sync.Mutex
upstream *webrtc.DataChannel
}
func newPionMediaRelay(relayConfig config.CodexLiveMediaRelayConfig) (*pionMediaRelay, error) {
return newPionMediaRelayWithLimiter(relayConfig, &mediaSessionLimiter{})
}
func newPionMediaRelayWithLimiter(relayConfig config.CodexLiveMediaRelayConfig, limiter *mediaSessionLimiter) (*pionMediaRelay, error) {
if errValidate := relayConfig.Validate(); errValidate != nil {
return nil, errValidate
}
downstreamAPI, errAPI := newPionAPI(relayConfig, relayConfig.DisablePrivateRemoteIPs)
if errAPI != nil {
return nil, errAPI
}
upstreamAPI, errAPI := newPionAPI(relayConfig, false)
if errAPI != nil {
return nil, errAPI
}
proxyUpstreamAPI, errAPI := newPionProxyAPI(relayConfig)
if errAPI != nil {
return nil, errAPI
}
iceServers := make([]webrtc.ICEServer, 0, len(relayConfig.ICEServers))
for _, server := range relayConfig.ICEServers {
urls := make([]string, 0, len(server.URLs))
for _, rawURL := range server.URLs {
urls = append(urls, strings.TrimSpace(rawURL))
}
iceServers = append(iceServers, webrtc.ICEServer{
URLs: urls,
Username: server.Username,
Credential: server.Credential,
CredentialType: webrtc.ICECredentialTypePassword,
})
}
if limiter == nil {
limiter = &mediaSessionLimiter{}
}
limiter.setLimit(relayConfig.EffectiveMaxSessions())
return &pionMediaRelay{
downstreamAPI: downstreamAPI,
upstreamAPI: upstreamAPI,
proxyUpstreamAPI: proxyUpstreamAPI,
configuration: webrtc.Configuration{ICEServers: iceServers},
limiter: limiter,
}, nil
}
func (l *mediaSessionLimiter) setLimit(limit int) {
if l == nil {
return
}
l.mu.Lock()
l.limit = limit
l.mu.Unlock()
}
func (l *mediaSessionLimiter) acquire() bool {
if l == nil {
return false
}
l.mu.Lock()
defer l.mu.Unlock()
if l.limit <= 0 || l.active >= l.limit {
return false
}
l.active++
return true
}
func (l *mediaSessionLimiter) release() {
if l == nil {
return
}
l.mu.Lock()
if l.active > 0 {
l.active--
}
l.mu.Unlock()
}
func newPionAPI(relayConfig config.CodexLiveMediaRelayConfig, filterPrivateRemoteIPs bool) (*webrtc.API, error) {
return newPionAPIWithOptions(relayConfig, filterPrivateRemoteIPs, false)
}
func newPionProxyAPI(relayConfig config.CodexLiveMediaRelayConfig) (*webrtc.API, error) {
return newPionAPIWithOptions(relayConfig, false, true)
}
func newPionAPIWithOptions(relayConfig config.CodexLiveMediaRelayConfig, filterPrivateRemoteIPs, loopbackOnly bool) (*webrtc.API, error) {
mediaEngine := &webrtc.MediaEngine{}
if errRegister := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: opusCodec,
PayloadType: 111,
}, webrtc.RTPCodecTypeAudio); errRegister != nil {
return nil, fmt.Errorf("register Opus codec: %w", errRegister)
}
interceptorRegistry := &interceptor.Registry{}
if errRegister := webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); errRegister != nil {
return nil, fmt.Errorf("register WebRTC interceptors: %w", errRegister)
}
settingEngine := webrtc.SettingEngine{}
if !loopbackOnly {
if relayConfig.UDPPortMin != 0 {
if errPorts := settingEngine.SetEphemeralUDPPortRange(relayConfig.UDPPortMin, relayConfig.UDPPortMax); errPorts != nil {
return nil, fmt.Errorf("configure WebRTC UDP port range: %w", errPorts)
}
}
if publicIP := strings.TrimSpace(relayConfig.PublicIP); publicIP != "" {
settingEngine.SetNAT1To1IPs([]string{publicIP}, webrtc.ICECandidateTypeHost)
}
}
if filterPrivateRemoteIPs {
settingEngine.SetRemoteIPFilter(isPublicRemoteIP)
}
if loopbackOnly {
settingEngine.SetNetworkTypes([]webrtc.NetworkType{
webrtc.NetworkTypeUDP4,
webrtc.NetworkTypeUDP6,
webrtc.NetworkTypeTCP4,
webrtc.NetworkTypeTCP6,
})
settingEngine.SetIncludeLoopbackCandidate(true)
settingEngine.SetIPFilter(func(ip net.IP) bool {
return ip != nil && ip.IsLoopback()
})
}
return webrtc.NewAPI(
webrtc.WithMediaEngine(mediaEngine),
webrtc.WithInterceptorRegistry(interceptorRegistry),
webrtc.WithSettingEngine(settingEngine),
), nil
}
func isPublicRemoteIP(ip net.IP) bool {
return ip != nil && !ip.IsUnspecified() && !ip.IsLoopback() && !ip.IsPrivate() &&
!ip.IsLinkLocalUnicast() && !ip.IsLinkLocalMulticast() && !ip.IsMulticast()
}
func (r *pionMediaRelay) NewSession(ctx context.Context, clientOffer string, route mediaSessionRoute) (mediaRelaySession, string, error) {
if r == nil || r.downstreamAPI == nil || r.upstreamAPI == nil || r.proxyUpstreamAPI == nil || r.limiter == nil {
return nil, "", errors.New("Codex live media relay unavailable")
}
if errContext := ctx.Err(); errContext != nil {
return nil, "", errContext
}
builtProxyDialer, proxyMode, errProxy := proxyutil.BuildDialer(route.proxyURL)
if errProxy != nil {
return nil, "", fmt.Errorf("configure Codex live remote TCP proxy: %w", errProxy)
}
proxied := proxyMode == proxyutil.ModeProxy
var proxyDialer proxy.ContextDialer
if proxied {
contextDialer, ok := builtProxyDialer.(proxy.ContextDialer)
if !ok {
return nil, "", errors.New("Codex live remote TCP proxy does not support cancellation")
}
proxyDialer = contextDialer
}
if !r.limiter.acquire() {
return nil, "", errors.New("Codex live media relay capacity exhausted")
}
releaseSlot := r.limiter.release
downstream, errDownstream := r.downstreamAPI.NewPeerConnection(r.configuration)
if errDownstream != nil {
releaseSlot()
return nil, "", fmt.Errorf("create downstream PeerConnection: %w", errDownstream)
}
upstreamAPI := r.upstreamAPI
upstreamConfiguration := r.configuration
if proxied {
upstreamAPI = r.proxyUpstreamAPI
upstreamConfiguration.ICEServers = nil
}
upstream, errUpstream := upstreamAPI.NewPeerConnection(upstreamConfiguration)
if errUpstream != nil {
releaseSlot()
if errClose := downstream.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live media: close downstream PeerConnection after setup error")
}
return nil, "", fmt.Errorf("create upstream PeerConnection: %w", errUpstream)
}
session := &pionMediaSession{
downstream: downstream,
upstream: upstream,
done: make(chan struct{}),
mediaSessionID: uuid.NewString(),
releaseSlot: releaseSlot,
proxyDialer: proxyDialer,
proxyScheme: proxyScheme(route.proxyURL),
credential: strings.TrimSpace(route.credential),
authIndex: strings.TrimSpace(route.authIndex),
}
session.bridge = newDataChannelBridge(session.done, func(err error) {
session.fail("data_channel_failed", err)
})
session.installStateHandlers()
log.WithFields(session.logFields("session")).Info("codex live WebRTC media session created")
if errRemote := downstream.SetRemoteDescription(webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer,
SDP: clientOffer,
}); errRemote != nil {
_ = session.Close()
return nil, "", fmt.Errorf("set downstream WebRTC offer: %w", errRemote)
}
toDesktop, errTrack := webrtc.NewTrackLocalStaticRTP(opusCodec, "audio", "codex-live")
if errTrack != nil {
_ = session.Close()
return nil, "", fmt.Errorf("create downstream audio track: %w", errTrack)
}
downstreamSender, errTrack := downstream.AddTrack(toDesktop)
if errTrack != nil {
_ = session.Close()
return nil, "", fmt.Errorf("add downstream audio track: %w", errTrack)
}
go drainRTCP("downstream", downstreamSender, session.done)
toOpenAI, errTrack := webrtc.NewTrackLocalStaticRTP(opusCodec, "audio", "codex-live")
if errTrack != nil {
_ = session.Close()
return nil, "", fmt.Errorf("create upstream audio track: %w", errTrack)
}
upstreamSender, errTrack := upstream.AddTrack(toOpenAI)
if errTrack != nil {
_ = session.Close()
return nil, "", fmt.Errorf("add upstream audio track: %w", errTrack)
}
go drainRTCP("upstream", upstreamSender, session.done)
downstream.OnTrack(func(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
if !strings.EqualFold(track.Codec().MimeType, webrtc.MimeTypeOpus) {
return
}
go relayRTP("downstream-to-upstream", track, toOpenAI, session.done)
})
upstream.OnTrack(func(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
if !strings.EqualFold(track.Codec().MimeType, webrtc.MimeTypeOpus) {
return
}
go relayRTP("upstream-to-downstream", track, toDesktop, session.done)
})
downstream.OnDataChannel(func(channel *webrtc.DataChannel) {
if channel.Label() != realtimeDataChannelLabel {
if errClose := channel.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live media: close unsupported downstream DataChannel")
}
return
}
session.bridge.attachDownstream(channel)
})
upstreamChannel, errChannel := upstream.CreateDataChannel(realtimeDataChannelLabel, nil)
if errChannel != nil {
_ = session.Close()
return nil, "", fmt.Errorf("create upstream DataChannel: %w", errChannel)
}
session.bridge.attachUpstream(upstreamChannel)
gatherComplete := webrtc.GatheringCompletePromise(upstream)
offer, errOffer := upstream.CreateOffer(nil)
if errOffer != nil {
_ = session.Close()
return nil, "", fmt.Errorf("create upstream WebRTC offer: %w", errOffer)
}
if errLocal := upstream.SetLocalDescription(offer); errLocal != nil {
_ = session.Close()
return nil, "", fmt.Errorf("set upstream WebRTC offer: %w", errLocal)
}
select {
case <-gatherComplete:
case <-ctx.Done():
_ = session.Close()
return nil, "", fmt.Errorf("gather upstream WebRTC candidates: %w", ctx.Err())
}
localDescription := upstream.LocalDescription()
if localDescription == nil || strings.TrimSpace(localDescription.SDP) == "" {
_ = session.Close()
return nil, "", errors.New("upstream WebRTC offer is empty")
}
session.localOffer = localDescription.SDP
return session, localDescription.SDP, nil
}
func (s *pionMediaSession) AcceptUpstreamAnswer(ctx context.Context, upstreamAnswer string) (string, error) {
if s == nil || s.upstream == nil || s.downstream == nil {
return "", errors.New("Codex live media session unavailable")
}
answerToApply := upstreamAnswer
if s.proxyDialer != nil {
rewrittenAnswer, tunnels, errProxy := prepareProxiedUpstreamAnswer(upstreamAnswer, s.localOffer, s.proxyDialer)
if errProxy != nil {
return "", errProxy
}
for _, tunnel := range tunnels {
tunnel.setForwardingStartedHandler(s.logForwardingStarted)
}
if !s.installCandidateTunnels(tunnels) {
errClosed := errors.New("Codex live media session closed while configuring TCP proxy")
if errClose := closeCandidateTunnels(tunnels); errClose != nil {
return "", errors.Join(errClosed, fmt.Errorf("close TCP candidate tunnels: %w", errClose))
}
return "", errClosed
}
answerToApply = rewrittenAnswer
}
if errRemote := s.upstream.SetRemoteDescription(webrtc.SessionDescription{
Type: webrtc.SDPTypeAnswer,
SDP: answerToApply,
}); errRemote != nil {
errSetRemote := fmt.Errorf("set upstream WebRTC answer: %w", errRemote)
if errClose := s.closeCandidateTunnels(); errClose != nil {
return "", errors.Join(errSetRemote, fmt.Errorf("close TCP candidate tunnels: %w", errClose))
}
return "", errSetRemote
}
gatherComplete := webrtc.GatheringCompletePromise(s.downstream)
answer, errAnswer := s.downstream.CreateAnswer(nil)
if errAnswer != nil {
return "", fmt.Errorf("create downstream WebRTC answer: %w", errAnswer)
}
if errLocal := s.downstream.SetLocalDescription(answer); errLocal != nil {
return "", fmt.Errorf("set downstream WebRTC answer: %w", errLocal)
}
select {
case <-gatherComplete:
case <-ctx.Done():
return "", fmt.Errorf("gather downstream WebRTC candidates: %w", ctx.Err())
}
localDescription := s.downstream.LocalDescription()
if localDescription == nil || strings.TrimSpace(localDescription.SDP) == "" {
return "", errors.New("downstream WebRTC answer is empty")
}
return localDescription.SDP, nil
}
func (s *pionMediaSession) installCandidateTunnels(tunnels []*tcpCandidateTunnel) bool {
if s == nil {
return false
}
s.tunnelsMu.Lock()
defer s.tunnelsMu.Unlock()
select {
case <-s.done:
return false
default:
}
s.tunnels = tunnels
return true
}
func (s *pionMediaSession) closeCandidateTunnels() error {
if s == nil {
return nil
}
s.tunnelsMu.Lock()
tunnels := s.tunnels
s.tunnels = nil
s.tunnelsMu.Unlock()
return closeCandidateTunnels(tunnels)
}
func (s *pionMediaSession) SetCallID(callID string) {
if s == nil {
return
}
s.handlerMu.Lock()
s.callID = strings.TrimSpace(callID)
s.handlerMu.Unlock()
}
func (s *pionMediaSession) logFields(peer string) log.Fields {
fields := log.Fields{
"media_session_id": s.mediaSessionID,
"peer": peer,
}
s.handlerMu.Lock()
callID := s.callID
s.handlerMu.Unlock()
if callID != "" {
fields["call_id"] = callID
}
if s.proxyDialer != nil && (peer == "remote" || peer == "session") {
fields["remote_transport"] = "tcp"
fields["proxy_scheme"] = s.proxyScheme
}
return fields
}
func (s *pionMediaSession) forwardingLogFields() log.Fields {
fields := s.logFields("remote")
if s.authIndex != "" {
fields["auth_index"] = s.authIndex
}
if s.credential != "" {
fields["credential"] = s.credential
}
if s.proxyDialer != nil {
fields["connection"] = "via " + s.proxyScheme + " proxy"
fields["remote_transport"] = "tcp"
} else {
fields["connection"] = "direct"
fields["remote_transport"] = "ice"
}
if s.upstream != nil {
fields["state"] = s.upstream.ConnectionState().String()
}
return fields
}
func (s *pionMediaSession) logForwardingStarted() {
if s == nil {
return
}
s.forwardingLogOnce.Do(func() {
log.WithFields(s.forwardingLogFields()).Info("codex live remote media forwarding started")
})
}
func (s *pionMediaSession) SetCloseHandler(handler func(string)) {
if s == nil {
return
}
s.handlerMu.Lock()
s.onClose = handler
reason := s.failureReason
callHandler := handler != nil && reason != "" && !s.handlerCalled
if callHandler {
s.handlerCalled = true
}
s.handlerMu.Unlock()
if callHandler {
handler(reason)
}
}
func (s *pionMediaSession) Close() error {
return s.CloseWithReason("closed")
}
func (s *pionMediaSession) CloseWithReason(reason string) error {
if s == nil {
return nil
}
s.closeOnce.Do(func() {
fields := s.logFields("session")
fields["reason"] = reason
log.WithFields(fields).Info("codex live WebRTC media session closing")
close(s.done)
if s.bridge != nil {
s.bridge.close()
}
var closeErrors []error
if errClose := s.closeCandidateTunnels(); errClose != nil {
closeErrors = append(closeErrors, fmt.Errorf("close TCP candidate tunnels: %w", errClose))
}
if errClose := s.closePeerConnection("local", s.downstream); errClose != nil {
closeErrors = append(closeErrors, fmt.Errorf("close downstream PeerConnection: %w", errClose))
}
if errClose := s.closePeerConnection("remote", s.upstream); errClose != nil {
closeErrors = append(closeErrors, fmt.Errorf("close upstream PeerConnection: %w", errClose))
}
if s.releaseSlot != nil {
s.releaseSlot()
}
s.closeErr = errors.Join(closeErrors...)
if s.closeErr != nil {
log.WithFields(fields).WithError(s.closeErr).Warn("codex live WebRTC media session closed with errors")
} else {
log.WithFields(fields).Info("codex live WebRTC media session closed")
}
})
return s.closeErr
}
func (s *pionMediaSession) closePeerConnection(peer string, connection *webrtc.PeerConnection) error {
if connection == nil {
return nil
}
fields := s.logFields(peer)
fields["state_before"] = connection.ConnectionState().String()
errClose := connection.Close()
fields["state_after"] = connection.ConnectionState().String()
if errClose != nil {
log.WithFields(fields).WithError(errClose).Warn("codex live WebRTC peer close failed")
return errClose
}
log.WithFields(fields).Info("codex live WebRTC peer closed")
return nil
}
func (s *pionMediaSession) installStateHandlers() {
handle := func(peer, reasonPrefix string) func(webrtc.PeerConnectionState) {
return func(state webrtc.PeerConnectionState) {
fields := s.logFields(peer)
fields["state"] = state.String()
switch state {
case webrtc.PeerConnectionStateConnecting:
log.WithFields(fields).Info("codex live WebRTC peer connecting")
case webrtc.PeerConnectionStateConnected:
log.WithFields(fields).Info("codex live WebRTC peer connected")
if peer == "remote" {
s.logForwardingStarted()
}
case webrtc.PeerConnectionStateDisconnected:
log.WithFields(fields).Warn("codex live WebRTC peer disconnected")
case webrtc.PeerConnectionStateFailed:
log.WithFields(fields).Warn("codex live WebRTC peer failed")
s.fail(reasonPrefix+"_failed", fmt.Errorf("%s PeerConnection failed", reasonPrefix))
case webrtc.PeerConnectionStateClosed:
select {
case <-s.done:
return
default:
log.WithFields(fields).Info("codex live WebRTC peer closed by remote")
s.fail(reasonPrefix+"_closed", fmt.Errorf("%s PeerConnection closed", reasonPrefix))
}
default:
log.WithFields(fields).Debug("codex live WebRTC peer state changed")
}
}
}
s.downstream.OnConnectionStateChange(handle("local", "downstream"))
s.upstream.OnConnectionStateChange(handle("remote", "upstream"))
}
func (s *pionMediaSession) fail(reason string, err error) {
s.failureOnce.Do(func() {
if err != nil {
log.WithFields(s.logFields("session")).WithField("reason", reason).WithError(err).Warn("codex live WebRTC media session failed")
}
if errClose := s.CloseWithReason(reason); errClose != nil {
log.WithError(errClose).Debug("codex live media: close failed session")
}
s.handlerMu.Lock()
s.failureReason = reason
handler := s.onClose
callHandler := handler != nil && !s.handlerCalled
if callHandler {
s.handlerCalled = true
}
s.handlerMu.Unlock()
if callHandler {
handler(reason)
}
})
}
func relayRTP(name string, source *webrtc.TrackRemote, destination *webrtc.TrackLocalStaticRTP, done <-chan struct{}) {
for {
packet, _, errRead := source.ReadRTP()
if errRead != nil {
if !isClosedMediaError(errRead, done) {
log.WithError(errRead).Debugf("codex live media: %s RTP read stopped", name)
}
return
}
normalizeRTPPacket(packet)
if errWrite := destination.WriteRTP(packet); errWrite != nil {
if !isClosedMediaError(errWrite, done) {
log.WithError(errWrite).Debugf("codex live media: %s RTP write stopped", name)
}
return
}
}
}
func normalizeRTPPacket(packet *rtp.Packet) {
if packet == nil {
return
}
packet.Extension = false
packet.ExtensionProfile = 0
packet.Extensions = nil
}
func drainRTCP(name string, sender *webrtc.RTPSender, done <-chan struct{}) {
for {
if _, _, errRead := sender.ReadRTCP(); errRead != nil {
if !isClosedMediaError(errRead, done) {
log.WithError(errRead).Debugf("codex live media: %s RTCP reader stopped", name)
}
return
}
}
}
func isClosedMediaError(err error, done <-chan struct{}) bool {
select {
case <-done:
return true
default:
}
return errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed)
}
func newDataChannelBridge(done <-chan struct{}, onError func(error)) *dataChannelBridge {
bridge := &dataChannelBridge{done: done}
bridge.downToUp = newDataChannelPipe("downstream-to-upstream", done, onError)
bridge.upToDown = newDataChannelPipe("upstream-to-downstream", done, onError)
return bridge
}
func newDataChannelPipe(name string, done <-chan struct{}, onError func(error)) *dataChannelPipe {
pipe := &dataChannelPipe{
name: name,
done: done,
queue: make(chan dataChannelMessage, mediaDataQueueSize),
ready: make(chan struct{}),
writable: make(chan struct{}, 1),
onError: onError,
}
go pipe.run()
return pipe
}
func (b *dataChannelBridge) attachDownstream(channel *webrtc.DataChannel) {
b.downstreamMu.Lock()
if b.downstream != nil {
b.downstreamMu.Unlock()
if errClose := channel.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live media: close duplicate downstream DataChannel")
}
return
}
b.downstream = channel
b.downstreamMu.Unlock()
b.upToDown.setDestination(channel)
b.bindSource(channel, b.downToUp)
}
func (b *dataChannelBridge) attachUpstream(channel *webrtc.DataChannel) {
b.upstreamMu.Lock()
if b.upstream != nil {
b.upstreamMu.Unlock()
if errClose := channel.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live media: close duplicate upstream DataChannel")
}
return
}
b.upstream = channel
b.upstreamMu.Unlock()
b.downToUp.setDestination(channel)
b.bindSource(channel, b.upToDown)
}
func (b *dataChannelBridge) bindSource(channel *webrtc.DataChannel, destination *dataChannelPipe) {
channel.OnMessage(func(message webrtc.DataChannelMessage) {
if len(message.Data) > mediaDataMessageMaxSize {
destination.reportError(fmt.Errorf("%s DataChannel message exceeds %d bytes", destination.name, mediaDataMessageMaxSize))
return
}
payload := append([]byte(nil), message.Data...)
select {
case destination.queue <- dataChannelMessage{data: payload, isString: message.IsString}:
case <-b.done:
}
})
channel.OnError(func(err error) {
destination.reportError(fmt.Errorf("%s DataChannel error: %w", destination.name, err))
})
channel.OnClose(func() {
select {
case <-b.done:
return
default:
destination.reportError(fmt.Errorf("%s DataChannel closed", destination.name))
}
})
}
func (b *dataChannelBridge) close() {
if b == nil {
return
}
b.closeOnce.Do(func() {
b.downstreamMu.Lock()
downstream := b.downstream
b.downstreamMu.Unlock()
if downstream != nil {
if errClose := downstream.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live media: close downstream DataChannel")
}
}
b.upstreamMu.Lock()
upstream := b.upstream
b.upstreamMu.Unlock()
if upstream != nil {
if errClose := upstream.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live media: close upstream DataChannel")
}
}
})
}
func (p *dataChannelPipe) setDestination(channel *webrtc.DataChannel) {
p.mu.Lock()
p.destination = channel
p.mu.Unlock()
markReady := func() {
p.readyOnce.Do(func() { close(p.ready) })
}
channel.SetBufferedAmountLowThreshold(mediaDataBufferedMaxSize / 2)
channel.OnBufferedAmountLow(func() {
select {
case p.writable <- struct{}{}:
default:
}
})
channel.OnOpen(markReady)
if channel.ReadyState() == webrtc.DataChannelStateOpen {
markReady()
}
}
func (p *dataChannelPipe) run() {
select {
case <-p.ready:
case <-p.done:
return
}
for {
select {
case message := <-p.queue:
p.mu.RLock()
destination := p.destination
p.mu.RUnlock()
if destination == nil {
p.reportError(fmt.Errorf("%s DataChannel destination unavailable", p.name))
return
}
if !p.waitWritable(destination, len(message.data)) {
return
}
var errSend error
if message.isString {
errSend = destination.SendText(string(message.data))
} else {
errSend = destination.Send(message.data)
}
if errSend != nil {
p.reportError(fmt.Errorf("send %s DataChannel message: %w", p.name, errSend))
return
}
case <-p.done:
return
}
}
}
func (p *dataChannelPipe) waitWritable(destination *webrtc.DataChannel, messageSize int) bool {
for destination.BufferedAmount()+uint64(messageSize) > mediaDataBufferedMaxSize {
select {
case <-p.writable:
case <-p.done:
return false
}
}
return true
}
func (p *dataChannelPipe) reportError(err error) {
if p.onError != nil {
p.onError(err)
}
}

View file

@ -0,0 +1,542 @@
package live
import (
"context"
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/pion/interceptor"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
log "github.com/sirupsen/logrus"
logtest "github.com/sirupsen/logrus/hooks/test"
)
func TestPionMediaRelaySelectsRemoteProxyMode(t *testing.T) {
clientAPI := newTestWebRTCAPI(t)
client, errClient := clientAPI.NewPeerConnection(webrtc.Configuration{})
if errClient != nil {
t.Fatalf("create client PeerConnection: %v", errClient)
}
defer closeTestPeerConnection(t, client)
if _, errChannel := client.CreateDataChannel(realtimeDataChannelLabel, nil); errChannel != nil {
t.Fatalf("create client DataChannel: %v", errChannel)
}
clientOffer := completeOffer(t, client)
relay, errRelay := newPionMediaRelay(config.CodexLiveMediaRelayConfig{
Enabled: true,
PublicIP: "198.51.100.1",
})
if errRelay != nil {
t.Fatalf("create media relay: %v", errRelay)
}
for name, testCase := range map[string]struct {
proxyURL string
proxied bool
}{
"inherit": {proxyURL: ""},
"direct": {proxyURL: "direct"},
"HTTP": {proxyURL: "http://proxy.example:8080", proxied: true},
"HTTPS": {proxyURL: "https://proxy.example:8443", proxied: true},
"SOCKS5": {proxyURL: "socks5://proxy.example:1080", proxied: true},
"SOCKS5H": {proxyURL: "socks5h://proxy.example:1080", proxied: true},
} {
t.Run(name, func(t *testing.T) {
session, upstreamOffer, errSession := relay.NewSession(context.Background(), clientOffer, mediaSessionRoute{proxyURL: testCase.proxyURL})
if errSession != nil {
t.Fatalf("create media session: %v", errSession)
}
pionSession, ok := session.(*pionMediaSession)
if !ok {
t.Fatalf("media session type = %T", session)
}
if got := pionSession.proxyDialer != nil; got != testCase.proxied {
t.Fatalf("proxied = %t, want %t", got, testCase.proxied)
}
if testCase.proxied && !offerCandidatesAreLoopback(t, upstreamOffer) {
t.Fatal("proxied upstream offer exposed a non-loopback candidate")
}
if errClose := session.Close(); errClose != nil {
t.Fatalf("close media session: %v", errClose)
}
})
}
if _, _, errSession := relay.NewSession(context.Background(), clientOffer, mediaSessionRoute{proxyURL: "invalid-proxy"}); errSession == nil {
t.Fatal("expected invalid proxy URL to fail media session creation")
}
}
func TestMediaForwardingStartedLogRedactsProxyCredentials(t *testing.T) {
logger := log.StandardLogger()
previousHooks := logger.ReplaceHooks(make(log.LevelHooks))
hook := logtest.NewLocal(logger)
defer logger.ReplaceHooks(previousHooks)
for name, testCase := range map[string]struct {
proxyURL string
connection string
credential string
}{
"direct": {
connection: "direct",
credential: "Voice credential",
},
"HTTP": {
proxyURL: "http://user:secret@proxy.example:8080",
connection: "via http proxy",
credential: "Voice credential",
},
"SOCKS5 without label": {
proxyURL: "socks5://user:secret@proxy.example:1080",
connection: "via socks5 proxy",
credential: "auth-index",
},
} {
t.Run(name, func(t *testing.T) {
session := &pionMediaSession{
mediaSessionID: "media-session-" + name,
proxyScheme: proxyScheme(testCase.proxyURL),
credential: testCase.credential,
authIndex: "auth-index",
}
if testCase.proxyURL != "" {
session.proxyDialer = &recordingProxyDialer{dials: make(chan recordedProxyDial, 1)}
}
earlyFields := session.logFields("session")
for _, field := range []string{"auth_id", "auth_label", "auth_index", "credential", "connection"} {
if _, exists := earlyFields[field]; exists {
t.Fatalf("session log exposed forwarding-only field %q before forwarding started: %#v", field, earlyFields)
}
}
session.logForwardingStarted()
session.logForwardingStarted()
matching := 0
for _, entry := range hook.AllEntries() {
if entry.Message != "codex live remote media forwarding started" || entry.Data["media_session_id"] != session.mediaSessionID {
continue
}
matching++
if entry.Data["connection"] != testCase.connection || entry.Data["credential"] != testCase.credential {
t.Fatalf("forwarding fields = %#v", entry.Data)
}
serialized := fmt.Sprint(entry.Data)
for _, secret := range []string{"user", "secret", "proxy.example"} {
if strings.Contains(serialized, secret) {
t.Fatalf("forwarding log leaked %q: %s", secret, serialized)
}
}
}
if matching != 1 {
t.Fatalf("forwarding log count = %d, want 1", matching)
}
})
}
}
func TestPionMediaRelayBridgesAudioAndDataChannel(t *testing.T) {
logger := log.StandardLogger()
previousHooks := logger.ReplaceHooks(make(log.LevelHooks))
previousLevel := logger.GetLevel()
logger.SetLevel(log.DebugLevel)
hook := logtest.NewLocal(logger)
defer func() {
logger.ReplaceHooks(previousHooks)
logger.SetLevel(previousLevel)
}()
clientAPI := newTestWebRTCAPI(t)
client, errClient := clientAPI.NewPeerConnection(webrtc.Configuration{})
if errClient != nil {
t.Fatalf("create client PeerConnection: %v", errClient)
}
defer closeTestPeerConnection(t, client)
clientDone := make(chan struct{})
defer close(clientDone)
clientAudio, errTrack := webrtc.NewTrackLocalStaticRTP(opusCodec, "client-audio", "client")
if errTrack != nil {
t.Fatalf("create client audio track: %v", errTrack)
}
clientSender, errTrack := client.AddTrack(clientAudio)
if errTrack != nil {
t.Fatalf("add client audio track: %v", errTrack)
}
go drainRTCP("test-client", clientSender, clientDone)
clientData, errData := client.CreateDataChannel(realtimeDataChannelLabel, nil)
if errData != nil {
t.Fatalf("create client DataChannel: %v", errData)
}
clientMessages := make(chan webrtc.DataChannelMessage, 4)
clientData.OnMessage(func(message webrtc.DataChannelMessage) {
message.Data = append([]byte(nil), message.Data...)
clientMessages <- message
})
clientAudioMessages := make(chan []byte, 1)
client.OnTrack(func(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
packet, _, errRead := track.ReadRTP()
if errRead == nil {
clientAudioMessages <- append([]byte(nil), packet.Payload...)
}
})
clientOffer := completeOffer(t, client)
relayConfig := config.CodexLiveMediaRelayConfig{
Enabled: true,
MaxSessions: 1,
DisablePrivateRemoteIPs: false,
}
relay, errRelay := newPionMediaRelay(relayConfig)
if errRelay != nil {
t.Fatalf("create media relay: %v", errRelay)
}
session, relayOffer, errSession := relay.NewSession(context.Background(), clientOffer, mediaSessionRoute{
credential: "Voice credential",
authIndex: "auth-index",
})
if errSession != nil {
t.Fatalf("create media relay session: %v", errSession)
}
session.SetCallID("call-log-test")
defer func() {
if errClose := session.Close(); errClose != nil {
t.Errorf("close media relay session: %v", errClose)
}
}()
reloadedRelay, errRelay := newPionMediaRelayWithLimiter(relayConfig, relay.limiter)
if errRelay != nil {
t.Fatalf("reload media relay: %v", errRelay)
}
if _, _, errCapacity := reloadedRelay.NewSession(context.Background(), clientOffer, mediaSessionRoute{}); errCapacity == nil {
t.Fatal("reloaded media relay bypassed the shared session capacity")
}
upstreamAPI := newTestWebRTCAPI(t)
upstream, errUpstream := upstreamAPI.NewPeerConnection(webrtc.Configuration{})
if errUpstream != nil {
t.Fatalf("create upstream PeerConnection: %v", errUpstream)
}
defer closeTestPeerConnection(t, upstream)
upstreamDone := make(chan struct{})
defer close(upstreamDone)
upstreamDataChannels := make(chan *webrtc.DataChannel, 1)
upstreamMessages := make(chan webrtc.DataChannelMessage, 4)
upstream.OnDataChannel(func(channel *webrtc.DataChannel) {
if channel.Label() != realtimeDataChannelLabel {
return
}
channel.OnMessage(func(message webrtc.DataChannelMessage) {
message.Data = append([]byte(nil), message.Data...)
upstreamMessages <- message
})
upstreamDataChannels <- channel
})
upstreamAudioMessages := make(chan []byte, 1)
upstream.OnTrack(func(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
packet, _, errRead := track.ReadRTP()
if errRead == nil {
upstreamAudioMessages <- append([]byte(nil), packet.Payload...)
}
})
if errRemote := upstream.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: relayOffer}); errRemote != nil {
t.Fatalf("set upstream offer: %v", errRemote)
}
upstreamAudio, errTrack := webrtc.NewTrackLocalStaticRTP(opusCodec, "upstream-audio", "upstream")
if errTrack != nil {
t.Fatalf("create upstream audio track: %v", errTrack)
}
upstreamSender, errTrack := upstream.AddTrack(upstreamAudio)
if errTrack != nil {
t.Fatalf("add upstream audio track: %v", errTrack)
}
go drainRTCP("test-upstream", upstreamSender, upstreamDone)
upstreamAnswer := completeAnswer(t, upstream)
downstreamAnswer, errAnswer := session.AcceptUpstreamAnswer(context.Background(), upstreamAnswer)
if errAnswer != nil {
t.Fatalf("accept upstream answer: %v", errAnswer)
}
if errRemote := client.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: downstreamAnswer}); errRemote != nil {
t.Fatalf("set client answer: %v", errRemote)
}
upstreamData := receiveDataChannel(t, upstreamDataChannels)
waitDataChannelOpen(t, clientData)
waitDataChannelOpen(t, upstreamData)
if errSend := clientData.SendText("from-client"); errSend != nil {
t.Fatalf("send client DataChannel message: %v", errSend)
}
if got := receiveDataMessage(t, upstreamMessages); !got.IsString || string(got.Data) != "from-client" {
t.Fatalf("upstream DataChannel message = %#v, want text from-client", got)
}
if errSend := upstreamData.SendText("from-upstream"); errSend != nil {
t.Fatalf("send upstream DataChannel message: %v", errSend)
}
if got := receiveDataMessage(t, clientMessages); !got.IsString || string(got.Data) != "from-upstream" {
t.Fatalf("client DataChannel message = %#v, want text from-upstream", got)
}
if errSend := clientData.Send([]byte{0x01, 0x02, 0x03}); errSend != nil {
t.Fatalf("send client binary DataChannel message: %v", errSend)
}
if got := receiveDataMessage(t, upstreamMessages); got.IsString || string(got.Data) != string([]byte{0x01, 0x02, 0x03}) {
t.Fatalf("upstream binary DataChannel message = %#v", got)
}
clientPayload := []byte{0xf8, 0xff, 0xfe}
sendTestRTP(t, clientAudio, clientPayload, upstreamAudioMessages)
upstreamPayload := []byte{0xf8, 0xfe, 0xfd}
sendTestRTP(t, upstreamAudio, upstreamPayload, clientAudioMessages)
if errClose := session.Close(); errClose != nil {
t.Fatalf("close media relay session for logging: %v", errClose)
}
replacementSession, _, errReplacement := reloadedRelay.NewSession(context.Background(), clientOffer, mediaSessionRoute{})
if errReplacement != nil {
t.Fatalf("shared capacity was not released: %v", errReplacement)
}
if errClose := replacementSession.CloseWithReason("test_complete"); errClose != nil {
t.Fatalf("close replacement media session: %v", errClose)
}
for _, peer := range []string{"local", "remote"} {
assertPeerLog(t, hook, "codex live WebRTC peer connected", peer, "call-log-test")
assertPeerLog(t, hook, "codex live WebRTC peer closed", peer, "call-log-test")
}
assertForwardingLog(t, hook, "direct", "Voice credential", "auth-index", "connected")
assertForwardingAfterRemoteConnected(t, hook)
assertSessionLog(t, hook, "codex live WebRTC media session closed", "closed", "call-log-test")
}
func TestIsPublicRemoteIP(t *testing.T) {
for rawIP, want := range map[string]bool{
"8.8.8.8": true,
"2001:4860::1": true,
"127.0.0.1": false,
"10.0.0.1": false,
"169.254.1.1": false,
"224.0.0.1": false,
"::1": false,
"fc00::1": false,
"fe80::1": false,
"ff02::1": false,
"0.0.0.0": false,
} {
if got := isPublicRemoteIP(net.ParseIP(rawIP)); got != want {
t.Errorf("isPublicRemoteIP(%q) = %t, want %t", rawIP, got, want)
}
}
if isPublicRemoteIP(nil) {
t.Fatal("isPublicRemoteIP(nil) = true, want false")
}
}
func offerCandidatesAreLoopback(t *testing.T, offer string) bool {
t.Helper()
lines := strings.Split(strings.ReplaceAll(offer, "\r\n", "\n"), "\n")
candidateCount := 0
for _, line := range lines {
if !strings.HasPrefix(line, "a=candidate:") {
continue
}
candidateCount++
fields := strings.Fields(strings.TrimPrefix(line, "a=candidate:"))
if len(fields) < 6 {
t.Fatalf("malformed offer candidate: %q", line)
}
address := net.ParseIP(fields[4])
if address == nil || !address.IsLoopback() {
return false
}
}
return candidateCount > 0
}
func newTestWebRTCAPI(t *testing.T) *webrtc.API {
t.Helper()
mediaEngine := &webrtc.MediaEngine{}
if errRegister := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: opusCodec,
PayloadType: 111,
}, webrtc.RTPCodecTypeAudio); errRegister != nil {
t.Fatalf("register test Opus codec: %v", errRegister)
}
interceptorRegistry := &interceptor.Registry{}
if errRegister := webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); errRegister != nil {
t.Fatalf("register test interceptors: %v", errRegister)
}
return webrtc.NewAPI(
webrtc.WithMediaEngine(mediaEngine),
webrtc.WithInterceptorRegistry(interceptorRegistry),
)
}
func completeOffer(t *testing.T, connection *webrtc.PeerConnection) string {
t.Helper()
gatherComplete := webrtc.GatheringCompletePromise(connection)
offer, errOffer := connection.CreateOffer(nil)
if errOffer != nil {
t.Fatalf("create offer: %v", errOffer)
}
if errLocal := connection.SetLocalDescription(offer); errLocal != nil {
t.Fatalf("set local offer: %v", errLocal)
}
select {
case <-gatherComplete:
case <-time.After(5 * time.Second):
t.Fatal("offer ICE gathering did not complete")
}
return connection.LocalDescription().SDP
}
func completeAnswer(t *testing.T, connection *webrtc.PeerConnection) string {
t.Helper()
gatherComplete := webrtc.GatheringCompletePromise(connection)
answer, errAnswer := connection.CreateAnswer(nil)
if errAnswer != nil {
t.Fatalf("create answer: %v", errAnswer)
}
if errLocal := connection.SetLocalDescription(answer); errLocal != nil {
t.Fatalf("set local answer: %v", errLocal)
}
select {
case <-gatherComplete:
case <-time.After(5 * time.Second):
t.Fatal("answer ICE gathering did not complete")
}
return connection.LocalDescription().SDP
}
func waitDataChannelOpen(t *testing.T, channel *webrtc.DataChannel) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if channel.ReadyState() == webrtc.DataChannelStateOpen {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("DataChannel %q did not open", channel.Label())
}
func receiveDataChannel(t *testing.T, channels <-chan *webrtc.DataChannel) *webrtc.DataChannel {
t.Helper()
select {
case channel := <-channels:
return channel
case <-time.After(5 * time.Second):
t.Fatal("upstream DataChannel was not created")
return nil
}
}
func receiveDataMessage(t *testing.T, messages <-chan webrtc.DataChannelMessage) webrtc.DataChannelMessage {
t.Helper()
select {
case message := <-messages:
return message
case <-time.After(5 * time.Second):
t.Fatal("DataChannel message was not relayed")
return webrtc.DataChannelMessage{}
}
}
func sendTestRTP(t *testing.T, track *webrtc.TrackLocalStaticRTP, payload []byte, received <-chan []byte) {
t.Helper()
for sequence := uint16(1); sequence <= 25; sequence++ {
packet := &rtp.Packet{
Header: rtp.Header{
Version: 2,
PayloadType: 111,
SequenceNumber: sequence,
Timestamp: uint32(sequence) * 960,
SSRC: 1234,
},
Payload: payload,
}
if errWrite := track.WriteRTP(packet); errWrite != nil {
t.Fatalf("write test RTP: %v", errWrite)
}
select {
case got := <-received:
if string(got) != string(payload) {
t.Fatalf("relayed RTP payload = %v, want %v", got, payload)
}
return
case <-time.After(20 * time.Millisecond):
}
}
t.Fatal("RTP packet was not relayed")
}
func assertForwardingAfterRemoteConnected(t *testing.T, hook *logtest.Hook) {
t.Helper()
connectedIndex := -1
forwardingIndex := -1
for index, entry := range hook.AllEntries() {
if entry.Message == "codex live WebRTC peer connected" && entry.Data["peer"] == "remote" && connectedIndex == -1 {
connectedIndex = index
}
if entry.Message == "codex live remote media forwarding started" && forwardingIndex == -1 {
forwardingIndex = index
}
}
if connectedIndex == -1 || forwardingIndex <= connectedIndex {
t.Fatalf("remote connected index=%d, forwarding index=%d", connectedIndex, forwardingIndex)
}
}
func assertForwardingLog(t *testing.T, hook *logtest.Hook, connection, credential, authIndex, state string) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
for _, entry := range hook.AllEntries() {
if entry.Message == "codex live remote media forwarding started" &&
entry.Data["connection"] == connection &&
entry.Data["credential"] == credential &&
entry.Data["auth_index"] == authIndex &&
entry.Data["state"] == state {
return
}
}
time.Sleep(time.Millisecond)
}
t.Fatalf("missing forwarding log for connection %q and credential %q", connection, credential)
}
func assertSessionLog(t *testing.T, hook *logtest.Hook, message, reason, callID string) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
for _, entry := range hook.AllEntries() {
if entry.Message == message && entry.Data["reason"] == reason && entry.Data["call_id"] == callID {
return
}
}
time.Sleep(time.Millisecond)
}
t.Fatalf("missing session log message %q for reason %q and call %q", message, reason, callID)
}
func assertPeerLog(t *testing.T, hook *logtest.Hook, message, peer, callID string) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
for _, entry := range hook.AllEntries() {
if entry.Message == message && entry.Data["peer"] == peer && entry.Data["call_id"] == callID {
return
}
}
time.Sleep(time.Millisecond)
}
t.Fatalf("missing log message %q for peer %q and call %q", message, peer, callID)
}
func closeTestPeerConnection(t *testing.T, connection *webrtc.PeerConnection) {
t.Helper()
if errClose := connection.Close(); errClose != nil {
t.Errorf("close test PeerConnection: %v", errClose)
}
}

View file

@ -0,0 +1,723 @@
package live
import (
"context"
"errors"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/router-for-me/CLIProxyAPI/v7/internal/clienterror"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus"
xproxy "golang.org/x/net/proxy"
)
const (
defaultSidebandAPIBaseURL = "wss://api.openai.com/v1"
sessionLifetime = time.Hour
)
var (
callIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{1,128}$`)
sidebandUpgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(*http.Request) bool {
return true
},
}
)
type liveSession struct {
callID string
authID string
model string
ownerPrincipal string
ownerProvider string
clientSecretPrincipal string
homeSelection *auth.HomeDispatchSelection
media mediaRelaySession
resources *liveSessionResources
token uint64
}
type liveSessionResources struct {
mu sync.Mutex
closed bool
closers []func() error
}
type storedSession struct {
session liveSession
claimed bool
timer *time.Timer
}
type sessionStore struct {
mu sync.Mutex
next uint64
lifetime time.Duration
sessions map[string]*storedSession
}
type sessionClaim int
const (
sessionClaimMissing sessionClaim = iota
sessionClaimBusy
sessionClaimAcquired
)
func newSessionStore() *sessionStore {
return &sessionStore{
lifetime: sessionLifetime,
sessions: make(map[string]*storedSession),
}
}
func (s *sessionStore) put(callID string, session liveSession) liveSession {
if s == nil || !callIDPattern.MatchString(callID) {
endLiveSession(session, "invalid_call_id")
return liveSession{}
}
if session.resources == nil {
session.resources = &liveSessionResources{}
}
s.mu.Lock()
s.next++
session.callID = callID
session.token = s.next
previous := s.sessions[callID]
entry := &storedSession{session: session}
entry.timer = time.AfterFunc(s.expiryDuration(), func() {
s.expire(callID, session.token)
})
s.sessions[callID] = entry
s.mu.Unlock()
if previous != nil {
if previous.timer != nil {
previous.timer.Stop()
}
if previous.session.resources != nil && previous.session.resources != session.resources {
previous.session.resources.close()
}
if previous.session.media != nil && previous.session.media != session.media {
if errClose := previous.session.media.CloseWithReason("session_replaced"); errClose != nil {
log.WithError(errClose).Debug("codex live media: close replaced session")
}
}
if previous.session.homeSelection != session.homeSelection {
endHomeSelection(previous.session, "session_replaced")
}
}
return session
}
func (s *sessionStore) claim(callID string) (liveSession, sessionClaim) {
if s == nil || !callIDPattern.MatchString(callID) {
return liveSession{}, sessionClaimMissing
}
s.mu.Lock()
defer s.mu.Unlock()
entry := s.sessions[callID]
if entry == nil {
return liveSession{}, sessionClaimMissing
}
if entry.claimed {
return liveSession{}, sessionClaimBusy
}
entry.claimed = true
if entry.timer != nil {
entry.timer.Stop()
entry.timer = nil
}
return entry.session, sessionClaimAcquired
}
func (s *sessionStore) release(session liveSession) {
if s == nil || session.callID == "" {
return
}
s.mu.Lock()
entry := s.sessions[session.callID]
if entry == nil || entry.session.token != session.token || !entry.claimed {
s.mu.Unlock()
return
}
entry.claimed = false
entry.timer = time.AfterFunc(s.expiryDuration(), func() {
s.expire(session.callID, session.token)
})
s.mu.Unlock()
}
func (s *sessionStore) complete(session liveSession, reason string) {
if s == nil || session.callID == "" {
endLiveSession(session, reason)
return
}
s.mu.Lock()
entry := s.sessions[session.callID]
if entry == nil || entry.session.token != session.token {
s.mu.Unlock()
return
}
delete(s.sessions, session.callID)
if entry.timer != nil {
entry.timer.Stop()
}
s.mu.Unlock()
endLiveSession(entry.session, reason)
}
func (s *sessionStore) closeAll(reason string) {
if s == nil {
return
}
s.mu.Lock()
entries := make([]*storedSession, 0, len(s.sessions))
for callID, entry := range s.sessions {
delete(s.sessions, callID)
if entry.timer != nil {
entry.timer.Stop()
}
entries = append(entries, entry)
}
s.mu.Unlock()
for _, entry := range entries {
endLiveSession(entry.session, reason)
}
}
func (s *sessionStore) expiryDuration() time.Duration {
if s.lifetime > 0 {
return s.lifetime
}
return sessionLifetime
}
func (s *sessionStore) expire(callID string, token uint64) {
s.mu.Lock()
entry := s.sessions[callID]
if entry == nil || entry.session.token != token || entry.claimed {
s.mu.Unlock()
return
}
delete(s.sessions, callID)
s.mu.Unlock()
endLiveSession(entry.session, "session_expired")
}
func (s *sessionStore) peek(callID string) (liveSession, bool) {
if s == nil {
return liveSession{}, false
}
s.mu.Lock()
entry := s.sessions[callID]
s.mu.Unlock()
if entry == nil {
return liveSession{}, false
}
return entry.session, true
}
func endLiveSession(session liveSession, reason string) {
if session.resources != nil {
session.resources.close()
}
if session.media != nil {
if errClose := session.media.CloseWithReason(reason); errClose != nil {
log.WithError(errClose).Debug("codex live media: close stored session")
}
}
endHomeSelection(session, reason)
}
func endHomeSelection(session liveSession, reason string) {
if session.homeSelection != nil {
session.homeSelection.End(reason)
}
}
func (r *liveSessionResources) add(closers ...func() error) {
if r == nil {
return
}
r.mu.Lock()
if !r.closed {
r.closers = append(r.closers, closers...)
r.mu.Unlock()
return
}
r.mu.Unlock()
closeSessionResources(closers)
}
func (r *liveSessionResources) close() {
if r == nil {
return
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return
}
r.closed = true
closers := r.closers
r.closers = nil
r.mu.Unlock()
closeSessionResources(closers)
}
func closeSessionResources(closers []func() error) {
for _, closer := range closers {
if closer == nil {
continue
}
if errClose := closer(); errClose != nil && !isNormalWebsocketClose(errClose) {
log.WithError(errClose).Debug("codex live: close session resource")
}
}
}
type sidebandStyle int
const (
sidebandFrameless sidebandStyle = iota
sidebandRealtimeCalls
sidebandRealtimeQuery
)
// HandleSideband relays live session sideband WebSocket frames bidirectionally.
func (h *Handler) HandleSideband(c *gin.Context) {
if h == nil || h.authManager == nil || h.sessions == nil {
writeLiveError(c, http.StatusServiceUnavailable, "Codex live sideband unavailable")
return
}
runtimeConfig := h.currentConfig()
if !websocket.IsWebSocketUpgrade(c.Request) {
c.Header("Upgrade", "websocket")
writeLiveError(c, http.StatusUpgradeRequired, "WebSocket upgrade required")
return
}
style, callID, ok := sidebandTarget(c)
if !ok {
writeLiveError(c, http.StatusBadRequest, "Invalid Codex live call ID")
return
}
session, claim := h.sessions.claim(callID)
switch claim {
case sessionClaimBusy:
writeLiveError(c, http.StatusConflict, "Codex live session already joining")
return
case sessionClaimAcquired:
default:
writeLiveError(c, http.StatusNotFound, "Codex live session not found")
return
}
if principal, hasClientSecret := c.Get(ClientSecretPrincipalContextKey); hasClientSecret {
principalValue, _ := principal.(string)
if session.clientSecretPrincipal == "" || principalValue != session.clientSecretPrincipal {
h.sessions.release(session)
writeRealtimeError(c, http.StatusForbidden, "Realtime client secret is not valid for this call", "invalid_request_error", "realtime_client_secret_scope_mismatch")
return
}
} else if ownerPrincipal, ownerProvider := requestOwner(c); session.ownerPrincipal != "" && (ownerPrincipal != session.ownerPrincipal || ownerProvider != session.ownerProvider) {
h.sessions.release(session)
writeRealtimeError(c, http.StatusForbidden, "Realtime call belongs to another API principal", "invalid_request_error", "realtime_call_scope_mismatch")
return
}
consumeSession := false
defer func() {
if consumeSession {
h.sessions.complete(session, "session_closed")
return
}
h.sessions.release(session)
}()
ctx := context.WithValue(c.Request.Context(), "gin", c)
ctx = coreexecutor.WithDownstreamWebsocket(ctx)
var selection *auth.HomeDispatchSelection
var selected *auth.Auth
var errSelect error
if session.homeSelection != nil {
if !session.homeSelection.Active() {
consumeSession = true
writeLiveError(c, http.StatusServiceUnavailable, "Codex live Home selection unavailable")
return
}
selection = session.homeSelection
selected = selection.CloneAuth()
} else {
selectionOpts := coreexecutor.Options{
Headers: liveSelectionHeaders(c),
Metadata: map[string]any{
coreexecutor.PinnedAuthMetadataKey: session.authID,
coreexecutor.ExecutionSessionMetadataKey: callID,
},
}
selection, selected, errSelect = h.selectOAuth(ctx, session.model, selectionOpts)
}
if errSelect != nil {
writeSelectionError(c, errSelect)
return
}
if selected == nil {
writeLiveError(c, http.StatusServiceUnavailable, "Codex auth unavailable")
return
}
if selection != nil {
attemptCtx, releaseAttempt, errAttempt := selection.AttemptContext(ctx)
if errAttempt != nil {
consumeSession = true
writeLiveError(c, http.StatusServiceUnavailable, errAttempt.Error())
return
}
ctx = attemptCtx
defer releaseAttempt()
}
logging.SetGinCPATraceID(c, selected.EnsureIndex())
upstreamURL := buildSidebandURL(h.sidebandAPIBaseURL, style, callID)
upstreamHTTPURL := websocketHTTPURL(upstreamURL)
dialUpstream := func(current *auth.Auth) (*websocket.Conn, *http.Response, error) {
req, errRequest := http.NewRequestWithContext(ctx, http.MethodGet, upstreamHTTPURL, nil)
if errRequest != nil {
return nil, nil, errRequest
}
req.Header = protocolHeaders(c.Request.Header)
setAccountHeader(req.Header, current)
if errPrepare := h.authManager.PrepareHttpRequest(ctx, current, req); errPrepare != nil {
return nil, nil, errPrepare
}
authType, authValue := current.AccountInfo()
helps.RecordAPIWebsocketRequest(ctx, runtimeConfig, helps.UpstreamRequestLog{
URL: upstreamURL,
Method: "WEBSOCKET",
Headers: headersForLogging(req.Header),
Provider: "codex",
AuthID: current.ID,
AuthLabel: current.Label,
AuthType: authType,
AuthValue: authValue,
})
dialer := newProxyAwareSidebandDialer(runtimeConfig, current)
dialer.Subprotocols = websocket.Subprotocols(c.Request)
return dialer.DialContext(ctx, upstreamURL, req.Header)
}
upstream, handshakeResponse, errDial := dialUpstream(selected)
if errDial != nil && selection != nil && handshakeResponse != nil && handshakeResponse.StatusCode == http.StatusUnauthorized {
h.authManager.ReportHomeUnauthorized(ctx, selected, "codex", session.model)
helps.RecordAPIWebsocketHandshake(ctx, runtimeConfig, handshakeResponse.StatusCode, callResponseHeaders(handshakeResponse.Header))
if handshakeResponse.Body != nil {
if errClose := handshakeResponse.Body.Close(); errClose != nil {
log.Errorf("codex live sideband: close unauthorized handshake body error: %v", errClose)
}
}
refreshed, didRefresh, errRefresh := h.authManager.RefreshHomeSelectionAfterUnauthorized(ctx, selection, selected)
if errRefresh != nil {
writeSelectionError(c, errRefresh)
return
}
if !didRefresh || refreshed == nil {
writeLiveError(c, http.StatusUnauthorized, "Codex credential unauthorized")
return
}
selected = refreshed
logging.SetGinCPATraceID(c, selected.EnsureIndex())
upstream, handshakeResponse, errDial = dialUpstream(selected)
if errDial != nil && handshakeResponse != nil && handshakeResponse.StatusCode == http.StatusUnauthorized {
h.authManager.ReportHomeUnauthorized(ctx, selected, "codex", session.model)
}
}
if errDial != nil {
handleSidebandDialError(c, ctx, runtimeConfig, handshakeResponse, errDial)
return
}
if handshakeResponse != nil {
helps.RecordAPIWebsocketHandshake(ctx, runtimeConfig, handshakeResponse.StatusCode, callResponseHeaders(handshakeResponse.Header))
if handshakeResponse.Body != nil {
if errClose := handshakeResponse.Body.Close(); errClose != nil {
log.Errorf("codex live sideband: close handshake response body error: %v", errClose)
}
}
}
closeUpstream := websocketCloseFunc("upstream", upstream)
if selection != nil {
if errBind := selection.Bind(closeUpstream); errBind != nil {
consumeSession = true
writeLiveError(c, http.StatusServiceUnavailable, errBind.Error())
return
}
} else {
defer func() { _ = closeUpstream() }()
}
upgradeHeaders := make(http.Header)
if subprotocol := upstream.Subprotocol(); subprotocol != "" {
upgradeHeaders.Set("Sec-WebSocket-Protocol", subprotocol)
}
downstream, errUpgrade := sidebandUpgrader.Upgrade(c.Writer, c.Request, upgradeHeaders)
if errUpgrade != nil {
_ = closeUpstream()
return
}
closeDownstream := websocketCloseFunc("downstream", downstream)
if selection != nil {
if errBind := selection.Bind(closeDownstream); errBind != nil {
consumeSession = true
return
}
} else {
defer func() { _ = closeDownstream() }()
}
if session.resources != nil {
session.resources.add(closeUpstream, closeDownstream)
}
consumeSession = true
if errRelay := relayWebsockets(downstream, upstream); errRelay != nil && !isNormalWebsocketClose(errRelay) {
helps.RecordAPIWebsocketError(ctx, runtimeConfig, "relay", errRelay)
log.WithError(errRelay).Debug("codex live sideband relay closed")
}
}
func sidebandTarget(c *gin.Context) (sidebandStyle, string, bool) {
if c == nil || c.Request == nil || c.Request.URL == nil {
return sidebandFrameless, "", false
}
if callID := strings.TrimSpace(c.Param("call_id")); callID != "" {
style := sidebandFrameless
if strings.Contains(c.Request.URL.Path, "/realtime/calls/") {
style = sidebandRealtimeCalls
}
return style, callID, callIDPattern.MatchString(callID)
}
callID := strings.TrimSpace(c.Query("call_id"))
return sidebandRealtimeQuery, callID, callIDPattern.MatchString(callID)
}
func buildSidebandURL(baseURL string, style sidebandStyle, callID string) string {
root := strings.TrimRight(baseURL, "/")
switch style {
case sidebandRealtimeCalls:
return root + "/realtime/calls/" + callID
case sidebandRealtimeQuery:
return root + "/realtime?intent=quicksilver&call_id=" + url.QueryEscape(callID)
default:
return root + "/live/" + callID
}
}
func websocketHTTPURL(rawURL string) string {
parsed, errParse := url.Parse(rawURL)
if errParse != nil {
return rawURL
}
switch strings.ToLower(parsed.Scheme) {
case "ws":
parsed.Scheme = "http"
case "wss":
parsed.Scheme = "https"
}
return parsed.String()
}
func callIDFromLocation(location string) string {
location = strings.TrimSpace(location)
if callIDPattern.MatchString(location) {
return location
}
parsed, errParse := url.Parse(location)
if errParse != nil {
return ""
}
if callID := strings.TrimSpace(parsed.Query().Get("call_id")); callIDPattern.MatchString(callID) {
return callID
}
parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
if len(parts) < 2 {
return ""
}
callID := parts[len(parts)-1]
previous := parts[len(parts)-2]
if !callIDPattern.MatchString(callID) || (previous != "live" && previous != "calls") {
return ""
}
return callID
}
func handleSidebandDialError(c *gin.Context, ctx context.Context, cfg *config.Config, response *http.Response, errDial error) {
status := clienterror.HTTPStatusFromErrorOr(errDial, http.StatusBadGateway)
if response != nil {
if response.StatusCode > 0 {
status = response.StatusCode
}
copyRealtimeHandshakeHeaders(c.Writer.Header(), response.Header)
helps.RecordAPIWebsocketHandshake(ctx, cfg, response.StatusCode, callResponseHeaders(response.Header))
if response.Body != nil {
if errClose := response.Body.Close(); errClose != nil {
log.Errorf("codex live sideband: close rejected handshake body error: %v", errClose)
}
}
}
helps.RecordAPIWebsocketError(ctx, cfg, "dial", errDial)
writeLiveError(c, status, "Codex live sideband upstream unavailable")
}
func websocketCloseFunc(name string, conn *websocket.Conn) func() error {
var once sync.Once
var closeErr error
return func() error {
once.Do(func() {
closeErr = conn.Close()
if closeErr != nil && !isNormalWebsocketClose(closeErr) {
log.Debugf("codex live sideband: close %s websocket error: %v", name, closeErr)
}
})
return closeErr
}
}
func relayWebsockets(downstream, upstream *websocket.Conn) error {
results := make(chan error, 2)
go func() { results <- copyWebsocket(upstream, downstream) }()
go func() { results <- copyWebsocket(downstream, upstream) }()
firstErr := <-results
closeCode, closeReason := websocketCloseDetails(firstErr)
payload := websocket.FormatCloseMessage(closeCode, closeReason)
_ = downstream.WriteControl(websocket.CloseMessage, payload, time.Time{})
_ = upstream.WriteControl(websocket.CloseMessage, payload, time.Time{})
_ = downstream.Close()
_ = upstream.Close()
<-results
return firstErr
}
func copyWebsocket(destination, source *websocket.Conn) error {
for {
messageType, reader, errReader := source.NextReader()
if errReader != nil {
return errReader
}
writer, errWriter := destination.NextWriter(messageType)
if errWriter != nil {
return errWriter
}
_, errCopy := io.Copy(writer, reader)
errClose := writer.Close()
if errCopy != nil {
return errCopy
}
if errClose != nil {
return errClose
}
}
}
func websocketCloseDetails(err error) (int, string) {
var closeErr *websocket.CloseError
if errors.As(err, &closeErr) {
switch closeErr.Code {
case websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure, websocket.CloseTLSHandshake:
return websocket.CloseNormalClosure, ""
default:
return closeErr.Code, closeErr.Text
}
}
if err == nil || errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
return websocket.CloseNormalClosure, ""
}
return websocket.CloseInternalServerErr, "relay closed"
}
func isNormalWebsocketClose(err error) bool {
if err == nil || errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
return true
}
return websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived)
}
func newProxyAwareSidebandDialer(cfg *config.Config, selected *auth.Auth) *websocket.Dialer {
return newSidebandDialer(proxyURLForAuth(cfg, selected))
}
func proxyURLForAuth(cfg *config.Config, selected *auth.Auth) string {
if selected != nil && strings.TrimSpace(selected.ProxyURL) != "" {
return strings.TrimSpace(selected.ProxyURL)
}
if cfg != nil {
return strings.TrimSpace(cfg.ProxyURL)
}
return ""
}
func newSidebandDialer(proxyURL string) *websocket.Dialer {
dialer := &websocket.Dialer{Proxy: http.ProxyFromEnvironment}
if strings.TrimSpace(proxyURL) == "" {
return dialer
}
setting, errParse := proxyutil.Parse(proxyURL)
if errParse != nil {
log.Errorf("codex live sideband: %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 *xproxy.Auth
if setting.URL.User != nil {
username := setting.URL.User.Username()
password, _ := setting.URL.User.Password()
proxyAuth = &xproxy.Auth{User: username, Password: password}
}
socksDialer, errSOCKS5 := xproxy.SOCKS5("tcp", setting.URL.Host, proxyAuth, xproxy.Direct)
if errSOCKS5 != nil {
log.Errorf("codex live sideband: create SOCKS5 dialer failed: %v", errSOCKS5)
return dialer
}
dialer.Proxy = nil
if contextDialer, ok := socksDialer.(xproxy.ContextDialer); ok {
dialer.NetDialContext = contextDialer.DialContext
} else {
dialer.NetDialContext = func(_ context.Context, network, address string) (net.Conn, error) {
return socksDialer.Dial(network, address)
}
}
case "http", "https":
dialer.Proxy = http.ProxyURL(setting.URL)
default:
log.Errorf("codex live sideband: unsupported proxy scheme: %s", setting.URL.Scheme)
}
return dialer
}

View file

@ -0,0 +1,548 @@
package live
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"strings"
"sync"
"github.com/pion/ice/v4"
"github.com/pion/sdp/v3"
"github.com/pion/stun/v3"
log "github.com/sirupsen/logrus"
"golang.org/x/net/proxy"
)
const (
maxUpstreamICECandidates = 64
maxProxiedTCPCandidates = 16
maxUnauthenticatedTCPConns = 4
maxInitialSTUNFrameSize = 4096
stunMessageHeaderSize = 20
)
var nonRoutableProxyTargetPrefixes = []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/8"),
netip.MustParsePrefix("10.0.0.0/8"),
netip.MustParsePrefix("100.64.0.0/10"),
netip.MustParsePrefix("127.0.0.0/8"),
netip.MustParsePrefix("169.254.0.0/16"),
netip.MustParsePrefix("172.16.0.0/12"),
netip.MustParsePrefix("192.0.0.0/24"),
netip.MustParsePrefix("192.0.2.0/24"),
netip.MustParsePrefix("192.88.99.0/24"),
netip.MustParsePrefix("192.168.0.0/16"),
netip.MustParsePrefix("198.18.0.0/15"),
netip.MustParsePrefix("198.51.100.0/24"),
netip.MustParsePrefix("203.0.113.0/24"),
netip.MustParsePrefix("224.0.0.0/4"),
netip.MustParsePrefix("240.0.0.0/4"),
netip.MustParsePrefix("::/96"),
netip.MustParsePrefix("::ffff:0:0:0/96"),
netip.MustParsePrefix("64:ff9b::/96"),
netip.MustParsePrefix("64:ff9b:1::/48"),
netip.MustParsePrefix("100::/64"),
netip.MustParsePrefix("2001::/23"),
netip.MustParsePrefix("2001:db8::/32"),
netip.MustParsePrefix("2002::/16"),
netip.MustParsePrefix("3fff::/20"),
netip.MustParsePrefix("5f00::/16"),
netip.MustParsePrefix("fc00::/7"),
netip.MustParsePrefix("fe80::/10"),
netip.MustParsePrefix("fec0::/10"),
netip.MustParsePrefix("ff00::/8"),
}
type iceCredentials struct {
ufrag string
password string
}
type tcpCandidateTunnel struct {
listener net.Listener
target netip.AddrPort
dialer proxy.ContextDialer
expectedUser string
remotePassword string
mu sync.Mutex
closed bool
claimed bool
connections map[net.Conn]struct{}
validationSlots chan struct{}
onForwardingStarted func()
ctx context.Context
cancel context.CancelFunc
}
type tcpCandidatePlan struct {
mediaIndex int
attributeIndex int
fields []string
target netip.AddrPort
}
func prepareProxiedUpstreamAnswer(answer, localOffer string, dialer proxy.ContextDialer) (string, []*tcpCandidateTunnel, error) {
if dialer == nil {
return "", nil, errors.New("Codex live TCP proxy dialer is unavailable")
}
var remoteDescription sdp.SessionDescription
if errUnmarshal := remoteDescription.UnmarshalString(answer); errUnmarshal != nil {
return "", nil, fmt.Errorf("parse upstream WebRTC answer for TCP proxy: %w", errUnmarshal)
}
var localDescription sdp.SessionDescription
if errUnmarshal := localDescription.UnmarshalString(localOffer); errUnmarshal != nil {
return "", nil, fmt.Errorf("parse upstream WebRTC offer for TCP proxy: %w", errUnmarshal)
}
remoteCredentials, errCredentials := bundledICECredentials(&remoteDescription)
if errCredentials != nil {
return "", nil, fmt.Errorf("read upstream WebRTC answer ICE credentials: %w", errCredentials)
}
localCredentials, errCredentials := bundledICECredentials(&localDescription)
if errCredentials != nil {
return "", nil, fmt.Errorf("read upstream WebRTC offer ICE credentials: %w", errCredentials)
}
plans := make([]tcpCandidatePlan, 0, 4)
candidateCount := 0
for mediaIndex, media := range remoteDescription.MediaDescriptions {
if media == nil {
continue
}
filtered := make([]sdp.Attribute, 0, len(media.Attributes))
for attributeIndex := range media.Attributes {
attribute := media.Attributes[attributeIndex]
if !attribute.IsICECandidate() {
filtered = append(filtered, attribute)
continue
}
candidateCount++
if candidateCount > maxUpstreamICECandidates {
return "", nil, fmt.Errorf("upstream WebRTC answer exceeds the %d candidate limit", maxUpstreamICECandidates)
}
plan, keep, errCandidate := proxiedTCPCandidatePlan(attribute.Value)
if errCandidate != nil {
return "", nil, errCandidate
}
if !keep {
continue
}
if len(plans) >= maxProxiedTCPCandidates {
return "", nil, fmt.Errorf("upstream WebRTC answer exceeds the %d TCP candidate proxy limit", maxProxiedTCPCandidates)
}
plan.mediaIndex = mediaIndex
plan.attributeIndex = len(filtered)
filtered = append(filtered, attribute)
plans = append(plans, plan)
}
media.Attributes = filtered
}
if len(plans) == 0 {
return "", nil, errors.New("upstream WebRTC answer has no supported public TCP passive candidate on port 443")
}
expectedUser := remoteCredentials.ufrag + ":" + localCredentials.ufrag
tunnels := make([]*tcpCandidateTunnel, 0, len(plans))
closeTunnels := func() {
for _, tunnel := range tunnels {
if errClose := tunnel.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live TCP proxy: close candidate tunnel after setup error")
}
}
}
for _, plan := range plans {
tunnel, errTunnel := newTCPCandidateTunnel(plan.target, dialer, expectedUser, remoteCredentials.password)
if errTunnel != nil {
closeTunnels()
return "", nil, errTunnel
}
tunnels = append(tunnels, tunnel)
listenerAddress, ok := tunnel.listener.Addr().(*net.TCPAddr)
if !ok || listenerAddress.IP == nil {
closeTunnels()
return "", nil, errors.New("Codex live TCP proxy listener returned an invalid address")
}
fields := append([]string(nil), plan.fields...)
fields[4] = listenerAddress.IP.String()
fields[5] = strconv.Itoa(listenerAddress.Port)
remoteDescription.MediaDescriptions[plan.mediaIndex].Attributes[plan.attributeIndex].Value = strings.Join(fields, " ")
}
rewritten, errMarshal := remoteDescription.Marshal()
if errMarshal != nil {
closeTunnels()
return "", nil, fmt.Errorf("marshal proxied upstream WebRTC answer: %w", errMarshal)
}
return string(rewritten), tunnels, nil
}
func proxiedTCPCandidatePlan(rawCandidate string) (tcpCandidatePlan, bool, error) {
trimmed := strings.TrimSpace(rawCandidate)
candidate, errCandidate := ice.UnmarshalCandidate(trimmed)
if errCandidate != nil {
return tcpCandidatePlan{}, false, fmt.Errorf("parse upstream WebRTC candidate: %w", errCandidate)
}
if candidate.NetworkType() != ice.NetworkTypeTCP4 && candidate.NetworkType() != ice.NetworkTypeTCP6 {
return tcpCandidatePlan{}, false, nil
}
if candidate.TCPType() != ice.TCPTypePassive {
return tcpCandidatePlan{}, false, nil
}
if candidate.Component() != uint16(ice.ComponentRTP) || candidate.Type() != ice.CandidateTypeHost {
return tcpCandidatePlan{}, false, nil
}
if candidate.Port() != 443 {
return tcpCandidatePlan{}, false, fmt.Errorf("upstream WebRTC TCP proxy candidate uses disallowed port %d", candidate.Port())
}
address, errAddress := netip.ParseAddr(candidate.Address())
if errAddress != nil {
return tcpCandidatePlan{}, false, errors.New("upstream WebRTC TCP proxy candidate address must be an IP")
}
address = address.Unmap()
if !isPublicProxyTarget(address) {
return tcpCandidatePlan{}, false, errors.New("upstream WebRTC TCP proxy candidate address must be globally routable")
}
fields := strings.Fields(trimmed)
if len(fields) < 8 {
return tcpCandidatePlan{}, false, errors.New("upstream WebRTC TCP proxy candidate is malformed")
}
return tcpCandidatePlan{
fields: fields,
target: netip.AddrPortFrom(address, uint16(candidate.Port())),
}, true, nil
}
func isPublicProxyTarget(address netip.Addr) bool {
if !address.IsValid() || !address.IsGlobalUnicast() || address.IsUnspecified() || address.IsLoopback() ||
address.IsPrivate() || address.IsLinkLocalUnicast() || address.IsLinkLocalMulticast() || address.IsMulticast() {
return false
}
for _, prefix := range nonRoutableProxyTargetPrefixes {
if prefix.Contains(address) {
return false
}
}
return true
}
func bundledICECredentials(description *sdp.SessionDescription) (iceCredentials, error) {
if description == nil {
return iceCredentials{}, errors.New("SDP is unavailable")
}
sessionUfrag, _ := description.Attribute("ice-ufrag")
sessionPassword, _ := description.Attribute("ice-pwd")
var selected iceCredentials
for _, media := range description.MediaDescriptions {
if media == nil {
continue
}
ufrag := sessionUfrag
if mediaUfrag, ok := media.Attribute("ice-ufrag"); ok {
ufrag = mediaUfrag
}
password := sessionPassword
if mediaPassword, ok := media.Attribute("ice-pwd"); ok {
password = mediaPassword
}
ufrag = strings.TrimSpace(ufrag)
password = strings.TrimSpace(password)
if ufrag == "" && password == "" {
continue
}
if ufrag == "" || password == "" {
return iceCredentials{}, errors.New("SDP contains incomplete ICE credentials")
}
current := iceCredentials{ufrag: ufrag, password: password}
if selected.ufrag == "" {
selected = current
continue
}
if selected != current {
return iceCredentials{}, errors.New("SDP contains inconsistent bundled ICE credentials")
}
}
if selected.ufrag == "" {
selected = iceCredentials{ufrag: strings.TrimSpace(sessionUfrag), password: strings.TrimSpace(sessionPassword)}
}
if selected.ufrag == "" || selected.password == "" {
return iceCredentials{}, errors.New("SDP is missing ICE credentials")
}
return selected, nil
}
func closeCandidateTunnels(tunnels []*tcpCandidateTunnel) error {
var closeErrors []error
for _, tunnel := range tunnels {
if errClose := tunnel.Close(); errClose != nil {
closeErrors = append(closeErrors, errClose)
}
}
return errors.Join(closeErrors...)
}
func newTCPCandidateTunnel(target netip.AddrPort, dialer proxy.ContextDialer, expectedUser, remotePassword string) (*tcpCandidateTunnel, error) {
if !isPublicProxyTarget(target.Addr()) || target.Port() != 443 {
return nil, errors.New("Codex live TCP proxy target is not allowed")
}
if dialer == nil || strings.TrimSpace(expectedUser) == "" || strings.TrimSpace(remotePassword) == "" {
return nil, errors.New("Codex live TCP proxy tunnel configuration is incomplete")
}
network := "tcp4"
listenAddress := "127.0.0.1:0"
if target.Addr().Is6() {
network = "tcp6"
listenAddress = "[::1]:0"
}
listener, errListen := net.Listen(network, listenAddress)
if errListen != nil {
return nil, fmt.Errorf("listen for Codex live TCP proxy candidate: %w", errListen)
}
tunnelContext, cancelTunnel := context.WithCancel(context.Background())
tunnel := &tcpCandidateTunnel{
listener: listener,
target: target,
dialer: dialer,
expectedUser: expectedUser,
remotePassword: remotePassword,
connections: make(map[net.Conn]struct{}),
validationSlots: make(chan struct{}, maxUnauthenticatedTCPConns),
ctx: tunnelContext,
cancel: cancelTunnel,
}
go tunnel.accept()
return tunnel, nil
}
func (t *tcpCandidateTunnel) accept() {
for {
connection, errAccept := t.listener.Accept()
if errAccept != nil {
if !errors.Is(errAccept, net.ErrClosed) {
log.WithError(errAccept).Warn("codex live TCP proxy: accept candidate connection failed")
}
return
}
if !t.trackConnection(connection) {
if errClose := connection.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live TCP proxy: close connection after tunnel shutdown")
}
return
}
select {
case t.validationSlots <- struct{}{}:
go func() {
defer func() { <-t.validationSlots }()
t.handleConnection(connection)
}()
default:
t.untrackAndClose(connection)
log.Warn("codex live TCP proxy: rejected excess unauthenticated candidate connection")
}
}
}
func (t *tcpCandidateTunnel) handleConnection(client net.Conn) {
firstFrame, errValidate := readValidatedICEBindingFrame(client, t.expectedUser, t.remotePassword)
if errValidate != nil {
t.untrackAndClose(client)
log.WithError(errValidate).Warn("codex live TCP proxy: rejected unauthenticated candidate connection")
return
}
if !t.claim() {
t.untrackAndClose(client)
return
}
if errClose := t.listener.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) {
log.WithError(errClose).Debug("codex live TCP proxy: close claimed candidate listener")
}
upstream, errDial := t.dialer.DialContext(t.ctx, "tcp", t.target.String())
if errDial != nil {
t.untrackAndClose(client)
log.WithError(errDial).Warn("codex live TCP proxy: connect fixed upstream candidate failed")
return
}
if !t.trackConnection(upstream) {
if errClose := upstream.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live TCP proxy: close upstream after tunnel shutdown")
}
t.untrackAndClose(client)
return
}
if errWrite := writeAll(upstream, firstFrame); errWrite != nil {
t.untrackAndClose(upstream)
t.untrackAndClose(client)
log.WithError(errWrite).Warn("codex live TCP proxy: forward authenticated ICE frame failed")
return
}
t.notifyForwardingStarted()
copyDone := make(chan struct{}, 2)
copyConnection := func(destination, source net.Conn) {
_, _ = io.Copy(destination, source)
copyDone <- struct{}{}
}
go copyConnection(upstream, client)
go copyConnection(client, upstream)
<-copyDone
t.untrackAndClose(upstream)
t.untrackAndClose(client)
<-copyDone
}
func (t *tcpCandidateTunnel) setForwardingStartedHandler(handler func()) {
if t == nil {
return
}
t.mu.Lock()
t.onForwardingStarted = handler
t.mu.Unlock()
}
func (t *tcpCandidateTunnel) notifyForwardingStarted() {
if t == nil {
return
}
t.mu.Lock()
handler := t.onForwardingStarted
t.mu.Unlock()
if handler != nil {
handler()
}
}
func (t *tcpCandidateTunnel) trackConnection(connection net.Conn) bool {
t.mu.Lock()
defer t.mu.Unlock()
if t.closed {
return false
}
t.connections[connection] = struct{}{}
return true
}
func (t *tcpCandidateTunnel) untrackAndClose(connection net.Conn) {
if connection == nil {
return
}
t.mu.Lock()
delete(t.connections, connection)
t.mu.Unlock()
if errClose := connection.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) {
log.WithError(errClose).Debug("codex live TCP proxy: close tunnel connection")
}
}
func (t *tcpCandidateTunnel) claim() bool {
t.mu.Lock()
defer t.mu.Unlock()
if t.closed || t.claimed {
return false
}
t.claimed = true
return true
}
func (t *tcpCandidateTunnel) Close() error {
if t == nil {
return nil
}
t.mu.Lock()
if t.closed {
t.mu.Unlock()
return nil
}
t.closed = true
cancel := t.cancel
connections := make([]net.Conn, 0, len(t.connections))
for connection := range t.connections {
connections = append(connections, connection)
}
t.connections = make(map[net.Conn]struct{})
t.mu.Unlock()
if cancel != nil {
cancel()
}
var closeErrors []error
if t.listener != nil {
if errClose := t.listener.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) {
closeErrors = append(closeErrors, errClose)
}
}
for _, connection := range connections {
if errClose := connection.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) {
closeErrors = append(closeErrors, errClose)
}
}
return errors.Join(closeErrors...)
}
func readValidatedICEBindingFrame(connection io.Reader, expectedUser, remotePassword string) ([]byte, error) {
var header [2]byte
if _, errRead := io.ReadFull(connection, header[:]); errRead != nil {
return nil, fmt.Errorf("read ICE-TCP frame header: %w", errRead)
}
frameSize := int(binary.BigEndian.Uint16(header[:]))
if frameSize < stunMessageHeaderSize || frameSize > maxInitialSTUNFrameSize {
return nil, fmt.Errorf("invalid initial ICE-TCP STUN frame size %d", frameSize)
}
payload := make([]byte, frameSize)
if _, errRead := io.ReadFull(connection, payload); errRead != nil {
return nil, fmt.Errorf("read ICE-TCP STUN frame: %w", errRead)
}
message := stun.NewWithOptions(stun.WithStrict(true))
if errDecode := stun.Decode(payload, message); errDecode != nil {
return nil, fmt.Errorf("decode initial ICE-TCP STUN message: %w", errDecode)
}
if len(payload) != stunMessageHeaderSize+int(message.Length) {
return nil, errors.New("initial ICE-TCP STUN message contains trailing data")
}
if message.Type != stun.BindingRequest {
return nil, fmt.Errorf("initial ICE-TCP STUN message has unexpected type %s", message.Type)
}
var username stun.Username
if errUsername := username.GetFrom(message); errUsername != nil {
return nil, fmt.Errorf("read initial ICE-TCP STUN username: %w", errUsername)
}
if string(username) != expectedUser {
return nil, errors.New("initial ICE-TCP STUN username does not match the media session")
}
if errIntegrity := stun.NewShortTermIntegrity(remotePassword).Check(message); errIntegrity != nil {
return nil, fmt.Errorf("verify initial ICE-TCP STUN integrity: %w", errIntegrity)
}
if errFingerprint := stun.Fingerprint.Check(message); errFingerprint != nil {
return nil, fmt.Errorf("verify initial ICE-TCP STUN fingerprint: %w", errFingerprint)
}
frame := make([]byte, len(header)+len(payload))
copy(frame, header[:])
copy(frame[len(header):], payload)
return frame, nil
}
func writeAll(writer io.Writer, data []byte) error {
for len(data) > 0 {
written, errWrite := writer.Write(data)
if errWrite != nil {
return errWrite
}
if written <= 0 {
return io.ErrShortWrite
}
data = data[written:]
}
return nil
}
func proxyScheme(rawProxyURL string) string {
trimmed := strings.TrimSpace(rawProxyURL)
if index := strings.Index(trimmed, "://"); index > 0 {
return strings.ToLower(trimmed[:index])
}
return "proxy"
}

View file

@ -0,0 +1,651 @@
package live
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/pion/sdp/v3"
"github.com/pion/stun/v3"
"github.com/pion/webrtc/v4"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
type recordedProxyDial struct {
address string
connection net.Conn
}
type recordingProxyDialer struct {
mu sync.Mutex
dials chan recordedProxyDial
err error
}
type blockingContextDialer struct {
started chan struct{}
canceled chan struct{}
}
type closedUpstreamDialer struct{}
func (*closedUpstreamDialer) DialContext(context.Context, string, string) (net.Conn, error) {
client, server := net.Pipe()
_ = server.Close()
return client, nil
}
func (d *blockingContextDialer) DialContext(ctx context.Context, _, _ string) (net.Conn, error) {
close(d.started)
<-ctx.Done()
close(d.canceled)
return nil, ctx.Err()
}
func (d *recordingProxyDialer) Dial(network, address string) (net.Conn, error) {
return d.DialContext(context.Background(), network, address)
}
func (d *recordingProxyDialer) DialContext(ctx context.Context, _ string, address string) (net.Conn, error) {
if errContext := ctx.Err(); errContext != nil {
return nil, errContext
}
d.mu.Lock()
channel := d.dials
errDial := d.err
d.mu.Unlock()
if errDial != nil {
channel <- recordedProxyDial{address: address}
return nil, errDial
}
client, server := net.Pipe()
channel <- recordedProxyDial{address: address, connection: server}
return client, nil
}
func TestPrepareProxiedUpstreamAnswerRestrictsAndRewritesCandidates(t *testing.T) {
dialer := &recordingProxyDialer{dials: make(chan recordedProxyDial, 1)}
answer := testProxySDP("remote-ufrag", "remote-password", []string{
"1 1 udp 2130706431 20.42.0.10 3478 typ host",
"2 1 tcp 1671430143 20.42.0.20 443 typ host tcptype passive",
})
localOffer := testProxySDP("local-ufrag", "local-password", nil)
rewritten, tunnels, errPrepare := prepareProxiedUpstreamAnswer(answer, localOffer, dialer)
if errPrepare != nil {
t.Fatalf("prepareProxiedUpstreamAnswer returned error: %v", errPrepare)
}
defer func() {
if errClose := closeCandidateTunnels(tunnels); errClose != nil {
t.Errorf("close candidate tunnels: %v", errClose)
}
}()
if len(tunnels) != 1 {
t.Fatalf("tunnel count = %d, want 1", len(tunnels))
}
if got := tunnels[0].target.String(); got != "20.42.0.20:443" {
t.Fatalf("fixed target = %q, want 20.42.0.20:443", got)
}
if tunnels[0].expectedUser != "remote-ufrag:local-ufrag" {
t.Fatalf("expected STUN username = %q", tunnels[0].expectedUser)
}
var description sdp.SessionDescription
if errUnmarshal := description.UnmarshalString(rewritten); errUnmarshal != nil {
t.Fatalf("unmarshal rewritten SDP: %v", errUnmarshal)
}
var candidates []string
for _, media := range description.MediaDescriptions {
for _, attribute := range media.Attributes {
if attribute.IsICECandidate() {
candidates = append(candidates, attribute.Value)
}
}
}
if len(candidates) != 1 {
t.Fatalf("rewritten candidate count = %d, want 1: %v", len(candidates), candidates)
}
fields := strings.Fields(candidates[0])
if len(fields) < 8 || fields[2] != "tcp" || fields[4] != "127.0.0.1" || fields[5] == "443" {
t.Fatalf("rewritten candidate = %q", candidates[0])
}
if !strings.Contains(candidates[0], "tcptype passive") {
t.Fatalf("rewritten candidate lost passive TCP type: %q", candidates[0])
}
}
func TestPrepareProxiedUpstreamAnswerRejectsUnsafeTargets(t *testing.T) {
for name, candidate := range map[string]string{
"private target": "1 1 tcp 1671430143 10.0.0.1 443 typ host tcptype passive",
"zero network target": "1 1 tcp 1671430143 0.0.0.1 443 typ host tcptype passive",
"carrier NAT target": "1 1 tcp 1671430143 100.64.0.1 443 typ host tcptype passive",
"reserved target": "1 1 tcp 1671430143 203.0.113.10 443 typ host tcptype passive",
"site-local IPv6 target": "1 1 tcp 1671430143 fec0::1 443 typ host tcptype passive",
"wrong port": "1 1 tcp 1671430143 20.42.0.10 8443 typ host tcptype passive",
"relay target": "1 1 tcp 1671430143 20.42.0.10 443 typ relay raddr 192.0.2.1 rport 5000 tcptype passive",
"active target": "1 1 tcp 1671430143 20.42.0.10 443 typ host tcptype active",
} {
t.Run(name, func(t *testing.T) {
dialer := &recordingProxyDialer{dials: make(chan recordedProxyDial, 1)}
_, tunnels, errPrepare := prepareProxiedUpstreamAnswer(
testProxySDP("remote", "remote-password", []string{candidate}),
testProxySDP("local", "local-password", nil),
dialer,
)
if errPrepare == nil {
_ = closeCandidateTunnels(tunnels)
t.Fatal("expected unsafe candidate to be rejected")
}
})
}
}
func TestPrepareProxiedUpstreamAnswerLimitsCandidateCount(t *testing.T) {
candidates := make([]string, 0, maxUpstreamICECandidates+1)
for index := 0; index <= maxUpstreamICECandidates; index++ {
candidates = append(candidates, fmt.Sprintf("%d 1 udp 2130706431 20.42.0.10 3478 typ host", index+1))
}
_, tunnels, errPrepare := prepareProxiedUpstreamAnswer(
testProxySDP("remote", "remote-password", candidates),
testProxySDP("local", "local-password", nil),
&recordingProxyDialer{dials: make(chan recordedProxyDial, 1)},
)
if errPrepare == nil || !strings.Contains(errPrepare.Error(), "candidate limit") {
_ = closeCandidateTunnels(tunnels)
t.Fatalf("error = %v, want candidate limit", errPrepare)
}
}
func TestReadValidatedICEBindingFrame(t *testing.T) {
validFrame := buildTestICEFrame(t, "remote:local", "remote-password", true)
for name, testCase := range map[string]struct {
frame []byte
expectedUser string
password string
wantError bool
}{
"valid": {
frame: validFrame,
expectedUser: "remote:local",
password: "remote-password",
},
"wrong username": {
frame: validFrame,
expectedUser: "local:remote",
password: "remote-password",
wantError: true,
},
"wrong password": {
frame: validFrame,
expectedUser: "remote:local",
password: "local-password",
wantError: true,
},
"missing fingerprint": {
frame: buildTestICEFrame(t, "remote:local", "remote-password", false),
expectedUser: "remote:local",
password: "remote-password",
wantError: true,
},
"undersized": {
frame: []byte{0, 1, 0},
wantError: true,
},
} {
t.Run(name, func(t *testing.T) {
validated, errValidate := readValidatedICEBindingFrame(
&fragmentedReader{data: testCase.frame, maximum: 3},
testCase.expectedUser,
testCase.password,
)
if testCase.wantError {
if errValidate == nil {
t.Fatal("expected validation error")
}
return
}
if errValidate != nil {
t.Fatalf("readValidatedICEBindingFrame returned error: %v", errValidate)
}
if !bytes.Equal(validated, testCase.frame) {
t.Fatal("validated frame changed")
}
})
}
}
func TestTCPCandidateTunnelAuthenticatesBeforeFixedTargetDial(t *testing.T) {
dialer := &recordingProxyDialer{dials: make(chan recordedProxyDial, 1)}
tunnel, errTunnel := newTCPCandidateTunnel(
netip.MustParseAddrPort("20.42.0.20:443"),
dialer,
"remote:local",
"remote-password",
)
if errTunnel != nil {
t.Fatalf("newTCPCandidateTunnel returned error: %v", errTunnel)
}
defer func() { _ = tunnel.Close() }()
forwardingStarted := make(chan struct{}, 1)
tunnel.setForwardingStartedHandler(func() {
forwardingStarted <- struct{}{}
})
client, errDial := net.Dial("tcp", tunnel.listener.Addr().String())
if errDial != nil {
t.Fatalf("dial candidate listener: %v", errDial)
}
defer func() { _ = client.Close() }()
frame := buildTestICEFrame(t, "remote:local", "remote-password", true)
if errWrite := writeAll(client, frame); errWrite != nil {
t.Fatalf("write authenticated frame: %v", errWrite)
}
var dial recordedProxyDial
select {
case dial = <-dialer.dials:
case <-time.After(time.Second):
t.Fatal("proxy dial was not attempted after STUN authentication")
}
defer func() { _ = dial.connection.Close() }()
if dial.address != "20.42.0.20:443" {
t.Fatalf("proxy target = %q, want fixed candidate", dial.address)
}
forwarded := make([]byte, len(frame))
if _, errRead := io.ReadFull(dial.connection, forwarded); errRead != nil {
t.Fatalf("read forwarded STUN frame: %v", errRead)
}
if !bytes.Equal(forwarded, frame) {
t.Fatal("forwarded STUN frame changed")
}
select {
case <-forwardingStarted:
case <-time.After(time.Second):
t.Fatal("forwarding start handler was not called")
}
if errWrite := writeAll(dial.connection, []byte("reply")); errWrite != nil {
t.Fatalf("write tunnel reply: %v", errWrite)
}
reply := make([]byte, len("reply"))
if _, errRead := io.ReadFull(client, reply); errRead != nil {
t.Fatalf("read tunnel reply: %v", errRead)
}
if string(reply) != "reply" {
t.Fatalf("tunnel reply = %q", reply)
}
}
func TestTCPCandidateTunnelCloseCancelsProxyDial(t *testing.T) {
dialer := &blockingContextDialer{
started: make(chan struct{}),
canceled: make(chan struct{}),
}
tunnel, errTunnel := newTCPCandidateTunnel(
netip.MustParseAddrPort("20.42.0.20:443"),
dialer,
"remote:local",
"remote-password",
)
if errTunnel != nil {
t.Fatalf("newTCPCandidateTunnel returned error: %v", errTunnel)
}
client, errDial := net.Dial("tcp", tunnel.listener.Addr().String())
if errDial != nil {
t.Fatalf("dial candidate listener: %v", errDial)
}
if errWrite := writeAll(client, buildTestICEFrame(t, "remote:local", "remote-password", true)); errWrite != nil {
t.Fatalf("write authenticated frame: %v", errWrite)
}
defer func() { _ = client.Close() }()
select {
case <-dialer.started:
case <-time.After(time.Second):
t.Fatal("proxy dial did not start")
}
forwardingStarted := make(chan struct{}, 1)
tunnel.setForwardingStartedHandler(func() { forwardingStarted <- struct{}{} })
if errClose := tunnel.Close(); errClose != nil {
t.Fatalf("close tunnel: %v", errClose)
}
select {
case <-dialer.canceled:
case <-time.After(time.Second):
t.Fatal("tunnel close did not cancel proxy dial")
}
assertNoForwardingStart(t, forwardingStarted)
}
func TestTCPCandidateTunnelProxyFailureDoesNotFallBack(t *testing.T) {
dialer := &recordingProxyDialer{
dials: make(chan recordedProxyDial, 1),
err: errors.New("proxy blocked"),
}
tunnel, errTunnel := newTCPCandidateTunnel(
netip.MustParseAddrPort("20.42.0.20:443"),
dialer,
"remote:local",
"remote-password",
)
if errTunnel != nil {
t.Fatalf("newTCPCandidateTunnel returned error: %v", errTunnel)
}
defer func() { _ = tunnel.Close() }()
forwardingStarted := make(chan struct{}, 1)
tunnel.setForwardingStartedHandler(func() { forwardingStarted <- struct{}{} })
client, errDial := net.Dial("tcp", tunnel.listener.Addr().String())
if errDial != nil {
t.Fatalf("dial candidate listener: %v", errDial)
}
if errWrite := writeAll(client, buildTestICEFrame(t, "remote:local", "remote-password", true)); errWrite != nil {
t.Fatalf("write authenticated frame: %v", errWrite)
}
defer func() { _ = client.Close() }()
select {
case dial := <-dialer.dials:
if dial.address != "20.42.0.20:443" || dial.connection != nil {
t.Fatalf("failed proxy dial = %#v", dial)
}
case <-time.After(time.Second):
t.Fatal("proxy dial was not attempted")
}
if _, errSecondDial := net.Dial("tcp", tunnel.listener.Addr().String()); errSecondDial == nil {
t.Fatal("candidate listener remained available after proxy failure")
}
assertNoForwardingStart(t, forwardingStarted)
}
func TestTCPCandidateTunnelWriteFailureDoesNotLogForwardingStart(t *testing.T) {
tunnel, errTunnel := newTCPCandidateTunnel(
netip.MustParseAddrPort("20.42.0.20:443"),
&closedUpstreamDialer{},
"remote:local",
"remote-password",
)
if errTunnel != nil {
t.Fatalf("newTCPCandidateTunnel returned error: %v", errTunnel)
}
defer func() { _ = tunnel.Close() }()
forwardingStarted := make(chan struct{}, 1)
tunnel.setForwardingStartedHandler(func() { forwardingStarted <- struct{}{} })
client, errDial := net.Dial("tcp", tunnel.listener.Addr().String())
if errDial != nil {
t.Fatalf("dial candidate listener: %v", errDial)
}
if errWrite := writeAll(client, buildTestICEFrame(t, "remote:local", "remote-password", true)); errWrite != nil {
t.Fatalf("write authenticated frame: %v", errWrite)
}
_ = client.Close()
assertNoForwardingStart(t, forwardingStarted)
}
func TestTCPCandidateTunnelRejectsUnauthenticatedConnectionWithoutDial(t *testing.T) {
dialer := &recordingProxyDialer{dials: make(chan recordedProxyDial, 1)}
tunnel, errTunnel := newTCPCandidateTunnel(
netip.MustParseAddrPort("20.42.0.20:443"),
dialer,
"remote:local",
"remote-password",
)
if errTunnel != nil {
t.Fatalf("newTCPCandidateTunnel returned error: %v", errTunnel)
}
defer func() { _ = tunnel.Close() }()
forwardingStarted := make(chan struct{}, 1)
tunnel.setForwardingStartedHandler(func() { forwardingStarted <- struct{}{} })
client, errDial := net.Dial("tcp", tunnel.listener.Addr().String())
if errDial != nil {
t.Fatalf("dial candidate listener: %v", errDial)
}
if errWrite := writeAll(client, buildTestICEFrame(t, "attacker:local", "remote-password", true)); errWrite != nil {
t.Fatalf("write unauthenticated frame: %v", errWrite)
}
_ = client.Close()
select {
case dial := <-dialer.dials:
_ = dial.connection.Close()
t.Fatalf("unauthenticated connection triggered proxy dial to %q", dial.address)
case <-time.After(100 * time.Millisecond):
}
assertNoForwardingStart(t, forwardingStarted)
}
func assertNoForwardingStart(t *testing.T, started <-chan struct{}) {
t.Helper()
select {
case <-started:
t.Fatal("forwarding start handler was called for an unestablished tunnel")
case <-time.After(100 * time.Millisecond):
}
}
func TestPionActiveTCPCandidatePassesTunnelAuthentication(t *testing.T) {
localAPI, errAPI := newPionProxyAPI(config.CodexLiveMediaRelayConfig{})
if errAPI != nil {
t.Fatalf("create local Pion API: %v", errAPI)
}
localPeer, errPeer := localAPI.NewPeerConnection(webrtc.Configuration{})
if errPeer != nil {
t.Fatalf("create local PeerConnection: %v", errPeer)
}
defer func() { _ = localPeer.Close() }()
if _, errChannel := localPeer.CreateDataChannel(realtimeDataChannelLabel, nil); errChannel != nil {
t.Fatalf("create local DataChannel: %v", errChannel)
}
localGathering := webrtc.GatheringCompletePromise(localPeer)
localOffer, errOffer := localPeer.CreateOffer(nil)
if errOffer != nil {
t.Fatalf("create local offer: %v", errOffer)
}
if errLocal := localPeer.SetLocalDescription(localOffer); errLocal != nil {
t.Fatalf("set local offer: %v", errLocal)
}
<-localGathering
localDescription := localPeer.LocalDescription()
if localDescription == nil {
t.Fatal("local description is nil")
}
tcpListener, errListen := net.Listen("tcp4", "127.0.0.1:0")
if errListen != nil {
t.Fatalf("listen for remote ICE-TCP: %v", errListen)
}
remoteSettings := webrtc.SettingEngine{}
remoteSettings.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4})
remoteSettings.SetIncludeLoopbackCandidate(true)
remoteSettings.SetIPFilter(func(ip net.IP) bool { return ip != nil && ip.IsLoopback() })
tcpMux := webrtc.NewICETCPMux(nil, tcpListener, 8)
remoteSettings.SetICETCPMux(tcpMux)
defer func() { _ = tcpMux.Close() }()
remoteAPI := webrtc.NewAPI(webrtc.WithSettingEngine(remoteSettings))
remotePeer, errPeer := remoteAPI.NewPeerConnection(webrtc.Configuration{})
if errPeer != nil {
t.Fatalf("create remote PeerConnection: %v", errPeer)
}
defer func() { _ = remotePeer.Close() }()
if errRemote := remotePeer.SetRemoteDescription(*localDescription); errRemote != nil {
t.Fatalf("set remote offer: %v", errRemote)
}
remoteGathering := webrtc.GatheringCompletePromise(remotePeer)
remoteAnswer, errAnswer := remotePeer.CreateAnswer(nil)
if errAnswer != nil {
t.Fatalf("create remote answer: %v", errAnswer)
}
if errLocal := remotePeer.SetLocalDescription(remoteAnswer); errLocal != nil {
t.Fatalf("set remote answer: %v", errLocal)
}
<-remoteGathering
remoteDescription := remotePeer.LocalDescription()
if remoteDescription == nil {
t.Fatal("remote description is nil")
}
publicAnswer := rewriteTestTCPCandidateTarget(t, remoteDescription.SDP, "20.42.0.20", 443)
dialer := &recordingProxyDialer{dials: make(chan recordedProxyDial, 1)}
rewrittenAnswer, tunnels, errPrepare := prepareProxiedUpstreamAnswer(publicAnswer, localDescription.SDP, dialer)
if errPrepare != nil {
t.Fatalf("prepare proxied Pion answer: %v", errPrepare)
}
defer func() { _ = closeCandidateTunnels(tunnels) }()
if errRemote := localPeer.SetRemoteDescription(webrtc.SessionDescription{
Type: webrtc.SDPTypeAnswer,
SDP: rewrittenAnswer,
}); errRemote != nil {
t.Fatalf("set rewritten remote answer: %v", errRemote)
}
var dial recordedProxyDial
select {
case dial = <-dialer.dials:
case <-time.After(5 * time.Second):
t.Fatal("Pion active ICE-TCP did not reach the authenticated tunnel")
}
defer func() { _ = dial.connection.Close() }()
localCredentials, errCredentials := bundledICECredentialsFromString(localDescription.SDP)
if errCredentials != nil {
t.Fatalf("read local credentials: %v", errCredentials)
}
remoteCredentials, errCredentials := bundledICECredentialsFromString(publicAnswer)
if errCredentials != nil {
t.Fatalf("read remote credentials: %v", errCredentials)
}
if _, errValidate := readValidatedICEBindingFrame(
dial.connection,
remoteCredentials.ufrag+":"+localCredentials.ufrag,
remoteCredentials.password,
); errValidate != nil {
t.Fatalf("forwarded Pion STUN request failed validation: %v", errValidate)
}
}
func TestBundledICECredentialsRejectsMixedCredentials(t *testing.T) {
mixed := strings.Replace(
testProxySDP("first", "first-password", nil),
"a=mid:1\r\na=ice-ufrag:first\r\na=ice-pwd:first-password",
"a=mid:1\r\na=ice-ufrag:second\r\na=ice-pwd:second-password",
1,
)
var description sdp.SessionDescription
if errUnmarshal := description.UnmarshalString(mixed); errUnmarshal != nil {
t.Fatalf("unmarshal mixed SDP: %v", errUnmarshal)
}
if _, errCredentials := bundledICECredentials(&description); errCredentials == nil {
t.Fatal("expected inconsistent bundled credentials to be rejected")
}
}
func buildTestICEFrame(t *testing.T, username, password string, fingerprint bool) []byte {
t.Helper()
setters := []stun.Setter{
stun.BindingRequest,
stun.TransactionID,
stun.NewUsername(username),
stun.NewShortTermIntegrity(password),
}
if fingerprint {
setters = append(setters, stun.Fingerprint)
}
message, errBuild := stun.Build(setters...)
if errBuild != nil {
t.Fatalf("build STUN request: %v", errBuild)
}
if len(message.Raw) > int(^uint16(0)) {
t.Fatal("test STUN request is too large")
}
frame := make([]byte, 2+len(message.Raw))
binary.BigEndian.PutUint16(frame[:2], uint16(len(message.Raw)))
copy(frame[2:], message.Raw)
return frame
}
func testProxySDP(ufrag, password string, candidates []string) string {
var builder strings.Builder
_, _ = fmt.Fprintf(&builder, "v=0\r\no=- 1 1 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\n")
for _, media := range []struct {
line string
mid string
}{
{line: "m=audio 9 UDP/TLS/RTP/SAVPF 111", mid: "0"},
{line: "m=application 9 UDP/DTLS/SCTP webrtc-datachannel", mid: "1"},
} {
_, _ = fmt.Fprintf(&builder, "%s\r\nc=IN IP4 0.0.0.0\r\na=mid:%s\r\na=ice-ufrag:%s\r\na=ice-pwd:%s\r\n", media.line, media.mid, ufrag, password)
if media.mid == "0" {
for _, candidate := range candidates {
_, _ = fmt.Fprintf(&builder, "a=candidate:%s\r\n", candidate)
}
}
}
return builder.String()
}
type fragmentedReader struct {
data []byte
maximum int
}
func (r *fragmentedReader) Read(destination []byte) (int, error) {
if len(r.data) == 0 {
return 0, io.EOF
}
limit := len(destination)
if limit > r.maximum {
limit = r.maximum
}
if limit > len(r.data) {
limit = len(r.data)
}
copy(destination, r.data[:limit])
r.data = r.data[limit:]
return limit, nil
}
func rewriteTestTCPCandidateTarget(t *testing.T, rawSDP, address string, port int) string {
t.Helper()
var description sdp.SessionDescription
if errUnmarshal := description.UnmarshalString(rawSDP); errUnmarshal != nil {
t.Fatalf("unmarshal test SDP: %v", errUnmarshal)
}
rewritten := 0
for _, media := range description.MediaDescriptions {
for index := range media.Attributes {
attribute := &media.Attributes[index]
if !attribute.IsICECandidate() {
continue
}
fields := strings.Fields(attribute.Value)
if len(fields) < 8 || !strings.EqualFold(fields[2], "tcp") || !strings.Contains(attribute.Value, "tcptype passive") {
continue
}
fields[4] = address
fields[5] = strconv.Itoa(port)
attribute.Value = strings.Join(fields, " ")
rewritten++
}
}
if rewritten == 0 {
t.Fatal("test SDP has no passive TCP candidate")
}
marshaled, errMarshal := description.Marshal()
if errMarshal != nil {
t.Fatalf("marshal test SDP: %v", errMarshal)
}
return string(marshaled)
}
func bundledICECredentialsFromString(rawSDP string) (iceCredentials, error) {
var description sdp.SessionDescription
if errUnmarshal := description.UnmarshalString(rawSDP); errUnmarshal != nil {
return iceCredentials{}, errUnmarshal
}
return bundledICECredentials(&description)
}

View file

@ -0,0 +1,251 @@
package live
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/router-for-me/CLIProxyAPI/v7/internal/clienterror"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
log "github.com/sirupsen/logrus"
)
const defaultStandardRealtimeModel = "gpt-realtime"
// HandleRealtimeWebsocket dispatches a standard Realtime WebSocket or an existing call sideband.
func (h *Handler) HandleRealtimeWebsocket(c *gin.Context) {
if strings.TrimSpace(c.Query("call_id")) != "" {
h.HandleSideband(c)
return
}
h.HandleDirectWebsocket(c)
}
// HandleDirectWebsocket relays a standard Realtime WebSocket through Codex OAuth.
func (h *Handler) HandleDirectWebsocket(c *gin.Context) {
if h == nil || h.authManager == nil {
writeRealtimeError(c, http.StatusServiceUnavailable, "Codex auth manager unavailable", "server_error", "codex_auth_unavailable")
return
}
if !websocket.IsWebSocketUpgrade(c.Request) {
c.Header("Upgrade", "websocket")
writeRealtimeError(c, http.StatusUpgradeRequired, "WebSocket upgrade required", "invalid_request_error", "websocket_upgrade_required")
return
}
requestedModel := strings.TrimSpace(c.Query("model"))
if requestedModel == "" {
requestedModel = defaultStandardRealtimeModel
}
selectionModel := codexRealtimeModel(requestedModel)
tokenSession := clientSecretSession(c)
if len(tokenSession) > 0 {
tokenModel := codexRealtimeModel(modelFromJSON(tokenSession))
if selectionModel != tokenModel {
writeRealtimeError(c, http.StatusForbidden, "Realtime client secret is not valid for the requested model", "invalid_request_error", "realtime_client_secret_scope_mismatch")
return
}
}
ctx := context.WithValue(c.Request.Context(), "gin", c)
ctx = coreexecutor.WithDownstreamWebsocket(ctx)
selectionOpts := coreexecutor.Options{Headers: liveSelectionHeaders(c)}
selection, selected, errSelect := h.selectOAuth(ctx, selectionModel, selectionOpts)
if errSelect != nil {
writeSelectionError(c, errSelect)
return
}
if selected == nil {
if selection != nil {
selection.End("missing_auth")
}
writeRealtimeError(c, http.StatusServiceUnavailable, "Codex auth unavailable", "server_error", "codex_auth_unavailable")
return
}
if selection != nil {
attemptCtx, releaseAttempt, errAttempt := selection.AttemptContext(ctx)
if errAttempt != nil {
selection.End("attempt_bind_failed")
writeRealtimeError(c, http.StatusServiceUnavailable, errAttempt.Error(), "server_error", "realtime_upstream_unavailable")
return
}
ctx = attemptCtx
defer releaseAttempt()
selection.Retain()
defer selection.End("session_closed")
}
logging.SetGinCPATraceID(c, selected.EnsureIndex())
upstreamURL := h.directRealtimeURL(requestedModel)
dialUpstream := func(current *auth.Auth) (*websocket.Conn, *http.Response, error) {
request, errRequest := http.NewRequestWithContext(ctx, http.MethodGet, websocketHTTPURL(upstreamURL), nil)
if errRequest != nil {
return nil, nil, errRequest
}
request.Header = directRealtimeHeaders(c.Request.Header)
setAccountHeader(request.Header, current)
if errPrepare := h.authManager.PrepareHttpRequest(ctx, current, request); errPrepare != nil {
return nil, nil, errPrepare
}
authType, authValue := current.AccountInfo()
helpersConfig := h.currentConfig()
helps.RecordAPIWebsocketRequest(ctx, helpersConfig, helps.UpstreamRequestLog{
URL: upstreamURL,
Method: "WEBSOCKET",
Headers: headersForLogging(request.Header),
Provider: "codex",
AuthID: current.ID,
AuthLabel: current.Label,
AuthType: authType,
AuthValue: authValue,
})
dialer := newProxyAwareSidebandDialer(helpersConfig, current)
dialer.Subprotocols = websocket.Subprotocols(c.Request)
return dialer.DialContext(ctx, upstreamURL, request.Header)
}
upstream, handshakeResponse, errDial := dialUpstream(selected)
if errDial != nil && selection != nil && handshakeResponse != nil && handshakeResponse.StatusCode == http.StatusUnauthorized {
h.authManager.ReportHomeUnauthorized(ctx, selected, "codex", selectionModel)
closeHandshakeBody(handshakeResponse, "direct websocket unauthorized")
refreshed, didRefresh, errRefresh := h.authManager.RefreshHomeSelectionAfterUnauthorized(ctx, selection, selected)
if errRefresh != nil {
writeSelectionError(c, errRefresh)
return
}
if didRefresh && refreshed != nil {
selected = refreshed
logging.SetGinCPATraceID(c, selected.EnsureIndex())
upstream, handshakeResponse, errDial = dialUpstream(selected)
}
}
if errDial != nil {
status := clienterror.HTTPStatusFromErrorOr(errDial, http.StatusBadGateway)
if handshakeResponse != nil && handshakeResponse.StatusCode > 0 {
status = handshakeResponse.StatusCode
copyRealtimeHandshakeHeaders(c.Writer.Header(), handshakeResponse.Header)
}
closeHandshakeBody(handshakeResponse, "direct websocket rejected")
helpConfig := h.currentConfig()
helpDetails := "Codex Realtime WebSocket upstream unavailable"
helpType := "api_error"
if status == http.StatusNotFound || status == http.StatusNotImplemented {
helpDetails = "Direct Realtime WebSocket is not supported by the Codex OAuth upstream"
helpType = "not_supported_error"
status = http.StatusNotImplemented
}
helpCode := "realtime_websocket_upstream_unavailable"
if helpType == "not_supported_error" {
helpCode = "realtime_capability_not_supported"
} else if status == http.StatusUnauthorized {
helpType = "authentication_error"
helpCode = "realtime_upstream_unauthorized"
}
helps.RecordAPIWebsocketError(ctx, helpConfig, "dial", errDial)
writeRealtimeError(c, status, helpDetails, helpType, helpCode)
return
}
closeHandshakeBody(handshakeResponse, "direct websocket handshake")
closeUpstream := websocketCloseFunc("upstream", upstream)
defer func() { _ = closeUpstream() }()
if len(tokenSession) > 0 {
updateSession, errSession := realtimeSessionUpdate(tokenSession)
if errSession != nil {
_ = closeUpstream()
writeRealtimeError(c, http.StatusInternalServerError, "Failed to apply Realtime client secret session", "server_error", "realtime_session_failed")
return
}
update, errMarshal := json.Marshal(struct {
Type string `json:"type"`
Session json.RawMessage `json:"session"`
}{Type: "session.update", Session: updateSession})
if errMarshal != nil {
_ = closeUpstream()
writeRealtimeError(c, http.StatusInternalServerError, "Failed to apply Realtime client secret session", "server_error", "realtime_session_failed")
return
}
if errWrite := upstream.WriteMessage(websocket.TextMessage, update); errWrite != nil {
_ = closeUpstream()
writeRealtimeError(c, http.StatusBadGateway, "Failed to apply Realtime client secret session", "api_error", "realtime_upstream_unavailable")
return
}
}
if selection != nil {
if errBind := selection.Bind(closeUpstream); errBind != nil {
writeRealtimeError(c, http.StatusServiceUnavailable, errBind.Error(), "server_error", "realtime_upstream_unavailable")
return
}
}
upgradeHeaders := make(http.Header)
if subprotocol := upstream.Subprotocol(); subprotocol != "" {
upgradeHeaders.Set("Sec-WebSocket-Protocol", subprotocol)
}
downstream, errUpgrade := sidebandUpgrader.Upgrade(c.Writer, c.Request, upgradeHeaders)
if errUpgrade != nil {
_ = closeUpstream()
return
}
closeDownstream := websocketCloseFunc("downstream", downstream)
defer func() { _ = closeDownstream() }()
if selection != nil {
if errBind := selection.Bind(closeDownstream); errBind != nil {
return
}
}
if errRelay := relayWebsockets(downstream, upstream); errRelay != nil && !isNormalWebsocketClose(errRelay) {
helps.RecordAPIWebsocketError(ctx, h.currentConfig(), "relay", errRelay)
log.WithError(errRelay).Debug("codex realtime direct websocket relay closed")
}
}
func realtimeSessionUpdate(session json.RawMessage) (json.RawMessage, error) {
var update map[string]json.RawMessage
if errUnmarshal := json.Unmarshal(session, &update); errUnmarshal != nil {
return nil, errUnmarshal
}
for _, field := range []string{"model", "id", "object", "expires_at", "client_secret"} {
delete(update, field)
}
return json.Marshal(update)
}
func (h *Handler) directRealtimeURL(model string) string {
values := make(url.Values)
values.Set("model", strings.TrimSpace(model))
return strings.TrimRight(h.sidebandAPIBaseURL, "/") + "/realtime?" + values.Encode()
}
func directRealtimeHeaders(source http.Header) http.Header {
headers := protocolHeaders(source)
headers.Del("OpenAI-Alpha")
if headers.Get("Originator") == "" {
headers.Set("Originator", "Codex Desktop")
}
return headers
}
func copyRealtimeHandshakeHeaders(destination, source http.Header) {
for _, name := range []string{"Retry-After", "X-Request-Id", "OpenAI-Request-Id"} {
for _, value := range source.Values(name) {
destination.Add(name, value)
}
}
}
func closeHandshakeBody(response *http.Response, label string) {
if response == nil || response.Body == nil {
return
}
if errClose := response.Body.Close(); errClose != nil {
log.Errorf("codex realtime: close %s response body error: %v", label, errClose)
}
}

View file

@ -0,0 +1,203 @@
package live
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
func TestHandleDirectWebsocketRejectsClientSecretModelMismatch(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := NewHandler(auth.NewManager(nil, nil, nil), nil)
router := gin.New()
router.GET("/v1/realtime", func(c *gin.Context) {
c.Set(ClientSecretSessionContextKey, json.RawMessage(`{"type":"realtime","model":"gpt-live-1-codex"}`))
c.Set(ClientSecretPrincipalContextKey, "sess_123")
c.Next()
}, handler.HandleRealtimeWebsocket)
request := httptest.NewRequest(http.MethodGet, "/v1/realtime?model=another-live-model", nil)
request.Header.Set("Connection", "Upgrade")
request.Header.Set("Upgrade", "websocket")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusForbidden, recorder.Body.String())
}
}
func TestHandleDirectWebsocketAppliesClientSecretSession(t *testing.T) {
gin.SetMode(gin.TestMode)
upstreamUpdate := make(chan []byte, 1)
upstreamServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
connection, errUpgrade := upgrader.Upgrade(writer, request, nil)
if errUpgrade != nil {
return
}
defer func() { _ = connection.Close() }()
_, payload, errRead := connection.ReadMessage()
if errRead != nil {
return
}
upstreamUpdate <- append([]byte(nil), payload...)
_ = connection.WriteMessage(websocket.TextMessage, []byte(`{"type":"session.created"}`))
}))
defer upstreamServer.Close()
manager := auth.NewManager(nil, nil, nil)
manager.RegisterExecutor(&captureExecutor{})
registerCredential(t, manager, &auth.Auth{
ID: "codex-oauth",
Provider: "codex",
Status: auth.StatusActive,
Metadata: map[string]any{"access_token": "oauth-token"},
})
handler := NewHandler(manager, nil)
handler.sidebandAPIBaseURL = "ws" + strings.TrimPrefix(upstreamServer.URL, "http") + "/v1"
router := gin.New()
router.GET("/v1/realtime", func(c *gin.Context) {
c.Set(ClientSecretSessionContextKey, json.RawMessage(`{"type":"realtime","model":"gpt-live-1-codex","instructions":"help"}`))
c.Set(ClientSecretPrincipalContextKey, "sess_123")
c.Next()
}, handler.HandleRealtimeWebsocket)
downstreamServer := httptest.NewServer(router)
defer downstreamServer.Close()
wsURL := "ws" + strings.TrimPrefix(downstreamServer.URL, "http") + "/v1/realtime?model=gpt-realtime"
connection, _, errDial := websocket.DefaultDialer.Dial(wsURL, nil)
if errDial != nil {
t.Fatalf("dial downstream websocket: %v", errDial)
}
defer func() { _ = connection.Close() }()
_, _, _ = connection.ReadMessage()
select {
case update := <-upstreamUpdate:
var event struct {
Type string `json:"type"`
Session struct {
Model string `json:"model"`
Instructions string `json:"instructions"`
} `json:"session"`
}
if errUnmarshal := json.Unmarshal(update, &event); errUnmarshal != nil {
t.Fatalf("unmarshal session update: %v", errUnmarshal)
}
if event.Type != "session.update" || event.Session.Model != "" || event.Session.Instructions != "help" {
t.Fatalf("session update = %+v", event)
}
case <-time.After(time.Second):
t.Fatal("session update not captured")
}
}
func TestHandleDirectWebsocketRelaysStandardRealtimeFrames(t *testing.T) {
gin.SetMode(gin.TestMode)
upstreamRequest := make(chan *http.Request, 1)
upstreamMessage := make(chan string, 1)
upstreamServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
connection, errUpgrade := upgrader.Upgrade(writer, request, nil)
if errUpgrade != nil {
return
}
defer func() { _ = connection.Close() }()
upstreamRequest <- request.Clone(request.Context())
if errWrite := connection.WriteMessage(websocket.TextMessage, []byte(`{"type":"session.created"}`)); errWrite != nil {
return
}
messageType, payload, errRead := connection.ReadMessage()
if errRead != nil {
return
}
upstreamMessage <- string(payload)
_ = connection.WriteMessage(messageType, append([]byte("echo:"), payload...))
}))
defer upstreamServer.Close()
manager := auth.NewManager(nil, nil, nil)
manager.RegisterExecutor(&captureExecutor{})
registerCredential(t, manager, &auth.Auth{
ID: "codex-oauth",
Provider: "codex",
Status: auth.StatusActive,
Metadata: map[string]any{
"access_token": "oauth-token",
"account_id": "account-123",
},
})
handler := NewHandler(manager, nil)
handler.sidebandAPIBaseURL = "ws" + strings.TrimPrefix(upstreamServer.URL, "http") + "/v1"
router := gin.New()
router.GET("/v1/realtime", handler.HandleRealtimeWebsocket)
downstreamServer := httptest.NewServer(router)
defer downstreamServer.Close()
wsURL := "ws" + strings.TrimPrefix(downstreamServer.URL, "http") + "/v1/realtime?model=gpt-realtime"
downstreamHeaders := make(http.Header)
downstreamHeaders.Set("OpenAI-Alpha", "quicksilver=v2")
connection, _, errDial := websocket.DefaultDialer.Dial(wsURL, downstreamHeaders)
if errDial != nil {
t.Fatalf("dial downstream websocket: %v", errDial)
}
defer func() { _ = connection.Close() }()
_, created, errRead := connection.ReadMessage()
if errRead != nil {
t.Fatalf("read session.created: %v", errRead)
}
if string(created) != `{"type":"session.created"}` {
t.Fatalf("created event = %s", created)
}
const event = `{"type":"response.create"}`
if errWrite := connection.WriteMessage(websocket.TextMessage, []byte(event)); errWrite != nil {
t.Fatalf("write downstream event: %v", errWrite)
}
_, echoed, errRead := connection.ReadMessage()
if errRead != nil {
t.Fatalf("read echoed event: %v", errRead)
}
if string(echoed) != "echo:"+event {
t.Fatalf("echoed event = %s", echoed)
}
select {
case request := <-upstreamRequest:
if request.Header.Get("Authorization") != "Bearer oauth-token" {
t.Fatalf("Authorization = %q", request.Header.Get("Authorization"))
}
if request.Header.Get("Chatgpt-Account-Id") != "account-123" {
t.Fatalf("Chatgpt-Account-Id = %q", request.Header.Get("Chatgpt-Account-Id"))
}
if request.Header.Get("OpenAI-Alpha") != "" {
t.Fatalf("OpenAI-Alpha must not be forwarded, got %q", request.Header.Get("OpenAI-Alpha"))
}
query, errParse := url.ParseQuery(request.URL.RawQuery)
if errParse != nil {
t.Fatalf("parse upstream query: %v", errParse)
}
if query.Get("model") != "gpt-realtime" || query.Has("intent") {
t.Fatalf("upstream query = %v", query)
}
case <-time.After(time.Second):
t.Fatal("upstream request not captured")
}
select {
case payload := <-upstreamMessage:
if payload != event {
t.Fatalf("upstream event = %s", payload)
}
case <-time.After(time.Second):
t.Fatal("upstream event not captured")
}
}

View file

@ -0,0 +1,502 @@
// Package models builds model catalogs for official Codex clients.
package models
import (
"encoding/json"
"sort"
"strings"
"sync"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
)
type codexClientModelsPayload struct {
Models []map[string]any `json:"models"`
}
// ProvidersForModelFunc returns the providers registered for a model.
type ProvidersForModelFunc func(string) []string
var (
codexClientModelTemplatesMu sync.Mutex
codexClientModelTemplatesLoaded bool
codexClientModelTemplatesRevision uint64
codexClientModelTemplates map[string]map[string]any
codexClientDefaultTemplate map[string]any
codexClientModelTemplatesErr error
)
var codexClientAllowedReasoningLevels = map[string]struct{}{
"none": {},
"minimal": {},
"low": {},
"medium": {},
"high": {},
"xhigh": {},
"max": {},
"ultra": {},
}
// BuildResponse builds a Codex client model response from available models.
func BuildResponse(availableModels []map[string]any, providersForModel ProvidersForModelFunc, optimizeMultiAgentV2 bool) map[string]any {
return map[string]any{
"models": buildCodexClientModels(availableModels, providersForModel, optimizeMultiAgentV2),
}
}
func buildCodexClientModels(models []map[string]any, providersForModel ProvidersForModelFunc, optimizeMultiAgentV2 bool) []map[string]any {
templates, defaultTemplate, err := loadCodexClientModelTemplates()
if err != nil || defaultTemplate == nil {
return nil
}
result := make([]map[string]any, 0, len(models))
for _, model := range models {
id := strings.TrimSpace(stringModelValue(model, "id"))
if id == "" {
continue
}
if template, ok := templates[id]; ok {
entry := cloneCodexClientModelMap(template)
applyCodexClientDisplayName(entry, model)
applyCodexClientMaxContextLengthOverride(entry, model)
applyCodexClientMaxTokens(entry, model)
applyCodexClientSearchToolSupport(entry, id, true, providersForModel)
sanitizeCodexClientReasoningMetadata(entry)
applyCodexClientVisibilityOverride(entry, id)
if optimizeMultiAgentV2 {
entry["multi_agent_version"] = "v2"
}
result = append(result, entry)
continue
}
entry := cloneCodexClientModelMap(defaultTemplate)
applyCodexClientModelMetadata(entry, id, model, optimizeMultiAgentV2)
applyCodexClientMaxTokens(entry, model)
applyCodexClientSearchToolSupport(entry, id, false, providersForModel)
sanitizeCodexClientReasoningMetadata(entry)
applyCodexClientVisibilityOverride(entry, id)
result = append(result, entry)
}
applyCodexClientNonTemplatePriorities(result, templates)
sort.SliceStable(result, func(i, j int) bool {
return codexClientModelPriority(result[i]) < codexClientModelPriority(result[j])
})
return result
}
func maxCodexClientTemplatePriority(templates map[string]map[string]any) int {
maxPriority := 0
for _, template := range templates {
priority := codexClientModelPriority(template)
if priority > maxPriority {
maxPriority = priority
}
}
return maxPriority
}
func applyCodexClientNonTemplatePriorities(result []map[string]any, templates map[string]map[string]any) {
if len(result) == 0 {
return
}
basePriority := maxCodexClientTemplatePriority(templates)
type nonTemplateEntry struct {
index int
displayName string
slug string
}
pending := make([]nonTemplateEntry, 0)
for index, entry := range result {
slug := stringModelValue(entry, "slug")
if _, ok := templates[slug]; ok {
continue
}
displayName := stringModelValue(entry, "display_name")
if displayName == "" {
displayName = slug
}
pending = append(pending, nonTemplateEntry{
index: index,
displayName: displayName,
slug: slug,
})
}
sort.SliceStable(pending, func(i, j int) bool {
left := strings.ToLower(pending[i].displayName)
right := strings.ToLower(pending[j].displayName)
if left == right {
return pending[i].slug < pending[j].slug
}
return left < right
})
for rank, entry := range pending {
result[entry.index]["priority"] = basePriority + 100*(rank+1)
}
}
func loadCodexClientModelTemplates() (map[string]map[string]any, map[string]any, error) {
raw, revision := registry.GetCodexClientModelsSnapshot()
return loadCodexClientModelTemplatesSnapshot(raw, revision)
}
func loadCodexClientModelTemplatesSnapshot(raw []byte, revision uint64) (map[string]map[string]any, map[string]any, error) {
codexClientModelTemplatesMu.Lock()
defer codexClientModelTemplatesMu.Unlock()
if codexClientModelTemplatesLoaded && codexClientModelTemplatesRevision == revision {
return codexClientModelTemplates, codexClientDefaultTemplate, codexClientModelTemplatesErr
}
var payload codexClientModelsPayload
err := json.Unmarshal(raw, &payload)
var templates map[string]map[string]any
var defaultTemplate map[string]any
if err == nil {
templates = make(map[string]map[string]any, len(payload.Models))
for _, model := range payload.Models {
slug := strings.TrimSpace(stringModelValue(model, "slug"))
if slug == "" {
continue
}
templates[slug] = cloneCodexClientModelMap(model)
if slug == "gpt-5.5" {
defaultTemplate = cloneCodexClientModelMap(model)
}
}
}
codexClientModelTemplatesLoaded = true
codexClientModelTemplatesRevision = revision
codexClientModelTemplates = templates
codexClientDefaultTemplate = defaultTemplate
codexClientModelTemplatesErr = err
return codexClientModelTemplates, codexClientDefaultTemplate, codexClientModelTemplatesErr
}
func applyCodexClientDisplayName(entry map[string]any, model map[string]any) {
if displayName := stringModelValue(model, "display_name"); displayName != "" {
entry["display_name"] = displayName
}
}
func applyCodexClientMaxContextLengthOverride(entry map[string]any, model map[string]any) {
if maxContextLength := intModelValue(model, "max_context_length"); maxContextLength > 0 {
entry["context_window"] = maxContextLength
entry["max_context_window"] = maxContextLength
}
}
func applyCodexClientMaxTokens(entry map[string]any, model map[string]any) {
if maxCompletionTokens := intModelValue(model, "max_completion_tokens"); maxCompletionTokens > 0 {
entry["max_tokens"] = maxCompletionTokens
}
}
func applyCodexClientSearchToolSupport(entry map[string]any, id string, templateModel bool, providersForModel ProvidersForModelFunc) {
supportsSearch, _ := entry["supports_search_tool"].(bool)
if !supportsSearch {
return
}
if !templateModel {
entry["supports_search_tool"] = false
return
}
if providersForModel == nil {
return
}
providers := providersForModel(id)
if len(providers) == 0 {
entry["supports_search_tool"] = false
return
}
for _, provider := range providers {
if !strings.EqualFold(strings.TrimSpace(provider), "codex") {
entry["supports_search_tool"] = false
return
}
}
}
func applyCodexClientModelMetadata(entry map[string]any, id string, model map[string]any, optimizeMultiAgentV2 bool) {
info := registry.LookupModelInfo(id)
displayName := stringModelValue(model, "display_name")
description := stringModelValue(model, "description")
contextWindow := intModelValue(model, "context_length")
if info != nil {
if info.DisplayName != "" {
displayName = info.DisplayName
}
if info.Description != "" {
description = info.Description
}
if info.ContextLength > 0 {
contextWindow = info.ContextLength
}
if info.Type == registry.OpenAIImageModelType {
entry["visibility"] = "hide"
delete(entry, "input_modalities")
delete(entry, "supports_image_detail_original")
} else {
applyCodexClientInputModalitiesMetadata(entry, info.SupportedInputModalities)
}
applyCodexClientThinkingMetadata(entry, info.Thinking)
}
if maxContextWindow := intModelValue(model, "max_context_length"); maxContextWindow > 0 {
contextWindow = maxContextWindow
}
if displayName == "" {
displayName = id
}
if description == "" {
description = id
}
entry["slug"] = id
entry["display_name"] = displayName
entry["description"] = description
entry["prefer_websockets"] = false
if optimizeMultiAgentV2 {
entry["multi_agent_version"] = "v2"
}
entry["service_tiers"] = []any{}
delete(entry, "apply_patch_tool_type")
delete(entry, "upgrade")
delete(entry, "availability_nux")
if contextWindow > 0 {
entry["context_window"] = contextWindow
entry["max_context_window"] = contextWindow
}
if baseInstructions := stringModelValue(model, "base_instructions"); baseInstructions != "" {
entry["base_instructions"] = baseInstructions
}
if plans, ok := model["available_in_plans"]; ok {
entry["available_in_plans"] = cloneCodexClientModelValue(plans)
}
}
func applyCodexClientVisibilityOverride(entry map[string]any, id string) {
switch strings.TrimSpace(id) {
case "grok-imagine-image-quality", "gpt-image-1.5", "gpt-image-2", "grok-imagine-image", "grok-imagine-image-2.0", "grok-imagine-video", "grok-imagine-video-1.5", "grok-imagine-video-1.5-preview":
entry["visibility"] = "hide"
}
}
func applyCodexClientInputModalitiesMetadata(entry map[string]any, modalities []string) {
if len(modalities) == 0 {
return
}
// Codex client only accepts text/image input modalities.
codexModalities := make([]any, 0, 2)
seen := make(map[string]struct{}, 2)
supportsImage := false
for _, raw := range modalities {
switch modality := strings.ToLower(strings.TrimSpace(raw)); modality {
case "text", "image":
if _, ok := seen[modality]; ok {
continue
}
seen[modality] = struct{}{}
codexModalities = append(codexModalities, modality)
if modality == "image" {
supportsImage = true
}
}
}
if len(codexModalities) == 0 {
return
}
entry["input_modalities"] = codexModalities
if supportsImage {
entry["supports_image_detail_original"] = true
} else {
delete(entry, "supports_image_detail_original")
}
}
func applyCodexClientThinkingMetadata(entry map[string]any, thinking *registry.ThinkingSupport) {
if thinking == nil || len(thinking.Levels) == 0 {
return
}
levels := make([]any, 0, len(thinking.Levels))
defaultLevel := ""
firstLevel := ""
for _, rawLevel := range thinking.Levels {
level := normalizeCodexClientReasoningLevel(rawLevel)
if level == "" {
continue
}
if firstLevel == "" {
firstLevel = level
}
if (defaultLevel == "" && level != "none") || level == "medium" {
defaultLevel = level
}
levels = append(levels, map[string]any{
"effort": level,
"description": codexClientReasoningDescription(level),
})
}
if len(levels) == 0 {
return
}
if defaultLevel == "" {
defaultLevel = firstLevel
}
entry["supported_reasoning_levels"] = levels
entry["default_reasoning_level"] = defaultLevel
}
func sanitizeCodexClientReasoningMetadata(entry map[string]any) {
rawLevels, ok := entry["supported_reasoning_levels"].([]any)
if !ok {
return
}
levels := make([]any, 0, len(rawLevels))
allowedDefaults := make(map[string]struct{}, len(rawLevels))
for _, rawLevelEntry := range rawLevels {
levelEntry, ok := rawLevelEntry.(map[string]any)
if !ok {
continue
}
level := normalizeCodexClientReasoningLevel(stringModelValue(levelEntry, "effort"))
if level == "" {
continue
}
clonedEntry := cloneCodexClientModelMap(levelEntry)
clonedEntry["effort"] = level
levels = append(levels, clonedEntry)
allowedDefaults[level] = struct{}{}
}
if len(levels) == 0 {
delete(entry, "supported_reasoning_levels")
delete(entry, "default_reasoning_level")
return
}
defaultLevel := normalizeCodexClientReasoningLevel(stringModelValue(entry, "default_reasoning_level"))
if _, ok := allowedDefaults[defaultLevel]; !ok {
defaultLevel = stringModelValue(levels[0].(map[string]any), "effort")
}
entry["supported_reasoning_levels"] = levels
entry["default_reasoning_level"] = defaultLevel
}
func normalizeCodexClientReasoningLevel(rawLevel string) string {
level := strings.ToLower(strings.TrimSpace(rawLevel))
if _, ok := codexClientAllowedReasoningLevels[level]; !ok {
return ""
}
return level
}
func codexClientReasoningDescription(level string) string {
switch level {
case "none":
return "No reasoning"
case "minimal":
return "Fastest responses with minimal reasoning"
case "low":
return "Fast responses with lighter reasoning"
case "medium":
return "Balances speed and reasoning depth for everyday tasks"
case "high":
return "Greater reasoning depth for complex problems"
case "xhigh":
return "Extra high reasoning depth for complex problems"
case "max":
return "Maximum available reasoning depth for complex problems"
default:
return level
}
}
func codexClientModelPriority(model map[string]any) int {
if priority, ok := model["priority"].(int); ok {
return priority
}
if priority, ok := model["priority"].(float64); ok {
return int(priority)
}
return 100
}
func stringModelValue(model map[string]any, key string) string {
if model == nil {
return ""
}
value, ok := model[key]
if !ok {
return ""
}
if s, ok := value.(string); ok {
return strings.TrimSpace(s)
}
return ""
}
func intModelValue(model map[string]any, key string) int {
if model == nil {
return 0
}
switch value := model[key].(type) {
case int:
return value
case int64:
return int(value)
case float64:
return int(value)
default:
return 0
}
}
func cloneCodexClientModelMap(model map[string]any) map[string]any {
if model == nil {
return nil
}
cloned := make(map[string]any, len(model))
for key, value := range model {
cloned[key] = cloneCodexClientModelValue(value)
}
return cloned
}
func cloneCodexClientModelValue(value any) any {
switch typed := value.(type) {
case map[string]any:
return cloneCodexClientModelMap(typed)
case []any:
cloned := make([]any, len(typed))
for i, entry := range typed {
cloned[i] = cloneCodexClientModelValue(entry)
}
return cloned
case []string:
return append([]string(nil), typed...)
default:
return value
}
}

View file

@ -0,0 +1,400 @@
package models
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
)
func TestCodexClientModelsResponse_InputModalitiesFromRegistry(t *testing.T) {
modelID := "mimo-v2.5-pro-codex-test"
textOnlyModelID := "mimo-text-only-codex-test"
modelRegistry := registry.GetGlobalRegistry()
modelRegistry.RegisterClient("codex-input-modalities-test", "openai-compatibility", []*registry.ModelInfo{
{
ID: modelID,
Object: "model",
OwnedBy: "mimo",
Type: "openai-compatibility",
DisplayName: modelID,
SupportedInputModalities: []string{"text", "image"},
},
{
ID: textOnlyModelID,
Object: "model",
OwnedBy: "mimo",
Type: "openai-compatibility",
DisplayName: textOnlyModelID,
SupportedInputModalities: []string{"text"},
},
{
ID: "mimo-mixed-modalities-codex-test",
Object: "model",
OwnedBy: "mimo",
Type: "openai-compatibility",
DisplayName: "mimo-mixed-modalities-codex-test",
SupportedInputModalities: []string{"text", "image", "audio", "video", "TEXT", "IMAGE"},
},
{
ID: "compat-image-only-codex-test",
Object: "model",
OwnedBy: "mimo",
Type: registry.OpenAIImageModelType,
},
})
t.Cleanup(func() {
modelRegistry.UnregisterClient("codex-input-modalities-test")
})
openaiModels := modelRegistry.GetAvailableModels("openai")
resp := BuildResponse(openaiModels, nil, false)
models, ok := resp["models"].([]map[string]any)
if !ok {
t.Fatalf("models type = %T, want []map[string]any", resp["models"])
}
var visionEntry map[string]any
var textOnlyEntry map[string]any
var mixedEntry map[string]any
var imageEntry map[string]any
for _, entry := range models {
slug := stringModelValue(entry, "slug")
switch slug {
case modelID:
visionEntry = entry
case textOnlyModelID:
textOnlyEntry = entry
case "mimo-mixed-modalities-codex-test":
mixedEntry = entry
case "compat-image-only-codex-test":
imageEntry = entry
}
}
if visionEntry == nil {
t.Fatalf("expected codex entry for %q", modelID)
}
modalities, ok := visionEntry["input_modalities"].([]any)
if !ok || len(modalities) != 2 {
t.Fatalf("input_modalities = %#v, want [text image]", visionEntry["input_modalities"])
}
if got, _ := modalities[0].(string); got != "text" {
t.Fatalf("input_modalities[0] = %q, want text", got)
}
if got, _ := modalities[1].(string); got != "image" {
t.Fatalf("input_modalities[1] = %q, want image", got)
}
if got, ok := visionEntry["supports_image_detail_original"].(bool); !ok || !got {
t.Fatalf("supports_image_detail_original = %#v, want true", visionEntry["supports_image_detail_original"])
}
if textOnlyEntry == nil {
t.Fatalf("expected codex entry for %q", textOnlyModelID)
}
textOnlyModalities, ok := textOnlyEntry["input_modalities"].([]any)
if !ok || len(textOnlyModalities) != 1 {
t.Fatalf("text-only input_modalities = %#v, want [text]", textOnlyEntry["input_modalities"])
}
if got, _ := textOnlyModalities[0].(string); got != "text" {
t.Fatalf("text-only input_modalities[0] = %q, want text", got)
}
if _, exists := textOnlyEntry["supports_image_detail_original"]; exists {
t.Fatalf("text-only model should not expose supports_image_detail_original: %#v", textOnlyEntry["supports_image_detail_original"])
}
if mixedEntry == nil {
t.Fatal("expected codex entry for mixed-modalities model")
}
mixedModalities, ok := mixedEntry["input_modalities"].([]any)
if !ok || len(mixedModalities) != 2 {
t.Fatalf("mixed input_modalities = %#v, want [text image]", mixedEntry["input_modalities"])
}
if got, _ := mixedModalities[0].(string); got != "text" {
t.Fatalf("mixed input_modalities[0] = %q, want text", got)
}
if got, _ := mixedModalities[1].(string); got != "image" {
t.Fatalf("mixed input_modalities[1] = %q, want image", got)
}
if got, ok := mixedEntry["supports_image_detail_original"].(bool); !ok || !got {
t.Fatalf("mixed supports_image_detail_original = %#v, want true", mixedEntry["supports_image_detail_original"])
}
if imageEntry == nil {
t.Fatal("expected codex entry for image-only compat model")
}
if got, _ := imageEntry["visibility"].(string); got != "hide" {
t.Fatalf("image model visibility = %q, want hide", got)
}
if _, exists := imageEntry["input_modalities"]; exists {
t.Fatalf("image endpoint model should not expose input_modalities from registry: %#v", imageEntry["input_modalities"])
}
}
func TestCodexClientModelsResponse_AppliesDisplayNameToTemplateModel(t *testing.T) {
resp := BuildResponse([]map[string]any{{
"id": "gpt-5.5",
"display_name": "Configured Codex Name",
}}, nil, false)
models, ok := resp["models"].([]map[string]any)
if !ok || len(models) != 1 {
t.Fatalf("models = %#v, want one model", resp["models"])
}
if got := stringModelValue(models[0], "display_name"); got != "Configured Codex Name" {
t.Fatalf("display_name = %q, want Configured Codex Name", got)
}
}
func TestCodexClientModelsResponse_RewritesTemplateMultiAgentVersionWhenEnabled(t *testing.T) {
modelIDs := []string{"gpt-5.6-luna", "gpt-5.5"}
resp := BuildResponse([]map[string]any{{"id": modelIDs[0]}, {"id": modelIDs[1]}}, nil, true)
models, ok := resp["models"].([]map[string]any)
if !ok {
t.Fatalf("models type = %T, want []map[string]any", resp["models"])
}
for _, model := range models {
if got := stringModelValue(model, "multi_agent_version"); got != "v2" {
t.Errorf("%s multi_agent_version = %q, want v2", stringModelValue(model, "slug"), got)
}
}
}
func TestCodexClientModelsResponse_DisablesSearchToolForSynthesizedModels(t *testing.T) {
resp := BuildResponse([]map[string]any{
{"id": "custom-openai-compatible-model"},
{"id": "gpt-5.5"},
}, nil, false)
models, ok := resp["models"].([]map[string]any)
if !ok {
t.Fatalf("models type = %T, want []map[string]any", resp["models"])
}
bySlug := make(map[string]map[string]any, len(models))
for _, model := range models {
bySlug[stringModelValue(model, "slug")] = model
}
custom := bySlug["custom-openai-compatible-model"]
if custom == nil {
t.Fatal("expected synthesized custom model entry")
}
if got, ok := custom["supports_search_tool"].(bool); !ok || got {
t.Fatalf("custom supports_search_tool = %#v, want false", custom["supports_search_tool"])
}
official := bySlug["gpt-5.5"]
if official == nil {
t.Fatal("expected official template model entry")
}
if got, ok := official["supports_search_tool"].(bool); !ok || !got {
t.Fatalf("official supports_search_tool = %#v, want true", official["supports_search_tool"])
}
}
func TestCodexClientModelsResponse_RequiresTemplateAndCodexProvidersForSearchTool(t *testing.T) {
providers := map[string][]string{
"new-codex-model": {"codex"},
"gpt-5.5": {"openai-compatible-deepseek"},
"gpt-5.4": {"codex", "xai"},
"gpt-5.6-sol": {"codex"},
}
resp := BuildResponse([]map[string]any{
{"id": "new-codex-model"},
{"id": "gpt-5.5"},
{"id": "gpt-5.4"},
{"id": "gpt-5.6-sol"},
}, func(id string) []string {
return providers[id]
}, false)
models, ok := resp["models"].([]map[string]any)
if !ok {
t.Fatalf("models type = %T, want []map[string]any", resp["models"])
}
bySlug := make(map[string]map[string]any, len(models))
for _, model := range models {
bySlug[stringModelValue(model, "slug")] = model
}
if got, ok := bySlug["gpt-5.6-sol"]["supports_search_tool"].(bool); !ok || !got {
t.Errorf("gpt-5.6-sol supports_search_tool = %#v, want true", bySlug["gpt-5.6-sol"]["supports_search_tool"])
}
for _, slug := range []string{"new-codex-model", "gpt-5.5", "gpt-5.4"} {
if got, ok := bySlug[slug]["supports_search_tool"].(bool); !ok || got {
t.Errorf("%s supports_search_tool = %#v, want false", slug, bySlug[slug]["supports_search_tool"])
}
}
}
func TestCodexClientModelsResponse_PreservesUltraReasoningEffort(t *testing.T) {
resp := BuildResponse([]map[string]any{{"id": "gpt-5.6-sol"}}, nil, false)
models, ok := resp["models"].([]map[string]any)
if !ok {
t.Fatalf("models type = %T, want []map[string]any", resp["models"])
}
var sol map[string]any
for _, entry := range models {
if stringModelValue(entry, "slug") == "gpt-5.6-sol" {
sol = entry
break
}
}
if sol == nil {
t.Fatal("expected codex client entry for gpt-5.6-sol")
}
levels, ok := sol["supported_reasoning_levels"].([]any)
if !ok {
t.Fatalf("supported_reasoning_levels = %T, want []any", sol["supported_reasoning_levels"])
}
for _, rawLevel := range levels {
level, ok := rawLevel.(map[string]any)
if ok && stringModelValue(level, "effort") == "ultra" {
return
}
}
t.Fatalf("supported_reasoning_levels = %#v, want ultra", levels)
}
func TestLoadCodexClientModelTemplatesRefreshesOnRevision(t *testing.T) {
codexClientModelTemplatesMu.Lock()
previousLoaded := codexClientModelTemplatesLoaded
previousRevision := codexClientModelTemplatesRevision
previousTemplates := codexClientModelTemplates
previousDefault := codexClientDefaultTemplate
previousErr := codexClientModelTemplatesErr
codexClientModelTemplatesLoaded = false
codexClientModelTemplatesMu.Unlock()
t.Cleanup(func() {
codexClientModelTemplatesMu.Lock()
codexClientModelTemplatesLoaded = previousLoaded
codexClientModelTemplatesRevision = previousRevision
codexClientModelTemplates = previousTemplates
codexClientDefaultTemplate = previousDefault
codexClientModelTemplatesErr = previousErr
codexClientModelTemplatesMu.Unlock()
})
first := []byte(`{"models":[{"slug":"gpt-5.5","display_name":"First"}]}`)
templates, defaultTemplate, err := loadCodexClientModelTemplatesSnapshot(first, 100)
if err != nil {
t.Fatalf("load first snapshot: %v", err)
}
if got := stringModelValue(templates["gpt-5.5"], "display_name"); got != "First" {
t.Fatalf("first display_name = %q, want First", got)
}
if got := stringModelValue(defaultTemplate, "display_name"); got != "First" {
t.Fatalf("first default display_name = %q, want First", got)
}
second := []byte(`{"models":[{"slug":"gpt-5.5","display_name":"Second"}]}`)
templates, defaultTemplate, err = loadCodexClientModelTemplatesSnapshot(second, 101)
if err != nil {
t.Fatalf("load second snapshot: %v", err)
}
if got := stringModelValue(templates["gpt-5.5"], "display_name"); got != "Second" {
t.Fatalf("second display_name = %q, want Second", got)
}
if got := stringModelValue(defaultTemplate, "display_name"); got != "Second" {
t.Fatalf("second default display_name = %q, want Second", got)
}
templates, _, err = loadCodexClientModelTemplatesSnapshot(first, 101)
if err != nil {
t.Fatalf("reload cached revision: %v", err)
}
if got := stringModelValue(templates["gpt-5.5"], "display_name"); got != "Second" {
t.Fatalf("cached display_name = %q, want Second", got)
}
}
func TestApplyCodexClientModelMetadataPreservesMultiAgentVersionWhenDisabled(t *testing.T) {
entry := map[string]any{"multi_agent_version": "v1"}
model := map[string]any{"id": "custom-model"}
applyCodexClientModelMetadata(entry, "custom-model", model, false)
if got := entry["multi_agent_version"]; got != "v1" {
t.Fatalf("disabled multi_agent_version = %#v, want preserved v1", got)
}
applyCodexClientModelMetadata(entry, "custom-model", model, true)
if got := entry["multi_agent_version"]; got != "v2" {
t.Fatalf("enabled multi_agent_version = %#v, want v2", got)
}
}
func TestCodexClientModelsResponseAppliesMaxContextLengthOverride(t *testing.T) {
const wantOverride = 1048576
const wantDefault = 272000
resp := BuildResponse([]map[string]any{
{"id": "deepseek-v4-flash", "max_context_length": wantOverride},
{"id": "deepseek-v4-pro"},
{"id": "gpt-5.5", "max_context_length": wantOverride},
}, nil, false)
models, ok := resp["models"].([]map[string]any)
if !ok {
t.Fatalf("models type = %T, want []map[string]any", resp["models"])
}
bySlug := make(map[string]map[string]any, len(models))
for _, model := range models {
bySlug[stringModelValue(model, "slug")] = model
}
for _, testCase := range []struct {
slug string
want int
}{
{slug: "deepseek-v4-flash", want: wantOverride},
{slug: "deepseek-v4-pro", want: wantDefault},
{slug: "gpt-5.5", want: wantOverride},
} {
entry := bySlug[testCase.slug]
if entry == nil {
t.Fatalf("missing model %q", testCase.slug)
}
if got := intModelValue(entry, "context_window"); got != testCase.want {
t.Errorf("%s context_window = %d, want %d", testCase.slug, got, testCase.want)
}
if got := intModelValue(entry, "max_context_window"); got != testCase.want {
t.Errorf("%s max_context_window = %d, want %d", testCase.slug, got, testCase.want)
}
}
}
func TestCodexClientModelsResponseMapsMaxCompletionTokensToMaxTokens(t *testing.T) {
const wantTemplateLimit = 64000
const wantSynthesizedLimit = 32000
resp := BuildResponse([]map[string]any{
{"id": "gpt-5.5", "max_completion_tokens": wantTemplateLimit},
{"id": "custom-output-limit-model", "max_completion_tokens": wantSynthesizedLimit},
}, nil, false)
models, ok := resp["models"].([]map[string]any)
if !ok {
t.Fatalf("models type = %T, want []map[string]any", resp["models"])
}
bySlug := make(map[string]map[string]any, len(models))
for _, model := range models {
bySlug[stringModelValue(model, "slug")] = model
}
for _, testCase := range []struct {
slug string
want int
}{
{slug: "gpt-5.5", want: wantTemplateLimit},
{slug: "custom-output-limit-model", want: wantSynthesizedLimit},
} {
entry := bySlug[testCase.slug]
if entry == nil {
t.Fatalf("missing model %q", testCase.slug)
}
if got := intModelValue(entry, "max_tokens"); got != testCase.want {
t.Errorf("%s max_tokens = %d, want %d", testCase.slug, got, testCase.want)
}
}
}

View file

@ -0,0 +1,988 @@
package multiagentv2
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"sync"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
const (
codexSpawnAgentDescriptionMarker = "Spawns an agent"
codexSpawnAgentModelsHeading = "Available model overrides (optional; inherited parent model is preferred):"
codexCollaborationNamespace = "collaboration"
codexOptimizedCollaborationNamespace = "collaboration-optimize"
codexOptimizedCollaborationNamePrefix = codexOptimizedCollaborationNamespace + "__"
)
// CodexMultiAgentV2ToolsPreparedContextKey marks a request whose collaboration
// tool definitions were prepared at the Responses API boundary.
const CodexMultiAgentV2ToolsPreparedContextKey = "codex_multi_agent_v2_tools_prepared"
// codexCollaborationMessageTools are the collaboration tool names whose
// parameters.properties.message.encrypted field must be stripped so that
// message content remains readable by the proxy.
var codexCollaborationMessageTools = map[string]struct{}{
"spawn_agent": {},
"send_message": {},
"followup_task": {},
}
type codexSpawnAgentModel struct {
id string
description string
reasoningEfforts []string
defaultReasoningEffort string
serviceTiers []string
priority int
displayName string
}
type codexClientModelsCatalog struct {
Models []map[string]any `json:"models"`
}
// RewriteCodexSpawnAgentDescription optimizes spawn_agent definitions for
// official Codex clients when multi-agent v2 optimization is enabled.
func RewriteCodexSpawnAgentDescription(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
updated, _ := OptimizeCodexMultiAgentV2Request(ctx, headers, payload, cfg)
return updated
}
// RewriteCodexMultiAgentV2Input converts official Codex multi-agent input into
// standard Responses API messages when multi-agent v2 optimization is enabled.
func RewriteCodexMultiAgentV2Input(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
if !codexMultiAgentV2Enabled(ctx, headers, cfg) {
return payload
}
return rewriteCodexAgentMessageInput(payload)
}
// TranslateRequestWithCodexMultiAgentV2 normalizes official Codex multi-agent
// input before translating it to a non-Codex target protocol.
func TranslateRequestWithCodexMultiAgentV2(ctx context.Context, headers http.Header, cfg *config.Config, from, to sdktranslator.Format, model string, payload []byte, stream bool) []byte {
if from == sdktranslator.FormatOpenAIResponse && to != sdktranslator.FormatCodex && to != sdktranslator.FormatOpenAIResponse {
payload = RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
}
return sdktranslator.TranslateRequest(from, to, model, payload, stream)
}
// PrepareCodexMultiAgentV2Tools prepares collaboration tool definitions at the
// Responses API boundary without changing the collaboration namespace.
func PrepareCodexMultiAgentV2Tools(ctx context.Context, headers http.Header, payload []byte, enabled, homeEnabled bool) ([]byte, bool) {
if !codexMultiAgentV2ClientEnabled(ctx, headers, enabled) {
return payload, false
}
toolPaths := codexSpawnAgentToolPaths(payload)
messageToolPaths := codexCollaborationMessageToolPaths(payload)
if len(toolPaths) == 0 && len(messageToolPaths) == 0 {
return payload, true
}
if hasCodexOptimizedCollaborationConflict(payload) {
return removeCodexCollaborationMessageEncryption(payload, messageToolPaths), true
}
var models []codexSpawnAgentModel
var formattedMarkdown string
if len(toolPaths) > 0 {
models, formattedMarkdown = codexSpawnAgentModelsAndMarkdownForRequest(ctx, headers, homeEnabled)
}
updated := rewriteCodexCollaborationTools(payload, messageToolPaths, toolPaths, models, formattedMarkdown)
return updated, true
}
// OptimizeCodexMultiAgentV2Request rewrites an eligible spawn_agent request and
// reports whether the collaboration namespace was renamed for upstream use.
func OptimizeCodexMultiAgentV2Request(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) ([]byte, bool) {
if !codexMultiAgentV2Enabled(ctx, headers, cfg) {
return payload, false
}
updated := rewriteCodexAgentMessageContent(payload)
if codexMultiAgentV2ToolsPrepared(ctx) {
updated = removeCodexCollaborationMessageEncryption(updated, codexCollaborationMessageToolPaths(updated))
} else {
updated, _ = PrepareCodexMultiAgentV2Tools(ctx, headers, updated, cfg.Codex.OptimizeMultiAgentV2, cfg.Home.Enabled)
}
toolPaths := codexSpawnAgentToolPaths(updated)
if len(toolPaths) == 0 || hasCodexOptimizedCollaborationConflict(updated) {
return updated, false
}
return optimizeCodexCollaborationNamespace(updated, toolPaths)
}
func codexMultiAgentV2Enabled(ctx context.Context, headers http.Header, cfg *config.Config) bool {
return cfg != nil && codexMultiAgentV2ClientEnabled(ctx, headers, cfg.Codex.OptimizeMultiAgentV2)
}
func codexMultiAgentV2ClientEnabled(ctx context.Context, headers http.Header, enabled bool) bool {
return enabled && isCodexMultiAgentClient(codexClientUserAgent(ctx, headers))
}
func codexMultiAgentV2ToolsPrepared(ctx context.Context) bool {
if ctx == nil {
return false
}
ginCtx, ok := ctx.Value("gin").(*gin.Context)
if !ok || ginCtx == nil {
return false
}
prepared, ok := ginCtx.Get(CodexMultiAgentV2ToolsPreparedContextKey)
isPrepared, _ := prepared.(bool)
return ok && isPrepared
}
func codexClientUserAgent(ctx context.Context, headers http.Header) string {
if ctx != nil {
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
return headerValueCaseInsensitive(ginCtx.Request.Header, "User-Agent")
}
}
return headerValueCaseInsensitive(headers, "User-Agent")
}
func headerValueCaseInsensitive(headers http.Header, name string) string {
if headers == nil {
return ""
}
if value := strings.TrimSpace(headers.Get(name)); value != "" {
return value
}
for key, values := range headers {
if !strings.EqualFold(key, name) {
continue
}
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
}
return ""
}
// IsCodexClientUserAgent reports whether a request uses an official Codex client identity.
func IsCodexClientUserAgent(userAgent string) bool {
userAgent = strings.TrimSpace(userAgent)
return strings.HasPrefix(userAgent, "Codex Desktop/") ||
strings.HasPrefix(userAgent, "codex-tui/") ||
userAgent == "codex_cli_rs" ||
strings.HasPrefix(userAgent, "codex_cli_rs/")
}
func isCodexMultiAgentClient(userAgent string) bool {
return IsCodexClientUserAgent(userAgent)
}
var (
codexCatalogTemplatesMu sync.RWMutex
codexCatalogTemplatesLoaded bool
codexCatalogTemplatesRevision uint64
codexCatalogTemplates map[string]map[string]any
codexCatalogDefaultTemplate map[string]any
codexSpawnAgentCacheMu sync.RWMutex
codexSpawnAgentCacheRevision uint64
codexSpawnAgentCacheGeneration uint64
codexSpawnAgentCachedModels []codexSpawnAgentModel
codexSpawnAgentCachedMarkdown string
)
func loadCodexCatalogTemplates() (map[string]map[string]any, map[string]any, uint64, error) {
currentRevision := registry.GetCodexClientModelsRevision()
codexCatalogTemplatesMu.RLock()
if codexCatalogTemplatesLoaded && codexCatalogTemplatesRevision == currentRevision {
templates := codexCatalogTemplates
defaultTemplate := codexCatalogDefaultTemplate
codexCatalogTemplatesMu.RUnlock()
return templates, defaultTemplate, currentRevision, nil
}
codexCatalogTemplatesMu.RUnlock()
codexCatalogTemplatesMu.Lock()
defer codexCatalogTemplatesMu.Unlock()
if codexCatalogTemplatesLoaded && codexCatalogTemplatesRevision == currentRevision {
return codexCatalogTemplates, codexCatalogDefaultTemplate, currentRevision, nil
}
raw, revision := registry.GetCodexClientModelsSnapshot()
var catalog codexClientModelsCatalog
errUnmarshal := json.Unmarshal(raw, &catalog)
if errUnmarshal != nil || len(catalog.Models) == 0 {
codexCatalogTemplatesLoaded = true
codexCatalogTemplatesRevision = revision
codexCatalogTemplates = nil
codexCatalogDefaultTemplate = nil
return nil, nil, revision, errUnmarshal
}
templates := make(map[string]map[string]any, len(catalog.Models))
var defaultTemplate map[string]any
for _, model := range catalog.Models {
modelID := mapString(model, "slug")
if modelID == "" {
continue
}
templates[modelID] = model
if modelID == "gpt-5.5" {
defaultTemplate = model
}
}
codexCatalogTemplatesLoaded = true
codexCatalogTemplatesRevision = revision
codexCatalogTemplates = templates
codexCatalogDefaultTemplate = defaultTemplate
return templates, defaultTemplate, revision, nil
}
func codexSpawnAgentModelsAndMarkdownForRequest(ctx context.Context, headers http.Header, homeEnabled bool) ([]codexSpawnAgentModel, string) {
if homeEnabled {
availableModels := codexHomeAvailableModels(ctx, headers)
templates, defaultTemplate, _, errLoad := loadCodexCatalogTemplates()
if errLoad != nil || defaultTemplate == nil {
return nil, ""
}
models := codexSpawnAgentModelsFromTemplates(availableModels, templates, defaultTemplate, func(modelID string) *registry.ModelInfo {
return registry.LookupModelInfo(modelID)
})
formatted := formatCodexSpawnAgentModels(models)
return models, formatted
}
currentRevision := registry.GetCodexClientModelsRevision()
currentGeneration := registry.GetGlobalRegistry().GetGeneration()
codexSpawnAgentCacheMu.RLock()
if codexSpawnAgentCachedModels != nil && codexSpawnAgentCacheRevision == currentRevision && codexSpawnAgentCacheGeneration == currentGeneration {
models := codexSpawnAgentCachedModels
markdown := codexSpawnAgentCachedMarkdown
codexSpawnAgentCacheMu.RUnlock()
return models, markdown
}
codexSpawnAgentCacheMu.RUnlock()
templates, defaultTemplate, _, errLoad := loadCodexCatalogTemplates()
if errLoad != nil || defaultTemplate == nil {
return nil, ""
}
availableModels := registry.GetGlobalRegistry().GetAvailableModels("openai")
lookup := func(modelID string) *registry.ModelInfo {
return registry.LookupModelInfo(modelID)
}
models := codexSpawnAgentModelsFromTemplates(availableModels, templates, defaultTemplate, lookup)
formatted := formatCodexSpawnAgentModels(models)
codexSpawnAgentCacheMu.Lock()
if currentRevision == registry.GetCodexClientModelsRevision() && currentGeneration == registry.GetGlobalRegistry().GetGeneration() {
codexSpawnAgentCacheRevision = currentRevision
codexSpawnAgentCacheGeneration = currentGeneration
codexSpawnAgentCachedModels = models
codexSpawnAgentCachedMarkdown = formatted
}
codexSpawnAgentCacheMu.Unlock()
return models, formatted
}
func codexSpawnAgentModelsForRequest(ctx context.Context, headers http.Header, homeEnabled bool) []codexSpawnAgentModel {
models, _ := codexSpawnAgentModelsAndMarkdownForRequest(ctx, headers, homeEnabled)
return models
}
func formatCodexSpawnAgentModelsForRequest(ctx context.Context, headers http.Header, homeEnabled bool) string {
_, formatted := codexSpawnAgentModelsAndMarkdownForRequest(ctx, headers, homeEnabled)
return formatted
}
func codexHomeAvailableModels(ctx context.Context, headers http.Header) []map[string]any {
client := home.Current()
if client == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
requestHeaders := headers
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
requestHeaders = ginCtx.Request.Header
}
query := make(url.Values)
query.Set("client_version", "")
raw, errGet := client.GetModels(ctx, requestHeaders, query)
if errGet != nil {
return nil
}
return decodeCodexHomeAvailableModels(raw)
}
func decodeCodexHomeAvailableModels(raw []byte) []map[string]any {
var sections map[string][]map[string]any
if err := json.Unmarshal(raw, &sections); err != nil || len(sections) == 0 {
return nil
}
seen := make(map[string]struct{})
models := make([]map[string]any, 0, 256)
for _, sectionModels := range sections {
for _, model := range sectionModels {
modelID := mapString(model, "id")
if modelID == "" {
modelID = strings.TrimPrefix(mapString(model, "name"), "models/")
}
if modelID == "" {
continue
}
if _, exists := seen[modelID]; exists {
continue
}
seen[modelID] = struct{}{}
displayName := mapString(model, "display_name")
if displayName == "" {
displayName = mapString(model, "displayName")
}
entry := map[string]any{"id": modelID}
if displayName != "" {
entry["display_name"] = displayName
entry["description"] = displayName
}
models = append(models, entry)
}
}
sort.Slice(models, func(i, j int) bool {
return mapString(models[i], "id") < mapString(models[j], "id")
})
return models
}
func codexSpawnAgentModelsFromSources(availableModels []map[string]any, catalogJSON []byte, lookupModel func(string) *registry.ModelInfo) []codexSpawnAgentModel {
var catalog codexClientModelsCatalog
if err := json.Unmarshal(catalogJSON, &catalog); err != nil || len(catalog.Models) == 0 {
return nil
}
templates := make(map[string]map[string]any, len(catalog.Models))
var defaultTemplate map[string]any
for _, model := range catalog.Models {
modelID := mapString(model, "slug")
if modelID == "" {
continue
}
templates[modelID] = model
if modelID == "gpt-5.5" {
defaultTemplate = model
}
}
if defaultTemplate == nil {
return nil
}
return codexSpawnAgentModelsFromTemplates(availableModels, templates, defaultTemplate, lookupModel)
}
func codexSpawnAgentModelsFromTemplates(availableModels []map[string]any, templates map[string]map[string]any, defaultTemplate map[string]any, lookupModel func(string) *registry.ModelInfo) []codexSpawnAgentModel {
if defaultTemplate == nil {
return nil
}
seen := make(map[string]struct{}, len(availableModels))
templateModels := make([]codexSpawnAgentModel, 0, len(availableModels))
synthesizedModels := make([]codexSpawnAgentModel, 0, len(availableModels))
for _, availableModel := range availableModels {
modelID := mapString(availableModel, "id")
if modelID == "" {
continue
}
if _, exists := seen[modelID]; exists {
continue
}
seen[modelID] = struct{}{}
if template, ok := templates[modelID]; ok {
templateModels = append(templateModels, codexSpawnAgentModelFromMetadata(modelID, template))
continue
}
profile := codexSpawnAgentModelFromMetadata(modelID, defaultTemplate)
profile.id = modelID
profile.description = mapString(availableModel, "description")
profile.displayName = mapString(availableModel, "display_name")
if profile.displayName == "" {
profile.displayName = modelID
}
if lookupModel != nil {
if info := lookupModel(modelID); info != nil {
if strings.TrimSpace(info.Description) != "" {
profile.description = strings.TrimSpace(info.Description)
}
applyCodexSpawnAgentThinking(&profile, info.Thinking)
}
}
if profile.description == "" {
profile.description = modelID
}
profile.serviceTiers = nil
synthesizedModels = append(synthesizedModels, profile)
}
sort.SliceStable(templateModels, func(i, j int) bool {
if templateModels[i].priority == templateModels[j].priority {
return templateModels[i].id < templateModels[j].id
}
return templateModels[i].priority < templateModels[j].priority
})
sort.SliceStable(synthesizedModels, func(i, j int) bool {
left := strings.ToLower(synthesizedModels[i].displayName)
right := strings.ToLower(synthesizedModels[j].displayName)
if left == right {
return synthesizedModels[i].id < synthesizedModels[j].id
}
return left < right
})
return append(templateModels, synthesizedModels...)
}
func codexSpawnAgentModelFromMetadata(modelID string, metadata map[string]any) codexSpawnAgentModel {
profile := codexSpawnAgentModel{
id: modelID,
description: mapString(metadata, "description"),
displayName: mapString(metadata, "display_name"),
priority: mapInt(metadata, "priority"),
}
profile.reasoningEfforts, profile.defaultReasoningEffort = codexReasoningMetadata(metadata)
profile.serviceTiers = codexServiceTierIDs(metadata)
return profile
}
func applyCodexSpawnAgentThinking(profile *codexSpawnAgentModel, thinking *registry.ThinkingSupport) {
if profile == nil || thinking == nil || len(thinking.Levels) == 0 {
return
}
efforts := make([]string, 0, len(thinking.Levels))
defaultEffort := ""
firstEffort := ""
for _, rawEffort := range thinking.Levels {
effort := normalizeCodexReasoningEffort(rawEffort)
if effort == "" {
continue
}
if firstEffort == "" {
firstEffort = effort
}
if (defaultEffort == "" && effort != "none") || effort == "medium" {
defaultEffort = effort
}
efforts = append(efforts, effort)
}
if len(efforts) == 0 {
return
}
if defaultEffort == "" {
defaultEffort = firstEffort
}
profile.reasoningEfforts = efforts
profile.defaultReasoningEffort = defaultEffort
}
func codexReasoningMetadata(metadata map[string]any) ([]string, string) {
rawLevels, _ := metadata["supported_reasoning_levels"].([]any)
efforts := make([]string, 0, len(rawLevels))
allowed := make(map[string]struct{}, len(rawLevels))
for _, rawLevel := range rawLevels {
level, _ := rawLevel.(map[string]any)
effort := normalizeCodexReasoningEffort(mapString(level, "effort"))
if effort == "" {
continue
}
efforts = append(efforts, effort)
allowed[effort] = struct{}{}
}
if len(efforts) == 0 {
return nil, ""
}
defaultEffort := normalizeCodexReasoningEffort(mapString(metadata, "default_reasoning_level"))
if _, ok := allowed[defaultEffort]; !ok {
defaultEffort = efforts[0]
}
return efforts, defaultEffort
}
func normalizeCodexReasoningEffort(effort string) string {
effort = strings.ToLower(strings.TrimSpace(effort))
switch effort {
case "none", "low", "medium", "high", "xhigh", "max", "ultra":
return effort
default:
return ""
}
}
func codexServiceTierIDs(metadata map[string]any) []string {
rawTiers, _ := metadata["service_tiers"].([]any)
tiers := make([]string, 0, len(rawTiers))
seen := make(map[string]struct{}, len(rawTiers))
for _, rawTier := range rawTiers {
tier, _ := rawTier.(map[string]any)
tierID := mapString(tier, "id")
if tierID == "" {
continue
}
if _, exists := seen[tierID]; exists {
continue
}
seen[tierID] = struct{}{}
tiers = append(tiers, tierID)
}
return tiers
}
func mapString(values map[string]any, key string) string {
if values == nil {
return ""
}
value, _ := values[key].(string)
return strings.TrimSpace(value)
}
func mapInt(values map[string]any, key string) int {
if values == nil {
return 0
}
switch value := values[key].(type) {
case int:
return value
case int64:
return int(value)
case float64:
return int(value)
default:
return 0
}
}
func rewriteCodexSpawnAgentDescription(payload []byte, models []codexSpawnAgentModel) []byte {
return rewriteCodexSpawnAgentTools(payload, codexSpawnAgentToolPaths(payload), models)
}
func rewriteCodexSpawnAgentTools(payload []byte, toolPaths []string, models []codexSpawnAgentModel) []byte {
return rewriteCodexCollaborationTools(payload, toolPaths, toolPaths, models, "")
}
func rewriteCodexCollaborationTools(payload []byte, messageToolPaths, spawnAgentToolPaths []string, models []codexSpawnAgentModel, modelList string) []byte {
if len(messageToolPaths) == 0 && len(spawnAgentToolPaths) == 0 {
return payload
}
if modelList == "" && len(models) > 0 {
modelList = formatCodexSpawnAgentModels(models)
}
updated := payload
for _, toolPath := range spawnAgentToolPaths {
descriptionPath := toolPath + ".description"
description := gjson.GetBytes(updated, descriptionPath)
if description.Type == gjson.String && modelList != "" {
rewritten := replaceCodexSpawnAgentModels(description.String(), modelList)
if rewritten != description.String() {
var errSet error
updated, errSet = sjson.SetBytes(updated, descriptionPath, rewritten)
if errSet != nil {
return payload
}
}
}
}
for _, toolPath := range messageToolPaths {
encryptedPath := toolPath + ".parameters.properties.message.encrypted"
if gjson.GetBytes(updated, encryptedPath).Exists() {
var errDelete error
updated, errDelete = sjson.DeleteBytes(updated, encryptedPath)
if errDelete != nil {
return payload
}
}
}
return updated
}
// HasCodexMultiAgentV2NamespaceConflict reports whether the request defines
// the reserved optimized namespace, which must remain untouched.
func HasCodexMultiAgentV2NamespaceConflict(payload []byte) bool {
return hasCodexOptimizedCollaborationConflict(payload)
}
func hasCodexOptimizedCollaborationConflict(payload []byte) bool {
if codexToolsHaveOptimizedCollaborationConflict(gjson.GetBytes(payload, "tools")) {
return true
}
input := gjson.GetBytes(payload, "input")
if !input.IsArray() {
return false
}
for _, item := range input.Array() {
if strings.TrimSpace(item.Get("type").String()) == "additional_tools" && codexToolsHaveOptimizedCollaborationConflict(item.Get("tools")) {
return true
}
}
return false
}
func codexToolsHaveOptimizedCollaborationConflict(tools gjson.Result) bool {
if !tools.IsArray() {
return false
}
for _, tool := range tools.Array() {
name := strings.TrimSpace(tool.Get("name").String())
if name == codexOptimizedCollaborationNamespace || strings.HasPrefix(name, codexOptimizedCollaborationNamePrefix) {
return true
}
if strings.TrimSpace(tool.Get("type").String()) == "namespace" && codexToolsHaveOptimizedCollaborationConflict(tool.Get("tools")) {
return true
}
}
return false
}
func optimizeCodexCollaborationNamespace(payload []byte, toolPaths []string) ([]byte, bool) {
updated := payload
optimized := false
for _, toolPath := range toolPaths {
separatorIndex := strings.LastIndex(toolPath, ".tools.")
if separatorIndex < 0 {
continue
}
namespacePath := toolPath[:separatorIndex]
namespace := gjson.GetBytes(updated, namespacePath)
if strings.TrimSpace(namespace.Get("type").String()) != "namespace" || strings.TrimSpace(namespace.Get("name").String()) != codexCollaborationNamespace {
continue
}
var errSet error
updated, errSet = sjson.SetBytes(updated, namespacePath+".name", codexOptimizedCollaborationNamespace)
if errSet != nil {
return payload, false
}
optimized = true
}
return updated, optimized
}
// RestoreCodexMultiAgentV2Response restores optimized collaboration namespace
// values before an upstream response is translated and returned to the client.
func RestoreCodexMultiAgentV2Response(payload []byte, optimized bool) []byte {
if !optimized || len(payload) == 0 || !gjson.ValidBytes(payload) {
return payload
}
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.UseNumber()
var value any
if errDecode := decoder.Decode(&value); errDecode != nil {
return payload
}
if !restoreCodexCollaborationValue(value) {
return payload
}
restored, errMarshal := json.Marshal(value)
if errMarshal != nil {
return payload
}
return restored
}
func restoreCodexCollaborationValue(value any) bool {
changed := false
switch typed := value.(type) {
case []any:
for _, item := range typed {
if restoreCodexCollaborationValue(item) {
changed = true
}
}
case map[string]any:
itemType := strings.TrimSpace(mapString(typed, "type"))
isToolCall := itemType == "function_call" || itemType == "custom_tool_call"
if isToolCall {
if namespace, ok := typed["namespace"].(string); ok && namespace == codexOptimizedCollaborationNamespace {
typed["namespace"] = codexCollaborationNamespace
changed = true
}
}
if name, ok := typed["name"].(string); ok {
switch {
case name == codexOptimizedCollaborationNamespace && itemType == "namespace":
typed["name"] = codexCollaborationNamespace
changed = true
case isToolCall && strings.HasPrefix(name, codexOptimizedCollaborationNamePrefix):
typed["name"] = codexCollaborationNamespace + "__" + strings.TrimPrefix(name, codexOptimizedCollaborationNamePrefix)
changed = true
}
}
for key, child := range typed {
if key == "arguments" || key == "input" || key == "output" && (itemType == "function_call_output" || itemType == "custom_tool_call_output") {
continue
}
if restoreCodexCollaborationValue(child) {
changed = true
}
}
}
return changed
}
func rewriteCodexAgentMessageInput(payload []byte) []byte {
input := gjson.GetBytes(payload, "input")
if !input.IsArray() {
return payload
}
updated := rewriteCodexAgentMessageContent(payload)
for itemIndex, item := range input.Array() {
if strings.TrimSpace(item.Get("type").String()) != "agent_message" {
continue
}
itemPath := fmt.Sprintf("input.%d", itemIndex)
var errSet error
updated, errSet = sjson.SetBytes(updated, itemPath+".role", "user")
if errSet != nil {
return payload
}
updated, errSet = sjson.SetBytes(updated, itemPath+".type", "message")
if errSet != nil {
return payload
}
}
return updated
}
func rewriteCodexAgentMessageContent(payload []byte) []byte {
input := gjson.GetBytes(payload, "input")
if !input.IsArray() {
return payload
}
updated := payload
for itemIndex, item := range input.Array() {
if strings.TrimSpace(item.Get("type").String()) != "agent_message" {
continue
}
content := item.Get("content")
if !content.IsArray() {
continue
}
for partIndex, part := range content.Array() {
if strings.TrimSpace(part.Get("type").String()) != "encrypted_content" {
continue
}
encryptedContent := part.Get("encrypted_content")
if encryptedContent.Type != gjson.String {
continue
}
partPath := fmt.Sprintf("input.%d.content.%d", itemIndex, partIndex)
var errSet error
updated, errSet = sjson.SetBytes(updated, partPath+".type", "input_text")
if errSet != nil {
return payload
}
updated, errSet = sjson.SetBytes(updated, partPath+".text", encryptedContent.String())
if errSet != nil {
return payload
}
updated, errSet = sjson.DeleteBytes(updated, partPath+".encrypted_content")
if errSet != nil {
return payload
}
}
}
return updated
}
func codexSpawnAgentToolPaths(payload []byte) []string {
return codexToolPathsByNames(payload, map[string]struct{}{"spawn_agent": {}})
}
// codexCollaborationMessageToolPaths discovers function tools named
// spawn_agent, send_message, or followup_task inside top-level tools arrays and
// input[].additional_tools arrays, including nested namespace tools.
func codexCollaborationMessageToolPaths(payload []byte) []string {
return codexToolPathsByNames(payload, codexCollaborationMessageTools)
}
func codexToolPathsByNames(payload []byte, names map[string]struct{}) []string {
paths := make([]string, 0, len(names))
collectCodexToolPathsByNames(gjson.GetBytes(payload, "tools"), "tools", &paths, names)
input := gjson.GetBytes(payload, "input")
if input.IsArray() {
for index, item := range input.Array() {
if strings.TrimSpace(item.Get("type").String()) != "additional_tools" {
continue
}
collectCodexToolPathsByNames(item.Get("tools"), fmt.Sprintf("input.%d.tools", index), &paths, names)
}
}
return paths
}
func collectCodexToolPathsByNames(tools gjson.Result, path string, paths *[]string, names map[string]struct{}) {
if !tools.IsArray() {
return
}
for index, tool := range tools.Array() {
toolPath := fmt.Sprintf("%s.%d", path, index)
toolType := strings.TrimSpace(tool.Get("type").String())
if toolType == "function" {
if _, ok := names[strings.TrimSpace(tool.Get("name").String())]; ok {
*paths = append(*paths, toolPath)
}
}
if toolType == "namespace" {
collectCodexToolPathsByNames(tool.Get("tools"), toolPath+".tools", paths, names)
}
}
}
// removeCodexCollaborationMessageEncryption deletes the
// parameters.properties.message.encrypted field from each discovered
// collaboration message tool so the proxy can read the plaintext message.
func removeCodexCollaborationMessageEncryption(payload []byte, toolPaths []string) []byte {
updated := payload
for _, toolPath := range toolPaths {
encryptedPath := toolPath + ".parameters.properties.message.encrypted"
if !gjson.GetBytes(updated, encryptedPath).Exists() {
continue
}
var errDelete error
updated, errDelete = sjson.DeleteBytes(updated, encryptedPath)
if errDelete != nil {
return payload
}
}
return updated
}
func formatCodexSpawnAgentModels(models []codexSpawnAgentModel) string {
var modelList strings.Builder
for _, model := range models {
modelID := strings.Join(strings.Fields(model.id), " ")
if modelID == "" {
continue
}
modelList.WriteString("- ")
modelList.WriteString(markdownCode(modelID))
modelList.WriteString(": ")
hasDetails := false
if description := strings.Join(strings.Fields(model.description), " "); description != "" {
writeSentence(&modelList, description)
hasDetails = true
}
if len(model.reasoningEfforts) > 0 {
if hasDetails {
modelList.WriteByte(' ')
}
modelList.WriteString("Reasoning efforts: ")
for index, effort := range model.reasoningEfforts {
if index > 0 {
modelList.WriteString(", ")
}
modelList.WriteString(effort)
if effort == model.defaultReasoningEffort {
modelList.WriteString(" (default)")
}
}
modelList.WriteByte('.')
hasDetails = true
}
if len(model.serviceTiers) > 0 {
if hasDetails {
modelList.WriteByte(' ')
}
modelList.WriteString("Service tiers: ")
modelList.WriteString(strings.Join(model.serviceTiers, ", "))
modelList.WriteByte('.')
}
modelList.WriteByte('\n')
}
return strings.TrimSuffix(modelList.String(), "\n")
}
func markdownCode(value string) string {
if strings.Contains(value, "`") {
return "`` " + value + " ``"
}
return "`" + value + "`"
}
func writeSentence(builder *strings.Builder, value string) {
builder.WriteString(value)
if !strings.ContainsAny(value[len(value)-1:], ".!?") {
builder.WriteByte('.')
}
}
func replaceCodexSpawnAgentModels(description, modelList string) string {
if modelList == "" {
return description
}
cleaned, headingIndent := removeCodexSpawnAgentModelSections(description)
section := headingIndent + codexSpawnAgentModelsHeading + "\n" + modelList + "\n"
markerIndex := strings.Index(cleaned, codexSpawnAgentDescriptionMarker)
if markerIndex >= 0 {
markerLineStart := strings.LastIndex(cleaned[:markerIndex], "\n") + 1
return cleaned[:markerLineStart] + section + cleaned[markerLineStart:]
}
separator := ""
if cleaned != "" && !strings.HasSuffix(cleaned, "\n") {
separator = "\n\n"
}
return cleaned + separator + strings.TrimSuffix(section, "\n")
}
func removeCodexSpawnAgentModelSections(description string) (string, string) {
if !strings.Contains(description, codexSpawnAgentModelsHeading) {
return description, ""
}
lines := strings.SplitAfter(description, "\n")
var cleaned strings.Builder
headingIndent := ""
for index := 0; index < len(lines); {
line := lines[index]
trimmedLine := strings.TrimSpace(line)
if trimmedLine != codexSpawnAgentModelsHeading {
cleaned.WriteString(line)
index++
continue
}
if headingIndent == "" {
headingIndex := strings.Index(line, codexSpawnAgentModelsHeading)
if headingIndex > 0 {
headingIndent = line[:headingIndex]
}
}
index++
for index < len(lines) && strings.HasPrefix(strings.TrimSpace(lines[index]), "- ") {
index++
}
}
return cleaned.String(), headingIndent
}