From 2c9bba20b3a6c8c46929edbae8755d316694f085 Mon Sep 17 00:00:00 2001 From: Hao OUYANG Date: Wed, 22 Jul 2026 20:43:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(media):=20Grok=20=E8=A7=86=E9=A2=91=20-=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20xAI=20=E4=B8=8E=20Sub2API=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #7 --- backend/internal/handler/security.go | 2 +- backend/internal/handler/security_test.go | 10 +-- backend/internal/model/models.go | 1 + backend/internal/service/admin.go | 2 +- .../internal/service/admin_channel_test.go | 20 ++++++ backend/internal/service/channel_models.go | 2 +- backend/internal/service/provider.go | 24 ++++---- backend/internal/service/provider_test.go | 61 +++++++++++++++++++ .../components/layout/app-config-modal.tsx | 3 + .../pages/admin/channels/channels-page.tsx | 8 +-- .../components/channel-model-manager.tsx | 2 +- web/src/services/api/video.ts | 23 ++++--- web/src/stores/use-config-store.ts | 8 +-- 13 files changed, 129 insertions(+), 37 deletions(-) diff --git a/backend/internal/handler/security.go b/backend/internal/handler/security.go index 7ad4bcefa..a4a7f196d 100644 --- a/backend/internal/handler/security.go +++ b/backend/internal/handler/security.go @@ -93,7 +93,7 @@ func interfaceAllowsProxyPath(interfaceType model.ChannelInterfaceType, requestP return requestPath == "/responses" case model.ChannelInterfaceOpenAIImage: return requestPath == "/images/generations" || requestPath == "/images/edits" - case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2: + case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo: return false default: return true diff --git a/backend/internal/handler/security_test.go b/backend/internal/handler/security_test.go index 05c1eac93..f705add20 100644 --- a/backend/internal/handler/security_test.go +++ b/backend/internal/handler/security_test.go @@ -48,10 +48,12 @@ func TestAuthorizeSystemProxyRestrictsConfiguredInterfaceType(t *testing.T) { } } -func TestAuthorizeSystemProxyBlocksNewAPIChannel2DirectBrowserProxy(t *testing.T) { +func TestAuthorizeSystemProxyBlocksBackendOnlyVideoInterfaces(t *testing.T) { body := []byte(`{"model":"grok-image-video"}`) - channel := &model.ModelChannel{APIFormat: "openai", InterfaceType: model.ChannelInterfaceNewAPIChannel2, ModelsJSON: `["grok-image-video"]`} - if err := authorizeSystemProxy(channel, http.MethodPost, "/video/generations", "application/json", body); err == nil { - t.Fatal("authorizeSystemProxy() error = nil for backend-only video interface") + for _, interfaceType := range []model.ChannelInterfaceType{model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo} { + channel := &model.ModelChannel{APIFormat: "openai", InterfaceType: interfaceType, ModelsJSON: `["grok-image-video"]`} + if err := authorizeSystemProxy(channel, http.MethodPost, "/video/generations", "application/json", body); err == nil { + t.Fatalf("authorizeSystemProxy() error = nil for backend-only interface %q", interfaceType) + } } } diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index 50fb98b78..0a666792c 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -54,6 +54,7 @@ const ( ChannelInterfaceNewAPIVideo ChannelInterfaceType = "newapi" ChannelInterfaceNewAPIChannel1 ChannelInterfaceType = "newapi-channel-1" ChannelInterfaceNewAPIChannel2 ChannelInterfaceType = "newapi-channel-2" + ChannelInterfaceXAIVideo ChannelInterfaceType = "xai-video" ApiCallStatusSucceeded ApiCallStatus = "succeeded" ApiCallStatusFailed ApiCallStatus = "failed" diff --git a/backend/internal/service/admin.go b/backend/internal/service/admin.go index 7bd59e7dc..9be7b0f23 100644 --- a/backend/internal/service/admin.go +++ b/backend/internal/service/admin.go @@ -558,7 +558,7 @@ func mergeChannelRequest(req ChannelRequest, channel model.ModelChannel) Channel func validChannelInterfaceType(value model.ChannelInterfaceType) bool { switch value { - case model.ChannelInterfaceChatCompletion, model.ChannelInterfaceOpenAIResponse, model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2: + case model.ChannelInterfaceChatCompletion, model.ChannelInterfaceOpenAIResponse, model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo: return true default: return false diff --git a/backend/internal/service/admin_channel_test.go b/backend/internal/service/admin_channel_test.go index 8208ae3bb..4fd6623ad 100644 --- a/backend/internal/service/admin_channel_test.go +++ b/backend/internal/service/admin_channel_test.go @@ -31,6 +31,26 @@ func TestChannelFromRequestStoresInterfaceType(t *testing.T) { } } +func TestChannelFromRequestStoresXAIVideoInterfaceType(t *testing.T) { + t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true") + server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + defer server.Close() + + channel, err := channelFromRequest(ChannelRequest{ + Name: "xAI Video", + BaseURL: server.URL + "/v1", + APIKey: "secret", + InterfaceType: "xai-video", + Models: []string{"grok-imagine-video-1.5"}, + }, model.ModelChannel{}) + if err != nil { + t.Fatalf("channelFromRequest() error = %v", err) + } + if channel.InterfaceType != model.ChannelInterfaceXAIVideo { + t.Fatalf("InterfaceType = %q", channel.InterfaceType) + } +} + func TestMergeChannelRequestSupportsEnabledOnlyPatch(t *testing.T) { enabled := false req := mergeChannelRequest(ChannelRequest{Enabled: &enabled}, model.ModelChannel{ diff --git a/backend/internal/service/channel_models.go b/backend/internal/service/channel_models.go index 81b627cd8..583476444 100644 --- a/backend/internal/service/channel_models.go +++ b/backend/internal/service/channel_models.go @@ -200,7 +200,7 @@ func capabilityForChannel(channel model.ModelChannel) string { switch channel.InterfaceType { case model.ChannelInterfaceOpenAIImage: return "image" - case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2: + case model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo: return "video" default: return "text" diff --git a/backend/internal/service/provider.go b/backend/internal/service/provider.go index 59fa0196a..544a25a52 100644 --- a/backend/internal/service/provider.go +++ b/backend/internal/service/provider.go @@ -574,12 +574,16 @@ func runVideoTask(ctx context.Context, input canvasGenerationInput) (map[string] } id := resumedProviderRequestID(ctx) var created map[string]interface{} - if id == "" && isGrokVideoConfig(input.Config) { + if id == "" && (input.Config.InterfaceType == "xai-video" || isGrokVideoConfig(input.Config)) { requestBody, err := grokVideoBody(input) if err != nil { return nil, err } - if err := postJSON(ctx, input.Config, "/videos", requestBody, &created); err != nil { + createPath := "/videos" + if input.Config.InterfaceType == "xai-video" { + createPath = "/videos/generations" + } + if err := postJSON(ctx, input.Config, createPath, requestBody, &created); err != nil { return nil, err } } else if id == "" { @@ -608,17 +612,11 @@ func runVideoTask(ctx context.Context, input canvasGenerationInput) (map[string] } } if id == "" { - id = stringField(created, "id") - if id == "" { - id = stringField(created, "task_id") - } + id = firstNonEmptyString(stringField(created, "id"), stringField(created, "request_id"), stringField(created, "task_id")) } if id == "" { if data, ok := created["data"].(map[string]interface{}); ok { - id = stringField(data, "id") - if id == "" { - id = stringField(data, "task_id") - } + id = firstNonEmptyString(stringField(data, "id"), stringField(data, "request_id"), stringField(data, "task_id")) } } if id == "" { @@ -633,7 +631,7 @@ func runVideoTask(ctx context.Context, input canvasGenerationInput) (map[string] state = data } status := strings.ToLower(stringField(state, "status")) - if status == "completed" || status == "succeeded" || status == "success" { + if status == "completed" || status == "succeeded" || status == "success" || status == "done" { if videoURL := newAPIVideoResultURL(state); videoURL != "" { data, mimeType, err := getExternalBinary(withProviderRequestKind(ctx, "download"), videoURL) if err != nil { @@ -664,7 +662,7 @@ func newAPIVideoResultURL(state map[string]interface{}) string { func nestedNewAPIVideoResultURL(payload map[string]interface{}, allowResultURL bool, depth int) string { if depth < 2 { - for _, key := range []string{"data", "result"} { + for _, key := range []string{"data", "result", "video"} { if nested, ok := payload[key].(map[string]interface{}); ok { if videoURL := nestedNewAPIVideoResultURL(nested, true, depth+1); videoURL != "" { return videoURL @@ -986,7 +984,7 @@ func validateGenerationInterface(mode string, interfaceType string) error { allowed := map[string]map[string]bool{ "text": {"chat-completion": true, "openai-response": true}, "image": {"openai-image": true}, - "video": {"newapi": true, "newapi-channel-1": true, "newapi-channel-2": true}, + "video": {"newapi": true, "newapi-channel-1": true, "newapi-channel-2": true, "xai-video": true}, } if allowed[mode] != nil && !allowed[mode][interfaceType] { return fmt.Errorf("接口类型 %s 不支持%s生成", interfaceType, mode) diff --git a/backend/internal/service/provider_test.go b/backend/internal/service/provider_test.go index 8c4bf9ed5..6e7f57813 100644 --- a/backend/internal/service/provider_test.go +++ b/backend/internal/service/provider_test.go @@ -369,6 +369,64 @@ func TestRunVideoTaskUsesJSONForGrokVideo(t *testing.T) { } } +func TestRunVideoTaskUsesXAIVideoGenerationEndpoint(t *testing.T) { + t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true") + paths := make([]string, 0, 3) + var server *httptest.Server + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + paths = append(paths, r.Method+" "+r.URL.Path) + switch r.Method + " " + r.URL.Path { + case "POST /v1/videos/generations": + if contentType := r.Header.Get("Content-Type"); !strings.HasPrefix(contentType, "application/json") { + t.Errorf("Content-Type = %q, want application/json", contentType) + } + var body map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode request: %v", err) + } + if body["model"] != "grok-imagine-video-1.5" || body["prompt"] != "make it move" { + t.Errorf("request body = %#v", body) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"request_id":"video-1"}`)) + case "GET /v1/videos/video-1": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status":"done","video":{"url":"` + server.URL + `/files/video.mp4"}}`)) + case "GET /files/video.mp4": + if authorization := r.Header.Get("Authorization"); authorization != "" { + t.Errorf("file Authorization = %q, want empty", authorization) + } + w.Header().Set("Content-Type", "video/mp4") + _, _ = w.Write([]byte("video")) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + result, err := runVideoTask(context.Background(), canvasGenerationInput{ + Prompt: "make it move", + Config: providerConfig{ + BaseURL: server.URL + "/v1", + APIKey: "test-key", + Model: "grok-imagine-video-1.5", + InterfaceType: "xai-video", + VideoSeconds: "10", + }, + }) + if err != nil { + t.Fatalf("runVideoTask() error = %v", err) + } + video, ok := result["video"].(map[string]interface{}) + if !ok || video["dataUrl"] != "data:video/mp4;base64,dmlkZW8=" { + t.Fatalf("video = %#v", result["video"]) + } + want := "POST /v1/videos/generations,GET /v1/videos/video-1,GET /files/video.mp4" + if got := strings.Join(paths, ","); got != want { + t.Fatalf("paths = %q, want %q", got, want) + } +} + func TestNewAPIVideoPromptKeepsTextOnlyPromptUnchanged(t *testing.T) { input := canvasGenerationInput{ Prompt: "make it move", @@ -622,6 +680,9 @@ func TestValidateGenerationInterfaceRejectsMismatchedType(t *testing.T) { if err := validateGenerationInterface("video", "newapi-channel-2"); err != nil { t.Fatalf("validateGenerationInterface() error = %v", err) } + if err := validateGenerationInterface("video", "xai-video"); err != nil { + t.Fatalf("validateGenerationInterface() error = %v", err) + } } func TestProcessTaskValidatesInterfaceBeforeHydratingMedia(t *testing.T) { diff --git a/web/src/components/layout/app-config-modal.tsx b/web/src/components/layout/app-config-modal.tsx index 6b9f30fd9..629bb8f52 100644 --- a/web/src/components/layout/app-config-modal.tsx +++ b/web/src/components/layout/app-config-modal.tsx @@ -80,6 +80,7 @@ const channelProtocolOptions = [ { label: "NewAPI 视频", value: "newapi" }, { label: "NewAPI 渠道 1", value: "newapi-channel-1" }, { label: "NewAPI 渠道 2", value: "newapi-channel-2" }, + { label: "xAI / Sub2API 视频", value: "xai-video" }, ], }, ]; @@ -750,6 +751,8 @@ function channelProtocolLabel(channel: ModelChannel) { return "NewAPI 渠道 1"; case "newapi-channel-2": return "NewAPI 渠道 2"; + case "xai-video": + return "xAI / Sub2API 视频"; default: return "OpenAI 自动兼容"; } diff --git a/web/src/pages/admin/channels/channels-page.tsx b/web/src/pages/admin/channels/channels-page.tsx index 9094505c4..6b7f22750 100644 --- a/web/src/pages/admin/channels/channels-page.tsx +++ b/web/src/pages/admin/channels/channels-page.tsx @@ -19,7 +19,7 @@ type ChannelFormValues = { name: string; baseUrl: string; apiKey?: string; inter const interfaceTypeOptions = [ { label: "文本", options: [{ label: "Chat Completions", value: "chat-completion" }, { label: "OpenAI Responses", value: "openai-response" }] }, { label: "图片", options: [{ label: "OpenAI Images", value: "openai-image" }] }, - { label: "视频", options: [{ label: "NewAPI 视频", value: "newapi" }, { label: "NewAPI 渠道 1", value: "newapi-channel-1" }, { label: "NewAPI 渠道 2", value: "newapi-channel-2" }] }, + { label: "视频", options: [{ label: "NewAPI 视频", value: "newapi" }, { label: "NewAPI 渠道 1", value: "newapi-channel-1" }, { label: "NewAPI 渠道 2", value: "newapi-channel-2" }, { label: "xAI / Sub2API 视频", value: "xai-video" }] }, ]; export default function ChannelsPage() { @@ -134,7 +134,7 @@ export default function ChannelsPage() { const columns: ColumnsType = [ { title: "渠道", dataIndex: "name", render: (_, channel) =>
{channel.name}
{channel.baseUrl}
}, - { title: "接口类型", dataIndex: "interfaceType", width: 160, render: (value: ChannelInterfaceType) => {interfaceTypeLabel(value)} }, + { title: "接口类型", dataIndex: "interfaceType", width: 160, render: (value: ChannelInterfaceType) => {interfaceTypeLabel(value)} }, { title: "模型", dataIndex: "models", width: 100, render: (models: string[]) => `${models?.length || 0} 个` }, { title: "密钥", dataIndex: "hasApiKey", width: 100, render: (configured) => {configured ? "已配置" : "未配置"} }, { title: "状态", dataIndex: "enabled", width: 100, render: (enabled) => {enabled !== false ? "已启用" : "已停用"} }, @@ -171,5 +171,5 @@ export default function ChannelsPage() { function positiveInt(value: string | null, fallback: number) { const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; } function normalizePageSize(value: string | null) { const parsed = positiveInt(value, 20); return [20, 50, 100].includes(parsed) ? parsed : 20; } function normalizeStatus(value: string | null): "all" | "enabled" | "disabled" { return value === "enabled" || value === "disabled" ? value : "all"; } -function normalizeInterface(value: string | null): "all" | ChannelInterfaceType { return ["chat-completion", "openai-response", "openai-image", "newapi", "newapi-channel-1", "newapi-channel-2"].includes(value || "") ? value as ChannelInterfaceType : "all"; } -function interfaceTypeLabel(value?: ChannelInterfaceType) { return ({ "chat-completion": "Chat Completions", "openai-response": "OpenAI Responses", "openai-image": "OpenAI Images", newapi: "NewAPI 视频", "newapi-channel-1": "NewAPI 渠道 1", "newapi-channel-2": "NewAPI 渠道 2" } as Record)[value || ""] || "未设置"; } +function normalizeInterface(value: string | null): "all" | ChannelInterfaceType { return ["chat-completion", "openai-response", "openai-image", "newapi", "newapi-channel-1", "newapi-channel-2", "xai-video"].includes(value || "") ? value as ChannelInterfaceType : "all"; } +function interfaceTypeLabel(value?: ChannelInterfaceType) { return ({ "chat-completion": "Chat Completions", "openai-response": "OpenAI Responses", "openai-image": "OpenAI Images", newapi: "NewAPI 视频", "newapi-channel-1": "NewAPI 渠道 1", "newapi-channel-2": "NewAPI 渠道 2", "xai-video": "xAI / Sub2API 视频" } as Record)[value || ""] || "未设置"; } diff --git a/web/src/pages/admin/components/channel-model-manager.tsx b/web/src/pages/admin/components/channel-model-manager.tsx index 10980809b..64ca39e65 100644 --- a/web/src/pages/admin/components/channel-model-manager.tsx +++ b/web/src/pages/admin/components/channel-model-manager.tsx @@ -193,7 +193,7 @@ export function ChannelModelManager({ channel, onClose, onChanged }: { channel: function capabilityFromInterface(value?: ModelChannel["interfaceType"]): ChannelModel["capability"] { if (value === "openai-image") return "image"; - if (value === "newapi" || value === "newapi-channel-1" || value === "newapi-channel-2") return "video"; + if (value === "newapi" || value === "newapi-channel-1" || value === "newapi-channel-2" || value === "xai-video") return "video"; return "text"; } diff --git a/web/src/services/api/video.ts b/web/src/services/api/video.ts index 7bbd2af2f..e2a76470d 100644 --- a/web/src/services/api/video.ts +++ b/web/src/services/api/video.ts @@ -8,8 +8,9 @@ import { buildApiUrl, isSystemProxyBaseUrl, modelOptionName, resolveModelRequest import type { ReferenceImage } from "@/types/image"; import type { ReferenceAudio, ReferenceVideo } from "@/types/media"; -type VideoResponse = { id: string; status?: string; error?: { message?: string } }; +type VideoResponse = { id?: string; request_id?: string; task_id?: string; status?: string; error?: { message?: string } }; type ApiVideoResponse = VideoResponse | { code?: number; data?: VideoResponse | null; msg?: string }; +type ResolvedAiConfig = ReturnType; type SeedanceTask = { id: string; task_id?: string; @@ -77,9 +78,9 @@ export async function storeGeneratedVideo(result: VideoGenerationResult): Promis throw new Error("视频接口没有返回可播放的视频"); } -async function createOpenAIVideoTask(config: AiConfig, model: string, prompt: string, references: ReferenceImage[], options?: RequestOptions): Promise { +async function createOpenAIVideoTask(config: ResolvedAiConfig, model: string, prompt: string, references: ReferenceImage[], options?: RequestOptions): Promise { const modelName = modelOptionName(model); - if (modelName.toLowerCase().includes("grok")) { + if (config.interfaceType === "xai-video" || modelName.toLowerCase().includes("grok")) { const images = await Promise.all(references.slice(0, 7).map((image) => imageToDataUrl(image))); const seconds = normalizeVideoSeconds(config.videoSeconds); const payload = { @@ -91,9 +92,11 @@ async function createOpenAIVideoTask(config: AiConfig, model: string, prompt: st ...(images.length ? { image: images[0], images } : {}), }; try { - const created = unwrapVideoResponse((await axios.post(aiApiUrl(config, "/videos"), payload, { headers: aiHeaders(config, "application/json"), signal: options?.signal })).data); - if (!created.id) throw new Error("视频接口没有返回任务 ID"); - return { id: created.id, provider: "openai", model }; + const createPath = config.interfaceType === "xai-video" ? "/videos/generations" : "/videos"; + const created = unwrapVideoResponse((await axios.post(aiApiUrl(config, createPath), payload, { headers: aiHeaders(config, "application/json"), signal: options?.signal })).data); + const id = videoTaskId(created); + if (!id) throw new Error("视频接口没有返回任务 ID"); + return { id, provider: "openai", model }; } catch (error) { throw new Error(readAxiosError(error, "视频任务创建失败")); } @@ -116,10 +119,10 @@ async function createOpenAIVideoTask(config: AiConfig, model: string, prompt: st } } -async function pollOpenAIVideoTask(config: AiConfig, task: VideoGenerationTask, options?: RequestOptions): Promise { +async function pollOpenAIVideoTask(config: ResolvedAiConfig, task: VideoGenerationTask, options?: RequestOptions): Promise { try { const video = unwrapVideoResponse((await axios.get(aiApiUrl(config, `/videos/${task.id}`), { headers: aiHeaders(config), signal: options?.signal })).data); - if (video.status === "completed") { + if (video.status === "completed" || video.status === "succeeded" || video.status === "success" || video.status === "done") { const content = await axios.get(aiApiUrl(config, `/videos/${task.id}/content`), { headers: aiHeaders(config), responseType: "blob", signal: options?.signal }); await assertVideoBlob(content.data); return { status: "completed", result: { blob: content.data } }; @@ -338,6 +341,10 @@ function unwrapVideoResponse(payload: ApiVideoResponse) { return unwrapEnvelope(payload, "接口没有返回视频任务"); } +function videoTaskId(payload: VideoResponse) { + return payload.id || payload.request_id || payload.task_id || ""; +} + function unwrapSeedanceTask(payload: ApiEnvelope) { return unwrapEnvelope(payload, "Seedance 接口没有返回任务"); } diff --git a/web/src/stores/use-config-store.ts b/web/src/stores/use-config-store.ts index 92b8cd305..45c346cf4 100644 --- a/web/src/stores/use-config-store.ts +++ b/web/src/stores/use-config-store.ts @@ -7,7 +7,7 @@ import { scopedLocalStorage } from "@/lib/user-scope"; import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options"; export type ApiCallFormat = "openai" | "gemini"; -export type ChannelInterfaceType = "chat-completion" | "openai-response" | "openai-image" | "newapi" | "newapi-channel-1" | "newapi-channel-2"; +export type ChannelInterfaceType = "chat-completion" | "openai-response" | "openai-image" | "newapi" | "newapi-channel-1" | "newapi-channel-2" | "xai-video"; export type ModelChannel = { id: string; @@ -441,14 +441,14 @@ export function defaultBaseUrlForApiFormat(apiFormat: ApiCallFormat) { } export function defaultBaseUrlForChannelInterface(interfaceType?: ChannelInterfaceType) { - if (interfaceType === "newapi" || interfaceType === "newapi-channel-1" || interfaceType === "newapi-channel-2") return ""; + if (interfaceType === "newapi" || interfaceType === "newapi-channel-1" || interfaceType === "newapi-channel-2" || interfaceType === "xai-video") return ""; return OPENAI_BASE_URL; } function capabilityForChannelInterface(interfaceType?: ChannelInterfaceType): ModelCapability | undefined { if (interfaceType === "chat-completion" || interfaceType === "openai-response") return "text"; if (interfaceType === "openai-image") return "image"; - if (interfaceType === "newapi" || interfaceType === "newapi-channel-1" || interfaceType === "newapi-channel-2") return "video"; + if (interfaceType === "newapi" || interfaceType === "newapi-channel-1" || interfaceType === "newapi-channel-2" || interfaceType === "xai-video") return "video"; return undefined; } @@ -457,7 +457,7 @@ function normalizeApiFormat(apiFormat: unknown): ApiCallFormat { } function normalizeChannelInterfaceType(value: unknown): ChannelInterfaceType | undefined { - return value === "chat-completion" || value === "openai-response" || value === "openai-image" || value === "newapi" || value === "newapi-channel-1" || value === "newapi-channel-2" ? value : undefined; + return value === "chat-completion" || value === "openai-response" || value === "openai-image" || value === "newapi" || value === "newapi-channel-1" || value === "newapi-channel-2" || value === "xai-video" ? value : undefined; } function uniqueRawModels(models: string[]) {