vibe-proxy/backend/sdk/api/handlers/openai/openai_videos_handlers_test.go
2026-08-24 00:10:41 +02:00

1020 lines
40 KiB
Go

package openai
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
apihandlers "github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
"github.com/tidwall/gjson"
)
func performVideosEndpointRequest(t *testing.T, method string, endpointPath string, contentType string, body io.Reader, handler gin.HandlerFunc) *httptest.ResponseRecorder {
t.Helper()
gin.SetMode(gin.TestMode)
router := gin.New()
switch method {
case http.MethodGet:
router.GET(endpointPath, handler)
default:
router.POST(endpointPath, handler)
}
req := httptest.NewRequest(method, endpointPath, body)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
return resp
}
func performVideosRouteRequest(t *testing.T, method string, routePath string, requestPath string, contentType string, body io.Reader, handler gin.HandlerFunc) *httptest.ResponseRecorder {
t.Helper()
gin.SetMode(gin.TestMode)
router := gin.New()
switch method {
case http.MethodGet:
router.GET(routePath, handler)
default:
router.POST(routePath, handler)
}
req := httptest.NewRequest(method, requestPath, body)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
return resp
}
type videoAuthCaptureExecutor struct {
mu sync.Mutex
requestID string
contentURL string
authIDs []string
models []string
payloadModels []string
}
func (e *videoAuthCaptureExecutor) Identifier() string { return "xai" }
func (e *videoAuthCaptureExecutor) Execute(_ context.Context, auth *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (coreexecutor.Response, error) {
authID := ""
if auth != nil {
authID = auth.ID
}
e.mu.Lock()
e.authIDs = append(e.authIDs, authID)
e.models = append(e.models, req.Model)
e.payloadModels = append(e.payloadModels, strings.TrimSpace(gjson.GetBytes(req.Payload, "model").String()))
e.mu.Unlock()
requestID := strings.TrimSpace(gjson.GetBytes(req.Payload, "request_id").String())
if requestID == "" {
requestID = e.requestID
}
contentURL := strings.TrimSpace(e.contentURL)
if contentURL == "" {
contentURL = "https://vidgen.x.ai/video.mp4"
}
payload := []byte(`{"request_id":` + strconv.Quote(requestID) + `,"status":"completed","progress":100,"video":{"url":` + strconv.Quote(contentURL) + `,"duration":4}}`)
return coreexecutor.Response{Payload: payload}, nil
}
func (e *videoAuthCaptureExecutor) ExecuteStream(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (*coreexecutor.StreamResult, error) {
return nil, &coreauth.Error{Code: "not_implemented", Message: "ExecuteStream not implemented"}
}
func (e *videoAuthCaptureExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *videoAuthCaptureExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, &coreauth.Error{Code: "not_implemented", Message: "CountTokens not implemented"}
}
func (e *videoAuthCaptureExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, &coreauth.Error{Code: "not_implemented", Message: "HttpRequest not implemented"}
}
func (e *videoAuthCaptureExecutor) AuthIDs() []string {
e.mu.Lock()
defer e.mu.Unlock()
out := make([]string, len(e.authIDs))
copy(out, e.authIDs)
return out
}
func (e *videoAuthCaptureExecutor) Models() []string {
e.mu.Lock()
defer e.mu.Unlock()
out := make([]string, len(e.models))
copy(out, e.models)
return out
}
func (e *videoAuthCaptureExecutor) PayloadModels() []string {
e.mu.Lock()
defer e.mu.Unlock()
out := make([]string, len(e.payloadModels))
copy(out, e.payloadModels)
return out
}
func resetVideoAuthBindingsForTest(t *testing.T) {
t.Helper()
previous := videoAuthBindings
videoAuthBindings = newVideoAuthBindingStore()
t.Cleanup(func() {
videoAuthBindings = previous
})
}
func newVideoAuthBindingTestHandler(t *testing.T, executor *videoAuthCaptureExecutor) *OpenAIAPIHandler {
t.Helper()
manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil)
manager.RegisterExecutor(executor)
authIDs := []string{executor.requestID + "-auth-a", executor.requestID + "-auth-b"}
for _, authID := range authIDs {
auth := &coreauth.Auth{
ID: authID,
Provider: "xai",
Status: coreauth.StatusActive,
}
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
t.Fatalf("manager.Register(%s): %v", authID, errRegister)
}
registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: defaultXAIVideosModel}})
manager.RefreshSchedulerEntry(authID)
}
t.Cleanup(func() {
for _, authID := range authIDs {
registry.GetGlobalRegistry().UnregisterClient(authID)
}
})
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
return NewOpenAIAPIHandler(base)
}
func TestVideosModelValidationAllowsXAIVideoModel(t *testing.T) {
for _, model := range []string{
"grok-imagine-video",
"xai/grok-imagine-video",
"x-ai/grok-imagine-video",
"grok/grok-imagine-video",
"grok-imagine-video-1.5",
"xai/grok-imagine-video-1.5",
"x-ai/grok-imagine-video-1.5",
"grok/grok-imagine-video-1.5",
"grok-imagine-video-1.5-preview",
"xai/grok-imagine-video-1.5-preview",
"x-ai/grok-imagine-video-1.5-preview",
"grok/grok-imagine-video-1.5-preview",
} {
if !isSupportedVideosModel(model) {
t.Fatalf("expected %s to be supported", model)
}
}
if !isSupportedVideosModel("sora-2") {
t.Fatal("expected sora-2 to be supported by the OpenAI video wrapper")
}
if isXAIVideosModel("sora-2") {
t.Fatal("expected sora-2 not to be treated as a native xAI video model")
}
if isSupportedVideosModel("codex/grok-imagine-video") {
t.Fatal("expected codex/grok-imagine-video to be rejected")
}
if isSupportedVideosModel("codex/grok-imagine-video-1.5") {
t.Fatal("expected codex/grok-imagine-video-1.5 to be rejected")
}
if isSupportedVideosModel("codex/grok-imagine-video-1.5-preview") {
t.Fatal("expected codex/grok-imagine-video-1.5-preview to be rejected")
}
}
func TestBuildXAIVideosCreateRequestMapsSoraModelToXAIBackend(t *testing.T) {
rawJSON := []byte(`{"model":"sora-2","prompt":"a cat playing piano","seconds":"8"}`)
req, meta, err := buildXAIVideosCreateRequest(rawJSON, "sora-2")
if err != nil {
t.Fatalf("buildXAIVideosCreateRequest() error = %v", err)
}
if got := gjson.GetBytes(req, "model").String(); got != defaultXAIVideosModel {
t.Fatalf("upstream model = %q, want %s", got, defaultXAIVideosModel)
}
if meta.Model != defaultXAIVideosModel {
t.Fatalf("response model = %q, want %s", meta.Model, defaultXAIVideosModel)
}
}
func TestBuildXAIVideosCreateRequest(t *testing.T) {
rawJSON := []byte(`{"model":"xai/grok-imagine-video","prompt":"a cat playing piano","seconds":"8","size":"1280x720","input_reference":{"image_url":"https://example.com/cat.png"}}`)
req, meta, err := buildXAIVideosCreateRequest(rawJSON, "xai/grok-imagine-video")
if err != nil {
t.Fatalf("buildXAIVideosCreateRequest() error = %v", err)
}
if got := gjson.GetBytes(req, "model").String(); got != defaultXAIVideosModel {
t.Fatalf("model = %q, want %s", got, defaultXAIVideosModel)
}
if got := gjson.GetBytes(req, "prompt").String(); got != "a cat playing piano" {
t.Fatalf("prompt = %q", got)
}
if got := gjson.GetBytes(req, "duration").Int(); got != 8 {
t.Fatalf("duration = %d, want 8", got)
}
if got := gjson.GetBytes(req, "aspect_ratio").String(); got != "16:9" {
t.Fatalf("aspect_ratio = %q, want 16:9", got)
}
if got := gjson.GetBytes(req, "resolution").String(); got != "720p" {
t.Fatalf("resolution = %q, want 720p", got)
}
if got := gjson.GetBytes(req, "image.url").String(); got != "https://example.com/cat.png" {
t.Fatalf("image.url = %q", got)
}
if meta.Seconds != "8" || meta.Size != "1280x720" || meta.Prompt != "a cat playing piano" {
t.Fatalf("unexpected meta: %+v", meta)
}
}
func TestBuildXAIVideosCreateRequestAllowsVideo15Model(t *testing.T) {
rawJSON := []byte(`{"model":"xai/grok-imagine-video-1.5","prompt":"a cat playing piano","seconds":"8"}`)
req, meta, err := buildXAIVideosCreateRequest(rawJSON, "xai/grok-imagine-video-1.5")
if err != nil {
t.Fatalf("buildXAIVideosCreateRequest() error = %v", err)
}
if got := gjson.GetBytes(req, "model").String(); got != xaiVideos15Model {
t.Fatalf("model = %q, want %s", got, xaiVideos15Model)
}
if meta.Model != xaiVideos15Model {
t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15Model)
}
if meta.RoutingModel != xaiVideos15Model {
t.Fatalf("routing model = %q, want %s", meta.RoutingModel, xaiVideos15Model)
}
}
func TestBuildXAIVideosCreateRequestNormalizesVideo15PreviewAlias(t *testing.T) {
rawJSON := []byte(`{"model":"xai/grok-imagine-video-1.5-preview","prompt":"a cat playing piano","seconds":"8"}`)
req, meta, err := buildXAIVideosCreateRequest(rawJSON, "xai/grok-imagine-video-1.5-preview")
if err != nil {
t.Fatalf("buildXAIVideosCreateRequest() error = %v", err)
}
if got := gjson.GetBytes(req, "model").String(); got != xaiVideos15Model {
t.Fatalf("model = %q, want %s", got, xaiVideos15Model)
}
if meta.Model != xaiVideos15Model {
t.Fatalf("meta model = %q, want %s", meta.Model, xaiVideos15Model)
}
if meta.RoutingModel != xaiVideos15PreviewAlias {
t.Fatalf("routing model = %q, want %s", meta.RoutingModel, xaiVideos15PreviewAlias)
}
}
func TestBuildXAIVideosCreateRequestAllowsCustomSeconds(t *testing.T) {
rawJSON := []byte(`{"model":"grok-imagine-video","prompt":"a cat playing piano","seconds":"6"}`)
req, meta, err := buildXAIVideosCreateRequest(rawJSON, "grok-imagine-video")
if err != nil {
t.Fatalf("buildXAIVideosCreateRequest() error = %v", err)
}
if got := gjson.GetBytes(req, "duration").Int(); got != 6 {
t.Fatalf("duration = %d, want 6", got)
}
if meta.Seconds != "6" {
t.Fatalf("meta seconds = %q, want 6", meta.Seconds)
}
}
func TestBuildXAIVideosCreateRequestRejectsFileIDReference(t *testing.T) {
rawJSON := []byte(`{"prompt":"animate","input_reference":{"file_id":"file_123"}}`)
_, _, err := buildXAIVideosCreateRequest(rawJSON, defaultXAIVideosModel)
if err == nil || !strings.Contains(err.Error(), "input_reference.file_id is not supported") {
t.Fatalf("error = %v, want unsupported file_id error", err)
}
}
func TestBuildVideosCreateAPIResponseFromXAI(t *testing.T) {
meta := xaiVideoCreateMetadata{
Model: defaultXAIVideosModel,
Prompt: "animate",
Seconds: "4",
Size: "720x1280",
CreatedAt: 123,
}
out, err := buildVideosCreateAPIResponseFromXAI([]byte(`{"request_id":"vid_123"}`), meta)
if err != nil {
t.Fatalf("buildVideosCreateAPIResponseFromXAI() error = %v", err)
}
if got := gjson.GetBytes(out, "id").String(); got != "vid_123" {
t.Fatalf("id = %q, want vid_123", got)
}
if got := gjson.GetBytes(out, "object").String(); got != "video" {
t.Fatalf("object = %q, want video", got)
}
if got := gjson.GetBytes(out, "status").String(); got != "queued" {
t.Fatalf("status = %q, want queued", got)
}
if got := gjson.GetBytes(out, "created_at").Int(); got != 123 {
t.Fatalf("created_at = %d, want 123", got)
}
}
func TestBuildVideosRetrieveAPIResponseFromXAI(t *testing.T) {
payload := []byte(`{"object":"video","id":"91989464-273f-95df-8197-703b4fefd40e","model":"grok-imagine-video","status":"completed","progress":100,"seconds":"4","video":{"url":"https://vidgen.x.ai/xai-vidgen-bucket/xai-video-08609066-e7e9-43ba-bd8d-bd29cb6221d9.mp4","duration":4,"respect_moderation":true},"usage":{"cost_in_usd_ticks":2800000000}}`)
out, err := buildVideosRetrieveAPIResponseFromXAI("91989464-273f-95df-8197-703b4fefd40e", payload, defaultOpenAIVideosModel)
if err != nil {
t.Fatalf("buildVideosRetrieveAPIResponseFromXAI() error = %v", err)
}
if got := gjson.GetBytes(out, "id").String(); got != "91989464-273f-95df-8197-703b4fefd40e" {
t.Fatalf("id = %q", got)
}
if got := gjson.GetBytes(out, "object").String(); got != "video" {
t.Fatalf("object = %q, want video", got)
}
if got := gjson.GetBytes(out, "model").String(); got != defaultXAIVideosModel {
t.Fatalf("model = %q, want %s", got, defaultXAIVideosModel)
}
if got := gjson.GetBytes(out, "status").String(); got != "completed" {
t.Fatalf("status = %q, want completed", got)
}
if got := gjson.GetBytes(out, "progress").Int(); got != 100 {
t.Fatalf("progress = %d, want 100", got)
}
if got := gjson.GetBytes(out, "seconds").String(); got != "4" {
t.Fatalf("seconds = %q, want 4", got)
}
if got := gjson.GetBytes(out, "video_url").String(); got != "https://vidgen.x.ai/xai-vidgen-bucket/xai-video-08609066-e7e9-43ba-bd8d-bd29cb6221d9.mp4" {
t.Fatalf("video_url = %q", got)
}
if gjson.GetBytes(out, "video").Exists() {
t.Fatalf("video field must not be exposed in OpenAI retrieve response: %s", string(out))
}
if gjson.GetBytes(out, "usage").Exists() {
t.Fatalf("usage field must not be exposed in OpenAI retrieve response: %s", string(out))
}
}
func TestBuildVideosRetrieveAPIResponseFromXAINormalizesTopLevelError(t *testing.T) {
payload := []byte(`{"code":"invalid-argument","error":"1080p video resolution is not available for your team."}`)
out, err := buildVideosRetrieveAPIResponseFromXAI("video_123", payload, defaultOpenAIVideosModel)
if err != nil {
t.Fatalf("buildVideosRetrieveAPIResponseFromXAI() error = %v", err)
}
if got := gjson.GetBytes(out, "status").String(); got != "failed" {
t.Fatalf("status = %q, want failed", got)
}
if got := gjson.GetBytes(out, "progress").Int(); got != 0 {
t.Fatalf("progress = %d, want 0", got)
}
if got := gjson.GetBytes(out, "error.code").String(); got != "invalid-argument" {
t.Fatalf("error.code = %q, want invalid-argument", got)
}
if got := gjson.GetBytes(out, "error.message").String(); got != "1080p video resolution is not available for your team." {
t.Fatalf("error.message = %q", got)
}
}
func TestBuildVideosRetrieveAPIResponseFromXAINormalizesNestedError(t *testing.T) {
payload := []byte(`{"status":"failed","error":{"message":"The request was rejected by the safety system.","type":"invalid_request_error","code":"content_policy_violation"}}`)
out, err := buildVideosRetrieveAPIResponseFromXAI("video_123", payload, defaultOpenAIVideosModel)
if err != nil {
t.Fatalf("buildVideosRetrieveAPIResponseFromXAI() error = %v", err)
}
if got := gjson.GetBytes(out, "error.code").String(); got != "content_policy_violation" {
t.Fatalf("error.code = %q, want content_policy_violation", got)
}
if got := gjson.GetBytes(out, "error.message").String(); got != "The request was rejected by the safety system." {
t.Fatalf("error.message = %q", got)
}
if gjson.GetBytes(out, "error.type").Exists() {
t.Fatalf("error.type must not be present: %s", string(out))
}
}
func TestXAIVideoContentURLFromPayload(t *testing.T) {
payload := []byte(`{"status":"done","video":{"url":"https://vidgen.x.ai/video.mp4","duration":6}}`)
got, err := xaiVideoContentURLFromPayload(payload)
if err != nil {
t.Fatalf("xaiVideoContentURLFromPayload() error = %v", err)
}
if got != "https://vidgen.x.ai/video.mp4" {
t.Fatalf("url = %q, want https://vidgen.x.ai/video.mp4", got)
}
}
func TestWriteVideoContentFromURL(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "video/mp4")
w.Header().Set("Content-Disposition", `attachment; filename="video.mp4"`)
_, _ = w.Write([]byte("video-bytes"))
}))
defer upstream.Close()
gin.SetMode(gin.TestMode)
resp := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(resp)
ctx.Request = httptest.NewRequest(http.MethodGet, "/openai/v1/videos/video_123/content", nil)
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, nil)
handler := NewOpenAIAPIHandler(base)
if err := handler.writeVideoContentFromURL(ctx, upstream.URL+"/video.mp4"); err != nil {
t.Fatalf("writeVideoContentFromURL() error = %v", err)
}
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, want %d body=%s", resp.Code, http.StatusOK, resp.Body.String())
}
if got := resp.Header().Get("Content-Type"); got != "video/mp4" {
t.Fatalf("Content-Type = %q, want video/mp4", got)
}
if got := resp.Header().Get("Content-Disposition"); got != `attachment; filename="video.mp4"` {
t.Fatalf("Content-Disposition = %q", got)
}
if got := resp.Body.String(); got != "video-bytes" {
t.Fatalf("body = %q, want video-bytes", got)
}
}
func TestWriteVideoContentFromURLUsesPinnedAuthProxy(t *testing.T) {
resetVideoAuthBindingsForTest(t)
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "video/mp4")
_, _ = w.Write([]byte("video-bytes"))
}))
defer upstream.Close()
manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil)
authID := "video-content-auth"
auth := &coreauth.Auth{
ID: authID,
Provider: "xai",
Status: coreauth.StatusActive,
ProxyURL: "direct",
}
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
t.Fatalf("manager.Register() error = %v", errRegister)
}
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}, manager)
handler := NewOpenAIAPIHandler(base)
videoAuthBindings.set("video_123", authID, time.Hour)
gin.SetMode(gin.TestMode)
resp := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(resp)
ctx.Params = gin.Params{{Key: "video_id", Value: "video_123"}}
ctx.Request = httptest.NewRequest(http.MethodGet, "/openai/v1/videos/video_123/content", nil)
if err := handler.writeVideoContentFromURL(ctx, upstream.URL+"/video.mp4"); err != nil {
t.Fatalf("writeVideoContentFromURL() error = %v", err)
}
client := handler.videoContentHTTPClient(ctx)
transport, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatalf("transport type = %T, want *http.Transport", client.Transport)
}
if transport.Proxy != nil {
t.Fatal("expected pinned auth direct proxy to bypass global proxy")
}
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, want %d body=%s", resp.Code, http.StatusOK, resp.Body.String())
}
}
func TestWriteVideoContentFromURLFallsBackToGlobalProxy(t *testing.T) {
resetVideoAuthBindingsForTest(t)
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}, nil)
handler := NewOpenAIAPIHandler(base)
gin.SetMode(gin.TestMode)
resp := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(resp)
ctx.Params = gin.Params{{Key: "video_id", Value: "video_456"}}
ctx.Request = httptest.NewRequest(http.MethodGet, "/openai/v1/videos/video_456/content", nil)
client := handler.videoContentHTTPClient(ctx)
transport, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatalf("transport type = %T, want *http.Transport", client.Transport)
}
req, errRequest := http.NewRequest(http.MethodGet, "https://example.com/video.mp4", nil)
if errRequest != nil {
t.Fatalf("http.NewRequest() error = %v", errRequest)
}
proxyURL, errProxy := transport.Proxy(req)
if errProxy != nil {
t.Fatalf("transport.Proxy() error = %v", errProxy)
}
if proxyURL == nil || proxyURL.String() != "http://global-proxy.example.com:8080" {
t.Fatalf("proxy URL = %v, want http://global-proxy.example.com:8080", proxyURL)
}
}
func TestVideosContentUsesSelectedAuthProxyForDownload(t *testing.T) {
resetVideoAuthBindingsForTest(t)
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "video/mp4")
_, _ = w.Write([]byte("video-bytes"))
}))
defer upstream.Close()
var proxyMu sync.Mutex
proxyHits := 0
globalProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
proxyMu.Lock()
proxyHits++
proxyMu.Unlock()
http.Error(w, "unexpected proxy", http.StatusBadGateway)
}))
defer globalProxy.Close()
videoID := "video-content-selected"
authID := "video-content-selected-auth"
executor := &videoAuthCaptureExecutor{
requestID: videoID,
contentURL: upstream.URL + "/video.mp4",
}
manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: authID,
Provider: "xai",
Status: coreauth.StatusActive,
ProxyURL: "direct",
}
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
t.Fatalf("manager.Register() error = %v", errRegister)
}
registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: defaultXAIVideosModel}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(authID)
})
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{ProxyURL: globalProxy.URL}, manager)
handler := NewOpenAIAPIHandler(base)
resp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id/content", openAIVideosPath+"/"+videoID+"/content", "", nil, handler.VideosContent)
if resp.Code != http.StatusOK {
t.Fatalf("content status = %d, want %d: %s", resp.Code, http.StatusOK, resp.Body.String())
}
if got := resp.Body.String(); got != "video-bytes" {
t.Fatalf("content body = %q, want video-bytes", got)
}
authIDs := executor.AuthIDs()
if len(authIDs) != 1 || authIDs[0] != authID {
t.Fatalf("authIDs = %v, want [%s]", authIDs, authID)
}
if boundAuthID, ok := videoAuthBindings.get(videoID); !ok || boundAuthID != authID {
t.Fatalf("bound auth = %q ok=%v, want %s", boundAuthID, ok, authID)
}
proxyMu.Lock()
gotProxyHits := proxyHits
proxyMu.Unlock()
if gotProxyHits != 0 {
t.Fatalf("global proxy hits = %d, want 0", gotProxyHits)
}
}
func TestVideosCreateRejectsUnsupportedModel(t *testing.T) {
handler := &OpenAIAPIHandler{}
body := strings.NewReader(`{"model":"not-a-video-model","prompt":"make a video"}`)
resp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", body, handler.VideosCreate)
if resp.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d: %s", resp.Code, http.StatusBadRequest, resp.Body.String())
}
if got := gjson.GetBytes(resp.Body.Bytes(), "object").String(); got != "video" {
t.Fatalf("object = %q, want video", got)
}
if got := gjson.GetBytes(resp.Body.Bytes(), "model").String(); got != "not-a-video-model" {
t.Fatalf("model = %q, want not-a-video-model", got)
}
if got := gjson.GetBytes(resp.Body.Bytes(), "status").String(); got != "failed" {
t.Fatalf("status = %q, want failed", got)
}
if got := gjson.GetBytes(resp.Body.Bytes(), "progress").Int(); got != 0 {
t.Fatalf("progress = %d, want 0", got)
}
if got := gjson.GetBytes(resp.Body.Bytes(), "error.code").String(); got != "invalid_request_error" {
t.Fatalf("error.code = %q, want invalid_request_error", got)
}
expectedMessage := "Model not-a-video-model is not supported on " + openAIVideosPath + ". Use " + defaultOpenAIVideosModel + "."
if got := gjson.GetBytes(resp.Body.Bytes(), "error.message").String(); got != expectedMessage {
t.Fatalf("error.message = %q, want %q", got, expectedMessage)
}
if gjson.GetBytes(resp.Body.Bytes(), "error.type").Exists() {
t.Fatalf("error.type must not be present: %s", resp.Body.String())
}
if id := gjson.GetBytes(resp.Body.Bytes(), "id").String(); !strings.HasPrefix(id, "video_") {
t.Fatalf("id = %q, want video_ prefix", id)
}
}
func TestVideosCreateInvalidSizeReturnsFailedVideoResource(t *testing.T) {
handler := &OpenAIAPIHandler{}
body := strings.NewReader(`{"model":"sora-2","prompt":"make a video","size":"1080x1920"}`)
resp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", body, handler.VideosCreate)
if resp.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d: %s", resp.Code, http.StatusBadRequest, resp.Body.String())
}
if got := gjson.GetBytes(resp.Body.Bytes(), "object").String(); got != "video" {
t.Fatalf("object = %q, want video", got)
}
if got := gjson.GetBytes(resp.Body.Bytes(), "model").String(); got != defaultXAIVideosModel {
t.Fatalf("model = %q, want %s", got, defaultXAIVideosModel)
}
if got := gjson.GetBytes(resp.Body.Bytes(), "status").String(); got != "failed" {
t.Fatalf("status = %q, want failed", got)
}
if got := gjson.GetBytes(resp.Body.Bytes(), "progress").Int(); got != 0 {
t.Fatalf("progress = %d, want 0", got)
}
if got := gjson.GetBytes(resp.Body.Bytes(), "error.code").String(); got != "invalid_request_error" {
t.Fatalf("error.code = %q, want invalid_request_error", got)
}
expectedMessage := "Invalid request: size must be one of 720x1280, 1280x720, 1024x1792, or 1792x1024"
if got := gjson.GetBytes(resp.Body.Bytes(), "error.message").String(); got != expectedMessage {
t.Fatalf("error.message = %q, want %q", got, expectedMessage)
}
if gjson.GetBytes(resp.Body.Bytes(), "error.type").Exists() {
t.Fatalf("error.type must not be present: %s", resp.Body.String())
}
}
func TestXAIVideosNativeRejectsUnsupportedModel(t *testing.T) {
handler := &OpenAIAPIHandler{}
body := strings.NewReader(`{"model":"sora-2","prompt":"make a video"}`)
resp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", body, handler.XAIVideosGenerations)
if resp.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d: %s", resp.Code, http.StatusBadRequest, resp.Body.String())
}
message := gjson.GetBytes(resp.Body.Bytes(), "error.message").String()
expectedMessage := "Model sora-2 is not supported on " + xaiVideosGenerationsAPI + ", " + xaiVideosEditsAPI + ", or " + xaiVideosExtensionsAPI + ". Use " + defaultXAIVideosModel + "."
if message != expectedMessage {
t.Fatalf("error message = %q, want %q", message, expectedMessage)
}
}
func TestXAIVideosNativeRejectsInvalidJSON(t *testing.T) {
handler := &OpenAIAPIHandler{}
body := strings.NewReader(`{"model":`)
resp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosEditsAPI, "application/json", body, handler.XAIVideosEdits)
if resp.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d: %s", resp.Code, http.StatusBadRequest, resp.Body.String())
}
if got := gjson.GetBytes(resp.Body.Bytes(), "error.type").String(); got != "invalid_request_error" {
t.Fatalf("error type = %q, want invalid_request_error", got)
}
}
func TestVideosCreateBindsRetrieveToSelectedAuth(t *testing.T) {
resetVideoAuthBindingsForTest(t)
executor := &videoAuthCaptureExecutor{requestID: "video-openai-bound"}
handler := newVideoAuthBindingTestHandler(t, executor)
createResp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", strings.NewReader(`{"model":"sora-2","prompt":"make a video"}`), handler.VideosCreate)
if createResp.Code != http.StatusOK {
t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String())
}
videoID := gjson.GetBytes(createResp.Body.Bytes(), "id").String()
if videoID != executor.requestID {
t.Fatalf("created video id = %q, want %q", videoID, executor.requestID)
}
if got := gjson.GetBytes(createResp.Body.Bytes(), "model").String(); got != defaultXAIVideosModel {
t.Fatalf("created model = %q, want %s", got, defaultXAIVideosModel)
}
retrieveResp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id", openAIVideosPath+"/"+videoID, "", nil, handler.VideosRetrieve)
if retrieveResp.Code != http.StatusOK {
t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String())
}
authIDs := executor.AuthIDs()
if len(authIDs) != 2 {
t.Fatalf("authIDs = %v, want two calls", authIDs)
}
if authIDs[1] != authIDs[0] {
t.Fatalf("retrieve auth = %q, want create auth %q; sequence=%v", authIDs[1], authIDs[0], authIDs)
}
}
func TestXAIVideosNativeCreateBindsRetrieveToSelectedAuth(t *testing.T) {
resetVideoAuthBindingsForTest(t)
executor := &videoAuthCaptureExecutor{requestID: "video-xai-bound"}
handler := newVideoAuthBindingTestHandler(t, executor)
createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video","prompt":"make a video"}`), handler.XAIVideosGenerations)
if createResp.Code != http.StatusOK {
t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String())
}
videoID := gjson.GetBytes(createResp.Body.Bytes(), "request_id").String()
if videoID != executor.requestID {
t.Fatalf("created request_id = %q, want %q", videoID, executor.requestID)
}
retrieveResp := performVideosRouteRequest(t, http.MethodGet, videosPath+"/:request_id", videosPath+"/"+videoID, "", nil, handler.XAIVideosRetrieve)
if retrieveResp.Code != http.StatusOK {
t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String())
}
authIDs := executor.AuthIDs()
if len(authIDs) != 2 {
t.Fatalf("authIDs = %v, want two calls", authIDs)
}
if authIDs[1] != authIDs[0] {
t.Fatalf("retrieve auth = %q, want create auth %q; sequence=%v", authIDs[1], authIDs[0], authIDs)
}
}
func TestXAIVideosNativeRetrieveUsesCanonicalBoundModel(t *testing.T) {
resetVideoAuthBindingsForTest(t)
executor := &videoAuthCaptureExecutor{requestID: "video-xai-1.5-bound"}
manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil)
manager.RegisterExecutor(executor)
authModels := []struct {
authID string
model string
}{
{authID: "video-xai-1.5-default-auth", model: defaultXAIVideosModel},
{authID: "video-xai-1.5-auth", model: xaiVideos15Model},
}
for _, entry := range authModels {
auth := &coreauth.Auth{
ID: entry.authID,
Provider: "xai",
Status: coreauth.StatusActive,
}
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
t.Fatalf("manager.Register(%s): %v", entry.authID, errRegister)
}
registry.GetGlobalRegistry().RegisterClient(entry.authID, auth.Provider, []*registry.ModelInfo{{ID: entry.model}})
manager.RefreshSchedulerEntry(entry.authID)
}
t.Cleanup(func() {
for _, entry := range authModels {
registry.GetGlobalRegistry().UnregisterClient(entry.authID)
}
})
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
handler := NewOpenAIAPIHandler(base)
createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5","prompt":"make a video"}`), handler.XAIVideosGenerations)
if createResp.Code != http.StatusOK {
t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String())
}
videoID := gjson.GetBytes(createResp.Body.Bytes(), "request_id").String()
if videoID != executor.requestID {
t.Fatalf("created request_id = %q, want %q", videoID, executor.requestID)
}
retrieveResp := performVideosRouteRequest(t, http.MethodGet, videosPath+"/:request_id", videosPath+"/"+videoID, "", nil, handler.XAIVideosRetrieve)
if retrieveResp.Code != http.StatusOK {
t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String())
}
authIDs := executor.AuthIDs()
if len(authIDs) != 2 {
t.Fatalf("authIDs = %v, want two calls", authIDs)
}
if authIDs[0] != "video-xai-1.5-auth" || authIDs[1] != authIDs[0] {
t.Fatalf("authIDs = %v, want both calls to use video-xai-1.5-auth", authIDs)
}
models := executor.Models()
if len(models) != 2 {
t.Fatalf("models = %v, want two calls", models)
}
if models[0] != xaiVideos15Model || models[1] != xaiVideos15Model {
t.Fatalf("models = %v, want both calls to use %s", models, xaiVideos15Model)
}
payloadModels := executor.PayloadModels()
if len(payloadModels) != 2 || payloadModels[0] != xaiVideos15Model {
t.Fatalf("payload models = %v, want create payload model %s", payloadModels, xaiVideos15Model)
}
binding, ok := videoAuthBindings.getBinding(videoID)
if !ok {
t.Fatal("video auth binding was not stored")
}
if binding.authID != "video-xai-1.5-auth" || binding.model != xaiVideos15Model {
t.Fatalf("binding = {authID:%q model:%q}, want {authID:%q model:%q}", binding.authID, binding.model, "video-xai-1.5-auth", xaiVideos15Model)
}
}
func TestVideosCreatePreviewAliasUsesPreviewAuthWithGAPayload(t *testing.T) {
resetVideoAuthBindingsForTest(t)
executor := &videoAuthCaptureExecutor{requestID: "video-openai-preview-alias"}
handler := newVideoSingleModelAuthTestHandler(t, executor, "video-openai-preview-auth", xaiVideos15PreviewAlias)
createResp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.VideosCreate)
if createResp.Code != http.StatusOK {
t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String())
}
videoID := gjson.GetBytes(createResp.Body.Bytes(), "id").String()
if got := gjson.GetBytes(createResp.Body.Bytes(), "model").String(); got != xaiVideos15Model {
t.Fatalf("response model = %q, want %s", got, xaiVideos15Model)
}
retrieveResp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id", openAIVideosPath+"/"+videoID, "", nil, handler.VideosRetrieve)
if retrieveResp.Code != http.StatusOK {
t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String())
}
assertPreviewAliasRouting(t, executor, videoID, "video-openai-preview-auth")
}
func TestVideosCreatePreviewAliasUsesDefaultXAIModelsWithGAPayload(t *testing.T) {
resetVideoAuthBindingsForTest(t)
executor := &videoAuthCaptureExecutor{requestID: "video-openai-preview-default-models"}
handler := newVideoAuthTestHandler(t, executor, "video-openai-preview-default-auth", registry.GetXAIModels())
createResp := performVideosEndpointRequest(t, http.MethodPost, openAIVideosPath, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.VideosCreate)
if createResp.Code != http.StatusOK {
t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String())
}
videoID := gjson.GetBytes(createResp.Body.Bytes(), "id").String()
if got := gjson.GetBytes(createResp.Body.Bytes(), "model").String(); got != xaiVideos15Model {
t.Fatalf("response model = %q, want %s", got, xaiVideos15Model)
}
retrieveResp := performVideosRouteRequest(t, http.MethodGet, openAIVideosPath+"/:video_id", openAIVideosPath+"/"+videoID, "", nil, handler.VideosRetrieve)
if retrieveResp.Code != http.StatusOK {
t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String())
}
assertPreviewAliasRouting(t, executor, videoID, "video-openai-preview-default-auth")
}
func TestXAIVideosNativePreviewAliasUsesPreviewAuthWithGAPayload(t *testing.T) {
resetVideoAuthBindingsForTest(t)
executor := &videoAuthCaptureExecutor{requestID: "video-native-preview-alias"}
handler := newVideoSingleModelAuthTestHandler(t, executor, "video-native-preview-auth", xaiVideos15PreviewAlias)
createResp := performVideosEndpointRequest(t, http.MethodPost, xaiVideosGenerationsAPI, "application/json", strings.NewReader(`{"model":"grok-imagine-video-1.5-preview","prompt":"make a video"}`), handler.XAIVideosGenerations)
if createResp.Code != http.StatusOK {
t.Fatalf("create status = %d, want %d: %s", createResp.Code, http.StatusOK, createResp.Body.String())
}
videoID := gjson.GetBytes(createResp.Body.Bytes(), "request_id").String()
retrieveResp := performVideosRouteRequest(t, http.MethodGet, videosPath+"/:request_id", videosPath+"/"+videoID, "", nil, handler.XAIVideosRetrieve)
if retrieveResp.Code != http.StatusOK {
t.Fatalf("retrieve status = %d, want %d: %s", retrieveResp.Code, http.StatusOK, retrieveResp.Body.String())
}
assertPreviewAliasRouting(t, executor, videoID, "video-native-preview-auth")
}
func newVideoSingleModelAuthTestHandler(t *testing.T, executor *videoAuthCaptureExecutor, authID string, model string) *OpenAIAPIHandler {
t.Helper()
return newVideoAuthTestHandler(t, executor, authID, []*registry.ModelInfo{{ID: model}})
}
func newVideoAuthTestHandler(t *testing.T, executor *videoAuthCaptureExecutor, authID string, models []*registry.ModelInfo) *OpenAIAPIHandler {
t.Helper()
manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: authID,
Provider: "xai",
Status: coreauth.StatusActive,
}
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
t.Fatalf("manager.Register(%s): %v", authID, errRegister)
}
registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, models)
manager.RefreshSchedulerEntry(authID)
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(authID)
})
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
return NewOpenAIAPIHandler(base)
}
func assertPreviewAliasRouting(t *testing.T, executor *videoAuthCaptureExecutor, videoID string, authID string) {
t.Helper()
authIDs := executor.AuthIDs()
if len(authIDs) != 2 || authIDs[0] != authID || authIDs[1] != authID {
t.Fatalf("authIDs = %v, want both calls to use %s", authIDs, authID)
}
models := executor.Models()
if len(models) != 2 || models[0] != xaiVideos15PreviewAlias || models[1] != xaiVideos15PreviewAlias {
t.Fatalf("models = %v, want both calls to route with %s", models, xaiVideos15PreviewAlias)
}
payloadModels := executor.PayloadModels()
if len(payloadModels) != 2 || payloadModels[0] != xaiVideos15Model {
t.Fatalf("payload models = %v, want create payload model %s", payloadModels, xaiVideos15Model)
}
binding, ok := videoAuthBindings.getBinding(videoID)
if !ok {
t.Fatal("video auth binding was not stored")
}
if binding.authID != authID || binding.model != xaiVideos15PreviewAlias {
t.Fatalf("binding = {authID:%q model:%q}, want {authID:%q model:%q}", binding.authID, binding.model, authID, xaiVideos15PreviewAlias)
}
}
func TestVideoAuthBindingTTLUsesConfig(t *testing.T) {
base := apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{VideoResultAuthCacheTTL: "45m"}, nil)
handler := NewOpenAIAPIHandler(base)
if got := handler.videoAuthBindingTTL(); got != 45*time.Minute {
t.Fatalf("videoAuthBindingTTL() = %v, want 45m", got)
}
base = apihandlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{VideoResultAuthCacheTTL: "invalid"}, nil)
handler = NewOpenAIAPIHandler(base)
if got := handler.videoAuthBindingTTL(); got != defaultVideoAuthBindingTTL {
t.Fatalf("invalid videoAuthBindingTTL() = %v, want %v", got, defaultVideoAuthBindingTTL)
}
}
func TestVideoAuthBindingStoreExpiresEntries(t *testing.T) {
store := newVideoAuthBindingStore()
store.entries["video-expired"] = videoAuthBinding{
authID: "auth-expired",
expiresAt: time.Now().Add(-time.Second),
}
if authID, ok := store.get("video-expired"); ok {
t.Fatalf("expired binding returned authID=%q", authID)
}
if _, exists := store.entries["video-expired"]; exists {
t.Fatal("expired binding was not removed")
}
}
func TestVideosCreateFormRequest(t *testing.T) {
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")
if err != nil {
t.Fatalf("videosCreateRequestFromFormContext() error = %v", err)
}
if got := gjson.GetBytes(rawJSON, "input_reference.image_url").String(); got != "https://example.com/a.png" {
t.Fatalf("input_reference.image_url = %q", got)
}
}
func videosCreateRequestFromFormContext(body string) ([]byte, error) {
gin.SetMode(gin.TestMode)
router := gin.New()
var rawJSON []byte
var err error
router.POST(videosPath, func(c *gin.Context) {
rawJSON, err = videosCreateRequestFromForm(c)
})
req := httptest.NewRequest(http.MethodPost, videosPath, strings.NewReader(body))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
return rawJSON, err
}