vibe-proxy/backend/internal/client/codex/live/capabilities.go
2026-08-24 00:10:41 +02:00

215 lines
8.4 KiB
Go

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")
}