mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +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.
112 lines
3.4 KiB
TypeScript
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)}`,
|
|
};
|
|
}
|
|
}
|