Skip to content

Commit 644ba74

Browse files
committed
feat(videos): implement auth binding for video requests and enhance proxy handling
- Added auth binding logic to tie video requests to specific authentication IDs. - Enhanced video content handlers to support proxy configuration based on selected auth. - Introduced helper functions for creating HTTP clients with direct or global proxy fallback. - Expanded unit tests to validate auth binding, proxy usage, and fallback behavior.
1 parent 96a8b0c commit 644ba74

2 files changed

Lines changed: 203 additions & 6 deletions

File tree

sdk/api/handlers/openai/openai_videos_handlers.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import (
1414

1515
"github.com/gin-gonic/gin"
1616
"github.com/google/uuid"
17+
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
1718
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
19+
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
1820
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
21+
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
1922
log "github.com/sirupsen/logrus"
2023
"github.com/tidwall/gjson"
2124
"github.com/tidwall/sjson"
@@ -738,7 +741,11 @@ func (h *OpenAIAPIHandler) VideosRetrieve(c *gin.Context) {
738741

739742
c.Header("Content-Type", "application/json")
740743
cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
744+
selectedAuthID := ""
741745
cliCtx = h.contextWithVideoAuthBinding(cliCtx, videoID)
746+
cliCtx = handlers.WithSelectedAuthIDCallback(cliCtx, func(authID string) {
747+
selectedAuthID = authID
748+
})
742749
stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx)
743750
resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiVideosHandlerType, defaultXAIVideosModel, payload, "")
744751
stopKeepAlive()
@@ -760,6 +767,7 @@ func (h *OpenAIAPIHandler) VideosRetrieve(c *gin.Context) {
760767
return
761768
}
762769

770+
videoAuthBindings.set(videoID, selectedAuthID, h.videoAuthBindingTTL())
763771
handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
764772
_, _ = c.Writer.Write(out)
765773
cliCancel(nil)
@@ -795,7 +803,11 @@ func (h *OpenAIAPIHandler) VideosContent(c *gin.Context) {
795803
payload, _ = sjson.SetBytes(payload, "request_id", videoID)
796804

797805
cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
806+
selectedAuthID := ""
798807
cliCtx = h.contextWithVideoAuthBinding(cliCtx, videoID)
808+
cliCtx = handlers.WithSelectedAuthIDCallback(cliCtx, func(authID string) {
809+
selectedAuthID = authID
810+
})
799811
stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx)
800812
resp, _, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiVideosHandlerType, defaultXAIVideosModel, payload, "")
801813
stopKeepAlive()
@@ -809,6 +821,7 @@ func (h *OpenAIAPIHandler) VideosContent(c *gin.Context) {
809821
return
810822
}
811823

824+
videoAuthBindings.set(videoID, selectedAuthID, h.videoAuthBindingTTL())
812825
contentURL, err := xaiVideoContentURLFromPayload(resp)
813826
if err != nil {
814827
errMsg := &interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: err}
@@ -832,7 +845,8 @@ func (h *OpenAIAPIHandler) writeVideoContentFromURL(c *gin.Context, contentURL s
832845
return err
833846
}
834847

835-
resp, err := http.DefaultClient.Do(req)
848+
httpClient := h.videoContentHTTPClient(c)
849+
resp, err := httpClient.Do(req)
836850
if err != nil {
837851
errMsg := &interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: err}
838852
h.WriteErrorResponse(c, errMsg)
@@ -864,6 +878,37 @@ func (h *OpenAIAPIHandler) writeVideoContentFromURL(c *gin.Context, contentURL s
864878
return err
865879
}
866880

