mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* 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:
committed by
GitHub
parent
5bacb719d3
commit
fb612867fd
@@ -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).
|
||||
@@ -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) | — |
|
||||
|
||||
@@ -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",
|
||||
|
||||
32
open-sse/config/providers/registry/segmind/imageModels.ts
Normal file
32
open-sse/config/providers/registry/segmind/imageModels.ts
Normal 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"],
|
||||
};
|
||||
17
open-sse/config/providers/registry/segmind/videoModels.ts
Normal file
17
open-sse/config/providers/registry/segmind/videoModels.ts
Normal 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" },
|
||||
];
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
80
open-sse/handlers/imageGeneration/providers/segmind.ts
Normal file
80
open-sse/handlers/imageGeneration/providers/segmind.ts
Normal 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] },
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
57
open-sse/handlers/videoGeneration/providers/segmind.ts
Normal file
57
open-sse/handlers/videoGeneration/providers/segmind.ts
Normal 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" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
111
open-sse/utils/segmindClient.ts
Normal file
111
open-sse/utils/segmindClient.ts
Normal 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)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
]);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
254
tests/unit/segmind-image-video-provider-6656.test.ts
Normal file
254
tests/unit/segmind-image-video-provider-6656.test.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user