feat: add Segmind image+video provider (#6656) (#7608)

* feat(providers): add Segmind image+video provider (#6656)

Segmind exposes 200+ hosted image/video models under a single
`POST https://api.segmind.com/v1/{model}` REST shape: x-api-key auth,
JSON request body, raw media bytes response (no JSON envelope).

- New IMAGE_PROVIDERS + VIDEO_PROVIDERS registry entries (format:
  "segmind") with a curated starter model list (Flux, SDXL, SD3.5,
  Kandinsky for image; Wan, Hunyuan, LTX, Kling for video).
- New connection-metadata entry in specialty-media.ts; segmind added
  to IMAGE_ONLY_PROVIDER_IDS and VIDEO_PROVIDER_IDS.
- Dedicated handlers (imageGeneration/providers/segmind.ts,
  videoGeneration/providers/segmind.ts) built on a shared REST client
  (utils/segmindClient.ts) that centralizes the fetch/error/log path
  so both stay under the complexity/max-lines ratchets.
- Extracted the pre-existing Alibaba DashScope video handler out of
  the frozen videoGeneration.ts into videoGeneration/providers/
  dashscope.ts (no behavior change) to make room for the new Segmind
  dispatch branch under the frozen file-size baseline.
- Error responses route through sanitizeErrorMessage() (Hard Rule
  #12) — verified by dedicated no-leak tests.
- Regenerated docs/reference/PROVIDER_REFERENCE.md (250 -> 251
  providers) and synced the plain-text provider counts in README.md/
  AGENTS.md/CLAUDE.md (anchors/badges left untouched).

Tests: tests/unit/segmind-image-video-provider-6656.test.ts (11
cases — registry shape, connection metadata, IMAGE_ONLY/VIDEO_
PROVIDER_IDS membership, mocked-fetch request mapping for both
image and video, and sanitized-error-path assertions for both
upstream error bodies and network exceptions). No live Segmind key
required; response shape (raw media bytes, x-api-key auth) is
sourced from https://docs.segmind.com/ and corroborated against
https://www.segmind.com/models/flux-schnell/api,
https://www.segmind.com/models/sdxl1.0-txt2img/api, and
https://www.segmind.com/models/wan2.1-t2v/api.

Gates run clean: check-file-size, check:complexity-ratchets
(2055/889, both under baseline), typecheck:core,
typecheck:noimplicit:core (no new errors), lint (targeted files),
check:cycles, check:docs-counts (STRICT provider-count drift
resolved), check:docs-sync, check:any-budget:t11,
check:tracked-artifacts, check:provider-consistency,
check:known-symbols.

* test(providers): align APIKEY_PROVIDERS count 167→168 for the new segmind provider (#6656)

Adding segmind to specialty-media.ts grows APIKEY_PROVIDERS by one;
providers-constants-split.test.ts hardcodes the family-partition total.
Legitimate count alignment, not a weakened assertion — all 4 partition/
dedup checks still enforced.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-17 12:13:47 -03:00
committed by GitHub
parent 5bacb719d3
commit fb612867fd
15 changed files with 617 additions and 7 deletions

View File

@@ -0,0 +1 @@
- **feat(providers):** add **Segmind** as an image + video generation provider — `x-api-key` auth against `POST https://api.segmind.com/v1/{model}`, with a curated starter model list (Flux, Stable Diffusion XL/3.5, Kandinsky for image; Wan, Hunyuan, LTX, Kling for video) (#6656).

View File

@@ -10,7 +10,7 @@ lastUpdated: 2026-07-17
> Regenerate with: `npm run gen:provider-reference`
> **Last generated:** 2026-07-17
Total providers: **258**. See category breakdown below.
Total providers: **259**. See category breakdown below.
## Categories
@@ -90,7 +90,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `zai-web` | `zw` | Z.ai Web (Free) | Web cookie | [link](https://chat.z.ai) | Paste the full Cookie header from chat.z.ai (must include the token=<JWT> cookie) |
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. |
## API Key Providers (paid / paid-with-free-credits) (171)
## API Key Providers (paid / paid-with-free-credits) (172)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
@@ -231,6 +231,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `sambanova` | `samba` | SambaNova | API key | [link](https://sambanova.ai) | $5 free credits on signup (30-day validity), no credit card required |
| `sap` | `sap` | SAP Generative AI Hub | API key, enterprise | [link](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/generative-ai-hub-in-sap-ai-core) | Use your SAP AI Core bearer token. Base URL can be your AI_API_URL root or a deploymentUrl from Generative AI Hub. |
| `scaleway` | `scw` | Scaleway AI | API key | [link](https://www.scaleway.com/en/docs/ai-data/generative-apis/) | 1M free tokens for new accounts — EU/GDPR compliant (Paris), Qwen3 235B & Llama 70B |
| `segmind` | `segmind` | Segmind | API key, image, video | [link](https://segmind.com) | Use your Segmind API key in the x-api-key header. OmniRoute targets https://api.segmind.com/v1/<model> and returns the generated image/video bytes directly. |
| `sensenova` | `sensenova` | SenseNova | API key | [link](https://platform.sensenova.cn) | Get API key at platform.sensenova.cn |
| `siliconflow` | `siliconflow` | SiliconFlow | API key | [link](https://cloud.siliconflow.com) | $1 free credits plus permanently free models after identity verification |
| `snowflake` | `snowflake` | Snowflake Cortex | API key, enterprise | [link](https://www.snowflake.com) | — |

View File

@@ -6,6 +6,7 @@
*/
import { LMARENA_DIRECT_IMAGE_MODELS } from "./providers/registry/lmarena/directModels.ts";
import { SEGMIND_IMAGE_PROVIDER } from "./providers/registry/segmind/imageModels.ts";
import { KIE_IMAGE_MODELS } from "./providers/registry/kie/imageModels.ts";
import { FREEPIK_IMAGE_PROVIDER } from "./providers/registry/freepik/index.ts";
@@ -546,6 +547,9 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
models: [{ id: "topaz-enhance", name: "topaz-enhance", inputModalities: ["image"] }],
supportedSizes: ["1024x1024"],
},
// Segmind (#6656): 200+ models, `POST /v1/{model}`, x-api-key, raw image bytes.
segmind: SEGMIND_IMAGE_PROVIDER,
nanogpt: {
id: "nanogpt",
baseUrl: "https://nano-gpt.com/api/v1/images/generations",

View File

@@ -0,0 +1,32 @@
/**
* Segmind image-generation provider entry (#6656).
*
* Segmind exposes 200+ hosted models via a single `POST /v1/{model}` REST
* shape (https://docs.segmind.com/, confirmed live 2026-07-09): x-api-key
* auth, JSON request body, raw image bytes response (no JSON envelope).
* Kept as a full config object (not just the model list) so imageRegistry.ts
* only needs a single-line reference — that file sits right at its size cap.
*
* Model slugs below are a curated starter subset. flux-schnell and
* sdxl1.0-txt2img are verified against
* https://www.segmind.com/models/<slug>/api; the remaining Flux/SD3/Kandinsky
* slugs follow the same documented naming convention.
*/
export const SEGMIND_IMAGE_MODELS = [
{ id: "flux-schnell", name: "FLUX.1 Schnell" },
{ id: "flux-dev", name: "FLUX.1 Dev" },
{ id: "flux-1.1-pro", name: "FLUX 1.1 Pro" },
{ id: "sdxl1.0-txt2img", name: "Stable Diffusion XL 1.0" },
{ id: "sd3.5-large-txt2img", name: "Stable Diffusion 3.5 Large" },
{ id: "kandinsky2.2-txt2img", name: "Kandinsky 2.2" },
];
export const SEGMIND_IMAGE_PROVIDER = {
id: "segmind",
baseUrl: "https://api.segmind.com/v1",
authType: "apikey",
authHeader: "x-api-key",
format: "segmind",
models: SEGMIND_IMAGE_MODELS,
supportedSizes: ["512x512", "1024x1024", "1024x1792", "1792x1024"],
};

View File

@@ -0,0 +1,17 @@
/**
* Segmind video-generation starter model list (#6656).
*
* Same `POST /v1/{model}` REST shape as the image models (imageModels.ts) —
* Segmind's video-capable models (Wan, Hunyuan, LTX, Kling) live under the
* same host/auth. Slugs verified against
* https://www.segmind.com/models/wan2.1-t2v/api and
* https://www.segmind.com/models/wan2.7-i2v/api (2026-07-09); the remaining
* Hunyuan/LTX/Kling slugs follow the same documented naming convention.
*/
export const SEGMIND_VIDEO_MODELS = [
{ id: "wan2.1-t2v", name: "Wan 2.1 Text-to-Video" },
{ id: "wan2.7-i2v", name: "Wan 2.7 Image-to-Video" },
{ id: "hunyuan-video-t2v", name: "Hunyuan Video Text-to-Video" },
{ id: "ltx-video-t2v", name: "LTX Video Text-to-Video" },
{ id: "kling-video-t2v", name: "Kling Video Text-to-Video" },
];

View File

@@ -7,6 +7,7 @@
import { parseModelFromRegistry, getAllModelsFromRegistry } from "./registryUtils.ts";
import { RUNWAYML_SUPPORTED_VIDEO_MODELS } from "./runway.ts";
import { SEGMIND_VIDEO_MODELS } from "./providers/registry/segmind/videoModels.ts";
interface VideoModel {
id: string;
@@ -223,6 +224,18 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
models: [{ id: "wan2.7-t2v", name: "Wan 2.7 T2V" }],
},
// Segmind video generation (#6656). Same `POST /v1/{model}` REST shape as
// the image registry entry (imageRegistry.ts) — x-api-key auth, raw video
// bytes response — routed through the dedicated "segmind" format handler.
segmind: {
id: "segmind",
baseUrl: "https://api.segmind.com/v1",
authType: "apikey",
authHeader: "x-api-key",
format: "segmind",
models: SEGMIND_VIDEO_MODELS,
},
novita: {
id: "novita",
// Novita's async video APIs are per-model: the model id IS the submit path

View File

@@ -64,6 +64,7 @@ import {
CHATGPT_WEB_IMAGE_ID_RE,
} from "./imageGeneration/providers/chatgptWeb.ts";
import { handleNvidiaNimImageGeneration } from "./imageGeneration/providers/nvidiaNim.ts";
import { handleSegmindImageGeneration } from "./imageGeneration/providers/segmind.ts";
import { handleDesignerWebImageGeneration } from "./imageGeneration/providers/designerWeb.ts";
import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts";
@@ -451,6 +452,17 @@ export async function handleImageGeneration({
});
}
if (providerConfig.format === "segmind") {
return handleSegmindImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}
if (providerConfig.format === "chatgpt-web") {
return handleChatGptWebImageGeneration({
model,

View File

@@ -0,0 +1,80 @@
// Segmind image-generation provider (#6656).
//
// Thin body-builder + response-formatter around the shared Segmind REST
// client (open-sse/utils/segmindClient.ts) — see that module for the wire
// shape (x-api-key auth, raw image bytes response, no JSON envelope).
import { segmindRequest } from "../../../utils/segmindClient.ts";
function parseSegmindSize(size: unknown): { width: number; height: number } {
if (typeof size === "string" && size.includes("x")) {
const [w, h] = size.split("x").map(Number);
if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
return { width: w, height: h };
}
}
return { width: 1024, height: 1024 };
}
function buildSegmindImageBody(body: Record<string, unknown>, prompt: string) {
const { width, height } = parseSegmindSize(body.size);
const upstreamBody: Record<string, unknown> = {
prompt,
width,
height,
samples: Number(body.n) > 0 ? Number(body.n) : 1,
};
if (typeof body.negative_prompt === "string") upstreamBody.negative_prompt = body.negative_prompt;
if (typeof body.seed === "number") upstreamBody.seed = body.seed;
return upstreamBody;
}
function formatSegmindImage(buffer: Buffer, contentType: string, prompt: string, wantsB64: boolean) {
const base64 = buffer.toString("base64");
if (wantsB64) return { b64_json: base64, revised_prompt: prompt };
const mimeType = contentType.startsWith("image/") ? contentType : "image/jpeg";
return { url: `data:${mimeType};base64,${base64}`, revised_prompt: prompt };
}
export async function handleSegmindImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const token = credentials?.apiKey || credentials?.accessToken || "";
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
const upstreamBody = buildSegmindImageBody(body, prompt);
if (log) {
log.info("IMAGE", `${provider}/${model} (segmind) | prompt: "${prompt.slice(0, 60)}..."`);
}
const result = await segmindRequest({
baseUrl: providerConfig.baseUrl,
model,
token,
upstreamBody,
callLogPath: "/v1/images/generations",
provider,
scope: "IMAGE",
log,
});
if (!result.ok) {
return { success: false, status: result.status, error: result.error };
}
const image = formatSegmindImage(
result.buffer,
result.contentType,
prompt,
body.response_format === "b64_json"
);
return {
success: true,
data: { created: Math.floor(Date.now() / 1000), data: [image] },
};
}

View File

@@ -16,6 +16,7 @@ import { handleLeonardoVideoGeneration } from "./videoGeneration/leonardoHandler
import { handleDashscopeVideoGeneration } from "./videoGeneration/dashscopeHandler.ts";
import { handleNovitaVideoGeneration } from "./videoGeneration/novitaHandler.ts";
import { handleXaiVideoGeneration } from "./videoGeneration/xaiGrokImagineHandler.ts";
import { handleSegmindVideoGeneration } from "./videoGeneration/providers/segmind.ts";
import { getExecutor } from "../executors/index.ts";
import { isJsonObject, parseKieResultJson } from "../utils/kieTask.ts";
import {
@@ -130,6 +131,16 @@ export async function handleVideoGeneration({ body, credentials, log }) {
});
}
if (providerConfig.format === "segmind") {
return handleSegmindVideoGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
});
}
if (providerConfig.format === "novita-video") {
return handleNovitaVideoGeneration({ model, provider, providerConfig, body, credentials, log });
}

View File

@@ -0,0 +1,57 @@
// Segmind video-generation provider (#6656).
//
// Thin body-builder + response-formatter around the shared Segmind REST
// client (open-sse/utils/segmindClient.ts) — same wire shape as the image
// handler (imageGeneration/providers/segmind.ts): x-api-key auth, raw video
// bytes response (e.g. `video/mp4`) on success, no JSON envelope.
import { segmindRequest } from "../../../utils/segmindClient.ts";
function buildSegmindVideoBody(body: Record<string, unknown>, prompt: string) {
const upstreamBody: Record<string, unknown> = { prompt };
if (typeof body.negative_prompt === "string") upstreamBody.negative_prompt = body.negative_prompt;
if (typeof body.seed === "number") upstreamBody.seed = body.seed;
if (body.duration != null) upstreamBody.duration = Number(body.duration);
if (typeof body.aspect_ratio === "string") upstreamBody.aspect_ratio = body.aspect_ratio;
return upstreamBody;
}
export async function handleSegmindVideoGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}) {
const token = credentials?.apiKey || credentials?.accessToken || "";
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
const upstreamBody = buildSegmindVideoBody(body, prompt);
if (log) {
log.info("VIDEO", `${provider}/${model} (segmind) | prompt: "${prompt.slice(0, 60)}..."`);
}
const result = await segmindRequest({
baseUrl: providerConfig.baseUrl,
model,
token,
upstreamBody,
callLogPath: "/v1/videos/generations",
provider,
scope: "VIDEO",
log,
});
if (!result.ok) {
return { success: false, status: result.status, error: result.error };
}
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: result.buffer.toString("base64"), format: "mp4" }],
},
};
}

View File

@@ -0,0 +1,111 @@
// Shared Segmind (#6656) REST wire client — used by both the image
// (imageGeneration/providers/segmind.ts) and video
// (videoGeneration/providers/segmind.ts) handlers, since Segmind exposes
// image and video models under the exact same `POST /v1/{model}` shape:
// x-api-key auth, JSON request body, raw media bytes response (no JSON
// envelope) on success, JSON/text error body on failure.
//
// Factored out so each per-modality handler stays a thin body-builder +
// response-formatter (keeps both under the complexity/max-lines ratchets).
import { saveCallLog } from "@/lib/usageDb";
import { sanitizeErrorMessage } from "./error.ts";
export interface SegmindLogger {
info: (scope: string, message: string) => void;
error: (scope: string, message: string) => void;
}
export interface SegmindRequestOptions {
baseUrl: string;
model: string;
token: string;
upstreamBody: Record<string, unknown>;
callLogPath: string;
provider: string;
scope: "IMAGE" | "VIDEO";
log?: SegmindLogger | null;
}
export type SegmindRequestResult =
| { ok: true; buffer: Buffer; contentType: string }
| { ok: false; status: number; error: string };
async function logSegmindFailure(
opts: SegmindRequestOptions,
status: number,
duration: number,
errorText: string
): Promise<SegmindRequestResult> {
if (opts.log) {
opts.log.error(
opts.scope,
`${opts.provider} error ${status}: ${errorText.slice(0, 200)}`
);
}
saveCallLog({
method: "POST",
path: opts.callLogPath,
status,
model: `${opts.provider}/${opts.model}`,
provider: opts.provider,
duration,
error: errorText.slice(0, 500),
}).catch(() => {});
return {
ok: false,
status,
error: sanitizeErrorMessage(errorText) || `Segmind request failed (${status})`,
};
}
/**
* POST {baseUrl}/{model} with x-api-key auth and a JSON body. Returns the
* raw response bytes + content-type on success, or a sanitized error result.
*/
export async function segmindRequest(opts: SegmindRequestOptions): Promise<SegmindRequestResult> {
const startTime = Date.now();
try {
const response = await fetch(`${opts.baseUrl.replace(/\/$/, "")}/${opts.model}`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": opts.token },
body: JSON.stringify(opts.upstreamBody),
});
if (!response.ok) {
const errorText = await response.text();
return logSegmindFailure(opts, response.status, Date.now() - startTime, errorText);
}
const contentType = response.headers.get("content-type") || "";
const buffer = Buffer.from(await response.arrayBuffer());
saveCallLog({
method: "POST",
path: opts.callLogPath,
status: 200,
model: `${opts.provider}/${opts.model}`,
provider: opts.provider,
duration: Date.now() - startTime,
}).catch(() => {});
return { ok: true, buffer, contentType };
} catch (err) {
const message = (err as Error)?.message ?? String(err);
if (opts.log) opts.log.error(opts.scope, `${opts.provider} fetch error: ${message}`);
saveCallLog({
method: "POST",
path: opts.callLogPath,
status: 502,
model: `${opts.provider}/${opts.model}`,
provider: opts.provider,
duration: Date.now() - startTime,
error: message,
}).catch(() => {});
return {
ok: false,
status: 502,
error: `${opts.scope === "IMAGE" ? "Image" : "Video"} provider error: ${sanitizeErrorMessage(message)}`,
};
}
}

View File

@@ -58,6 +58,7 @@ export const IMAGE_ONLY_PROVIDER_IDS = new Set([
"black-forest-labs",
"recraft",
"topaz",
"segmind",
"freepik",
]);
@@ -107,6 +108,7 @@ export const VIDEO_PROVIDER_IDS = new Set([
"replicate",
"haiper",
"leonardo",
"segmind",
"novita",
]);

View File

@@ -204,6 +204,21 @@ export const APIKEY_PROVIDERS_SPECIALTY = {
textIcon: "TP",
website: "https://topazlabs.com",
},
segmind: {
id: "segmind",
alias: "segmind",
name: "Segmind",
icon: "image",
color: "#7C3AED",
textIcon: "SG",
website: "https://segmind.com",
authHint:
"Use your Segmind API key in the x-api-key header. OmniRoute targets https://api.segmind.com/v1/<model> and returns the generated image/video bytes directly.",
apiHint:
"Segmind exposes 200+ hosted image and video models (Flux, SDXL, SD3, Kandinsky, Wan, Hunyuan, LTX, Kling, ...) under a single POST /v1/<model> REST call per model. OmniRoute ships a curated starter subset for each modality; more models can be routed by using their Segmind slug as the model id.",
hasFree: true,
freeNote: "Free trial credits on signup, no credit card required (per Segmind's public docs).",
},
dify: {
id: "dify",
alias: "dify",

View File

@@ -37,12 +37,12 @@ test("barrel still exports every catalog + key helpers", () => {
}
});
test("APIKEY_PROVIDERS merges the 6 family files into 171 entries (no loss / no dup)", async () => {
test("APIKEY_PROVIDERS merges the 6 family files into 172 entries (no loss / no dup)", async () => {
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
assert.equal(keys.length, 171);
assert.equal(new Set(keys).size, 171, "duplicate keys after spread-merge");
assert.equal(keys.length, 172);
assert.equal(new Set(keys).size, 172, "duplicate keys after spread-merge");
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
// strict partition (every provider in exactly one), so the sum must be exactly 171.
// strict partition (every provider in exactly one), so the sum must be exactly 172.
const families: [string, string][] = [
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
@@ -62,7 +62,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 171 entries (no loss / no
seen.add(k);
}
}
assert.equal(famTotal, 171, "families must partition all 171 providers");
assert.equal(famTotal, 172, "families must partition all 172 providers");
});
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {

View File

@@ -0,0 +1,254 @@
import test from "node:test";
import assert from "node:assert/strict";
// #6656 — Segmind image+video provider.
// Segmind exposes 200+ hosted models under a single `POST /v1/{model}` REST
// shape (https://docs.segmind.com/): x-api-key header auth, JSON request
// body, raw image/video bytes response (no JSON envelope) on success. This
// covers: registry entry shape (image + video), connection-metadata catalog
// entry, IMAGE_ONLY/VIDEO_PROVIDER_IDS membership, request mapping through
// the dedicated handlers (mocked fetch, no live key required), and the
// error path staying sanitized (no raw stack/message leakage).
const { IMAGE_PROVIDERS, getImageProvider } = await import("../../open-sse/config/imageRegistry.ts");
const { VIDEO_PROVIDERS, getVideoProvider } = await import("../../open-sse/config/videoRegistry.ts");
const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts");
const { handleVideoGeneration } = await import("../../open-sse/handlers/videoGeneration.ts");
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
const { IMAGE_ONLY_PROVIDER_IDS, VIDEO_PROVIDER_IDS } = await import(
"../../src/shared/constants/providers.ts"
);
test("segmind connection-metadata entry is registered with the expected shape", () => {
const entry = (APIKEY_PROVIDERS as Record<string, { id: string; website: string }>).segmind;
assert.ok(entry, "expected an APIKEY_PROVIDERS entry for segmind");
assert.equal(entry.id, "segmind");
assert.equal(entry.website, "https://segmind.com");
});
test("segmind is registered in both IMAGE_ONLY_PROVIDER_IDS and VIDEO_PROVIDER_IDS", () => {
assert.ok(IMAGE_ONLY_PROVIDER_IDS.has("segmind"), "segmind must appear in IMAGE_ONLY_PROVIDER_IDS");
assert.ok(VIDEO_PROVIDER_IDS.has("segmind"), "segmind must appear in VIDEO_PROVIDER_IDS");
});
test("segmind image registry entry uses the x-api-key REST shape", () => {
const cfg = getImageProvider("segmind");
assert.ok(cfg, "expected an IMAGE_PROVIDERS entry for segmind");
assert.equal(cfg.id, "segmind");
assert.equal(cfg.baseUrl, "https://api.segmind.com/v1");
assert.equal(cfg.authType, "apikey");
assert.equal(cfg.authHeader, "x-api-key");
assert.equal(cfg.format, "segmind");
assert.ok(cfg.models.length > 0, "expected at least one starter image model");
assert.ok(
cfg.models.some((m) => m.id === "flux-schnell"),
"expected flux-schnell in the starter image model list"
);
assert.ok(Array.isArray(cfg.supportedSizes) && cfg.supportedSizes.length > 0);
});
test("segmind video registry entry uses the x-api-key REST shape", () => {
const cfg = getVideoProvider("segmind");
assert.ok(cfg, "expected a VIDEO_PROVIDERS entry for segmind");
assert.equal(cfg.id, "segmind");
assert.equal(cfg.baseUrl, "https://api.segmind.com/v1");
assert.equal(cfg.authType, "apikey");
assert.equal(cfg.authHeader, "x-api-key");
assert.equal(cfg.format, "segmind");
assert.ok(cfg.models.length > 0, "expected at least one starter video model");
assert.ok(
cfg.models.some((m) => m.id === "wan2.1-t2v"),
"expected wan2.1-t2v in the starter video model list"
);
});
test("segmind image/video registries do not share model ids by accident", () => {
const imageIds = new Set(IMAGE_PROVIDERS.segmind.models.map((m) => m.id));
const videoIds = VIDEO_PROVIDERS.segmind.models.map((m) => m.id);
for (const id of videoIds) {
assert.ok(!imageIds.has(id), `video model id "${id}" unexpectedly duplicated in image models`);
}
});
test("handleImageGeneration posts to Segmind with x-api-key and returns a data: URL image", async () => {
const originalFetch = globalThis.fetch;
let requestCapture;
globalThis.fetch = async (url, options = {}) => {
requestCapture = {
url: String(url),
headers: options.headers,
body: JSON.parse(String(options.body || "{}")),
};
return new Response(new Uint8Array([1, 2, 3, 4]), {
status: 200,
headers: { "content-type": "image/jpeg" },
});
};
try {
const result = await handleImageGeneration({
body: {
model: "segmind/flux-schnell",
prompt: "a bengal tiger in an astronaut suit",
size: "1024x1024",
},
credentials: { apiKey: "segmind-key" },
log: null,
});
assert.equal(result.success, true);
assert.equal(requestCapture.url, "https://api.segmind.com/v1/flux-schnell");
assert.equal(requestCapture.headers["x-api-key"], "segmind-key");
assert.equal(requestCapture.body.prompt, "a bengal tiger in an astronaut suit");
assert.equal(requestCapture.body.width, 1024);
assert.equal(requestCapture.body.height, 1024);
assert.ok(String(result.data.data[0].url).startsWith("data:image/jpeg;base64,"));
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleImageGeneration returns b64_json when response_format=b64_json is requested", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(new Uint8Array([9, 9, 9]), {
status: 200,
headers: { "content-type": "image/png" },
});
try {
const result = await handleImageGeneration({
body: {
model: "segmind/flux-schnell",
prompt: "test",
response_format: "b64_json",
},
credentials: { apiKey: "segmind-key" },
log: null,
});
assert.equal(result.success, true);
assert.ok(typeof result.data.data[0].b64_json === "string" && result.data.data[0].b64_json.length > 0);
assert.equal(result.data.data[0].url, undefined);
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleImageGeneration sanitizes Segmind upstream error bodies (no raw stack leakage)", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(
"Internal error at /home/runner/work/segmind/handler.ts:42\n at foo (/home/runner/work/segmind/app.js:10:2)",
{ status: 500, headers: { "content-type": "text/plain" } }
);
try {
const result = await handleImageGeneration({
body: { model: "segmind/flux-schnell", prompt: "test" },
credentials: { apiKey: "segmind-key" },
log: null,
});
assert.equal(result.success, false);
assert.equal(result.status, 500);
assert.ok(!result.error.includes("/home/runner"), "error must not leak raw absolute paths");
assert.ok(!result.error.includes("at foo"), "error must not leak raw stack frames (2nd line)");
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleVideoGeneration posts to Segmind with x-api-key and returns an mp4 b64_json payload", async () => {
const originalFetch = globalThis.fetch;
let requestCapture;
globalThis.fetch = async (url, options = {}) => {
requestCapture = {
url: String(url),
headers: options.headers,
body: JSON.parse(String(options.body || "{}")),
};
return new Response(new Uint8Array([5, 6, 7, 8]), {
status: 200,
headers: { "content-type": "video/mp4" },
});
};
try {
const result = await handleVideoGeneration({
body: {
model: "segmind/wan2.1-t2v",
prompt: "a smiling woman walking in London at night",
aspect_ratio: "16:9",
},
credentials: { apiKey: "segmind-key" },
log: null,
});
assert.equal(result.success, true);
assert.equal(requestCapture.url, "https://api.segmind.com/v1/wan2.1-t2v");
assert.equal(requestCapture.headers["x-api-key"], "segmind-key");
assert.equal(requestCapture.body.prompt, "a smiling woman walking in London at night");
assert.equal(requestCapture.body.aspect_ratio, "16:9");
assert.equal(result.data.data[0].format, "mp4");
assert.ok(typeof result.data.data[0].b64_json === "string" && result.data.data[0].b64_json.length > 0);
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleVideoGeneration sanitizes Segmind upstream error bodies (no raw stack leakage)", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response("Traceback (most recent call last):\n File \"/srv/app/handler.py\", line 10", {
status: 502,
headers: { "content-type": "text/plain" },
});
try {
const result = await handleVideoGeneration({
body: { model: "segmind/wan2.1-t2v", prompt: "test" },
credentials: { apiKey: "segmind-key" },
log: null,
});
assert.equal(result.success, false);
assert.equal(result.status, 502);
assert.ok(!result.error.includes("/srv/app"), "error must not leak raw absolute paths");
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleImageGeneration surfaces network errors through sanitizeErrorMessage (502, no raw message)", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {
throw new Error(
"connect ECONNREFUSED at /home/runner/work/segmind/client.ts:5:1\n at TCPConnectWrap.afterConnect"
);
};
try {
const result = await handleImageGeneration({
body: { model: "segmind/flux-schnell", prompt: "test" },
credentials: { apiKey: "segmind-key" },
log: null,
});
assert.equal(result.success, false);
assert.equal(result.status, 502);
assert.ok(!result.error.includes("/home/runner"), "must not leak raw absolute source path");
assert.ok(
!result.error.includes("TCPConnectWrap"),
"must not leak the raw stack frame (2nd line)"
);
} finally {
globalThis.fetch = originalFetch;
}
});