881+
func (h *OpenAIAPIHandler) videoContentHTTPClient(c *gin.Context) *http.Client {
882+
ctx := context.Background()
883+
if c != nil && c.Request != nil {
884+
ctx = c.Request.Context()
885+
}
886+
var cfg *config.Config
887+
if h != nil && h.BaseAPIHandler != nil && h.Cfg != nil {
888+
cfg = &config.Config{SDKConfig: *h.Cfg}
889+
}
890+
return helps.NewProxyAwareHTTPClient(ctx, cfg, h.videoContentDownloadAuth(c), 0)
891+
}
892+
893+
func (h *OpenAIAPIHandler) videoContentDownloadAuth(c *gin.Context) *coreauth.Auth {
894+
if h == nil || h.BaseAPIHandler == nil || h.AuthManager == nil || c == nil {
895+
return nil
896+
}
897+
videoID := strings.TrimSpace(c.Param("video_id"))
898+
if videoID == "" {
899+
return nil
900+
}
901+
authID, ok := videoAuthBindings.get(videoID)
902+
if !ok {
903+
return nil
904+
}
905+
auth, ok := h.AuthManager.GetByID(authID)
906+
if !ok {
907+
return nil
908+
}
909+
return auth
910+
}
911+
867912
func copyVideoContentHeaders(dst http.Header, src http.Header) {
868913
for _, key := range []string{"Content-Type", "Content-Length", "Content-Disposition", "Cache-Control", "ETag", "Last-Modified"} {
869914
if value := src.Get(key); value != "" {

sdk/api/handlers/openai/openai_videos_handlers_test.go

Lines changed: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"net/http"
77
"net/http/httptest"
8+
"strconv"
89
"strings"
910
"sync"
1011
"testing"
@@ -62,9 +63,10 @@ func performVideosRouteRequest(t *testing.T, method string, routePath string, re
6263
}
6364

6465
type videoAuthCaptureExecutor struct {
65-
mu sync.Mutex
66-
requestID string
67-
authIDs []string
66+
mu sync.Mutex
67+
requestID string
68+
contentURL string
69+
authIDs []string
6870
}
6971

7072
func (e *videoAuthCaptureExecutor) Identifier() string { return "xai" }
@@ -82,7 +84,11 @@ func (e *videoAuthCaptureExecutor) Execute(_ context.Context, auth *coreauth.Aut
8284
if requestID == "" {
8385
requestID = e.requestID
8486
}
85-
payload := []byte(`{"request_id":"` + requestID + `","status":"completed","progress":100,"video":{"url":"https://vidgen.x.ai/video.mp4","duration":4}}`)
87+
contentURL := strings.TrimSpace(e.contentURL)
88+
if contentURL == "" {
89+
contentURL = "https://vidgen.x.ai/video.mp4"
90+
}
91+
payload := []byte(`{"request_id":` + strconv.Quote(requestID) + `,"status":"completed","progress":100,"video":{"url":` + strconv.Quote(contentURL) + `,"duration":4}}`)
8692
return coreexecutor.Response{Payload: payload}, nil
8793
}
8894

@@ -394,7 +400,8 @@ func TestWriteVideoContentFromURL(t *testing.T) {
394400
ctx, _ := gin.CreateTestContext(resp)
395401
ctx.Request = httptest.NewRequest(http.MethodGet, "/openai/v1/videos/video_123/content", nil)
396402

397-
handler := &OpenAIAPIHandler{}
403+
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, nil)
404+
handler := NewOpenAIAPIHandler(base)
398405
if err := handler.writeVideoContentFromURL(ctx, upstream.URL+"/video.mp4"); err != nil {
399406
t.Fatalf("writeVideoContentFromURL() error = %v", err)
400407
}
@@ -413,6 +420,151 @@ func TestWriteVideoContentFromURL(t *testing.T) {
413420
}
414421
}
415422

423+
func TestWriteVideoContentFromURLUsesPinnedAuthProxy(t *testing.T) {
424+
resetVideoAuthBindingsForTest(t)
425+
426+
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
427+
w.Header().Set("Content-Type", "video/mp4")
428+
_, _ = w.Write([]byte("video-bytes"))
429+
}))
430+
defer upstream.Close()
431+
432+
manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil)
433+
authID := "video-content-auth"
434+
auth := &coreauth.Auth{
435+
ID: authID,
436+
Provider: "xai",
437+
Status: coreauth.StatusActive,
438+
ProxyURL: "direct",
439+
}
440+
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
441+
t.Fatalf("manager.Register() error = %v", errRegister)
442+
}
443+
444+
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}, manager)
445+
handler := NewOpenAIAPIHandler(base)
446+
videoAuthBindings.set("video_123", authID, time.Hour)
447+
448+
gin.SetMode(gin.TestMode)
449+
resp := httptest.NewRecorder()
450+
ctx, _ := gin.CreateTestContext(resp)
451+
ctx.Params = gin.Params{{Key: "video_id", Value: "video_123"}}
452+
ctx.Request = httptest.NewRequest(http.MethodGet, "/openai/v1/videos/video_123/content", nil)
453+
454+
if err := handler.writeVideoContentFromURL(ctx, upstream.URL+"/video.mp4"); err != nil {
455+
t.Fatalf("writeVideoContentFromURL() error = %v", err)
456+
}
457+
458+
client := handler.videoContentHTTPClient(ctx)
459+
transport, ok := client.Transport.(*http.Transport)
460+
if !ok {
461+
t.Fatalf("transport type = %T, want *http.Transport", client.Transport)
462+
}
463+
if transport.Proxy != nil {
464+
t.Fatal("expected pinned auth direct proxy to bypass global proxy")
465+
}
466+
if resp.Code != http.StatusOK {
467+
t.Fatalf("status = %d, want %d body=%s", resp.Code, http.StatusOK, resp.Body.String())
468+
}
469+
}
470+
471+
func TestWriteVideoContentFromURLFallsBackToGlobalProxy(t *testing.T) {
472+
resetVideoAuthBindingsForTest(t)
473+
474+
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}, nil)
475+
handler := NewOpenAIAPIHandler(base)
476+
477+
gin.SetMode(gin.TestMode)
478+
resp := httptest.NewRecorder()
479+
ctx, _ := gin.CreateTestContext(resp)
480+
ctx.Params = gin.Params{{Key: "video_id", Value: "video_456"}}
481+
ctx.Request = httptest.NewRequest(http.MethodGet, "/openai/v1/videos/video_456/content", nil)
482+
483+
client := handler.videoContentHTTPClient(ctx)
484+
transport, ok := client.Transport.(*http.Transport)
485+
if !ok {
486+
t.Fatalf("transport type = %T, want *http.Transport", client.Transport)
487+
}
488+
489+
req, errRequest := http.NewRequest(http.MethodGet, "https://example.com/video.mp4", nil)
490+
if errRequest != nil {
491+
t.Fatalf("http.NewRequest() error = %v", errRequest)
492+
}
493+
proxyURL, errProxy := transport.Proxy(req)
494+
if errProxy != nil {
495+
t.Fatalf("transport.Proxy() error = %v", errProxy)
496+
}
497+
if proxyURL == nil || proxyURL.String() != "http://global-proxy.example.com:8080" {
498+
t.Fatalf("proxy URL = %v, want http://global-proxy.example.com:8080", proxyURL)
499+
}
500+
}
501+
502+
func TestVideosContentUsesSelectedAuthProxyForDownload(t *testing.T) {
503+
resetVideoAuthBindingsForTest(t)
504+
505+
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
506+
w.Header().Set("Content-Type", "video/mp4")
507+
_, _ = w.Write([]byte("video-bytes"))
508+
}))
509+
defer upstream.Close()
510+
511+
var proxyMu sync.Mutex
512+
proxyHits := 0
513+
globalProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
514+
proxyMu.Lock()
515+
proxyHits++
516+
proxyMu.Unlock()
517+
http.Error(w, "unexpected proxy", http.StatusBadGateway)
518+
}))
519+
defer globalProxy.Close()
520+
521+
videoID := "video-content-selected"
522+
authID := "video-content-selected-auth"
523+
executor := &videoAuthCaptureExecutor{
524+
requestID: videoID,
525+
contentURL: upstream.URL + "/video.mp4",
526+
}
527+
manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil)
528+
manager.RegisterExecutor(executor)
529+
auth := &coreauth.Auth{
530+
ID: authID,
531+
Provider: "xai",
532+
Status: coreauth.StatusActive,
533+
ProxyURL: "direct",
534+
}
535+
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
536+
t.Fatalf("manager.Register() error = %v", errRegister)
537+
}
538+
registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: defaultXAIVideosModel}})
539+
t.Cleanup(func() {
540+
registry.GetGlobalRegistry().UnregisterClient(authID)
541+
})
542+
543+
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{ProxyURL: globalProxy.URL}, manager)
544+
handler := NewOpenAIAPIHandler(base)
545+
546+
resp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id/content", openAIVideosPath+"/"+videoID+"/content", "", nil, handler.VideosContent)
547+
if resp.Code != http.StatusOK {
548+
t.Fatalf("content status = %d, want %d: %s", resp.Code, http.StatusOK, resp.Body.String())
549+
}
550+
if got := resp.Body.String(); got != "video-bytes" {
551+
t.Fatalf("content body = %q, want video-bytes", got)
552+
}
553+
authIDs := executor.AuthIDs()
554+
if len(authIDs) != 1 || authIDs[0] != authID {
555+
t.Fatalf("authIDs = %v, want [%s]", authIDs, authID)
556+
}
557+
if boundAuthID, ok := videoAuthBindings.get(videoID); !ok || boundAuthID != authID {
558+
t.Fatalf("bound auth = %q ok=%v, want %s", boundAuthID, ok, authID)
559+
}
560+
proxyMu.Lock()
561+
gotProxyHits := proxyHits
562+
proxyMu.Unlock()
563+
if gotProxyHits != 0 {
564+
t.Fatalf("global proxy hits = %d, want 0", gotProxyHits)
565+
}
566+
}
567+
416568
func TestVideosCreateRejectsUnsupportedModel(t *testing.T) {
417569
handler := &OpenAIAPIHandler{}
418570
body := strings.NewReader(`{"model":"not-a-video-model","prompt":"make a video"}`)

0 commit comments

Comments
 (0)