Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
215
backend/internal/client/codex/live/capabilities.go
Normal file
215
backend/internal/client/codex/live/capabilities.go
Normal 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")
|
||||
}
|
||||
Loading…
Reference in a new issue