diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 90a37532e2..a9a09d5202 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -477,8 +477,8 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "ovhcloud", modelId: "Qwen3.6-27B", displayName: "Qwen3.6 27B (OVH anonymous)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "ovhcloud-anon", tos: "ok" }, { provider: "ovhcloud", modelId: "Mistral-Small-3.2-24B-Instruct-2506", displayName: "Mistral Small 3.2 24B (OVH anonymous)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "ovhcloud-anon", tos: "ok" }, { provider: "ovhcloud", modelId: "Qwen2.5-VL-72B-Instruct", displayName: "Qwen2.5 VL 72B (OVH anonymous)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "ovhcloud-anon", tos: "ok" }, - { provider: "agnes", modelId: "agnes-2.0-flash", displayName: "Agnes 2.0 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" }, - { provider: "agnes", modelId: "agnes-1.5-flash", displayName: "Agnes 1.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" }, + { provider: "agnes", modelId: "agnes-2.5-pro", displayName: "Agnes 2.5 Pro", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" }, + { provider: "agnes", modelId: "agnes-2.5-flash", displayName: "Agnes 2.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" }, { provider: "glm", modelId: "glm-4.7-flash", displayName: "GLM-4.7-Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "zhipu-flash-free", tos: "ok" }, { provider: "glm", modelId: "glm-4.5-flash", displayName: "GLM-4.5-Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "zhipu-flash-free", tos: "ok" }, { provider: "navy", modelId: "shared-pool", displayName: "NavyAI free pool (150K tokens/day, shared)", monthlyTokens: 4500000, creditTokens: 0, freeType: "recurring-daily", poolKey: "navy-free", tos: "ok" }, diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 5039cc105c..26ad771d30 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -150,6 +150,23 @@ function resolveAliasImageRequired(alias, modelConfig) { } export const IMAGE_PROVIDERS: Record = { + agnes: { + id: "agnes", + baseUrl: "https://apihub.agnes-ai.com/v1/images/generations", + authType: "apikey", + authHeader: "bearer", + format: "agnes-image", + models: [ + { + id: "agnes-image-2.1-flash", + name: "Agnes Image 2.1 Flash", + inputModalities: ["text", "image"], + description: "Agnes text-to-image, image-to-image, and multi-image composition model", + }, + ], + supportedSizes: ["1K", "2K", "3K", "4K"], + }, + "qwen-cloud-token-plan": { id: "qwen-cloud-token-plan", alias: "qct", diff --git a/open-sse/config/providers/registry/agnes/index.ts b/open-sse/config/providers/registry/agnes/index.ts index c3120a562c..848ad0242c 100644 --- a/open-sse/config/providers/registry/agnes/index.ts +++ b/open-sse/config/providers/registry/agnes/index.ts @@ -1,14 +1,17 @@ import type { RegistryEntry } from "../../shared.ts"; -import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; -export const agnesProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ +export const agnesProvider: RegistryEntry = { id: "agnes", - baseUrl: "https://apihub.agnes-ai.com/v1/chat/completions", + format: "openai-responses", + executor: "default", + baseUrl: "https://apihub.agnes-ai.com/v1/responses", + authType: "apikey", + authHeader: "bearer", models: [ { - id: "agnes-2.0-flash", - name: "Agnes 2.0 Flash", - contextLength: 524288, + id: "agnes-2.5-pro", + name: "Agnes 2.5 Pro", + contextLength: 1048576, maxOutputTokens: 65536, supportsReasoning: true, supportsVision: true, @@ -16,11 +19,14 @@ export const agnesProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ interleavedField: "reasoning_content", }, { - id: "agnes-1.5-flash", - name: "Agnes 1.5 Flash", - contextLength: 262144, + id: "agnes-2.5-flash", + name: "Agnes 2.5 Flash", + contextLength: 524288, maxOutputTokens: 65536, + supportsReasoning: true, supportsVision: true, + toolCalling: true, + interleavedField: "reasoning_content", }, ], -}); +}; diff --git a/open-sse/config/videoRegistry.ts b/open-sse/config/videoRegistry.ts index b9de79b355..27dc28ab06 100644 --- a/open-sse/config/videoRegistry.ts +++ b/open-sse/config/videoRegistry.ts @@ -30,6 +30,21 @@ interface VideoProvider { } export const VIDEO_PROVIDERS: Record = { + agnes: { + id: "agnes", + baseUrl: "https://apihub.agnes-ai.com", + statusUrl: "https://apihub.agnes-ai.com/agnesapi", + authType: "apikey", + authHeader: "bearer", + format: "agnes-video-job", + models: [ + { + id: "agnes-video-v2.0", + name: "Agnes Video V2.0", + }, + ], + }, + "qwen-cloud-token-plan": { id: "qwen-cloud-token-plan", alias: "qct", diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index 76770bf679..485a31659f 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -627,6 +627,17 @@ export async function handleImageGeneration({ }); } + if ( + providerConfig.format === "agnes-image" && + (typeof body.size !== "string" || body.size.trim().length === 0) + ) { + return { + success: false, + status: 400, + error: "Size is required for Agnes Image 2.1 Flash", + }; + } + if ( providerConfig.format === "alibaba-image" || providerConfig.format === "qwen-cloud-image" || @@ -1017,6 +1028,33 @@ async function handleGeminiImageGeneration({ model, providerConfig, body, creden /** * Handle OpenAI-compatible image generation (standard providers + Nebius fallback) */ +function buildAgnesImageRequestBody(model, body) { + const upstreamBody: Record = { + model, + prompt: body.prompt, + }; + + if (body.size !== undefined) upstreamBody.size = body.size; + if (body.ratio !== undefined) { + upstreamBody.ratio = body.ratio; + } else if (body.aspect_ratio !== undefined) { + upstreamBody.ratio = body.aspect_ratio; + } + if (body.return_base64 !== undefined) upstreamBody.return_base64 = body.return_base64; + + const explicitExtraBody = + body.extra_body && typeof body.extra_body === "object" && !Array.isArray(body.extra_body) + ? body.extra_body + : {}; + const extraBody: Record = { ...explicitExtraBody }; + const { imageUrls } = extractImageInputs(body); + if (imageUrls.length > 0) extraBody.image = imageUrls; + if (body.response_format !== undefined) extraBody.response_format = body.response_format; + if (Object.keys(extraBody).length > 0) upstreamBody.extra_body = extraBody; + + return upstreamBody; +} + async function handleOpenAIImageGeneration({ model, provider, @@ -1040,21 +1078,26 @@ async function handleOpenAIImageGeneration({ }; // Build upstream request (OpenAI-compatible format) - const upstreamBody: Record = { - model: model, - prompt: body.prompt, - }; + const upstreamBody: Record = + providerConfig.format === "agnes-image" + ? buildAgnesImageRequestBody(model, body) + : { + model, + prompt: body.prompt, + }; - // Pass optional parameters - if (body.n !== undefined) upstreamBody.n = body.n; - if (body.size !== undefined) upstreamBody.size = body.size; - if (body.quality !== undefined) upstreamBody.quality = body.quality; - if (body.response_format !== undefined) upstreamBody.response_format = body.response_format; - if (body.style !== undefined) upstreamBody.style = body.style; + if (providerConfig.format !== "agnes-image") { + // Pass optional parameters for ordinary OpenAI-compatible providers. + if (body.n !== undefined) upstreamBody.n = body.n; + if (body.size !== undefined) upstreamBody.size = body.size; + if (body.quality !== undefined) upstreamBody.quality = body.quality; + if (body.response_format !== undefined) upstreamBody.response_format = body.response_format; + if (body.style !== undefined) upstreamBody.style = body.style; - const { imageUrl } = extractImageInputs(body); - if (imageUrl && OPENAI_IMAGE_TO_IMAGE_MODELS.has(model)) { - upstreamBody.image_url = imageUrl; + const { imageUrl } = extractImageInputs(body); + if (imageUrl && OPENAI_IMAGE_TO_IMAGE_MODELS.has(model)) { + upstreamBody.image_url = imageUrl; + } } // Build headers diff --git a/open-sse/handlers/videoGeneration.ts b/open-sse/handlers/videoGeneration.ts index bbf60fa3bb..ff09a95a35 100644 --- a/open-sse/handlers/videoGeneration.ts +++ b/open-sse/handlers/videoGeneration.ts @@ -194,6 +194,15 @@ export async function handleVideoGeneration({ body, credentials, log, resolvedPr log, }); } + if (getVideoJobPreset(providerConfig.format)) { + return handleVideoJobGeneration({ + model, + presetName: providerConfig.format, + body, + credentials, + log, + }); + } if (providerConfig.format === "openai-video") { return handleOpenAIVideoGeneration({ model, provider, providerConfig, body, credentials, log }); } diff --git a/open-sse/handlers/videoGeneration/job.ts b/open-sse/handlers/videoGeneration/job.ts index aea6a8b9cc..030fe97a48 100644 --- a/open-sse/handlers/videoGeneration/job.ts +++ b/open-sse/handlers/videoGeneration/job.ts @@ -104,10 +104,10 @@ const VIDEO_JOB_PRESETS: Record = { "agnes-video-job": { id: "agnes-video-job", displayName: "Agnes Video V2.0", - authHeaderName: "x-api-key", - authScheme: "raw", - // Real default, matching the Agnes Video V2.0 reference: POST /v1/videos with - // x-api-key auth; GET /v1/videos/{task_id} returns status/progress/metadata. + authHeaderName: "Authorization", + authScheme: "bearer", + // Official Agnes flow: POST /v1/videos returns video_id, then the recommended + // status endpoint GET /agnesapi?video_id=… exposes status and metadata.url. baseUrlFallback: "https://apihub.agnes-ai.com", submit: { method: "POST", @@ -120,8 +120,8 @@ const VIDEO_JOB_PRESETS: Record = { ...extras, }), }, - taskIdPath: "task_id", - poll: { pathTemplate: "/v1/videos/{taskId}" }, + taskIdPath: "video_id", + poll: { pathTemplate: "/agnesapi?video_id={taskId}" }, statusPath: "status", statusDone: ["completed"], statusFailed: ["failed"], diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 8c20da4471..84f5b4b83c 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -115,6 +115,7 @@ export const ENTERPRISE_CLOUD_PROVIDER_IDS = new Set([ ]); export const VIDEO_PROVIDER_IDS = new Set([ + "agnes", "runwayml", "veoaifree-web", "pollinations", diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 8efd332b27..3abe62192d 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -89,7 +89,7 @@ } }, "agnes": { - "format": "openai", + "format": "openai-responses", "headers": { "apiKey": { "Accept": "text/event-stream", @@ -107,8 +107,8 @@ } }, "url": { - "nonStream": "https://apihub.agnes-ai.com/v1/chat/completions", - "stream": "https://apihub.agnes-ai.com/v1/chat/completions" + "nonStream": "https://apihub.agnes-ai.com/v1/responses", + "stream": "https://apihub.agnes-ai.com/v1/responses" } }, "agy": { diff --git a/tests/unit/agnes-provider.test.ts b/tests/unit/agnes-provider.test.ts index 784703ebff..90e9a26047 100644 --- a/tests/unit/agnes-provider.test.ts +++ b/tests/unit/agnes-provider.test.ts @@ -1,10 +1,33 @@ import test from "node:test"; import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-agnes-provider-")); +process.env.DATA_DIR = TEST_DATA_DIR; const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); +const { VIDEO_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts"); const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts"); +const { IMAGE_PROVIDERS, getAllImageModels } = + await import("../../open-sse/config/imageRegistry.ts"); +const { VIDEO_PROVIDERS, getAllVideoModels } = + await import("../../open-sse/config/videoRegistry.ts"); +const { FREE_MODEL_BUDGETS } = await import("../../open-sse/config/freeModelCatalog.ts"); +const { DefaultExecutor } = await import("../../open-sse/executors/default.ts"); +const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts"); +const { handleVideoGeneration } = await import("../../open-sse/handlers/videoGeneration.ts"); +const { resolveChatCoreTargetFormat } = + await import("../../open-sse/handlers/chatCore/targetFormat.ts"); +const dbCore = await import("../../src/lib/db/core.ts"); -const AGNES_CHAT_URL = "https://apihub.agnes-ai.com/v1/chat/completions"; +test.after(() => { + dbCore.closeDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +const AGNES_RESPONSES_URL = "https://apihub.agnes-ai.com/v1/responses"; test("agnes is registered as an API-key provider with complete metadata", () => { const entry = APIKEY_PROVIDERS.agnes; @@ -21,37 +44,67 @@ test("agnes is registered as an API-key provider with complete metadata", () => assert.ok(entry.authHint, "authHint must be defined"); }); -test("agnes registry entry uses OpenAI format with bearer API-key auth", () => { +test("agnes registry entry uses Responses format with bearer API-key auth", () => { const entry = providerRegistry.agnes; assert.ok(entry, "providerRegistry.agnes must be defined"); assert.equal(entry.id, "agnes"); - assert.equal(entry.format, "openai"); + assert.equal(entry.format, "openai-responses"); assert.equal(entry.executor, "default"); assert.equal(entry.authType, "apikey"); assert.equal(entry.authHeader, "bearer"); - assert.equal(entry.baseUrl, AGNES_CHAT_URL); + assert.equal(entry.baseUrl, AGNES_RESPONSES_URL); }); -test("agnes ships two models with correct capabilities", () => { +test("agnes routes Chat Completions clients through its Responses upstream", () => { + const { targetFormat } = resolveChatCoreTargetFormat({ + provider: "agnes", + resolvedModel: "agnes-2.5-pro", + apiFormat: undefined, + sourceFormat: "openai", + customModelTargetFormat: undefined, + providerSpecificData: null, + }); + + assert.equal(targetFormat, "openai-responses"); + assert.equal( + new DefaultExecutor("agnes").buildUrl("agnes-2.5-pro", true, 0, null), + AGNES_RESPONSES_URL + ); +}); + +test("agnes ships the current public chat models with correct capabilities", () => { const entry = providerRegistry.agnes; - assert.equal(entry.models.length, 2, "must have 2 models"); + assert.deepEqual( + entry.models.map((model) => model.id), + ["agnes-2.5-pro", "agnes-2.5-flash"] + ); - const flash2 = entry.models.find((m) => m.id === "agnes-2.0-flash"); - assert.ok(flash2, "agnes-2.0-flash must be defined"); - assert.equal(flash2.contextLength, 524288); - assert.equal(flash2.maxOutputTokens, 65536); - assert.equal(flash2.supportsReasoning, true); - assert.equal(flash2.supportsVision, true); - assert.equal(flash2.toolCalling, true); - assert.equal(flash2.interleavedField, "reasoning_content"); + const flash25 = entry.models.find((m) => m.id === "agnes-2.5-flash"); + assert.ok(flash25, "agnes-2.5-flash must be defined"); + assert.equal(flash25.contextLength, 524288); + assert.equal(flash25.maxOutputTokens, 65536); + assert.equal(flash25.supportsReasoning, true); + assert.equal(flash25.supportsVision, true); + assert.equal(flash25.toolCalling, true); + assert.equal(flash25.interleavedField, "reasoning_content"); - const flash15 = entry.models.find((m) => m.id === "agnes-1.5-flash"); - assert.ok(flash15, "agnes-1.5-flash must be defined"); - assert.equal(flash15.contextLength, 262144); - assert.equal(flash15.maxOutputTokens, 65536); - assert.equal(flash15.supportsVision, true); - assert.equal(flash15.supportsReasoning, undefined, "1.5-flash has no thinking mode"); - assert.equal(flash15.toolCalling, undefined, "1.5-flash has no documented tool calling"); + const pro25 = entry.models.find((model) => model.id === "agnes-2.5-pro"); + assert.ok(pro25, "agnes-2.5-pro must be defined"); + assert.equal(pro25.contextLength, 1048576); + assert.equal(pro25.maxOutputTokens, 65536); + assert.equal(pro25.supportsReasoning, true); + assert.equal(pro25.supportsVision, true); + assert.equal(pro25.toolCalling, true); + assert.equal(pro25.interleavedField, "reasoning_content"); +}); + +test("agnes free catalog exposes the current free chat models through one shared pool", () => { + const rows = FREE_MODEL_BUDGETS.filter((model) => model.provider === "agnes"); + assert.deepEqual( + rows.map((model) => model.modelId), + ["agnes-2.5-pro", "agnes-2.5-flash"] + ); + assert.ok(rows.every((model) => model.poolKey === "agnes-free")); }); test("agnes has no collision with zenmux-free sapiens-ai prefixed models", (t) => { @@ -68,3 +121,202 @@ test("agnes has no collision with zenmux-free sapiens-ai prefixed models", (t) = ); } }); + +test("agnes registers Image 2.1 Flash on the current image-generation contract", () => { + const entry = IMAGE_PROVIDERS.agnes; + assert.ok(entry, "IMAGE_PROVIDERS.agnes must be defined"); + assert.equal(entry.baseUrl, "https://apihub.agnes-ai.com/v1/images/generations"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.format, "agnes-image"); + assert.deepEqual(entry.supportedSizes, ["1K", "2K", "3K", "4K"]); + assert.deepEqual(entry.models, [ + { + id: "agnes-image-2.1-flash", + name: "Agnes Image 2.1 Flash", + inputModalities: ["text", "image"], + description: "Agnes text-to-image, image-to-image, and multi-image composition model", + }, + ]); + assert.ok(getAllImageModels().some((model) => model.id === "agnes/agnes-image-2.1-flash")); +}); + +test("agnes Image 2.1 maps standard image inputs into extra_body", async () => { + const originalFetch = globalThis.fetch; + let captured: + { url: string; headers: Record; body: Record } | undefined; + + globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => { + captured = { + url: String(url), + headers: init?.headers as Record, + body: JSON.parse(String(init?.body)) as Record, + }; + return new Response( + JSON.stringify({ + created: 123, + data: [{ b64_json: "generated-image", revised_prompt: "combined references" }], + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + }) as typeof fetch; + + try { + const result = await handleImageGeneration({ + body: { + model: "agnes/agnes-image-2.1-flash", + prompt: "Combine both references into one cinematic poster", + size: "2K", + aspect_ratio: "16:9", + image_urls: ["https://example.com/one.png", "data:image/png;base64,dHdv"], + response_format: "b64_json", + extra_body: { workflow_hint: "preserve-composition" }, + }, + credentials: { apiKey: "agnes-key" }, + log: null, + }); + + assert.equal(result.success, true); + assert.ok(captured, "Agnes image request must be sent upstream"); + assert.equal(captured.url, "https://apihub.agnes-ai.com/v1/images/generations"); + assert.equal(captured.headers.Authorization, "Bearer agnes-key"); + assert.deepEqual(captured.body, { + model: "agnes-image-2.1-flash", + prompt: "Combine both references into one cinematic poster", + size: "2K", + ratio: "16:9", + extra_body: { + workflow_hint: "preserve-composition", + image: ["https://example.com/one.png", "data:image/png;base64,dHdv"], + response_format: "b64_json", + }, + }); + assert.equal(result.data.data[0].b64_json, "generated-image"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("agnes Image 2.1 requires the current size parameter", async () => { + const result = await handleImageGeneration({ + body: { + model: "agnes/agnes-image-2.1-flash", + prompt: "A detailed cityscape", + }, + credentials: { apiKey: "agnes-key" }, + log: null, + }); + + assert.equal(result.success, false); + assert.equal(result.status, 400); + assert.equal(result.error, "Size is required for Agnes Image 2.1 Flash"); +}); + +test("agnes registers Video V2.0 on the current video_id job contract", () => { + const entry = VIDEO_PROVIDERS.agnes; + assert.ok(entry, "VIDEO_PROVIDERS.agnes must be defined"); + assert.equal(entry.baseUrl, "https://apihub.agnes-ai.com"); + assert.equal(entry.statusUrl, "https://apihub.agnes-ai.com/agnesapi"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.format, "agnes-video-job"); + assert.deepEqual(entry.models, [{ id: "agnes-video-v2.0", name: "Agnes Video V2.0" }]); + assert.equal(VIDEO_PROVIDER_IDS.has("agnes"), true); + assert.ok(getAllVideoModels().some((model) => model.id === "agnes/agnes-video-v2.0")); +}); + +test("agnes Video V2.0 submits with Bearer auth and polls by video_id", async () => { + const originalFetch = globalThis.fetch; + const originalSetTimeout = globalThis.setTimeout; + const calls: Array<{ + url: string; + method: string; + headers: Record; + body?: Record; + }> = []; + + globalThis.setTimeout = ((callback: (...args: unknown[]) => void, _ms?: number, ...args) => { + callback(...args); + return 0; + }) as typeof setTimeout; + globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => { + const call = { + url: String(url), + method: init?.method || "GET", + headers: init?.headers as Record, + ...(init?.body ? { body: JSON.parse(String(init.body)) as Record } : {}), + }; + calls.push(call); + + if (call.method === "POST") { + return new Response( + JSON.stringify({ + id: "task-123", + task_id: "task-123", + video_id: "video-123", + status: "queued", + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + return new Response( + JSON.stringify({ + status: "completed", + metadata: { url: "https://platform-outputs.agnes-ai.space/video-123.mp4" }, + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + }) as typeof fetch; + + try { + const result = await handleVideoGeneration({ + body: { + model: "agnes/agnes-video-v2.0", + prompt: "A product rotates slowly under studio lighting", + width: 1152, + height: 768, + num_frames: 121, + frame_rate: 24, + extra_body: { + image: ["https://example.com/keyframe-one.png", "https://example.com/keyframe-two.png"], + mode: "keyframes", + }, + }, + credentials: { apiKey: "agnes-key" }, + log: null, + }); + + assert.equal(result.success, true); + assert.equal(result.data.data[0].url, "https://platform-outputs.agnes-ai.space/video-123.mp4"); + assert.equal(calls.length, 2); + assert.deepEqual(calls[0], { + url: "https://apihub.agnes-ai.com/v1/videos", + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer agnes-key", + }, + body: { + model: "agnes-video-v2.0", + prompt: "A product rotates slowly under studio lighting", + width: 1152, + height: 768, + num_frames: 121, + frame_rate: 24, + extra_body: { + image: ["https://example.com/keyframe-one.png", "https://example.com/keyframe-two.png"], + mode: "keyframes", + }, + }, + }); + assert.deepEqual(calls[1], { + url: "https://apihub.agnes-ai.com/agnesapi?video_id=video-123", + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer agnes-key", + }, + }); + } finally { + globalThis.fetch = originalFetch; + globalThis.setTimeout = originalSetTimeout; + } +}); diff --git a/tests/unit/video-custom-provider-route.test.ts b/tests/unit/video-custom-provider-route.test.ts index 57f0960ada..749bef97d8 100644 --- a/tests/unit/video-custom-provider-route.test.ts +++ b/tests/unit/video-custom-provider-route.test.ts @@ -30,7 +30,7 @@ function immediateButSafeTimeout( ms?: number, ...args: unknown[] ) { - if (ms === 20_000 || ms === 5_000) { + if (ms === 20_000 || ms === 5_000 || ms === 2_000) { return originalSetTimeout(callback as TimerHandler, 0, ...args); } return originalSetTimeout(callback as TimerHandler, ms, ...args); @@ -208,12 +208,12 @@ test("video route dispatches submit→poll job flow for custom model with agnes- calls.push({ url: stringUrl, method, body: requestBody, headers }); if (stringUrl === "https://custom.example.com/v1/videos") { - return createResponse(JSON.stringify({ task_id: "task-123" }), { + return createResponse(JSON.stringify({ video_id: "video-123", task_id: "task-123" }), { status: 200, headers: { "content-type": "application/json" }, }); } - if (stringUrl === "https://custom.example.com/v1/videos/task-123") { + if (stringUrl === "https://custom.example.com/agnesapi?video_id=video-123") { return createResponse( JSON.stringify({ status: "completed", @@ -250,13 +250,13 @@ test("video route dispatches submit→poll job flow for custom model with agnes- assert.equal(calls.length, 2); assert.equal(calls[0].method, "POST"); assert.equal(calls[0].url, "https://custom.example.com/v1/videos"); - assert.equal(calls[0].headers["x-api-key"], "custom-key"); + assert.equal(calls[0].headers.Authorization, "Bearer custom-key"); assert.deepEqual(calls[0].body, { model: "job-video-v1", prompt: "a cat playing piano", }); assert.equal(calls[1].method, "GET"); - assert.equal(calls[1].url, "https://custom.example.com/v1/videos/task-123"); + assert.equal(calls[1].url, "https://custom.example.com/agnesapi?video_id=video-123"); }); test("video route returns 502 when job preset reports failed status", async () => { @@ -284,7 +284,7 @@ test("video route returns 502 when job preset reports failed status", async () = globalThis.fetch = (async (url: unknown, init?: RequestInit) => { if (String(url).endsWith("/v1/videos")) { - return createResponse(JSON.stringify({ task_id: "task-fail" }), { + return createResponse(JSON.stringify({ video_id: "video-fail", task_id: "task-fail" }), { status: 200, headers: { "content-type": "application/json" }, });