Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate images via OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream, and Replicate APIs with batch support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/providers/zai.ts
1import type { CliArgs, Quality } from "../types";23type ZaiModelFamily = "glm" | "legacy";45type ZaiRequestBody = {6model: string;7prompt: string;8quality: "hd" | "standard";9size: string;10};1112type ZaiResponse = {13data?: Array<{ url?: string }>;14};1516const DEFAULT_MODEL = "glm-image";17const GLM_MAX_PIXELS = 2 ** 22;18const LEGACY_MAX_PIXELS = 2 ** 21;19const GLM_SIZE_STEP = 32;20const LEGACY_SIZE_STEP = 16;2122const GLM_RECOMMENDED_SIZES: Record<string, string> = {23"1:1": "1280x1280",24"3:2": "1568x1056",25"2:3": "1056x1568",26"4:3": "1472x1088",27"3:4": "1088x1472",28"16:9": "1728x960",29"9:16": "960x1728",30};3132const LEGACY_RECOMMENDED_SIZES: Record<string, string> = {33"1:1": "1024x1024",34"9:16": "768x1344",35"3:4": "864x1152",36"16:9": "1344x768",37"4:3": "1152x864",38"2:1": "1440x720",39"1:2": "720x1440",40};4142export function getDefaultModel(): string {43return process.env.ZAI_IMAGE_MODEL || process.env.BIGMODEL_IMAGE_MODEL || DEFAULT_MODEL;44}4546function getApiKey(): string | null {47return process.env.ZAI_API_KEY || process.env.BIGMODEL_API_KEY || null;48}4950export function buildZaiUrl(): string {51const base = (process.env.ZAI_BASE_URL || process.env.BIGMODEL_BASE_URL || "https://api.z.ai/api/paas/v4")52.replace(/\/+$/g, "");53if (base.endsWith("/images/generations")) return base;54if (base.endsWith("/api/paas/v4")) return `${base}/images/generations`;55if (base.endsWith("/v4")) return `${base}/images/generations`;56return `${base}/api/paas/v4/images/generations`;57}5859export function getModelFamily(model: string): ZaiModelFamily {60return model.trim().toLowerCase() === "glm-image" ? "glm" : "legacy";61}6263export function parseAspectRatio(ar: string): { width: number; height: number } | null {64const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);65if (!match) return null;66const width = Number(match[1]);67const height = Number(match[2]);68if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {69return null;70}71return { width, height };72}7374export function parseSize(size: string): { width: number; height: number } | null {75const match = size.trim().match(/^(\d+)\s*[xX*]\s*(\d+)$/);76if (!match) return null;77const width = parseInt(match[1]!, 10);78const height = parseInt(match[2]!, 10);79if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {80return null;81}82return { width, height };83}8485function formatSize(width: number, height: number): string {86return `${width}x${height}`;87}8889function roundToStep(value: number, step: number): number {90return Math.max(step, Math.round(value / step) * step);91}9293function getRatioValue(ar: string): number | null {94const parsed = parseAspectRatio(ar);95if (!parsed) return null;96return parsed.width / parsed.height;97}9899function findClosestRatioKey(ar: string, candidates: string[]): string | null {100const targetRatio = getRatioValue(ar);101if (targetRatio == null) return null;102103let bestKey: string | null = null;104let bestDiff = Infinity;105for (const candidate of candidates) {106const candidateRatio = getRatioValue(candidate);107if (candidateRatio == null) continue;108const diff = Math.abs(candidateRatio - targetRatio);109if (diff < bestDiff) {110bestDiff = diff;111bestKey = candidate;112}113}114115return bestDiff <= 0.05 ? bestKey : null;116}117118function getTargetPixels(quality: Quality): number {119return quality === "normal" ? 1024 * 1024 : 1536 * 1536;120}121122function fitToPixelBudget(123width: number,124height: number,125targetPixels: number,126maxPixels: number,127step: number,128): { width: number; height: number } {129let nextWidth = width;130let nextHeight = height;131const pixels = nextWidth * nextHeight;132133if (pixels > maxPixels) {134const scale = Math.sqrt(maxPixels / pixels);135nextWidth *= scale;136nextHeight *= scale;137} else {138const scale = Math.sqrt(targetPixels / pixels);139nextWidth *= scale;140nextHeight *= scale;141}142143let roundedWidth = roundToStep(nextWidth, step);144let roundedHeight = roundToStep(nextHeight, step);145let roundedPixels = roundedWidth * roundedHeight;146147while (roundedPixels > maxPixels && (roundedWidth > step || roundedHeight > step)) {148if (roundedWidth >= roundedHeight && roundedWidth > step) {149roundedWidth -= step;150} else if (roundedHeight > step) {151roundedHeight -= step;152} else {153break;154}155roundedPixels = roundedWidth * roundedHeight;156}157158return { width: roundedWidth, height: roundedHeight };159}160161function validateCustomSize(162size: string,163family: ZaiModelFamily,164): string {165const parsed = parseSize(size);166if (!parsed) {167throw new Error("Z.AI --size must be in WxH format, for example 1280x1280.");168}169170const widthStep = family === "glm" ? GLM_SIZE_STEP : LEGACY_SIZE_STEP;171const minEdge = family === "glm" ? 1024 : 512;172const maxPixels = family === "glm" ? GLM_MAX_PIXELS : LEGACY_MAX_PIXELS;173174if (parsed.width < minEdge || parsed.width > 2048 || parsed.height < minEdge || parsed.height > 2048) {175throw new Error(176family === "glm"177? "GLM-image custom size requires width and height between 1024 and 2048."178: "Z.AI legacy image models require width and height between 512 and 2048."179);180}181182if (parsed.width % widthStep !== 0 || parsed.height % widthStep !== 0) {183throw new Error(184family === "glm"185? "GLM-image custom size requires width and height divisible by 32."186: "Z.AI legacy image models require width and height divisible by 16."187);188}189190if (parsed.width * parsed.height > maxPixels) {191throw new Error(192family === "glm"193? "GLM-image custom size must not exceed 2^22 total pixels."194: "Z.AI legacy image size must not exceed 2^21 total pixels."195);196}197198return formatSize(parsed.width, parsed.height);199}200201export function resolveSizeForModel(202model: string,203args: Pick<CliArgs, "size" | "aspectRatio" | "quality">,204): string {205const family = getModelFamily(model);206const quality = args.quality === "normal" ? "normal" : "2k";207208if (args.size) {209return validateCustomSize(args.size, family);210}211212const recommended = family === "glm" ? GLM_RECOMMENDED_SIZES : LEGACY_RECOMMENDED_SIZES;213const defaultSize = family === "glm" ? "1280x1280" : "1024x1024";214215if (!args.aspectRatio) return defaultSize;216217const recommendedRatio = findClosestRatioKey(args.aspectRatio, Object.keys(recommended));218if (recommendedRatio) {219return recommended[recommendedRatio]!;220}221222const parsedRatio = parseAspectRatio(args.aspectRatio);223if (!parsedRatio) return defaultSize;224225const targetPixels = getTargetPixels(quality);226const maxPixels = family === "glm" ? GLM_MAX_PIXELS : LEGACY_MAX_PIXELS;227const step = family === "glm" ? GLM_SIZE_STEP : LEGACY_SIZE_STEP;228const fit = fitToPixelBudget(229parsedRatio.width,230parsedRatio.height,231targetPixels,232maxPixels,233step,234);235return formatSize(fit.width, fit.height);236}237238function getZaiQuality(quality: CliArgs["quality"]): "hd" | "standard" {239return quality === "normal" ? "standard" : "hd";240}241242export function validateArgs(_model: string, args: CliArgs): void {243if (args.referenceImages.length > 0) {244throw new Error("Z.AI GLM-image currently supports text-to-image only in baoyu-image-gen. Remove --ref or choose another provider.");245}246247if (args.n > 1) {248throw new Error("Z.AI image generation currently returns a single image per request in baoyu-image-gen.");249}250}251252export function buildRequestBody(253prompt: string,254model: string,255args: CliArgs,256): ZaiRequestBody {257validateArgs(model, args);258return {259model,260prompt,261quality: getZaiQuality(args.quality),262size: resolveSizeForModel(model, args),263};264}265266export async function extractImageFromResponse(result: ZaiResponse): Promise<Uint8Array> {267const url = result.data?.[0]?.url;268if (!url) {269throw new Error("No image URL in Z.AI response");270}271272const imageResponse = await fetch(url);273if (!imageResponse.ok) {274throw new Error(`Failed to download image from Z.AI: ${imageResponse.status}`);275}276277return new Uint8Array(await imageResponse.arrayBuffer());278}279280export async function generateImage(281prompt: string,282model: string,283args: CliArgs,284): Promise<Uint8Array> {285const apiKey = getApiKey();286if (!apiKey) {287throw new Error("ZAI_API_KEY is required. Get one from https://docs.z.ai/.");288}289290const response = await fetch(buildZaiUrl(), {291method: "POST",292headers: {293"Content-Type": "application/json",294Authorization: `Bearer ${apiKey}`,295},296body: JSON.stringify(buildRequestBody(prompt, model, args)),297});298299if (!response.ok) {300const err = await response.text();301throw new Error(`Z.AI API error (${response.status}): ${err}`);302}303304const result = (await response.json()) as ZaiResponse;305return extractImageFromResponse(result);306}307