mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 21:32:20 +03:00
feat(providers): Agnes 3.0 Flash catalog, CN host, live /v1/models (#13120)
Live-verified end to end: 1.5 gone from `GET /v1/models` with a 503, 3.0 answering 200 on a pong probe, and the CN-region host difference across 10 accounts. Aliasing 1.5 to 3.0 through `BUILT_IN_ALIASES` rather than leaving a dead id is the right retirement path, and noting that the #11503 as-is guard must not skip the rewrite is the detail that makes it work. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017). - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓ - complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline - 531 of 532 focused assertions green across the batch's 46 test files - `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here). Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
This commit is contained in:
1
changelog.d/features/agnes-30-flash-catalog.md
Normal file
1
changelog.d/features/agnes-30-flash-catalog.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(providers): **list Agnes 3.0 Flash as the current free chat model, drop retired 1.5 Flash, add Image 2.0/2.5 Flash plus Video 2.5/2.5 Flash, and discover the live `/v1/models` catalog (including the CN host `api.agnes-ai.cn`).** `agnes-1.5-flash` now forwards to `agnes-3.0-flash`. Video 2.5 polls `GET /v1/videos/{id}` (not the V2.0 `/agnesapi` contract). Live `/v1/models` (2026-09-09) no longer serves 1.5; the wiki marks it deprecated. 3.0 Flash is 512K context / 65,536 max output, same window as 2.5. CN-region keys use the existing per-connection base-URL field, default stays `apihub.agnes-ai.com`.
|
||||
@@ -22,7 +22,7 @@ import type { FreeModelBudget } from "./freeModelCatalog.ts";
|
||||
* rewrites file timestamps on every deploy, which would report a months-old
|
||||
* catalog as "updated today". Bump this whenever the entries below change.
|
||||
*/
|
||||
export const FREE_CATALOG_CURATED_AT = "2026-09-03";
|
||||
export const FREE_CATALOG_CURATED_AT = "2026-09-09";
|
||||
|
||||
export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
|
||||
{ provider: "agentrouter", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
|
||||
@@ -458,9 +458,11 @@ 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-1.5-flash", displayName: "Agnes 1.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" },
|
||||
// evidence: public-page wiki.agnes-ai.com/docs/pricing 2026-09-09 current $0
|
||||
// for 2.0/2.5 flash; live GET /v1/models lists 3.0-flash (1.5-flash 503, retired).
|
||||
{ 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-2.5-flash", displayName: "Agnes 2.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "agnes-free", tos: "ok" },
|
||||
{ provider: "agnes", modelId: "agnes-3.0-flash", displayName: "Agnes 3.0 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" },
|
||||
|
||||
@@ -168,12 +168,24 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
authHeader: "bearer",
|
||||
format: "agnes-image",
|
||||
models: [
|
||||
{
|
||||
id: "agnes-image-2.0-flash",
|
||||
name: "Agnes Image 2.0 Flash",
|
||||
inputModalities: ["text", "image"],
|
||||
description: "Agnes text-to-image, image-to-image, and multi-image composition model",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
id: "agnes-image-2.5-flash",
|
||||
name: "Agnes Image 2.5 Flash",
|
||||
inputModalities: ["text", "image"],
|
||||
description: "Agnes text-to-image, image-to-image, and multi-image composition model",
|
||||
},
|
||||
],
|
||||
supportedSizes: ["1K", "2K", "3K", "4K"],
|
||||
},
|
||||
|
||||
@@ -5,17 +5,10 @@ export const agnesProvider: RegistryEntry = {
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://apihub.agnes-ai.com/v1/chat/completions",
|
||||
modelsUrl: "https://apihub.agnes-ai.com/v1/models",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [
|
||||
{
|
||||
id: "agnes-1.5-flash",
|
||||
name: "Agnes 1.5 Flash",
|
||||
contextLength: 262144,
|
||||
maxOutputTokens: 65536,
|
||||
supportsVision: true,
|
||||
toolCalling: true,
|
||||
},
|
||||
{
|
||||
id: "agnes-2.0-flash",
|
||||
name: "Agnes 2.0 Flash",
|
||||
@@ -35,5 +28,17 @@ export const agnesProvider: RegistryEntry = {
|
||||
toolCalling: true,
|
||||
interleavedField: "reasoning_content",
|
||||
},
|
||||
{
|
||||
// Wiki (2026-09-10) lists agnes-3.0-flash at 512K context / 65,536 max
|
||||
// output, same window as 2.5 Flash. Live GET /v1/models includes it.
|
||||
id: "agnes-3.0-flash",
|
||||
name: "Agnes 3.0 Flash",
|
||||
contextLength: 524288,
|
||||
maxOutputTokens: 65536,
|
||||
supportsReasoning: true,
|
||||
supportsVision: true,
|
||||
toolCalling: true,
|
||||
interleavedField: "reasoning_content",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@ interface VideoModel {
|
||||
isMarket?: boolean;
|
||||
supportedSizes?: string[];
|
||||
mediaCapabilities?: Record<string, unknown>;
|
||||
/** Override the provider-level job preset for this model. */
|
||||
jobPreset?: string;
|
||||
}
|
||||
|
||||
interface VideoProvider {
|
||||
@@ -48,6 +50,16 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
|
||||
id: "agnes-video-v2.0",
|
||||
name: "Agnes Video V2.0",
|
||||
},
|
||||
{
|
||||
id: "agnes-video-2.5-flash",
|
||||
name: "Agnes Video 2.5 Flash",
|
||||
jobPreset: "agnes-video-2.5-job",
|
||||
},
|
||||
{
|
||||
id: "agnes-video-2.5",
|
||||
name: "Agnes Video 2.5",
|
||||
jobPreset: "agnes-video-2.5-job",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -195,10 +195,13 @@ export async function handleVideoGeneration({ body, credentials, log, resolvedPr
|
||||
log,
|
||||
});
|
||||
}
|
||||
if (getVideoJobPreset(providerConfig.format)) {
|
||||
const modelJobPreset = providerConfig.models.find((entry) => entry.id === model)?.jobPreset;
|
||||
const jobPresetName =
|
||||
typeof modelJobPreset === "string" && modelJobPreset ? modelJobPreset : providerConfig.format;
|
||||
if (getVideoJobPreset(jobPresetName)) {
|
||||
return handleVideoJobGeneration({
|
||||
model,
|
||||
presetName: providerConfig.format,
|
||||
presetName: jobPresetName,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
|
||||
@@ -129,6 +129,36 @@ const VIDEO_JOB_PRESETS: Record<string, VideoJobPreset> = {
|
||||
maxPolls: 60,
|
||||
pollIntervalMs: 2000,
|
||||
},
|
||||
"agnes-video-2.5-job": {
|
||||
id: "agnes-video-2.5-job",
|
||||
displayName: "Agnes Video 2.5",
|
||||
authHeaderName: "Authorization",
|
||||
authScheme: "bearer",
|
||||
// Wiki 2026-09-09 + live probe: POST /v1/videos returns `id`, poll GET /v1/videos/{id}, result `url`.
|
||||
// seconds is a string. Do not reuse agnes-video-job (video_id + /agnesapi).
|
||||
baseUrlFallback: "https://apihub.agnes-ai.com",
|
||||
submit: {
|
||||
method: "POST",
|
||||
path: "/v1/videos",
|
||||
buildBody: ({ model, prompt, extras }) => {
|
||||
const seconds = extras.seconds;
|
||||
return {
|
||||
model,
|
||||
prompt,
|
||||
...extras,
|
||||
...(typeof seconds === "number" ? { seconds: String(seconds) } : {}),
|
||||
};
|
||||
},
|
||||
},
|
||||
taskIdPath: "id",
|
||||
poll: { pathTemplate: "/v1/videos/{taskId}" },
|
||||
statusPath: "status",
|
||||
statusDone: ["completed"],
|
||||
statusFailed: ["failed"],
|
||||
resultPath: "url",
|
||||
maxPolls: 60,
|
||||
pollIntervalMs: 2000,
|
||||
},
|
||||
"muapi-video-job": {
|
||||
id: "muapi-video-job",
|
||||
displayName: "muapi.ai",
|
||||
|
||||
@@ -65,9 +65,13 @@ const BUILT_IN_ALIASES: Record<string, string> = {
|
||||
// Llama short aliases
|
||||
"llama-3.3": "llama-3.3-70b-versatile",
|
||||
"llama-3-70b": "llama-3.3-70b-versatile",
|
||||
// #11503: llama3-8b-8192 was deprecated by Groq on 2025-08-30 and is not in the
|
||||
// #11503: llama3-8b-8192 deprecated on Groq 2025-08-30 and not in the
|
||||
// catalog; llama-3.1-8b-instant is the replacement Groq names.
|
||||
"llama-3-8b": "llama-3.1-8b-instant",
|
||||
|
||||
// Agnes 1.5 Flash: wiki marks deprecated; live GET /v1/models
|
||||
// (2026-09-09) no longer lists it (503 no channel).
|
||||
"agnes-1.5-flash": "agnes-3.0-flash",
|
||||
};
|
||||
|
||||
// ── Custom Aliases (persisted via Settings API) ─────────────────────────────
|
||||
|
||||
@@ -244,6 +244,10 @@ export const CONFIGURABLE_BASE_URL_PROVIDERS = new Set([
|
||||
// the existing override affordance for these two ids.
|
||||
"kimi",
|
||||
"moonshot",
|
||||
// Agnes CN-region keys (agnes-ai.cn) use a separate host from
|
||||
// the international apihub.agnes-ai.com default. Same always-on field as
|
||||
// #7447 Kimi/Moonshot so Add-connection can point at api.agnes-ai.cn.
|
||||
"agnes",
|
||||
]);
|
||||
|
||||
export const DEFAULT_PROVIDER_BASE_URLS: Record<string, string> = {
|
||||
@@ -261,6 +265,7 @@ export const DEFAULT_PROVIDER_BASE_URLS: Record<string, string> = {
|
||||
// before; a CN-region user overrides it (see placeholder hint below).
|
||||
kimi: "https://api.moonshot.ai/v1",
|
||||
moonshot: "https://api.moonshot.ai/v1",
|
||||
agnes: "https://apihub.agnes-ai.com/v1",
|
||||
};
|
||||
|
||||
export function getLocalProviderMetadata(providerId?: string | null) {
|
||||
@@ -372,6 +377,8 @@ export function getProviderBaseUrlPlaceholder(providerId?: string | null) {
|
||||
// #7447 — surfaces the CN-region alternative host as the placeholder
|
||||
// example (mirrors the siliconflow.com/siliconflow.cn pattern above).
|
||||
return "https://api.moonshot.cn/v1";
|
||||
case "agnes":
|
||||
return "https://api.agnes-ai.cn/v1";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -100,6 +100,11 @@ export const NAMED_OPENAI_STYLE_PROVIDERS = new Set([
|
||||
// (11 chat-capable). Live fetch keeps it fresh; the registry seed stays as the
|
||||
// offline fallback.
|
||||
"logfare",
|
||||
// Agnes hosts a live OpenAI-style /v1/models catalog on both the
|
||||
// international (apihub.agnes-ai.com) and CN (api.agnes-ai.cn) hosts.
|
||||
// Without this, sync-models serves the static registry seed and CN
|
||||
// connections never discover 2.5/3.0 Flash.
|
||||
"agnes",
|
||||
]);
|
||||
|
||||
export function isNamedOpenAIStyleProvider(provider: string): boolean {
|
||||
|
||||
@@ -308,13 +308,19 @@ export const providerModelMutationSchema = z.object({
|
||||
.optional(),
|
||||
// #9820: optional async video-generation job preset for a custom
|
||||
// OpenAI-compatible provider whose /videos surface is a submit→poll API
|
||||
// (agnes-video-job, muapi-video-job, sora-job). Persisted on the custom model
|
||||
// (agnes-video-job, agnes-video-2.5-job, muapi-video-job, sora-job). Persisted on the custom model
|
||||
// row; the /v1/videos/generations handler branches on it between the
|
||||
// synchronous OpenAI-compatible path and the job/poll path. `"openai-video"`
|
||||
// is a legacy no-op value that keeps the sync handler selected.
|
||||
generationConfig: z
|
||||
.object({
|
||||
preset: z.enum(["agnes-video-job", "muapi-video-job", "sora-job", "openai-video"]),
|
||||
preset: z.enum([
|
||||
"agnes-video-job",
|
||||
"agnes-video-2.5-job",
|
||||
"muapi-video-job",
|
||||
"sora-job",
|
||||
"openai-video",
|
||||
]),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ const { handleImageGeneration } = await import("../../open-sse/handlers/imageGen
|
||||
const { handleVideoGeneration } = await import("../../open-sse/handlers/videoGeneration.ts");
|
||||
const { resolveChatCoreTargetFormat } =
|
||||
await import("../../open-sse/handlers/chatCore/targetFormat.ts");
|
||||
const { resolveModelAlias } = await import("../../open-sse/services/modelDeprecation.ts");
|
||||
const dbCore = await import("../../src/lib/db/core.ts");
|
||||
|
||||
test.after(() => {
|
||||
@@ -28,6 +29,8 @@ test.after(() => {
|
||||
});
|
||||
|
||||
const AGNES_CHAT_URL = "https://apihub.agnes-ai.com/v1/chat/completions";
|
||||
const AGNES_MODELS_URL = "https://apihub.agnes-ai.com/v1/models";
|
||||
const AGNES_CN_BASE_URL = "https://api.agnes-ai.cn/v1";
|
||||
|
||||
test("agnes is registered as an API-key provider with complete metadata", () => {
|
||||
const entry = APIKEY_PROVIDERS.agnes;
|
||||
@@ -72,20 +75,13 @@ test("agnes routes Chat Completions clients through its OpenAI chat upstream", (
|
||||
);
|
||||
});
|
||||
|
||||
test("agnes ships the current public chat models with correct capabilities", () => {
|
||||
test("agnes ships the current public chat models with the correct capabilities", () => {
|
||||
const entry = providerRegistry.agnes;
|
||||
assert.deepEqual(
|
||||
entry.models.map((model) => model.id),
|
||||
["agnes-1.5-flash", "agnes-2.0-flash", "agnes-2.5-flash"]
|
||||
["agnes-2.0-flash", "agnes-2.5-flash", "agnes-3.0-flash"]
|
||||
);
|
||||
|
||||
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.toolCalling, true);
|
||||
|
||||
const flash20 = entry.models.find((m) => m.id === "agnes-2.0-flash");
|
||||
assert.ok(flash20, "agnes-2.0-flash must be defined");
|
||||
assert.equal(flash20.contextLength, 262144);
|
||||
@@ -98,12 +94,59 @@ test("agnes ships the current public chat models with correct capabilities", ()
|
||||
assert.ok(flash25, "agnes-2.5-flash must be defined");
|
||||
assert.equal(flash25.contextLength, 524288);
|
||||
assert.equal(flash25.maxOutputTokens, 65536);
|
||||
|
||||
const flash30 = entry.models.find((m) => m.id === "agnes-3.0-flash");
|
||||
assert.ok(flash30, "agnes-3.0-flash must be defined");
|
||||
assert.equal(flash30.contextLength, 524288);
|
||||
assert.equal(flash30.maxOutputTokens, 65536);
|
||||
assert.equal(flash30.supportsReasoning, true);
|
||||
assert.equal(flash30.supportsVision, true);
|
||||
assert.equal(flash30.toolCalling, true);
|
||||
assert.equal(flash30.interleavedField, "reasoning_content");
|
||||
});
|
||||
|
||||
test("agnes registry advertises the live OpenAI-style /models endpoint", () => {
|
||||
const entry = providerRegistry.agnes;
|
||||
assert.equal(entry.modelsUrl, AGNES_MODELS_URL);
|
||||
});
|
||||
|
||||
test("agnes is classified for live OpenAI-style /models discovery", async () => {
|
||||
const { isNamedOpenAIStyleProvider } = await import(
|
||||
"../../src/app/api/providers/[id]/models/discovery/providerSets.ts"
|
||||
);
|
||||
assert.equal(isNamedOpenAIStyleProvider("agnes"), true);
|
||||
});
|
||||
|
||||
test("agnes honors per-connection CN base URL override", () => {
|
||||
const url = new DefaultExecutor("agnes").buildUrl("agnes-3.0-flash", true, 0, {
|
||||
providerSpecificData: { baseUrl: AGNES_CN_BASE_URL },
|
||||
});
|
||||
assert.equal(url, `${AGNES_CN_BASE_URL}/chat/completions`);
|
||||
});
|
||||
|
||||
test("agnes base-URL field is always-on so CN keys can point at api.agnes-ai.cn", async () => {
|
||||
const helpers = await import(
|
||||
"../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts"
|
||||
);
|
||||
assert.equal(helpers.isBaseUrlConfigurableProvider("agnes"), true);
|
||||
assert.equal(helpers.getProviderBaseUrlDefault("agnes"), "https://apihub.agnes-ai.com/v1");
|
||||
assert.equal(helpers.getProviderBaseUrlPlaceholder("agnes"), AGNES_CN_BASE_URL);
|
||||
});
|
||||
|
||||
test("agnes-1.5-flash is retired and forwards to agnes-3.0-flash", () => {
|
||||
const entry = providerRegistry.agnes;
|
||||
assert.equal(
|
||||
entry.models.some((model) => model.id === "agnes-1.5-flash"),
|
||||
false
|
||||
);
|
||||
assert.equal(resolveModelAlias("agnes-1.5-flash", "agnes"), "agnes-3.0-flash");
|
||||
});
|
||||
|
||||
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-1.5-flash", "agnes-2.0-flash", "agnes-2.5-flash"]
|
||||
["agnes-2.0-flash", "agnes-2.5-flash", "agnes-3.0-flash"]
|
||||
);
|
||||
assert.ok(rows.every((model) => model.poolKey === "agnes-free"));
|
||||
});
|
||||
@@ -123,22 +166,19 @@ 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", () => {
|
||||
test("agnes registers Image 2.x Flash models 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.deepEqual(
|
||||
entry.models.map((model) => model.id),
|
||||
["agnes-image-2.0-flash", "agnes-image-2.1-flash", "agnes-image-2.5-flash"]
|
||||
);
|
||||
assert.ok(getAllImageModels().some((model) => model.id === "agnes/agnes-image-2.1-flash"));
|
||||
assert.ok(getAllImageModels().some((model) => model.id === "agnes/agnes-image-2.5-flash"));
|
||||
});
|
||||
|
||||
test("agnes Image 2.1 maps standard image inputs into extra_body", async () => {
|
||||
@@ -212,16 +252,21 @@ test("agnes Image 2.1 requires the current size parameter", async () => {
|
||||
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", () => {
|
||||
test("agnes registers Video V2.0 and Video 2.5 on the current job contracts", () => {
|
||||
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.deepEqual(
|
||||
entry.models.map((model) => model.id),
|
||||
["agnes-video-v2.0", "agnes-video-2.5-flash", "agnes-video-2.5"]
|
||||
);
|
||||
assert.equal(VIDEO_PROVIDER_IDS.has("agnes"), true);
|
||||
assert.ok(getAllVideoModels().some((model) => model.id === "agnes/agnes-video-v2.0"));
|
||||
assert.ok(getAllVideoModels().some((model) => model.id === "agnes/agnes-video-2.5-flash"));
|
||||
assert.ok(getAllVideoModels().some((model) => model.id === "agnes/agnes-video-2.5"));
|
||||
});
|
||||
|
||||
test("agnes Video V2.0 submits with Bearer auth and polls by video_id and model_name", async () => {
|
||||
@@ -321,3 +366,78 @@ test("agnes Video V2.0 submits with Bearer auth and polls by video_id and model_
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
test("agnes Video 2.5-flash submits Bearer auth and polls /v1/videos/{id}", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
const calls: Array<{
|
||||
url: string;
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
body?: Record<string, unknown>;
|
||||
}> = [];
|
||||
|
||||
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<string, string>,
|
||||
...(init?.body ? { body: JSON.parse(String(init.body)) as Record<string, unknown> } : {}),
|
||||
};
|
||||
calls.push(call);
|
||||
|
||||
if (call.method === "POST") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "task_nEV6cJjyzWnix1g1O9QHjnHzTstegDGM",
|
||||
status: "queued",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "task_nEV6cJjyzWnix1g1O9QHjnHzTstegDGM",
|
||||
status: "completed",
|
||||
url: "https://platform-outputs.agnes-ai.space/video-25.mp4",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await handleVideoGeneration({
|
||||
body: {
|
||||
model: "agnes/agnes-video-2.5-flash",
|
||||
prompt: "a red ball rolling on a white floor",
|
||||
seconds: "4",
|
||||
mode: "text",
|
||||
size: "720P",
|
||||
aspect_ratio: "16:9",
|
||||
},
|
||||
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-25.mp4");
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[0].url, "https://apihub.agnes-ai.com/v1/videos");
|
||||
assert.equal(calls[0].method, "POST");
|
||||
assert.equal(calls[0].body?.model, "agnes-video-2.5-flash");
|
||||
assert.equal(calls[0].body?.seconds, "4");
|
||||
assert.equal(calls[0].body?.mode, "text");
|
||||
assert.equal(
|
||||
calls[1].url,
|
||||
"https://apihub.agnes-ai.com/v1/videos/task_nEV6cJjyzWnix1g1O9QHjnHzTstegDGM"
|
||||
);
|
||||
assert.equal(calls[1].method, "GET");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user