mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host. Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean. Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
272 lines
9.9 KiB
TypeScript
272 lines
9.9 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-aihorde-image-"));
|
|
|
|
import {
|
|
capHordeN,
|
|
mapHordeGenerateRequest,
|
|
parseHordeSize,
|
|
stripHordeModelPrefix,
|
|
} from "../../open-sse/handlers/imageGeneration/providers/aihordeMapRequest.ts";
|
|
import { handleAiHordeImageGeneration } from "../../open-sse/handlers/imageGeneration/providers/aihorde.ts";
|
|
import { handleImageGeneration } from "../../open-sse/handlers/imageGeneration.ts";
|
|
import { aiHordeImageCatalog } from "../../open-sse/services/aihordeImageCatalog.ts";
|
|
|
|
test("map helpers snap size, cap n, and strip prefixes", () => {
|
|
assert.equal(stripHordeModelPrefix("aihorde/FLUX.1-schnell"), "FLUX.1-schnell");
|
|
assert.equal(stripHordeModelPrefix("horde/AlbedoBase XL (SDXL)"), "AlbedoBase XL (SDXL)");
|
|
assert.deepEqual(parseHordeSize("1000x1000"), { width: 1024, height: 1024 });
|
|
assert.equal(capHordeN(9), 4);
|
|
});
|
|
|
|
test("mapHordeGenerateRequest builds a native Horde payload", () => {
|
|
const payload = mapHordeGenerateRequest({
|
|
model: "aihorde/FLUX.1-schnell",
|
|
prompt: "a red fox in snow",
|
|
n: 2,
|
|
size: "1024x768",
|
|
});
|
|
assert.equal(payload.prompt, "a red fox in snow");
|
|
assert.deepEqual(payload.models, ["FLUX.1-schnell"]);
|
|
assert.equal((payload.params as { n: number }).n, 2);
|
|
assert.equal((payload.params as { width: number }).width, 1024);
|
|
assert.equal((payload.params as { height: number }).height, 768);
|
|
assert.equal(payload.r2, true);
|
|
});
|
|
|
|
test("handleAiHordeImageGeneration rejects a model with zero workers", async () => {
|
|
aiHordeImageCatalog.replace([
|
|
{ name: "AlbedoBase XL (SDXL)", count: 1, queued: 0, eta: 1, performance: 1, jobs: 0 },
|
|
]);
|
|
const result = await handleAiHordeImageGeneration({
|
|
model: "FLUX.1-schnell",
|
|
provider: "aihorde",
|
|
body: { model: "aihorde/FLUX.1-schnell", prompt: "fox" },
|
|
credentials: { apiKey: "horde-key" },
|
|
});
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 400);
|
|
assert.match(String(result.error), /No Horde workers/);
|
|
});
|
|
|
|
test("exceeding the deadline issues a DELETE cancel to Horde, not just a local timeout", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const calls: Array<{ method: string; url: string }> = [];
|
|
|
|
aiHordeImageCatalog.replace([
|
|
{ name: "FLUX.1-schnell", count: 3, queued: 0, eta: 1, performance: 1, jobs: 0 },
|
|
]);
|
|
|
|
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
|
|
const url = String(input);
|
|
const method = (init?.method || "GET").toUpperCase();
|
|
calls.push({ method, url });
|
|
|
|
if (url.includes("/v2/generate/async")) {
|
|
return new Response(JSON.stringify({ id: "job-timeout" }), { status: 202 });
|
|
}
|
|
if (method === "DELETE" && url.includes("/v2/generate/status/")) {
|
|
return new Response(JSON.stringify({ id: "job-timeout" }), { status: 200 });
|
|
}
|
|
if (url.includes("/v2/generate/check/")) {
|
|
// Never reports done — the generation deadline must be what ends the loop.
|
|
return new Response(JSON.stringify({ done: false, is_possible: true, faulted: false }), {
|
|
status: 200,
|
|
});
|
|
}
|
|
return new Response("unexpected", { status: 500 });
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const result = await handleAiHordeImageGeneration({
|
|
model: "FLUX.1-schnell",
|
|
provider: "aihorde",
|
|
body: { model: "aihorde/FLUX.1-schnell", prompt: "a red fox in snow" },
|
|
credentials: { apiKey: "horde-key" },
|
|
// Small enough that the poll loop's 1s interval crosses the deadline
|
|
// on its first iteration, but non-zero so submit itself isn't rejected.
|
|
timeoutMs: 50,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 504);
|
|
assert.match(String(result.error), /timed out/);
|
|
|
|
const cancelCall = calls.find(
|
|
(call) => call.method === "DELETE" && call.url.includes("/v2/generate/status/job-timeout")
|
|
);
|
|
assert.ok(cancelCall, "expected a DELETE cancel call to Horde's status endpoint");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("a private-host R2 image URL is blocked by the SSRF guard, not fetched", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let downloadAttempted = false;
|
|
|
|
aiHordeImageCatalog.replace([
|
|
{ name: "FLUX.1-schnell", count: 3, queued: 0, eta: 1, performance: 1, jobs: 0 },
|
|
]);
|
|
|
|
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
|
|
const url = String(input);
|
|
const method = (init?.method || "GET").toUpperCase();
|
|
|
|
if (url.includes("/v2/generate/async")) {
|
|
return new Response(JSON.stringify({ id: "job-ssrf" }), { status: 202 });
|
|
}
|
|
if (method === "DELETE") {
|
|
return new Response("{}", { status: 200 });
|
|
}
|
|
if (url.includes("/v2/generate/check/")) {
|
|
return new Response(JSON.stringify({ done: true, is_possible: true, faulted: false }), {
|
|
status: 200,
|
|
});
|
|
}
|
|
if (url.includes("/v2/generate/status/")) {
|
|
return new Response(
|
|
JSON.stringify({ generations: [{ img: "http://127.0.0.1:9999/internal-secret.png" }] }),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
// A real HTTP fetch reaching the private host means the guard failed to
|
|
// block it before the network call.
|
|
downloadAttempted = true;
|
|
return new Response("unexpected", { status: 500 });
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const result = await handleAiHordeImageGeneration({
|
|
model: "FLUX.1-schnell",
|
|
provider: "aihorde",
|
|
body: { model: "aihorde/FLUX.1-schnell", prompt: "a red fox in snow" },
|
|
credentials: { apiKey: "horde-key" },
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(downloadAttempted, false, "the private-host URL must never reach fetch()");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("an oversized R2 image download is rejected instead of buffered whole", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
aiHordeImageCatalog.replace([
|
|
{ name: "FLUX.1-schnell", count: 3, queued: 0, eta: 1, performance: 1, jobs: 0 },
|
|
]);
|
|
|
|
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
|
|
const url = String(input);
|
|
const method = (init?.method || "GET").toUpperCase();
|
|
|
|
if (url.includes("/v2/generate/async")) {
|
|
return new Response(JSON.stringify({ id: "job-oversized" }), { status: 202 });
|
|
}
|
|
if (method === "DELETE") {
|
|
return new Response("{}", { status: 200 });
|
|
}
|
|
if (url.includes("/v2/generate/check/")) {
|
|
return new Response(JSON.stringify({ done: true, is_possible: true, faulted: false }), {
|
|
status: 200,
|
|
});
|
|
}
|
|
// A raw public IP literal (not a hostname) skips the SSRF guard's real DNS
|
|
// lookup entirely — this test only cares about the byte-cap, not the host
|
|
// resolution path (already covered by the private-host test above), and
|
|
// the sandboxed test env has no DNS egress.
|
|
if (url.includes("/v2/generate/status/")) {
|
|
return new Response(
|
|
JSON.stringify({ generations: [{ img: "https://93.184.216.34/huge.png" }] }),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
if (url.includes("93.184.216.34")) {
|
|
return new Response("x", {
|
|
status: 200,
|
|
headers: { "content-length": String(30 * 1024 * 1024) },
|
|
});
|
|
}
|
|
return new Response("unexpected", { status: 500 });
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const result = await handleAiHordeImageGeneration({
|
|
model: "FLUX.1-schnell",
|
|
provider: "aihorde",
|
|
body: { model: "aihorde/FLUX.1-schnell", prompt: "a red fox in snow" },
|
|
credentials: { apiKey: "horde-key" },
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.match(String(result.error), /exceeds|byte limit|too large/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleImageGeneration dispatches aihorde and sends the apikey header", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const calls: Array<{ url: string; headers: Record<string, string>; body?: unknown }> = [];
|
|
|
|
aiHordeImageCatalog.replace([
|
|
{ name: "FLUX.1-schnell", count: 3, queued: 0, eta: 1, performance: 1, jobs: 0 },
|
|
]);
|
|
|
|
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
|
|
const url = String(input);
|
|
const headers = Object.fromEntries(new Headers(init?.headers).entries());
|
|
let body: unknown;
|
|
if (typeof init?.body === "string") {
|
|
try {
|
|
body = JSON.parse(init.body);
|
|
} catch {
|
|
body = init.body;
|
|
}
|
|
}
|
|
calls.push({ url, headers, body });
|
|
|
|
if (url.includes("/v2/generate/async")) {
|
|
return new Response(JSON.stringify({ id: "job-1" }), { status: 202 });
|
|
}
|
|
if (url.includes("/v2/generate/check/")) {
|
|
return new Response(JSON.stringify({ done: true, is_possible: true, faulted: false }), {
|
|
status: 200,
|
|
});
|
|
}
|
|
if (url.includes("/v2/generate/status/")) {
|
|
return new Response(
|
|
JSON.stringify({ generations: [{ img: Buffer.from("png-bytes").toString("base64") }] }),
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
return new Response("unexpected", { status: 500 });
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const result = await handleImageGeneration({
|
|
body: { model: "aihorde/FLUX.1-schnell", prompt: "a red fox in snow", size: "1024x1024" },
|
|
credentials: { apiKey: "horde-registered-key" },
|
|
log: null,
|
|
});
|
|
assert.equal(result.success, true);
|
|
const submit = calls.find((call) => call.url.includes("/v2/generate/async"));
|
|
assert.ok(submit);
|
|
assert.equal(submit.headers.apikey, "horde-registered-key");
|
|
assert.ok(submit.headers["client-agent"]);
|
|
assert.deepEqual((submit.body as { models: string[] }).models, ["FLUX.1-schnell"]);
|
|
assert.equal(
|
|
(result as { data: { data: Array<{ b64_json: string }> } }).data.data[0].b64_json,
|
|
Buffer.from("png-bytes").toString("base64")
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|