diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 4d64d4a6f5..27bb0d41b9 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -276,45 +276,6 @@ export const IMAGE_PROVIDERS: Record = { supportedSizes: ["1024x1024", "1024x1536", "1536x1024", "1024x1792", "1792x1024"], }, - // UC (uncensored.com) image generation. Two surfaces served by one handler - // (handleUcImageGeneration picks by credential): PERSONA web (un-metered, - // Clerk JWT -> internal.chatuncensored.ai/v2/image-gen + result-URL polling) - // and uc-direct REST (metered, X-api-key -> api.uncensored.com, OpenAI-shaped). - uc: { - id: "uc", - baseUrl: "https://internal.chatuncensored.ai/v2/image-gen", - authType: "apikey", - authHeader: "bearer", - format: "uc-image", - models: [ - { id: "model-dev", name: "Flux Dev (UC)" }, - { id: "model-pro", name: "Flux Pro (UC)" }, - { id: "model-1.1", name: "Flux Pro 1.1 (UC)" }, - { id: "model-1.2", name: "Wan 2.2 (UC)" }, - { id: "seedream-v4.5", name: "Seedream v4.5 (UC)" }, - { id: "seedream-v5", name: "Seedream v5 (UC)" }, - { id: "flux-2", name: "FLUX.2 (UC)" }, - { id: "flux-2-pro", name: "FLUX.2 Pro (UC)" }, - { id: "lustify-v7", name: "Lustify v7 (UC)" }, - { id: "nano-banana", name: "Nano Banana (UC)" }, - { id: "nano-banana-2", name: "Nano Banana 2 (UC)" }, - { id: "nano-banana-pro", name: "Nano Banana Pro (UC)" }, - { id: "nano-banana-ultra", name: "Nano Banana Ultra (UC)" }, - { id: "gpt-image", name: "GPT Image (UC)" }, - { id: "gpt-image-2", name: "GPT Image 2 (UC)" }, - { id: "realism", name: "Realism (UC)" }, - { id: "realism-2", name: "Realism 2 (UC)" }, - { id: "z-image-turbo", name: "Z-Image Turbo (UC)" }, - { id: "prefect-pony-xl", name: "Prefect Pony XL (UC)" }, - { id: "wan-2.6", name: "Wan 2.6 (UC)" }, - { id: "wan-2.7-text-to-image", name: "Wan 2.7 Text-to-Image (UC)" }, - { id: "wan-2.7-text-to-image-pro", name: "Wan 2.7 Text-to-Image Pro (UC)" }, - ], - // Persona web derives imageWidth/imageHeight from an aspect ratio; uc-direct - // passes any OpenAI-style size through. These are the aspect buckets. - supportedSizes: ["1024x1024", "1024x576", "576x1024", "1024x768", "768x1024"], - }, - xai: { id: "xai", baseUrl: "https://api.x.ai/v1/images/generations", @@ -894,6 +855,44 @@ export const IMAGE_PROVIDERS: Record = { supportedSizes: ["1024x1024", "2048x2048"], }, aihorde: AI_HORDE_IMAGE_PROVIDER, + + // Keep UC after every existing image provider because parseImageModel() resolves + // bare duplicate ids by first match. Explicit `uc/` routes remain available while + // historical owners retain bare ids such as nano-banana and z-image-turbo. + uc: { + id: "uc", + baseUrl: "https://internal.chatuncensored.ai/v2/image-gen", + authType: "apikey", + authHeader: "bearer", + format: "uc-image", + models: [ + { id: "model-dev", name: "Flux Dev (UC)" }, + { id: "model-pro", name: "Flux Pro (UC)" }, + { id: "model-1.1", name: "Flux Pro 1.1 (UC)" }, + { id: "model-1.2", name: "Wan 2.2 (UC)" }, + { id: "seedream-v4.5", name: "Seedream v4.5 (UC)" }, + { id: "seedream-v5", name: "Seedream v5 (UC)" }, + { id: "flux-2", name: "FLUX.2 (UC)" }, + { id: "flux-2-pro", name: "FLUX.2 Pro (UC)" }, + { id: "lustify-v7", name: "Lustify v7 (UC)" }, + { id: "nano-banana", name: "Nano Banana (UC)" }, + { id: "nano-banana-2", name: "Nano Banana 2 (UC)" }, + { id: "nano-banana-pro", name: "Nano Banana Pro (UC)" }, + { id: "nano-banana-ultra", name: "Nano Banana Ultra (UC)" }, + { id: "gpt-image", name: "GPT Image (UC)" }, + { id: "gpt-image-2", name: "GPT Image 2 (UC)" }, + { id: "realism", name: "Realism (UC)" }, + { id: "realism-2", name: "Realism 2 (UC)" }, + { id: "z-image-turbo", name: "Z-Image Turbo (UC)" }, + { id: "prefect-pony-xl", name: "Prefect Pony XL (UC)" }, + { id: "wan-2.6", name: "Wan 2.6 (UC)" }, + { id: "wan-2.7-text-to-image", name: "Wan 2.7 Text-to-Image (UC)" }, + { id: "wan-2.7-text-to-image-pro", name: "Wan 2.7 Text-to-Image Pro (UC)" }, + ], + // Persona web derives imageWidth/imageHeight from an aspect ratio; uc-direct + // passes any OpenAI-style size through. These are the aspect buckets. + supportedSizes: ["1024x1024", "1024x576", "576x1024", "1024x768", "768x1024"], + }, }; /** diff --git a/scripts/build/pack-artifact-policy.ts b/scripts/build/pack-artifact-policy.ts index 2358399fa6..240b62813b 100644 --- a/scripts/build/pack-artifact-policy.ts +++ b/scripts/build/pack-artifact-policy.ts @@ -216,12 +216,6 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [ "bin/mcpStdioConsoleGuard.mjs", "bin/nodeRuntimeSupport.mjs", "bin/omniroute.mjs", - // #11437: bin/omniroute.mjs imports ./cli/utils/volatileEnvPath.mjs at startup - // (describeVolatileEnvWarning — flags a .env living inside the installed package). - // bin/cli/ is only an allowlist PREFIX, so its absence would never fail the - // unexpected-paths check; list it REQUIRED so a regression is loud (#7065 class, - // enforced by tests/unit/pack-artifact-entrypoint-closures.test.ts). - "bin/cli/utils/volatileEnvPath.mjs", // #7808: aliasResolver + its hook file. bin/omniroute.mjs imports // bin/aliasResolver.mjs at startup, which in turn registers // bin/aliasResolverHook.mjs as the ESM loader. Both must ship in the tarball diff --git a/src/lib/db/probeUtils.ts b/src/lib/db/probeUtils.ts index 4f0df076e8..987f62c829 100644 --- a/src/lib/db/probeUtils.ts +++ b/src/lib/db/probeUtils.ts @@ -22,7 +22,14 @@ import path from "node:path"; */ export function isTransientProbeError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); - if (/SQLITE_BUSY|SQLITE_PROTOCOL|SQLITE_IOERR|ENOENT|database is locked/i.test(message)) { + // #12423 widened the message side: SQLite also reports "database table is + // locked", "database schema is locked" and "database is busy" for the same + // transient contention that "database is locked" covers. + if ( + /SQLITE_BUSY|SQLITE_PROTOCOL|SQLITE_IOERR|ENOENT|database(?: table| schema)? is (?:locked|busy)/i.test( + message + ) + ) { return true; } // The real drivers do not put the result-code name in the message: both diff --git a/src/shared/providers/webSessionCredentials.ts b/src/shared/providers/webSessionCredentials.ts index 5ede7e9f55..b95f8d1e31 100644 --- a/src/shared/providers/webSessionCredentials.ts +++ b/src/shared/providers/webSessionCredentials.ts @@ -348,6 +348,8 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = { "maxaiDeviceId", "userId", "maxaiUserId", + ], + }, uc: { // UC (uncensored.com) persona: auth is the durable Clerk `__client` cookie // (a JWT with no exp) plus the session id + user id, all stored in diff --git a/tests/unit/probe-9541-repro.test.ts b/tests/unit/probe-9541-repro.test.ts index 1a1caa76a6..0bda9dea7b 100644 --- a/tests/unit/probe-9541-repro.test.ts +++ b/tests/unit/probe-9541-repro.test.ts @@ -50,6 +50,11 @@ test("FIX-GREEN: isTransientProbeError does NOT classify fatal errors", () => { test("FIX-GREEN: isTransientProbeError classifies BUSY/PROTOCOL/IOERR/ENOENT", () => { const transientPatterns = [ "SQLITE_BUSY: database is locked", + // better-sqlite3 can omit the symbolic SQLite error code entirely. + "database is locked", + "database table is locked", + "database schema is locked: main", + "database is busy", "SQLITE_PROTOCOL: locking protocol", "SQLITE_IOERR: disk I/O error", "ENOENT: no such file or directory, open '/tmp/db.sqlite'", diff --git a/tests/unit/uc-image.test.ts b/tests/unit/uc-image.test.ts index 1b75832468..cb947cef45 100644 --- a/tests/unit/uc-image.test.ts +++ b/tests/unit/uc-image.test.ts @@ -8,7 +8,7 @@ import { UC_PERSONA_IMAGE_URL, UC_DIRECT_IMAGE_URL, } from "../../open-sse/handlers/imageGeneration/providers/ucImage.ts"; -import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts"; +import { IMAGE_PROVIDERS, parseImageModel } from "../../open-sse/config/imageRegistry.ts"; // A valid PERSONA credential (durable Clerk cookie + sid + uid in psd). No API // key, so the handler takes the persona web path (mint -> POST -> poll). @@ -51,6 +51,25 @@ test("uc is registered in IMAGE_PROVIDERS with the uc-image format + 22 models", assert.equal((entry.models ?? []).length, 22); }); +test("uc image models require an explicit prefix when an existing provider owns the bare id", () => { + assert.deepEqual(parseImageModel("uc/nano-banana"), { + provider: "uc", + model: "nano-banana", + }); + assert.deepEqual(parseImageModel("uc/z-image-turbo"), { + provider: "uc", + model: "z-image-turbo", + }); + assert.deepEqual(parseImageModel("nano-banana"), { + provider: "adobe-firefly", + model: "nano-banana", + }); + assert.deepEqual(parseImageModel("z-image-turbo"), { + provider: "nanogpt", + model: "z-image-turbo", + }); +}); + // --- Pure helpers -------------------------------------------------------- test("resolveUcImageModel strips uc/ and uc-direct/ prefixes", () => { @@ -226,7 +245,9 @@ test("handleUcImageGeneration (persona) 401s (retryable) when the credential is test("handleUcImageGeneration (persona) times out with 504 when the result never readies", async () => { const resultUrl = "https://gen.moveinwater.com/img_never.png"; const fetchImpl = personaFetch({ - pendingPolls: 1000, // never becomes ready within the window + // The injected no-op sleep can execute more than 1,000 polls inside 5 ms on + // fast runners, so use an unbounded pending count to make the timeout deterministic. + pendingPolls: Number.POSITIVE_INFINITY, resultUrl, jwt: fakeJwt("uid", FUTURE_EXP), });