chore: release v1.110.0

This commit is contained in:
Jim Liu 宝玉
2026-04-21 15:15:19 -05:00
parent f8e434d2a0
commit a4bd37c390
13 changed files with 238 additions and 53 deletions
+2 -2
View File
@@ -133,7 +133,7 @@ default_image_size: 2K
default_image_api_dialect: ratio-metadata
default_model:
google: gemini-3-pro-image-preview
openai: gpt-image-1.5
openai: gpt-image-2
zai: glm-image
azure: image-prod
minimax: image-01
@@ -165,7 +165,7 @@ batch:
assert.equal(config.default_image_size, "2K");
assert.equal(config.default_image_api_dialect, "ratio-metadata");
assert.equal(config.default_model?.google, "gemini-3-pro-image-preview");
assert.equal(config.default_model?.openai, "gpt-image-1.5");
assert.equal(config.default_model?.openai, "gpt-image-2");
assert.equal(config.default_model?.zai, "glm-image");
assert.equal(config.default_model?.azure, "image-prod");
assert.equal(config.default_model?.minimax, "image-01");
+2 -2
View File
@@ -124,7 +124,7 @@ Environment variables:
JIMENG_ACCESS_KEY_ID Jimeng Access Key ID
JIMENG_SECRET_ACCESS_KEY Jimeng Secret Access Key
ARK_API_KEY Seedream/Ark API key
OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-1.5)
OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-2)
OPENROUTER_IMAGE_MODEL Default OpenRouter model (google/gemini-3.1-flash-image-preview)
GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview)
DASHSCOPE_IMAGE_MODEL Default DashScope model (qwen-image-2.0-pro)
@@ -151,7 +151,7 @@ Environment variables:
AZURE_OPENAI_BASE_URL Azure OpenAI resource or deployment endpoint
AZURE_OPENAI_DEPLOYMENT Default Azure deployment name
AZURE_API_VERSION Azure API version (default: 2025-04-01-preview)
AZURE_OPENAI_IMAGE_MODEL Backward-compatible Azure deployment/model alias (defaults to gpt-image-1.5)
AZURE_OPENAI_IMAGE_MODEL Backward-compatible Azure deployment/model alias (defaults to gpt-image-2)
SEEDREAM_BASE_URL Custom Seedream endpoint
BAOYU_IMAGE_GEN_MAX_WORKERS Override batch worker cap
BAOYU_IMAGE_GEN_<PROVIDER>_CONCURRENCY Override provider concurrency
@@ -46,7 +46,7 @@ export function getDefaultModel(): string {
}
}
return process.env.AZURE_OPENAI_IMAGE_MODEL || "gpt-image-1.5";
return process.env.AZURE_OPENAI_IMAGE_MODEL || "gpt-image-2";
}
function getEndpoint(): AzureEndpoint {
@@ -1,9 +1,11 @@
import assert from "node:assert/strict";
import test from "node:test";
import type { CliArgs } from "../types.ts";
import {
buildOpenAIGenerationsBody,
extractImageFromResponse,
getDefaultModel,
getOpenAIAspectRatio,
getOpenAIImageApiDialect,
getOpenAIResolution,
@@ -13,9 +15,33 @@ import {
inferAspectRatioFromSize,
inferResolutionFromSize,
parseAspectRatio,
validateArgs,
} from "./openai.ts";
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
return {
prompt: null,
promptFiles: [],
imagePath: null,
provider: null,
model: null,
aspectRatio: null,
size: null,
quality: "2k",
imageSize: null,
imageApiDialect: null,
referenceImages: [],
n: 1,
batchFile: null,
jobs: null,
json: false,
help: false,
...overrides,
};
}
test("OpenAI aspect-ratio parsing and size selection match model families", () => {
assert.equal(getDefaultModel(), "gpt-image-2");
assert.deepEqual(parseAspectRatio("16:9"), { width: 16, height: 9 });
assert.equal(parseAspectRatio("wide"), null);
assert.equal(parseAspectRatio("0:1"), null);
@@ -25,6 +51,10 @@ test("OpenAI aspect-ratio parsing and size selection match model families", () =
assert.equal(getOpenAISize("dall-e-2", "16:9", "2k"), "1024x1024");
assert.equal(getOpenAISize("gpt-image-1.5", "16:9", "2k"), "1536x1024");
assert.equal(getOpenAISize("gpt-image-1.5", "4:3", "2k"), "1024x1024");
assert.equal(getOpenAISize("gpt-image-2", "16:9", "2k"), "2048x1152");
assert.equal(getOpenAISize("gpt-image-2", "9:16", "2k"), "1152x2048");
assert.equal(getOpenAISize("gpt-image-2", "4:3", "2k"), "2048x1536");
assert.equal(getOpenAISize("gpt-image-2", "2.35:1", "normal"), "1248x528");
assert.equal(inferAspectRatioFromSize("1536x1024"), "3:2");
assert.equal(inferResolutionFromSize("1536x1024"), "2K");
assert.equal(getOpenAIAspectRatio({ aspectRatio: null, size: "2048x1152" }), "16:9");
@@ -37,7 +67,7 @@ test("OpenAI aspect-ratio parsing and size selection match model families", () =
test("OpenAI generations body switches between native and ratio-metadata dialects", () => {
assert.deepEqual(
buildOpenAIGenerationsBody("Draw a skyline", "gpt-image-1.5", {
buildOpenAIGenerationsBody("Draw a skyline", "gpt-image-2", {
aspectRatio: "16:9",
size: null,
quality: "2k",
@@ -45,9 +75,10 @@ test("OpenAI generations body switches between native and ratio-metadata dialect
imageApiDialect: null,
}),
{
model: "gpt-image-1.5",
model: "gpt-image-2",
prompt: "Draw a skyline",
size: "1536x1024",
size: "2048x1152",
quality: "high",
},
);
@@ -90,6 +121,28 @@ test("OpenAI generations body switches between native and ratio-metadata dialect
);
});
test("OpenAI validates gpt-image-2 custom size constraints", () => {
assert.doesNotThrow(() =>
validateArgs("gpt-image-2", makeArgs({ size: "3840x2160" })),
);
assert.doesNotThrow(() =>
validateArgs("gpt-image-2-2026-04-21", makeArgs({ aspectRatio: "2.35:1" })),
);
assert.throws(
() => validateArgs("gpt-image-2", makeArgs({ size: "1024x576" })),
/total pixels/,
);
assert.throws(
() => validateArgs("gpt-image-2", makeArgs({ size: "1025x1024" })),
/multiples of 16px/,
);
assert.throws(
() => validateArgs("gpt-image-2", makeArgs({ aspectRatio: "4:1" })),
/must not exceed 3:1/,
);
});
test("OpenAI mime-type detection covers supported reference image extensions", () => {
assert.equal(getMimeType("frame.png"), "image/png");
assert.equal(getMimeType("frame.jpg"), "image/jpeg");
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
import type { CliArgs, OpenAIImageApiDialect } from "../types";
export function getDefaultModel(): string {
return process.env.OPENAI_IMAGE_MODEL || "gpt-image-1.5";
return process.env.OPENAI_IMAGE_MODEL || "gpt-image-2";
}
type OpenAIImageResponse = { data: Array<{ url?: string; b64_json?: string }> };
@@ -25,6 +25,55 @@ type SizeMapping = {
type OpenAIGenerationsBody = Record<string, unknown>;
function isGptImageModel(model: string): boolean {
return model.includes("gpt-image");
}
function isGptImage2Model(model: string): boolean {
return model.includes("gpt-image-2");
}
function roundToMultiple(value: number, multiple: number): number {
return Math.max(multiple, Math.round(value / multiple) * multiple);
}
function buildGptImage2SizeFromAspectRatio(
ar: string | null,
quality: CliArgs["quality"],
): string {
const parsed = ar ? parseAspectRatio(ar) : null;
const ratio = parsed ? parsed.width / parsed.height : 1;
if (!parsed || Math.abs(ratio - 1) < 0.1) {
const edge = quality === "2k" ? 2048 : 1024;
return `${edge}x${edge}`;
}
const targetLongEdge = quality === "2k" ? 2048 : 1024;
let width: number;
let height: number;
if (ratio > 1) {
width = targetLongEdge;
height = roundToMultiple(width / ratio, 16);
} else {
height = targetLongEdge;
width = roundToMultiple(height * ratio, 16);
}
while (width * height < 655_360) {
if (ratio > 1) {
width += 16;
height = roundToMultiple(width / ratio, 16);
} else {
height += 16;
width = roundToMultiple(height * ratio, 16);
}
}
return `${width}x${height}`;
}
export function getOpenAISize(
model: string,
ar: string | null,
@@ -37,6 +86,10 @@ export function getOpenAISize(
return "1024x1024";
}
if (isGptImage2Model(model)) {
return buildGptImage2SizeFromAspectRatio(ar, quality);
}
const sizes: SizeMapping = isDalle3
? {
square: "1024x1024",
@@ -127,6 +180,18 @@ export function getOpenAIResolution(
return args.quality === "normal" ? "1K" : "2K";
}
function getOpenAIQuality(model: string, quality: CliArgs["quality"]): "standard" | "hd" | "medium" | "high" | null {
if (model.includes("dall-e-3")) {
return quality === "2k" ? "hd" : "standard";
}
if (isGptImageModel(model)) {
return quality === "2k" ? "high" : "medium";
}
return null;
}
export function getOrientationFromAspectRatio(ar: string): "landscape" | "portrait" | null {
const parsed = parseAspectRatio(ar);
if (!parsed) return null;
@@ -163,13 +228,53 @@ export function buildOpenAIGenerationsBody(
size: args.size || getOpenAISize(model, args.aspectRatio, args.quality),
};
if (model.includes("dall-e-3")) {
body.quality = args.quality === "2k" ? "hd" : "standard";
const quality = getOpenAIQuality(model, args.quality);
if (quality) {
body.quality = quality;
}
return body;
}
export function validateArgs(model: string, args: CliArgs): void {
if (!isGptImage2Model(model)) return;
if (args.aspectRatio && !args.size) {
const parsed = parseAspectRatio(args.aspectRatio);
if (!parsed) {
throw new Error(`Invalid gpt-image-2 aspect ratio: ${args.aspectRatio}`);
}
const ratio = parsed.width / parsed.height;
if (Math.max(ratio, 1 / ratio) > 3) {
throw new Error("gpt-image-2 aspect ratio must not exceed 3:1.");
}
}
if (!args.size) return;
const parsedSize = parsePixelSize(args.size);
if (!parsedSize) {
throw new Error(`Invalid gpt-image-2 --size: ${args.size}. Expected <width>x<height>.`);
}
const { width, height } = parsedSize;
const totalPixels = width * height;
const ratio = Math.max(width, height) / Math.min(width, height);
if (Math.max(width, height) > 3840) {
throw new Error("gpt-image-2 --size maximum edge length must be 3840px or less.");
}
if (width % 16 !== 0 || height % 16 !== 0) {
throw new Error("gpt-image-2 --size width and height must both be multiples of 16px.");
}
if (ratio > 3) {
throw new Error("gpt-image-2 --size long edge to short edge ratio must not exceed 3:1.");
}
if (totalPixels < 655_360 || totalPixels > 8_294_400) {
throw new Error("gpt-image-2 --size total pixels must be between 655,360 and 8,294,400.");
}
}
export async function generateImage(
prompt: string,
model: string,
@@ -198,7 +303,7 @@ export async function generateImage(
}
if (model.includes("dall-e-2") || model.includes("dall-e-3")) {
throw new Error(
"Reference images with OpenAI in this skill require GPT Image models. Use --model gpt-image-1.5 (or another gpt-image model)."
"Reference images with OpenAI in this skill require GPT Image models. Use --model gpt-image-2 (or another gpt-image model)."
);
}
const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);
@@ -283,8 +388,9 @@ async function generateWithOpenAIEdits(
form.append("prompt", prompt);
form.append("size", size);
if (model.includes("gpt-image")) {
form.append("quality", quality === "2k" ? "high" : "medium");
const openAIQuality = getOpenAIQuality(model, quality);
if (openAIQuality && openAIQuality !== "standard" && openAIQuality !== "hd") {
form.append("quality", openAIQuality);
}
for (const refPath of referenceImages) {