Compare commits

...

4 Commits

Author SHA1 Message Date
Markus Hartung
a443cb71be fix(kie): correct 12 more mismatched KIE Market model ids (#11296)
PR #11326 claimed every KIE Market catalog id except google-imagen
already matched its real KIE-documented upstream id. That claim was
false: GPT Image 2 T2I/I2I, GPT Image 1.5 T2I/I2I, Seedream 5.0 Lite
T2I/I2I, all 4 Flux 2 variants, and Wan 2.7 Image/Image Pro were all
sent to KIE's createTask unchanged despite diverging from the id KIE
actually documents, reproducing the reporter's post-fix failures.

Each new mapping was verified individually against the literal example
request JSON on docs.kie.ai (never inferred by pattern, since that is
exactly how the prior "everything else matches" claim went wrong).

z-image/4.0-*, z-image/4.5-* and flux/kontext remain unresolved and
are documented as open follow-ups in the map's comment: the only
documented Z-Image page shows a single fixed model id with no
version-specific variant, and flux/kontext has no Market createTask
documentation at all (it lives under a separate /flux-kontext-api/*
endpoint), so neither can be safely auto-fixed without further
investigation.
2026-08-26 13:10:48 -03:00
Diego Rodrigues de Sa e Souza
91aeca0440 fix(security): redact NVIDIA_API_KEY at the log sink in the ad-hoc diag script (#11639)
Merged via /merge-batch (2026-08-26, v3.8.51). Validado no worktree combinado.
2026-08-26 10:59:26 -03:00
Webman
49749eb0d0 fix(types): clear the two type regressions red on release/v3.8.51 (#11640)
Merged via /merge-batch (2026-08-26, v3.8.51). Confirma o fix do typecheck:core que eu vinha classificando como drift pré-existente nas últimas levas (zai-web.ts mimeType) — obrigado por resolver de vez. Validado: typecheck limpo, 7/7 testes passando.
2026-08-26 10:59:19 -03:00
Bob.Hou
8d17110082 fix(build): resolve workspace: protocol dependencies to concrete semver versions during prepublish (#11540) (#11641)
Merged via /merge-batch (2026-08-26, v3.8.51). Validado: typecheck/complexity/file-size/changelog verdes, 3/3 testes do prepublish workspace-protocol passando. Obrigado pela contribuição.
2026-08-26 10:59:14 -03:00
13 changed files with 898 additions and 24 deletions

View File

@@ -0,0 +1 @@
- **fix(kie):** correct 12 more KIE Market catalog ids that were sent to `createTask` unchanged but diverge from KIE's documented upstream `model` values — GPT Image 2 T2I/I2I (drops the `gpt/` prefix), GPT Image 1.5 T2I/I2I (`gpt-image/` namespace), Seedream 5.0 Lite T2I/I2I (drops the `.0`), all 4 Flux 2 variants (`flux-2/` namespace, generic variant renamed `flex`), and Wan 2.7 Image / Image Pro (dash instead of dot) — each verified individually against the literal example request published on docs.kie.ai. `#11326`'s "everything else already matches" claim was wrong a second time (#11296); `z-image/4.0-*`/`z-image/4.5-*` and `flux/kontext` remain open, documented as unresolved in `KIE_MARKET_UPSTREAM_MODEL_IDS`'s comment pending further verification.

View File

@@ -90,13 +90,19 @@ async function resolveZaiBrowserAttachments(
> {
try {
// Browser-page upload: keep the original bytes/mimeType (no Cursor wire prep).
// EncodedImage.mimeType is optional on the wire type, but every producer
// reachable here (decodeDataUrl / fetchImageBytes) validates an image/*
// string before pushing; the fallback only satisfies the attachment type.
const images = await resolveCursorImages(imageUrls, { prepareForWire: false });
return {
attachments: images.map((image, index) => ({
name: zaiImageFileName(image.mimeType, index),
mimeType: image.mimeType,
buffer: image.data,
})),
attachments: images.map((image, index) => {
const mimeType = image.mimeType ?? "image/jpeg";
return {
name: zaiImageFileName(mimeType, index),
mimeType,
buffer: image.data,
};
}),
};
} catch (error) {
const message =

View File

@@ -92,19 +92,65 @@ interface KieImageOptions {
}
// KIE Market catalog ids are namespaced for OmniRoute's catalog
// (`google-imagen/<model>`), but the KIE Market createTask API expects
// (`<vendor>/<model>`), but the KIE Market createTask API expects
// vendor-specific upstream ids that do not follow a single consistent
// pattern (confirmed against docs.kie.ai/market/google/* — see #11225,
// #11296): nano-banana-2 and nano-banana-pro drop the vendor namespace
// entirely, while nano-banana and nano-banana-edit use a `google/` prefix
// instead of `google-imagen/`. Every other KIE Market namespace (seedream,
// flux, ideogram, qwen, wan, grok-imagine, gpt) already matches its real
// upstream id byte-for-byte, so this map stays scoped to google-imagen.
// pattern. Every entry below was confirmed individually against the literal
// example request JSON published on docs.kie.ai (never inferred by pattern —
// see #11326's false "everything else already matches" claim and #11296's
// follow-up correction):
// - google-imagen: nano-banana-2 and nano-banana-pro drop the vendor
// namespace entirely; nano-banana and nano-banana-edit use a `google/`
// prefix instead of `google-imagen/` (docs.kie.ai/market/google/*).
// - gpt: gpt-image-2-* drops the `gpt/` namespace entirely
// (docs.kie.ai/market/gpt/gpt-image-2-*); gpt-image-1.5-* uses a
// `gpt-image/` namespace instead of `gpt/gpt-image-1.5-`, and keeps the
// dot in "1.5" (docs.kie.ai/market/gpt-image/1-5-*).
// - seedream: 5.0-lite-* drops the ".0" — real id is `5-lite-*`
// (docs.kie.ai/market/seedream/5-lite-text-to-image); seedream 4.5 (T2I
// and edit) already matches byte-for-byte.
// - flux: `flux/2-*` uses a `flux-2/` namespace (dash, not slash); the
// generic (non-"pro") variant is named `flex` upstream, not `2`
// (docs.kie.ai/market/flux2/pro-*, .../flex-*).
// - wan: `wan/2.7-*` keeps the dot in our catalog, but KIE's documented
// enum uses a dash — real id is `wan/2-7-*`
// (docs.kie.ai/market/wan/2-7-image[-pro]).
// - ideogram (v3-text-to-image, v3-edit, v3-remix), qwen, qwen2, and
// grok-imagine already match byte-for-byte
// (docs.kie.ai/market/{ideogram,qwen,qwen2,grok-imagine}/*).
// ideogram/v3-reframe has no dedicated docs.kie.ai page as of this sweep
// (its 3 siblings above are all direct id matches, so it is assumed
// correct by pattern, not independently confirmed).
// Two catalog entries remain UNRESOLVED after this sweep and are
// deliberately left untouched pending a follow-up (see #11296 discussion):
// - z-image/4.0-text-to-image and z-image/4.5-text-to-image: the only
// documented Z-Image Market page (docs.kie.ai/market/z-image/z-image)
// shows a single fixed `model` enum value `"z-image"` with no
// version-specific id or "version" input field found — unclear whether
// both catalog ids should collapse to the same upstream call.
// - flux/kontext: no `docs.kie.ai/market/flux2/kontext` (or similar)
// Market page exists; Flux Kontext is documented under the separate
// `/flux-kontext-api/*` docs tree with its own endpoint
// (`POST /api/v1/flux/kontext/generate`, models `flux-kontext-pro`/
// `flux-kontext-max`), not the Market `createTask` flow this map feeds.
// This entry may be miscatalogued as `isMarket: true` and need a
// dedicated reroute rather than an id rewrite.
export const KIE_MARKET_UPSTREAM_MODEL_IDS: ReadonlyMap<string, string> = new Map([
["google-imagen/nano-banana", "google/nano-banana"],
["google-imagen/nano-banana-2", "nano-banana-2"],
["google-imagen/nano-banana-pro", "nano-banana-pro"],
["google-imagen/nano-banana-edit", "google/nano-banana-edit"],
["gpt/gpt-image-2-text-to-image", "gpt-image-2-text-to-image"],
["gpt/gpt-image-2-image-to-image", "gpt-image-2-image-to-image"],
["gpt/gpt-image-1.5-text-to-image", "gpt-image/1.5-text-to-image"],
["gpt/gpt-image-1.5-image-to-image", "gpt-image/1.5-image-to-image"],
["seedream/5.0-lite-text-to-image", "seedream/5-lite-text-to-image"],
["seedream/5.0-lite-image-to-image", "seedream/5-lite-image-to-image"],
["flux/2-pro-text-to-image", "flux-2/pro-text-to-image"],
["flux/2-pro-image-to-image", "flux-2/pro-image-to-image"],
["flux/2-text-to-image", "flux-2/flex-text-to-image"],
["flux/2-image-to-image", "flux-2/flex-image-to-image"],
["wan/2.7-image", "wan/2-7-image"],
["wan/2.7-image-pro", "wan/2-7-image-pro"],
]);
export function resolveKieMarketUpstreamModelId(publicModelId: string): string {

View File

@@ -15,6 +15,7 @@ interface ErrorResponseBody {
message: string;
type?: string;
code?: string;
reason?: string;
};
upstream_details?: Record<string, unknown> | null; // sanitized upstream provider body
}
@@ -108,6 +109,7 @@ export function sanitizeUpstreamDetails(value: unknown, depth = 0): unknown {
export type ErrorBodyClassification = {
type?: string;
code?: string;
reason?: string;
};
/**
@@ -132,6 +134,7 @@ export function buildErrorBody(
message: safeMessage,
type: classification?.type ?? errorInfo.type,
code: classification?.code ?? errorInfo.code,
reason: classification?.reason,
},
};

View File

@@ -19,12 +19,20 @@
*/
const KEY = process.env.NVIDIA_API_KEY ?? "";
const BASE_URL = process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1/chat/completions";
const BASE_URL =
process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1/chat/completions";
const MODEL = process.env.NVIDIA_MODEL || "openai/gpt-oss-120b";
// Neutralize CR/LF before logging so env-derived values (NVIDIA_MODEL, etc.)
// cannot forge extra log lines (S5145 log injection).
const line = (s = "") => console.log(String(s).replace(/[\r\n]+/g, " "));
// cannot forge extra log lines (S5145 log injection). Also strip any raw
// occurrence of the API key so an upstream error/response that echoes it
// back (e.g. inside err.stack or a validation result) never reaches the
// terminal in clear text (js/clear-text-logging, CWE-312/532).
const line = (s = "") => {
let out = String(s).replace(/[\r\n]+/g, " ");
if (KEY) out = out.split(KEY).join("[REDACTED]");
console.log(out);
};
const hr = () => line("─".repeat(72));
function show(label: string, value: unknown) {
@@ -52,8 +60,13 @@ async function partA() {
});
line(" ✅ validateProviderApiKey retornou (sem crash):");
show("resultado", result);
if (typeof (result as any)?.error === "string" && (result as any).error.includes("startsWith")) {
line(" ⚠️ A mensagem de erro contém 'startsWith' → crash CAPTURADO dentro do try/catch da validação.");
if (
typeof (result as any)?.error === "string" &&
(result as any).error.includes("startsWith")
) {
line(
" ⚠️ A mensagem de erro contém 'startsWith' → crash CAPTURADO dentro do try/catch da validação."
);
}
} catch (err: any) {
line(" ❌ validateProviderApiKey LANÇOU (crash não tratado):");

View File

@@ -23,7 +23,7 @@ import {
statSync,
chmodSync,
} from "node:fs";
import { join, dirname } from "node:path";
import { join, dirname, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { assembleStandalone } from "./assembleStandalone.mjs";
@@ -35,6 +35,12 @@ import {
APP_STAGING_REMOVAL_PATHS,
findUnexpectedArtifactPaths,
} from "./pack-artifact-policy.ts";
import {
collectWorkspaceVersions,
findPackageJsonFiles,
hasWorkspaceProtocol,
resolvePackageJsonWorkspaceProtocols,
} from "./resolveWorkspaceProtocols.ts";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -707,6 +713,33 @@ if (remainingUnexpectedFiles.length > 0) {
process.exit(1);
}
// -- Step 11: Resolve workspace: protocol dependencies -----------------
// npm/pnpm workspace protocol specifiers (workspace:*, workspace:^, ...)
// are meaningless to the npm registry and make `npm install -g omniroute`
// fail with EUNSUPPORTEDPROTOCOL. Rewrite any that leaked into published
// package.json files to the concrete workspace package version.
// Only touch files inside the staged dist/ tree; workspace member source
// package.json files must never be mutated by the publish step.
const workspaceVersions = collectWorkspaceVersions(ROOT);
const publishablePackageJsonDirs = [DIST_DIR];
const publishablePackageJsonPaths = publishablePackageJsonDirs
.flatMap((dir) => (existsSync(dir) ? findPackageJsonFiles(dir) : []))
.filter((filePath) => existsSync(filePath));
for (const pkgJsonPath of publishablePackageJsonPaths) {
let pkg: Record<string, unknown>;
try {
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as Record<string, unknown>;
} catch {
continue;
}
if (!hasWorkspaceProtocol(pkg)) continue;
const resolved = resolvePackageJsonWorkspaceProtocols(pkg, workspaceVersions);
writeFileSync(pkgJsonPath, JSON.stringify(resolved, null, 2) + "\n");
console.log(` [resolved] Resolved workspace: protocols in ${relative(ROOT, pkgJsonPath)}`);
}
// ── Done ───────────────────────────────────────────────────
const distPkg = join(DIST_DIR, "package.json");
if (existsSync(distPkg)) {

View File

@@ -0,0 +1,228 @@
/**
* Resolve pnpm/npm workspace protocol dependencies to concrete semver versions.
*
* The npm registry clients cannot parse `workspace:` specifiers. During prepublish
* we rewrite any `workspace:*`, `workspace:^`, `workspace:~` (or explicit
* `workspace:<range>`) dependency declarations to the matching workspace package's
* actual version before npm pack/publish sees them.
*/
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
import * as yaml from "js-yaml";
const WORKSPACE_PROTOCOL_RE = /^workspace:/;
const DEPENDENCY_FIELDS = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
] as const;
/**
* Parse a simple workspace glob entry into concrete directories relative to a root.
* Supports entries like "packages/*" and literal directory names like "open-sse".
*/
function expandWorkspaceEntry(root: string, entry: string): string[] {
const trimmed = entry.trim();
if (!trimmed) return [];
if (!trimmed.endsWith("/*")) {
const dir = join(root, trimmed);
try {
return statSync(dir).isDirectory() ? [dir] : [];
} catch {
return [];
}
}
const parent = join(root, trimmed.slice(0, -2));
let entries: string[] = [];
try {
entries = readdirSync(parent);
} catch {
return [];
}
return entries
.map((name) => join(parent, name))
.filter((dir) => {
try {
return statSync(dir).isDirectory();
} catch {
return false;
}
});
}
/**
* Read the root package.json and, if present, pnpm-workspace.yaml to discover
* workspace member directories. Returns a map of package name -> version.
*/
export function collectWorkspaceVersions(projectRoot: string): Map<string, string> {
const versions = new Map<string, string>();
const rootPkgPath = join(projectRoot, "package.json");
let workspaceEntries: string[] = [];
try {
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf8")) as {
workspaces?: string[];
};
if (Array.isArray(rootPkg.workspaces)) {
workspaceEntries.push(...rootPkg.workspaces);
}
} catch {
// ignore unreadable root package.json
}
const pnpmWorkspacePath = join(projectRoot, "pnpm-workspace.yaml");
try {
const yamlContent = readFileSync(pnpmWorkspacePath, "utf8");
const doc = yaml.load(yamlContent) as { packages?: unknown } | null | undefined;
if (doc && Array.isArray(doc.packages)) {
for (const entry of doc.packages) {
if (typeof entry === "string" && entry) {
workspaceEntries.push(entry);
}
}
}
} catch {
// ignore missing or malformed pnpm-workspace.yaml
}
const seenDirs = new Set<string>();
for (const entry of workspaceEntries) {
for (const dir of expandWorkspaceEntry(projectRoot, entry)) {
if (seenDirs.has(dir)) continue;
seenDirs.add(dir);
try {
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
name?: string;
version?: string;
};
if (pkg.name && pkg.version) {
versions.set(pkg.name, pkg.version);
}
} catch {
// skip unreadable workspace member package.json
}
}
}
return versions;
}
/**
* Resolve workspace protocol dependencies inside a package.json object.
*
* Replaces `workspace:*`, `workspace:^`, `workspace:~`, `workspace:<range>`,
* and `workspace:<packageName>` with the concrete version of the referenced
* workspace package. Throws if a workspace specifier cannot be resolved.
*/
export function resolvePackageJsonWorkspaceProtocols(
pkg: Record<string, unknown>,
workspaceVersions: Map<string, string>
): Record<string, unknown> {
const resolved: Record<string, unknown> = { ...pkg };
for (const field of DEPENDENCY_FIELDS) {
const deps = pkg[field];
if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
const resolvedDeps: Record<string, string> = {};
let changed = false;
for (const [depName, versionSpec] of Object.entries(deps as Record<string, unknown>)) {
if (typeof versionSpec !== "string") {
resolvedDeps[depName] = String(versionSpec ?? "");
continue;
}
if (!WORKSPACE_PROTOCOL_RE.test(versionSpec)) {
resolvedDeps[depName] = versionSpec;
continue;
}
const body = versionSpec.slice("workspace:".length);
let concrete: string | undefined;
if (body === "*") {
concrete = workspaceVersions.get(depName);
} else if (body === "^") {
const version = workspaceVersions.get(depName);
concrete = version ? `^${version}` : undefined;
} else if (body === "~") {
const version = workspaceVersions.get(depName);
concrete = version ? `~${version}` : undefined;
} else if (body.startsWith("^") || body.startsWith("~") || /^[\d<>=]/.test(body)) {
// Explicit range inside workspace: protocol - strip the protocol prefix.
concrete = body;
} else {
// workspace:<packageName> - resolve to that package's version.
concrete = workspaceVersions.get(body);
}
if (concrete) {
resolvedDeps[depName] = concrete;
changed = true;
} else {
throw new Error(
`Cannot resolve workspace protocol "${versionSpec}" for dependency "${depName}". ` +
"Make sure the referenced package is a declared workspace member with a version."
);
}
}
if (changed) {
resolved[field] = resolvedDeps;
}
}
return resolved;
}
/**
* Return true if any dependency field in the package contains a workspace: specifier.
*/
export function hasWorkspaceProtocol(pkg: Record<string, unknown>): boolean {
for (const field of DEPENDENCY_FIELDS) {
const deps = pkg[field];
if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
for (const versionSpec of Object.values(deps as Record<string, unknown>)) {
if (typeof versionSpec === "string" && WORKSPACE_PROTOCOL_RE.test(versionSpec)) {
return true;
}
}
}
return false;
}
/**
* Recursively walk a directory and return every package.json path found.
* Stops descending after maxDepth to avoid runaway recursion on deep trees.
*/
export function findPackageJsonFiles(dir: string, maxDepth = 10): string[] {
const results: string[] = [];
if (maxDepth < 0) return results;
let entries: string[] = [];
try {
entries = readdirSync(dir);
} catch {
return results;
}
for (const entry of entries) {
if (entry === "node_modules") continue;
const fullPath = join(dir, entry);
let stat;
try {
stat = statSync(fullPath);
} catch {
continue;
}
if (stat.isDirectory()) {
results.push(...findPackageJsonFiles(fullPath, maxDepth - 1));
} else if (entry === "package.json") {
results.push(fullPath);
}
}
return results;
}

View File

@@ -66,8 +66,8 @@ export function structuralRejectionResponse(status: 413 | 503, maxMessages: numb
{
type: historyLimit ? "payload_too_large" : "server_error",
code: historyLimit ? "chat_history_too_large" : "chat_admission_busy",
reason: historyLimit ? "message_limit" : "structure_limit",
}
);
body.error.reason = historyLimit ? "message_limit" : "structure_limit";
return new Response(JSON.stringify(body), { status, headers });
}

View File

@@ -0,0 +1,269 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
collectWorkspaceVersions,
resolvePackageJsonWorkspaceProtocols,
hasWorkspaceProtocol,
findPackageJsonFiles,
} from "../../../scripts/build/resolveWorkspaceProtocols.ts";
function tmpDir(prefix: string): string {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function writeJson(filePath: string, data: unknown): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
function readJson(filePath: string): Record<string, unknown> {
return JSON.parse(fs.readFileSync(filePath, "utf8")) as Record<string, unknown>;
}
test("resolvePackageJsonWorkspaceProtocols replaces workspace:*, workspace:^, workspace:~", () => {
const versions = new Map([
["@omniroute/open-sse", "3.8.51"],
["@omniroute/shared", "1.2.3"],
]);
const resolved = resolvePackageJsonWorkspaceProtocols(
{
name: "omniroute",
version: "3.8.51",
dependencies: {
"@omniroute/open-sse": "workspace:^",
"@omniroute/shared": "workspace:*",
lodash: "^4.17.0",
},
devDependencies: {
"@omniroute/open-sse": "workspace:~",
},
peerDependencies: {
"@omniroute/shared": "workspace:1.2.3",
},
optionalDependencies: {
"@omniroute/open-sse": "workspace:>=3.0.0",
},
},
versions
);
assert.equal((resolved.dependencies as Record<string, string>)["@omniroute/open-sse"], "^3.8.51");
assert.equal((resolved.dependencies as Record<string, string>)["@omniroute/shared"], "1.2.3");
assert.equal((resolved.dependencies as Record<string, string>).lodash, "^4.17.0");
assert.equal(
(resolved.devDependencies as Record<string, string>)["@omniroute/open-sse"],
"~3.8.51"
);
assert.equal((resolved.peerDependencies as Record<string, string>)["@omniroute/shared"], "1.2.3");
assert.equal(
(resolved.optionalDependencies as Record<string, string>)["@omniroute/open-sse"],
">=3.0.0"
);
});
test("resolvePackageJsonWorkspaceProtocols leaves non-workspace specs untouched", () => {
const resolved = resolvePackageJsonWorkspaceProtocols(
{
name: "x",
dependencies: {
a: "^1.0.0",
b: "file:../b",
c: "npm:alias@1.0.0",
},
},
new Map()
);
assert.equal((resolved.dependencies as Record<string, string>).a, "^1.0.0");
assert.equal((resolved.dependencies as Record<string, string>).b, "file:../b");
assert.equal((resolved.dependencies as Record<string, string>).c, "npm:alias@1.0.0");
assert.equal(hasWorkspaceProtocol(resolved), false);
});
test("resolvePackageJsonWorkspaceProtocols throws for unresolvable workspace protocol", () => {
assert.throws(
() =>
resolvePackageJsonWorkspaceProtocols(
{
name: "x",
dependencies: {
"@missing/pkg": "workspace:^",
},
},
new Map()
),
/Cannot resolve workspace protocol/
);
});
test("collectWorkspaceVersions reads npm workspaces and pnpm-workspace.yaml", () => {
const root = tmpDir("workspace-versions-");
writeJson(path.join(root, "package.json"), {
name: "root",
version: "0.0.0",
workspaces: ["packages/*", "open-sse"],
});
fs.mkdirSync(path.join(root, "packages", "a"), { recursive: true });
writeJson(path.join(root, "packages", "a", "package.json"), {
name: "@scope/a",
version: "1.0.0",
});
fs.mkdirSync(path.join(root, "open-sse"), { recursive: true });
writeJson(path.join(root, "open-sse", "package.json"), {
name: "@scope/open-sse",
version: "2.0.0",
});
// pnpm-workspace.yaml adds an extra directory not in npm workspaces.
fs.mkdirSync(path.join(root, "packages", "b"), { recursive: true });
writeJson(path.join(root, "packages", "b", "package.json"), {
name: "@scope/b",
version: "3.0.0",
});
fs.writeFileSync(path.join(root, "pnpm-workspace.yaml"), "packages:\n - 'packages/*'\n");
const versions = collectWorkspaceVersions(root);
assert.equal(versions.get("@scope/a"), "1.0.0");
assert.equal(versions.get("@scope/open-sse"), "2.0.0");
assert.equal(versions.get("@scope/b"), "3.0.0");
});
test("findPackageJsonFiles skips node_modules and respects maxDepth", () => {
const root = tmpDir("pkg-json-files-");
fs.mkdirSync(path.join(root, "a"), { recursive: true });
writeJson(path.join(root, "a", "package.json"), {});
fs.mkdirSync(path.join(root, "node_modules", "x"), { recursive: true });
writeJson(path.join(root, "node_modules", "x", "package.json"), {});
const files = findPackageJsonFiles(root);
assert.equal(files.length, 1);
assert.ok(files[0].endsWith(path.join("a", "package.json")));
// Build a deep tree and confirm maxDepth bounds the walk.
const deep = tmpDir("pkg-json-deep-");
let current = deep;
for (let i = 0; i < 12; i += 1) {
current = path.join(current, `level${i}`);
fs.mkdirSync(current, { recursive: true });
}
writeJson(path.join(current, "package.json"), {});
assert.equal(findPackageJsonFiles(deep, 10).length, 0);
assert.equal(findPackageJsonFiles(deep, 12).length, 1);
});
test("collectWorkspaceVersions parses pnpm-workspace.yaml with js-yaml", () => {
const root = tmpDir("pnpm-yaml-");
writeJson(path.join(root, "package.json"), { name: "root", version: "0.0.0" });
// Flow-style array, nested quotes, comments inside the packages list, and an
// unrelated top-level key before packages are all valid YAML that the old line
// scanner could not handle.
fs.writeFileSync(
path.join(root, "pnpm-workspace.yaml"),
"preferWorkspacePackages: true\n" +
"packages:\n" +
' - "packages/*"\n' +
" - 'apps/*'\n" +
" # comment inside the list\n" +
" - open-sse\n"
);
fs.mkdirSync(path.join(root, "packages", "a"), { recursive: true });
writeJson(path.join(root, "packages", "a", "package.json"), {
name: "@scope/a",
version: "1.0.0",
});
fs.mkdirSync(path.join(root, "apps", "web"), { recursive: true });
writeJson(path.join(root, "apps", "web", "package.json"), {
name: "@scope/web",
version: "2.0.0",
});
fs.mkdirSync(path.join(root, "open-sse"), { recursive: true });
writeJson(path.join(root, "open-sse", "package.json"), {
name: "@scope/open-sse",
version: "3.0.0",
});
const versions = collectWorkspaceVersions(root);
assert.equal(versions.get("@scope/a"), "1.0.0");
assert.equal(versions.get("@scope/web"), "2.0.0");
assert.equal(versions.get("@scope/open-sse"), "3.0.0");
});
test("prepublish Step 11 fixture resolves workspace: protocols in dist package.json files", () => {
const root = tmpDir("prepublish-step11-");
const distDir = path.join(root, "dist");
// Workspace member source files contain a workspace: specifier (simulating the
// monorepo source). They must NOT be mutated by the publish step.
fs.mkdirSync(path.join(root, "packages", "shared"), { recursive: true });
const sourcePkgPath = path.join(root, "packages", "shared", "package.json");
writeJson(sourcePkgPath, {
name: "@scope/shared",
version: "1.2.3",
dependencies: {
"@scope/other": "workspace:*",
},
});
fs.mkdirSync(path.join(root, "packages", "other"), { recursive: true });
writeJson(path.join(root, "packages", "other", "package.json"), {
name: "@scope/other",
version: "4.5.6",
});
writeJson(path.join(root, "package.json"), {
name: "root",
version: "0.0.0",
workspaces: ["packages/*"],
});
// The staged dist/ package.json contains workspace: specifiers that leaked
// into the publish artifact and must be rewritten to concrete versions.
fs.mkdirSync(distDir, { recursive: true });
const distPkgPath = path.join(distDir, "package.json");
writeJson(distPkgPath, {
name: "omniroute",
version: "3.8.51",
dependencies: {
"@scope/shared": "workspace:^",
"@scope/other": "workspace:*",
lodash: "^4.17.0",
},
});
// This is the same logic prepublish.ts Step 11 runs, scoped to the fixture.
const workspaceVersions = collectWorkspaceVersions(root);
const publishablePackageJsonPaths = findPackageJsonFiles(distDir).filter((filePath) =>
fs.existsSync(filePath)
);
for (const pkgJsonPath of publishablePackageJsonPaths) {
const pkg = readJson(pkgJsonPath);
if (!hasWorkspaceProtocol(pkg)) continue;
const resolved = resolvePackageJsonWorkspaceProtocols(pkg, workspaceVersions);
fs.writeFileSync(pkgJsonPath, JSON.stringify(resolved, null, 2) + "\n");
}
// dist/package.json must have concrete versions.
const distPkg = readJson(distPkgPath);
assert.equal((distPkg.dependencies as Record<string, string>)["@scope/shared"], "^1.2.3");
assert.equal((distPkg.dependencies as Record<string, string>)["@scope/other"], "4.5.6");
assert.equal((distPkg.dependencies as Record<string, string>).lodash, "^4.17.0");
assert.equal(hasWorkspaceProtocol(distPkg), false);
// Source package.json must remain untouched.
const sourcePkg = readJson(sourcePkgPath);
assert.equal((sourcePkg.dependencies as Record<string, string>)["@scope/other"], "workspace:*");
});

View File

@@ -0,0 +1,35 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
structuralRejectionResponse,
} from "../../src/shared/middleware/chatAdmissionResponses.ts";
// Pins the machine-readable error.reason contract of the chat admission
// structural rejections (#TS2339 regression guard): buildErrorBody now owns
// the reason field via ErrorBodyClassification, so the response bodies keep
// carrying it without post-construction mutation of an untyped field.
test("structuralRejectionResponse 413 carries reason=message_limit classification", () => {
const res = structuralRejectionResponse(413, 40);
assert.equal(res.status, 413);
assert.ok(!res.headers.has("Retry-After"), "413 is not retryable-by-header");
return res.text().then((raw) => {
const body = JSON.parse(raw);
assert.equal(body.error.reason, "message_limit");
assert.equal(body.error.type, "payload_too_large");
assert.equal(body.error.code, "chat_history_too_large");
assert.ok(!body.error.message.includes("at /"), "must not leak stack traces");
});
});
test("structuralRejectionResponse 503 carries reason=structure_limit and Retry-After", () => {
const res = structuralRejectionResponse(503, 40);
assert.equal(res.status, 503);
assert.equal(res.headers.get("Retry-After"), "1");
return res.text().then((raw) => {
const body = JSON.parse(raw);
assert.equal(body.error.reason, "structure_limit");
assert.equal(body.error.type, "server_error");
assert.equal(body.error.code, "chat_admission_busy");
});
});

