History

feat(videos): add video authentication binding and update handler behavior

- Introduced `videoAuthBindingStore` for managing mappings of video IDs to credentials with TTL support. - Updated video creation and retrieval handlers to bind and utilize credentials for authentication. - Enhanced response models to include upstream models and adjusted request preparation logic. - Added test coverage for video auth binding, TTL configuration, and expiration handling.

Luis Pater committed 5 months ago

Commit bbef8da1 parent f85768e4 files · +369 −26Browse files

Unified · Split

4 files
  1. config.example.yaml +4 −0
  2. internal/config/sdk_config.go +5 −0
  3. sdk/api/handlers/openai/openai_videos_handlers.go +153 −22
  4. sdk/api/handlers/openai/openai_videos_handlers_test.go +207 −4
config.example.yaml+4−0
@@ -129,6 +129,10 @@ disable-image-generation: false · 128 unmodified lines
129129 # Must start with "gpt-" (case-insensitive). If unset or invalid, defaults to "gpt-5.4-mini".
130130 # gpt-image-2-base-model: "gpt-5.4-mini"
131131
132+# How long video IDs returned by /openai/v1/videos and xAI video creation stay bound
133+# to the credential that created them. Default: 3h.
134+video-result-auth-cache-ttl: "3h"
135+
132136 # Core auth auto-refresh worker pool size (OAuth/file-based auth token refresh).
133137 # When > 0, overrides the default worker count (16).
134138 # auth-auto-refresh-workers: 16
internal/config/sdk_config.go+5−0
@@ -28,6 +28,11 @@ type SDKConfig struct { · 27 unmodified lines
2828 // default base model ("gpt-5.4-mini") is used.
2929 GPTImage2BaseModel string `yaml:"gpt-image-2-base-model,omitempty" json:"gpt-image-2-base-model,omitempty"`
3030
31+ // VideoResultAuthCacheTTL controls how long video IDs stay pinned to the credential
32+ // that created them. Accepts duration strings like "30m" or "3h".
33+ // Empty or invalid values use the default 3h.
34+ VideoResultAuthCacheTTL string `yaml:"video-result-auth-cache-ttl,omitempty" json:"video-result-auth-cache-ttl,omitempty"`
35+
3136 // EnableGeminiCLIEndpoint controls whether Gemini CLI internal endpoints (/v1internal:*) are enabled.
3237 // Default is false for safety; when false, /v1internal:* requests are rejected.
3338 EnableGeminiCLIEndpoint bool `yaml:"enable-gemini-cli-endpoint" json:"enable-gemini-cli-endpoint"`
sdk/api/handlers/openai/openai_videos_handlers.go+153−22
@@ -9,6 +9,7 @@ import ( · 8 unmodified lines
99 "net/url"
1010 "strconv"
1111 "strings"
12+ "sync"
1213 "time"
1314
1415 "github.com/gin-gonic/gin"
@@ -36,12 +37,89 @@ const ( · 21 unmodified lines
3637 maxXAIVideoReferences = 7
3738 )
3839
40+const defaultVideoAuthBindingTTL = 3 * time.Hour
41+
42+var videoAuthBindings = newVideoAuthBindingStore()
43+
3944 type xaiVideoCreateMetadata struct {
40− Model string
41− Prompt string
42− Seconds string
43− Size string
44− CreatedAt int64
45+ Model string
46+ UpstreamModel string
47+ Prompt string
48+ Seconds string
49+ Size string
50+ CreatedAt int64
51+}
52+
53+type videoAuthBinding struct {
54+ authID string
55+ expiresAt time.Time
56+}
57+
58+type videoAuthBindingStore struct {
59+ mu sync.RWMutex
60+ entries map[string]videoAuthBinding
61+}
62+
63+func newVideoAuthBindingStore() *videoAuthBindingStore {
64+ return &videoAuthBindingStore{
65+ entries: make(map[string]videoAuthBinding),
66+ }
67+}
68+
69+func (s *videoAuthBindingStore) set(videoID string, authID string, ttl time.Duration) {
70+ if s == nil {
71+ return
72+ }
73+ videoID = strings.TrimSpace(videoID)
74+ authID = strings.TrimSpace(authID)
75+ if videoID == "" || authID == "" {
76+ return
77+ }
78+ if ttl <= 0 {
79+ ttl = defaultVideoAuthBindingTTL
80+ }
81+ now := time.Now()
82+ s.mu.Lock()
83+ s.cleanupExpiredLocked(now)
84+ s.entries[videoID] = videoAuthBinding{
85+ authID: authID,
86+ expiresAt: now.Add(ttl),
87+ }
88+ s.mu.Unlock()
89+}
90+
91+func (s *videoAuthBindingStore) get(videoID string) (string, bool) {
92+ if s == nil {
93+ return "", false
94+ }
95+ videoID = strings.TrimSpace(videoID)
96+ if videoID == "" {
97+ return "", false
98+ }
99+ now := time.Now()
100+ s.mu.RLock()
101+ entry, ok := s.entries[videoID]
102+ s.mu.RUnlock()
103+ if !ok {
104+ return "", false
105+ }
106+ if now.After(entry.expiresAt) {
107+ s.mu.Lock()
108+ if current, exists := s.entries[videoID]; exists && now.After(current.expiresAt) {
109+ delete(s.entries, videoID)
110+ }
111+ s.mu.Unlock()
112+ return "", false
113+ }
114+ return entry.authID, true
115+}
116+
117+func (s *videoAuthBindingStore) cleanupExpiredLocked(now time.Time) {
118+ for videoID, entry := range s.entries {
119+ if now.After(entry.expiresAt) {
120+ delete(s.entries, videoID)
121+ }
122+ }
45123 }
46124
47125 func videosModelBase(model string) string {
@@ -111,11 +189,6 @@ func canonicalXAIVideosModel(model string) string { · 63 unmodified lines
111189 }
112190
113191 func responseVideosModel(model string) string {
114− _, baseModel := imagesModelParts(model)
115− baseModel = strings.TrimSpace(baseModel)
116− if isSoraVideosModel(baseModel) {
117− return baseModel
118− }
119192 return canonicalXAIVideosModel(model)
120193 }
121194
@@ -179,6 +252,41 @@ func firstPostForm(c *gin.Context, keys ...string) string { · 57 unmodified lines
179252 return ""
180253 }
181254
255+func (h *OpenAIAPIHandler) videoAuthBindingTTL() time.Duration {
256+ if h != nil && h.BaseAPIHandler != nil && h.Cfg != nil {
257+ raw := strings.TrimSpace(h.Cfg.VideoResultAuthCacheTTL)
258+ if raw != "" {
259+ if ttl, err := time.ParseDuration(raw); err == nil && ttl > 0 {
260+ return ttl
261+ }
262+ }
263+ }
264+ return defaultVideoAuthBindingTTL
265+}
266+
267+func videoIDFromPayload(payload []byte) string {
268+ videoID := strings.TrimSpace(gjson.GetBytes(payload, "request_id").String())
269+ if videoID == "" {
270+ videoID = strings.TrimSpace(gjson.GetBytes(payload, "id").String())
271+ }
272+ return videoID
273+}
274+
275+func (h *OpenAIAPIHandler) bindVideoAuthIDFromPayload(payload []byte, authID string) {
276+ videoID := videoIDFromPayload(payload)
277+ if videoID == "" {
278+ return
279+ }
280+ videoAuthBindings.set(videoID, authID, h.videoAuthBindingTTL())
281+}
282+
283+func (h *OpenAIAPIHandler) contextWithVideoAuthBinding(ctx context.Context, videoID string) context.Context {
284+ if authID, ok := videoAuthBindings.get(videoID); ok {
285+ return handlers.WithPinnedAuthID(ctx, authID)
286+ }
287+ return ctx
288+}
289+
182290 func buildXAIVideosCreateRequest(rawJSON []byte, model string) ([]byte, xaiVideoCreateMetadata, error) {
183291 prompt := strings.TrimSpace(gjson.GetBytes(rawJSON, "prompt").String())
184292 if prompt == "" {
@@ -232,11 +340,12 @@ func buildXAIVideosCreateRequest(rawJSON []byte, model string) ([]byte, xaiVideo · 47 unmodified lines
232340 }
233341
234342 meta := xaiVideoCreateMetadata{
235− Model: responseVideosModel(model),
236− Prompt: prompt,
237− Seconds: seconds,
238− Size: size,
239− CreatedAt: time.Now().Unix(),
343+ Model: responseVideosModel(model),
344+ UpstreamModel: videoModel,
345+ Prompt: prompt,
346+ Seconds: seconds,
347+ Size: size,
348+ CreatedAt: time.Now().Unix(),
240349 }
241350 return req, meta, nil
242351 }
@@ -398,7 +507,7 @@ func buildVideosCreateAPIResponseFromXAI(payload []byte, meta xaiVideoCreateMeta · 155 unmodified lines
398507 func buildVideosFailedAPIResponse(model string, code string, message string) []byte {
399508 model = strings.TrimSpace(model)
400509 if model == "" {
401− model = defaultOpenAIVideosModel
510+ model = defaultXAIVideosModel
402511 }
403512 code = strings.TrimSpace(code)
404513 if code == "" {
@@ -533,7 +642,7 @@ func openAIVideoStatus(status string) string { · 128 unmodified lines
533642 func (h *OpenAIAPIHandler) VideosCreate(c *gin.Context) {
534643 rawJSON, err := readVideosCreateRequest(c)
535644 if err != nil {
536− writeVideosFailedError(c, http.StatusBadRequest, defaultOpenAIVideosModel, "invalid_request_error", fmt.Sprintf("Invalid request: %v", err))
645+ writeVideosFailedError(c, http.StatusBadRequest, defaultXAIVideosModel, "invalid_request_error", fmt.Sprintf("Invalid request: %v", err))
537646 return
538647 }
539648
@@ -547,7 +656,7 @@ func (h *OpenAIAPIHandler) VideosCreate(c *gin.Context) { · 7 unmodified lines
547656
548657 xaiReq, meta, err := buildXAIVideosCreateRequest(rawJSON, videoModel)
549658 if err != nil {
550− writeVideosFailedError(c, http.StatusBadRequest, videoModel, "invalid_request_error", fmt.Sprintf("Invalid request: %v", err))
659+ writeVideosFailedError(c, http.StatusBadRequest, responseVideosModel(videoModel), "invalid_request_error", fmt.Sprintf("Invalid request: %v", err))
551660 return
552661 }
553662
@@ -586,7 +695,7 @@ func (h *OpenAIAPIHandler) handleXAIVideosNativePost(c *gin.Context) { · 32 unmodified lines
586695 return
587696 }
588697
589− h.collectXAIVideosNative(c, rawJSON, videoModel)
698+ h.collectXAIVideosNative(c, rawJSON, videoModel, true)
590699 }
591700
592701 func (h *OpenAIAPIHandler) XAIVideosRetrieve(c *gin.Context) {
@@ -606,7 +715,7 @@ func (h *OpenAIAPIHandler) XAIVideosRetrieve(c *gin.Context) { · 13 unmodified lines
606715
607716 payload := []byte(`{}`)
608717 payload, _ = sjson.SetBytes(payload, "request_id", requestID)
609− h.collectXAIVideosNative(c, payload, defaultXAIVideosModel)
718+ h.collectXAIVideosNative(c, payload, defaultXAIVideosModel, false)
610719 }
611720
612721 func (h *OpenAIAPIHandler) VideosRetrieve(c *gin.Context) {
@@ -626,6 +735,7 @@ func (h *OpenAIAPIHandler) VideosRetrieve(c *gin.Context) { · 13 unmodified lines
626735
627736 c.Header("Content-Type", "application/json")
628737 cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
738+ cliCtx = h.contextWithVideoAuthBinding(cliCtx, videoID)
629739 stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx)
630740 resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiVideosHandlerType, defaultXAIVideosModel, payload, "")
631741 stopKeepAlive()
@@ -682,6 +792,7 @@ func (h *OpenAIAPIHandler) VideosContent(c *gin.Context) { · 50 unmodified lines
682792 payload, _ = sjson.SetBytes(payload, "request_id", videoID)
683793
684794 cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
795+ cliCtx = h.contextWithVideoAuthBinding(cliCtx, videoID)
685796 stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx)
686797 resp, _, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiVideosHandlerType, defaultXAIVideosModel, payload, "")
687798 stopKeepAlive()
@@ -758,10 +869,18 @@ func copyVideoContentHeaders(dst http.Header, src http.Header) { · 70 unmodified lines
758869 }
759870 }
760871
761−func (h *OpenAIAPIHandler) collectXAIVideosNative(c *gin.Context, rawJSON []byte, model string) {
872+func (h *OpenAIAPIHandler) collectXAIVideosNative(c *gin.Context, rawJSON []byte, model string, bindCreatedVideoAuth bool) {
762873 c.Header("Content-Type", "application/json")
763874
764875 cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
876+ selectedAuthID := ""
877+ if bindCreatedVideoAuth {
878+ cliCtx = handlers.WithSelectedAuthIDCallback(cliCtx, func(authID string) {
879+ selectedAuthID = authID
880+ })
881+ } else {
882+ cliCtx = h.contextWithVideoAuthBinding(cliCtx, videoIDFromPayload(rawJSON))
883+ }
765884 stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx)
766885 resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiVideosHandlerType, model, rawJSON, "")
767886 stopKeepAlive()
@@ -775,6 +894,9 @@ func (h *OpenAIAPIHandler) collectXAIVideosNative(c *gin.Context, rawJSON []byte · 7 unmodified lines
775894 return
776895 }
777896
897+ if bindCreatedVideoAuth {
898+ h.bindVideoAuthIDFromPayload(resp, selectedAuthID)
899+ }
778900 handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
779901 _, _ = c.Writer.Write(resp)
780902 cliCancel(nil)
@@ -784,8 +906,16 @@ func (h *OpenAIAPIHandler) collectXAIVideosCreate(c *gin.Context, xaiReq []byte, · 3 unmodified lines
784906 c.Header("Content-Type", "application/json")
785907
786908 cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
909+ selectedAuthID := ""
910+ cliCtx = handlers.WithSelectedAuthIDCallback(cliCtx, func(authID string) {
911+ selectedAuthID = authID
912+ })
913+ upstreamModel := strings.TrimSpace(meta.UpstreamModel)
914+ if upstreamModel == "" {
915+ upstreamModel = meta.Model
916+ }
787917 stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx)
788− resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiVideosHandlerType, meta.Model, xaiReq, "")
918+ resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiVideosHandlerType, upstreamModel, xaiReq, "")
789919 stopKeepAlive()
790920 if errMsg != nil {
791921 h.WriteErrorResponse(c, errMsg)
@@ -805,6 +935,7 @@ func (h *OpenAIAPIHandler) collectXAIVideosCreate(c *gin.Context, xaiReq []byte, · 13 unmodified lines
805935 return
806936 }
807937
938+ h.bindVideoAuthIDFromPayload(out, selectedAuthID)
808939 handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
809940 _, _ = c.Writer.Write(out)
810941 cliCancel(nil)
sdk/api/handlers/openai/openai_videos_handlers_test.go+207−4
@@ -1,13 +1,21 @@
11 package openai
22
33 import (
4+ "context"
45 "io"
56 "net/http"
67 "net/http/httptest"
78 "strings"
9+ "sync"
810 "testing"
11+ "time"
912
1013 "github.com/gin-gonic/gin"
14+ "github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
15+ apihandlers "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
16+ coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
17+ coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
18+ sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
1119 "github.com/tidwall/gjson"
1220 )
1321
@@ -32,6 +40,113 @@ func performVideosEndpointRequest(t *testing.T, method string, endpointPath stri · 18 unmodified lines
3240 return resp
3341 }
3442
43+func performVideosRouteRequest(t *testing.T, method string, routePath string, requestPath string, contentType string, body io.Reader, handler gin.HandlerFunc) *httptest.ResponseRecorder {
44+ t.Helper()
45+
46+ gin.SetMode(gin.TestMode)
47+ router := gin.New()
48+ switch method {
49+ case http.MethodGet:
50+ router.GET(routePath, handler)
51+ default:
52+ router.POST(routePath, handler)
53+ }
54+
55+ req := httptest.NewRequest(method, requestPath, body)
56+ if contentType != "" {
57+ req.Header.Set("Content-Type", contentType)
58+ }
59+ resp := httptest.NewRecorder()
60+ router.ServeHTTP(resp, req)
61+ return resp
62+}
63+
64+type videoAuthCaptureExecutor struct {
65+ mu sync.Mutex
66+ requestID string
67+ authIDs []string
68+}
69+
70+func (e *videoAuthCaptureExecutor) Identifier() string { return "xai" }
71+
72+func (e *videoAuthCaptureExecutor) Execute(_ context.Context, auth *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (coreexecutor.Response, error) {
73+ authID := ""
74+ if auth != nil {
75+ authID = auth.ID
76+ }
77+ e.mu.Lock()
78+ e.authIDs = append(e.authIDs, authID)
79+ e.mu.Unlock()
80+
81+ requestID := strings.TrimSpace(gjson.GetBytes(req.Payload, "request_id").String())
82+ if requestID == "" {
83+ requestID = e.requestID
84+ }
85+ payload := []byte(`{"request_id":"` + requestID + `","status":"completed","progress":100,"video":{"url":"https://vidgen.x.ai/video.mp4","duration":4}}`)
86+ return coreexecutor.Response{Payload: payload}, nil
87+}
88+
89+func (e *videoAuthCaptureExecutor) ExecuteStream(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (*coreexecutor.StreamResult, error) {
90+ return nil, &coreauth.Error{Code: "not_implemented", Message: "ExecuteStream not implemented"}
91+}
92+
93+func (e *videoAuthCaptureExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
94+ return auth, nil
95+}
96+
97+func (e *videoAuthCaptureExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
98+ return coreexecutor.Response{}, &coreauth.Error{Code: "not_implemented", Message: "CountTokens not implemented"}
99+}
100+
101+func (e *videoAuthCaptureExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
102+ return nil, &coreauth.Error{Code: "not_implemented", Message: "HttpRequest not implemented"}
103+}
104+
105+func (e *videoAuthCaptureExecutor) AuthIDs() []string {
106+ e.mu.Lock()
107+ defer e.mu.Unlock()
108+ out := make([]string, len(e.authIDs))
109+ copy(out, e.authIDs)
110+ return out
111+}
112+
113+func resetVideoAuthBindingsForTest(t *testing.T) {
114+ t.Helper()
115+ previous := videoAuthBindings
116+ videoAuthBindings = newVideoAuthBindingStore()
117+ t.Cleanup(func() {
118+ videoAuthBindings = previous
119+ })
120+}
121+
122+func newVideoAuthBindingTestHandler(t *testing.T, executor *videoAuthCaptureExecutor) *OpenAIAPIHandler {
123+ t.Helper()
124+
125+ manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil)
126+ manager.RegisterExecutor(executor)
127+
128+ authIDs := []string{executor.requestID + "-auth-a", executor.requestID + "-auth-b"}
129+ for _, authID := range authIDs {
130+ auth := &coreauth.Auth{
131+ ID: authID,
132+ Provider: "xai",
133+ Status: coreauth.StatusActive,
134+ }
135+ if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
136+ t.Fatalf("manager.Register(%s): %v", authID, errRegister)
137+ }
138+ registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: defaultXAIVideosModel}})
139+ }
140+ t.Cleanup(func() {
141+ for _, authID := range authIDs {
142+ registry.GetGlobalRegistry().UnregisterClient(authID)
143+ }
144+ })
145+
146+ base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
147+ return NewOpenAIAPIHandler(base)
148+}
149+
35150 func TestVideosModelValidationAllowsXAIVideoModel(t *testing.T) {
36151 for _, model := range []string{
37152 "grok-imagine-video",
@@ -72,8 +187,8 @@ func TestBuildXAIVideosCreateRequestMapsSoraModelToXAIBackend(t *testing.T) { · 34 unmodified lines
72187 if got := gjson.GetBytes(req, "model").String(); got != defaultXAIVideosModel {
73188 t.Fatalf("upstream model = %q, want %s", got, defaultXAIVideosModel)
74189 }
75− if meta.Model != "sora-2" {
76− t.Fatalf("response model = %q, want sora-2", meta.Model)
190+ if meta.Model != defaultXAIVideosModel {
191+ t.Fatalf("response model = %q, want %s", meta.Model, defaultXAIVideosModel)
77192 }
78193 }
79194
@@ -343,8 +458,8 @@ func TestVideosCreateInvalidSizeReturnsFailedVideoResource(t *testing.T) { · 263 unmodified lines
343458 if got := gjson.GetBytes(resp.Body.Bytes(), "object").String(); got != "video" {
344459 t.Fatalf("object = %q, want video", got)
345460 }
346− if got := gjson.GetBytes(resp.Body.Bytes(), "model").String(); got != "sora-2" {
347− t.Fatalf("model = %q, want sora-2", got)
461+ if got := gjson.GetBytes(resp.Body.Bytes(), "model").String(); got != defaultXAIVideosModel {
462+ t.Fatalf("model = %q, want %s", got, defaultXAIVideosModel)
348463 }
349464 if got := gjson.GetBytes(resp.Body.Bytes(), "status").String(); got != "failed" {
350465 t.Fatalf("status = %q, want failed", got)
@@ -394,6 +509,94 @@ func TestXAIVideosNativeRejectsInvalidJSON(t *testing.T) { · 43 unmodified lines
394509 }
395510 }
396511
512+func TestVideosCreateBindsRetrieveToSelectedAuth(t *testing.T) {
513+ resetVideoAuthBindingsForTest(t)
514+ executor := &videoAuthCaptureExecutor{requestID: "video-openai-bound"}
515+ handler := newVideoAuthBindingTestHandler(t, executor)
516+
517+ createResp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", strings.NewReader(`{"model":"sora-2","prompt":"make a video"}`), handler.VideosCreate)
518+ if createResp.Code != http.StatusOK {
519+ t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String())
520+ }
521+ videoID := gjson.GetBytes(createResp.Body.Bytes(), "id").String()
522+ if videoID != executor.requestID {
523+ t.Fatalf("created video id = %q, want %q", videoID, executor.requestID)
524+ }
525+ if got := gjson.GetBytes(createResp.Body.Bytes(), "model").String(); got != defaultXAIVideosModel {
526+ t.Fatalf("created model = %q, want %s", got, defaultXAIVideosModel)
527+ }
528+
529+ retrieveResp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id", openAIVideosPath+"/"+videoID, "", nil, handler.VideosRetrieve)
530+ if retrieveResp.Code != http.StatusOK {
531+ t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String())
532+ }
533+
534+ authIDs := executor.AuthIDs()
535+ if len(authIDs) != 2 {
536+ t.Fatalf("authIDs = %v, want two calls", authIDs)
537+ }
538+ if authIDs[1] != authIDs[0] {
539+ t.Fatalf("retrieve auth = %q, want create auth %q; sequence=%v", authIDs[1], authIDs[0], authIDs)
540+ }
541+}
542+
543+func TestXAIVideosNativeCreateBindsRetrieveToSelectedAuth(t *testing.T) {
544+ resetVideoAuthBindingsForTest(t)
545+ executor := &videoAuthCaptureExecutor{requestID: "video-xai-bound"}
546+ handler := newVideoAuthBindingTestHandler(t, executor)
547+
548+ createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video","prompt":"make a video"}`), handler.XAIVideosGenerations)
549+ if createResp.Code != http.StatusOK {
550+ t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String())
551+ }
552+ videoID := gjson.GetBytes(createResp.Body.Bytes(), "request_id").String()
553+ if videoID != executor.requestID {
554+ t.Fatalf("created request_id = %q, want %q", videoID, executor.requestID)
555+ }
556+
557+ retrieveResp := performVideosRouteRequest(t, http.MethodGet, videosPath+"/:request_id", videosPath+"/"+videoID, "", nil, handler.XAIVideosRetrieve)
558+ if retrieveResp.Code != http.StatusOK {
559+ t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String())
560+ }
561+
562+ authIDs := executor.AuthIDs()
563+ if len(authIDs) != 2 {
564+ t.Fatalf("authIDs = %v, want two calls", authIDs)
565+ }
566+ if authIDs[1] != authIDs[0] {
567+ t.Fatalf("retrieve auth = %q, want create auth %q; sequence=%v", authIDs[1], authIDs[0], authIDs)
568+ }
569+}
570+
571+func TestVideoAuthBindingTTLUsesConfig(t *testing.T) {
572+ base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{VideoResultAuthCacheTTL: "45m"}, nil)
573+ handler := NewOpenAIAPIHandler(base)
574+ if got := handler.videoAuthBindingTTL(); got != 45*time.Minute {
575+ t.Fatalf("videoAuthBindingTTL() = %v, want 45m", got)
576+ }
577+
578+ base = apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{VideoResultAuthCacheTTL: "invalid"}, nil)
579+ handler = NewOpenAIAPIHandler(base)
580+ if got := handler.videoAuthBindingTTL(); got != defaultVideoAuthBindingTTL {
581+ t.Fatalf("invalid videoAuthBindingTTL() = %v, want %v", got, defaultVideoAuthBindingTTL)
582+ }
583+}
584+
585+func TestVideoAuthBindingStoreExpiresEntries(t *testing.T) {
586+ store := newVideoAuthBindingStore()
587+ store.entries["video-expired"] = videoAuthBinding{
588+ authID: "auth-expired",
589+ expiresAt: time.Now().Add(-time.Second),
590+ }
591+
592+ if authID, ok := store.get("video-expired"); ok {
593+ t.Fatalf("expired binding returned authID=%q", authID)
594+ }
595+ if _, exists := store.entries["video-expired"]; exists {
596+ t.Fatal("expired binding was not removed")
597+ }
598+}
599+
397600 func TestVideosCreateFormRequest(t *testing.T) {
398601 rawJSON, err := videosCreateRequestFromFormContext("model=grok-imagine-video&prompt=make+a+video&seconds=4&size=720x1280&input_reference%5Bimage_url%5D=https%3A%2F%2Fexample.com%2Fa.png")
399602 if err != nil {