Files
CLIProxyAPI/sdk/api/handlers/openai/openai_videos_handlers_test.go
T
Luis Pater 53d1fd6c5c feat(api, xai): add xAI Grok video model support with API integration
- Introduced new xAI `grok-imagine-video` model for video generation with configurable options (e.g., duration, size, resolution).
- Implemented video-specific API endpoints (`/v1/videos`, `/v1/videos/generations`, `/v1/videos/edits`, `/v1/videos/extensions`), including request validation and model handling.
- Enhanced model registry with `xaiBuiltinVideoModelID` and metadata for video capabilities.
- Added unit tests to validate video model support, request structures, and API response handling.
- Extended `XAIExecutor` to integrate video generation and retrieval via runtime requests.
2026-05-17 02:53:50 +08:00

228 lines
8.2 KiB
Go

package openai
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"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 TestVideosModelValidationAllowsXAIVideoModel(t *testing.T) {
for _, model := range []string{"grok-imagine-video", "xai/grok-imagine-video", "x-ai/grok-imagine-video", "grok/grok-imagine-video"} {
if !isSupportedVideosModel(model) {
t.Fatalf("expected %s to be supported", model)
}
}
if isSupportedVideosModel("sora-2") {
t.Fatal("expected sora-2 to be rejected")
}
if isSupportedVideosModel("codex/grok-imagine-video") {
t.Fatal("expected codex/grok-imagine-video to be rejected")
}
}
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 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(`{"status":"done","video":{"url":"https://vidgen.x.ai/video.mp4","duration":6,"respect_moderation":true},"model":"grok-imagine-video","usage":{"cost_in_usd_ticks":500000000},"progress":100}`)
out, err := buildVideosRetrieveAPIResponseFromXAI("vid_123", payload, defaultXAIVideosModel)
if err != nil {
t.Fatalf("buildVideosRetrieveAPIResponseFromXAI() 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, "status").String(); got != "completed" {
t.Fatalf("status = %q, want completed", got)
}
if got := gjson.GetBytes(out, "seconds").String(); got != "6" {
t.Fatalf("seconds = %q, want 6", got)
}
if got := gjson.GetBytes(out, "video.url").String(); got != "https://vidgen.x.ai/video.mp4" {
t.Fatalf("video.url = %q", got)
}
if !gjson.GetBytes(out, "usage").Exists() {
t.Fatalf("usage missing: %s", string(out))
}
}
func TestVideosCreateRejectsUnsupportedModel(t *testing.T) {
handler := &OpenAIAPIHandler{}
body := strings.NewReader(`{"model":"sora-2","prompt":"make a video"}`)
resp := performVideosEndpointRequest(t, http.MethodPost, videosPath, "application/json", body, handler.VideosCreate)
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 " + videosPath + ". Use " + defaultXAIVideosModel + "."
if message != expectedMessage {
t.Fatalf("error message = %q, want %q", message, expectedMessage)
}
}
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 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
}