View File

@@ -0,0 +1,88 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-kie-11296-probe-"));
const { handleImageGeneration } = await import(
"../../open-sse/handlers/imageGeneration.ts"
);
interface CapturedCreate {
url: string;
body: Record<string, unknown>;
}
async function captureCreateTaskModel(publicModel: string): Promise<string> {
const originalFetch = globalThis.fetch;
let captured: CapturedCreate | undefined;
globalThis.fetch = (async (url: unknown, options: { body?: unknown } = {}) => {
const stringUrl = String(url);
if (stringUrl === "https://api.kie.ai/api/v1/jobs/createTask") {
captured = {
url: stringUrl,
body: JSON.parse(String(options.body ?? "{}")) as Record<string, unknown>,
};
return new Response(JSON.stringify({ code: 200, data: { taskId: "kie-probe-task-1" } }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (stringUrl.startsWith("https://api.kie.ai/api/v1/jobs/recordInfo")) {
return new Response(
JSON.stringify({
code: 200,
data: {
state: "success",
resultJson: JSON.stringify({ resultUrls: ["https://example.com/kie-probe.png"] }),
},
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
throw new Error(`Unexpected URL: ${stringUrl}`);
}) as typeof globalThis.fetch;
try {
await handleImageGeneration({
body: { model: publicModel, prompt: "probe prompt", size: "1024x1024", n: 1 },
credentials: { apiKey: "test-kie-key" },
log: null,
});
assert.ok(captured, "expected a createTask request to be captured");
return String(captured.body.model);
} finally {
globalThis.fetch = originalFetch;
}
}
test("#11296: GPT Image 2 T2I sends KIE's real upstream id (no namespace prefix)", async () => {
const sentModel = await captureCreateTaskModel("kie/gpt/gpt-image-2-text-to-image");
assert.equal(sentModel, "gpt-image-2-text-to-image");
});
test("#11296: GPT Image 2 I2I sends KIE's real upstream id (no namespace prefix)", async () => {
const sentModel = await captureCreateTaskModel("kie/gpt/gpt-image-2-image-to-image");
assert.equal(sentModel, "gpt-image-2-image-to-image");
});
test("#11296: GPT Image 1.5 T2I sends KIE's real 'gpt-image/' namespace", async () => {
const sentModel = await captureCreateTaskModel("kie/gpt/gpt-image-1.5-text-to-image");
assert.equal(sentModel, "gpt-image/1.5-text-to-image");
});
test("#11296: Seedream 5.0 Lite T2I sends KIE's real id without the '.0'", async () => {
const sentModel = await captureCreateTaskModel("kie/seedream/5.0-lite-text-to-image");
assert.equal(sentModel, "seedream/5-lite-text-to-image");
});
test("#11296: Flux 2 Pro T2I sends KIE's real 'flux-2/' namespace (dash, not slash)", async () => {
const sentModel = await captureCreateTaskModel("kie/flux/2-pro-text-to-image");
assert.equal(sentModel, "flux-2/pro-text-to-image");
});

View File

@@ -103,13 +103,21 @@ function resolveLiveKieMarketCatalog() {
}));
}
test("KIE Market resolver changes exactly the 4 google-imagen ids in the live market catalog", () => {
test("KIE Market resolver changes exactly the documented mismatched ids in the live market catalog", () => {
const roundTrips = resolveLiveKieMarketCatalog();
const changed = roundTrips.filter(({ publicModelId, upstreamModelId }) => {
return upstreamModelId !== publicModelId;
});
assert.deepEqual(changed, [
{
publicModelId: "seedream/5.0-lite-text-to-image",
upstreamModelId: "seedream/5-lite-text-to-image",
},
{
publicModelId: "seedream/5.0-lite-image-to-image",
upstreamModelId: "seedream/5-lite-image-to-image",
},
{
publicModelId: "google-imagen/nano-banana-2",
upstreamModelId: "nano-banana-2",
@@ -126,19 +134,71 @@ test("KIE Market resolver changes exactly the 4 google-imagen ids in the live ma
publicModelId: "google-imagen/nano-banana-edit",
upstreamModelId: "google/nano-banana-edit",
},
{
publicModelId: "flux/2-pro-image-to-image",
upstreamModelId: "flux-2/pro-image-to-image",
},
{
publicModelId: "flux/2-pro-text-to-image",
upstreamModelId: "flux-2/pro-text-to-image",
},
{
publicModelId: "flux/2-image-to-image",
upstreamModelId: "flux-2/flex-image-to-image",
},
{
publicModelId: "flux/2-text-to-image",
upstreamModelId: "flux-2/flex-text-to-image",
},
{
publicModelId: "gpt/gpt-image-1.5-text-to-image",
upstreamModelId: "gpt-image/1.5-text-to-image",
},
{
publicModelId: "gpt/gpt-image-1.5-image-to-image",
upstreamModelId: "gpt-image/1.5-image-to-image",
},
{
publicModelId: "gpt/gpt-image-2-text-to-image",
upstreamModelId: "gpt-image-2-text-to-image",
},
{
publicModelId: "gpt/gpt-image-2-image-to-image",
upstreamModelId: "gpt-image-2-image-to-image",
},
{
publicModelId: "wan/2.7-image",
upstreamModelId: "wan/2-7-image",
},
{
publicModelId: "wan/2.7-image-pro",
upstreamModelId: "wan/2-7-image-pro",
},
]);
});
const REWRITTEN_GOOGLE_IMAGEN_MARKET_IDS = new Set([
const REWRITTEN_MARKET_IDS = new Set([
"google-imagen/nano-banana",
"google-imagen/nano-banana-2",
"google-imagen/nano-banana-pro",
"google-imagen/nano-banana-edit",
"gpt/gpt-image-2-text-to-image",
"gpt/gpt-image-2-image-to-image",
"gpt/gpt-image-1.5-text-to-image",
"gpt/gpt-image-1.5-image-to-image",
"seedream/5.0-lite-text-to-image",
"seedream/5.0-lite-image-to-image",
"flux/2-pro-text-to-image",
"flux/2-pro-image-to-image",
"flux/2-text-to-image",
"flux/2-image-to-image",
"wan/2.7-image",
"wan/2.7-image-pro",
]);
test("KIE Market resolver preserves every other live market catalog id byte-identically", () => {
for (const { publicModelId, upstreamModelId } of resolveLiveKieMarketCatalog()) {
if (!REWRITTEN_GOOGLE_IMAGEN_MARKET_IDS.has(publicModelId)) {
if (!REWRITTEN_MARKET_IDS.has(publicModelId)) {
assert.equal(
upstreamModelId,
publicModelId,
@@ -148,8 +208,8 @@ test("KIE Market resolver preserves every other live market catalog id byte-iden
}
});
test("KIE Market resolver keeps exactly the explicit google-imagen upstream id mappings (#11296)", () => {
assert.equal(KIE_MARKET_UPSTREAM_MODEL_IDS.size, 4);
test("KIE Market resolver keeps exactly the explicit upstream id mappings (#11296)", () => {
assert.equal(KIE_MARKET_UPSTREAM_MODEL_IDS.size, 16);
});
test("KIE Market resolver passes an unknown namespaced id through byte-identically", () => {
@@ -209,6 +269,78 @@ test("KIE Market createTask sends the KIE upstream id for Nano Banana Edit (#112
);
});
test("KIE Market createTask sends the unprefixed upstream id for GPT Image 2 T2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/gpt/gpt-image-2-text-to-image");
assert.equal(captured.create.body.model, "gpt-image-2-text-to-image");
});
test("KIE Market createTask sends the unprefixed upstream id for GPT Image 2 I2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/gpt/gpt-image-2-image-to-image");
assert.equal(captured.create.body.model, "gpt-image-2-image-to-image");
});
test("KIE Market createTask sends the 'gpt-image/' namespace for GPT Image 1.5 T2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/gpt/gpt-image-1.5-text-to-image");
assert.equal(captured.create.body.model, "gpt-image/1.5-text-to-image");
});
test("KIE Market createTask sends the 'gpt-image/' namespace for GPT Image 1.5 I2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/gpt/gpt-image-1.5-image-to-image");
assert.equal(captured.create.body.model, "gpt-image/1.5-image-to-image");
});
test("KIE Market createTask drops the '.0' for Seedream 5.0 Lite T2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/seedream/5.0-lite-text-to-image");
assert.equal(captured.create.body.model, "seedream/5-lite-text-to-image");
});
test("KIE Market createTask drops the '.0' for Seedream 5.0 Lite I2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/seedream/5.0-lite-image-to-image");
assert.equal(captured.create.body.model, "seedream/5-lite-image-to-image");
});
test("KIE Market createTask sends the 'flux-2/' namespace for Flux 2 Pro T2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/flux/2-pro-text-to-image");
assert.equal(captured.create.body.model, "flux-2/pro-text-to-image");
});
test("KIE Market createTask sends the 'flux-2/' namespace for Flux 2 Pro I2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/flux/2-pro-image-to-image");
assert.equal(captured.create.body.model, "flux-2/pro-image-to-image");
});
test("KIE Market createTask sends the 'flux-2/flex-' name for Flux 2 T2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/flux/2-text-to-image");
assert.equal(captured.create.body.model, "flux-2/flex-text-to-image");
});
test("KIE Market createTask sends the 'flux-2/flex-' name for Flux 2 I2I (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/flux/2-image-to-image");
assert.equal(captured.create.body.model, "flux-2/flex-image-to-image");
});
test("KIE Market createTask sends the dash-separated id for Wan 2.7 Image (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/wan/2.7-image");
assert.equal(captured.create.body.model, "wan/2-7-image");
});
test("KIE Market createTask sends the dash-separated id for Wan 2.7 Image Pro (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/wan/2.7-image-pro");
assert.equal(captured.create.body.model, "wan/2-7-image-pro");
});
test("KIE Market createTask leaves genuinely namespaced upstream ids untouched (#11225 control)", async () => {
const captured = await runKieMarketGeneration("kie/seedream/4.5-text-to-image");

View File

@@ -0,0 +1,20 @@
import test from "node:test";
import assert from "node:assert/strict";
import { resolveCursorImages } from "../../open-sse/utils/cursorImages.ts";
// zai-web maps resolveCursorImages() output into browser-upload attachments
// whose mimeType is REQUIRED. EncodedImage.mimeType is optional on the wire
// type, so zai-web carries an `?? "image/jpeg"` fallback — this test pins the
// producer contract that makes the fallback dead code in practice: every
// image that reaches a browser upload must arrive with a concrete image/*
// mime string (decodeDataUrl / fetchImageBytes validate it before pushing).
const PIXEL_PNG =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
test("resolveCursorImages (prepareForWire:false) always yields a concrete image/* mimeType", async () => {
const images = await resolveCursorImages([PIXEL_PNG], { prepareForWire: false });
assert.equal(images.length, 1);
assert.equal(typeof images[0]!.mimeType, "string");
assert.match(images[0]!.mimeType as string, /^image\//);
});