Files
OmniRoute/open-sse/utils/segmindClient.ts
Diego Rodrigues de Sa e Souza fb612867fd 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.
2026-07-17 12:13:47 -03:00

112 lines
3.4 KiB
TypeScript

// 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)}`,
};
}
}