From ae8a7d2dc5d1a036a806f8ad8390ad96fc02b7af Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:03:39 -0300 Subject: [PATCH] Release v3.7.3 (#1724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update image-model list * docs: update CHANGELOG.md with merged PR entries for v3.7.3 * chore(release): bump to v3.7.3 — changelog, docs, version sync * chore(workflow): update generate-release with Phase 2 validation step --------- Co-authored-by: backryun Co-authored-by: diegosouzapw --- .agents/workflows/generate-release.md | 66 ++++-- CHANGELOG.md | 10 +- electron/package.json | 2 +- llm.txt | 4 +- open-sse/config/imageRegistry.ts | 218 +++++++++--------- open-sse/handlers/imageGeneration.ts | 39 ++-- open-sse/package.json | 2 +- package-lock.json | 6 +- package.json | 2 +- .../unit/bailian-coding-plan-provider.test.ts | 2 +- tests/unit/build-next-isolated.test.ts | 6 +- tests/unit/guide-settings-route.test.ts | 4 + tests/unit/image-generation-handler.test.ts | 36 +-- tests/unit/image-generation-route.test.ts | 4 +- tests/unit/models-catalog-route.test.ts | 10 +- tests/unit/openrouter-registry.test.ts | 6 +- tests/unit/pricing-sync.test.ts | 2 +- 17 files changed, 227 insertions(+), 192 deletions(-) diff --git a/.agents/workflows/generate-release.md b/.agents/workflows/generate-release.md index 8d0ac8ca17..aec69380de 100644 --- a/.agents/workflows/generate-release.md +++ b/.agents/workflows/generate-release.md @@ -213,11 +213,43 @@ Inform the user: --- -## Phase 2: Post-Merge (only after user confirms) +## Phase 2: Post-Merge Validation (Local VPS) -> Run these steps only AFTER the user has merged the PR. +> Run these steps only AFTER the user has merged the PR into `main` and all CI jobs have passed. -### 11. Create Git Tag and GitHub Release (MANDATORY) +### 11. Deploy to Local VPS for Final Validation (MANDATORY) + +Before cutting the official git tag and publishing to the world, deploy the `main` branch to the Local VPS for a final homologation test. + +```bash +git checkout main +git pull origin main + +# Build and pack locally +cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts + +# Deploy to LOCAL VPS (192.168.0.15) +scp omniroute-*.tgz root@192.168.0.15:/tmp/ +ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'" + +# Verify +curl -s -o /dev/null -w "LOCAL: HTTP %{http_code}\n" http://192.168.0.15:20128/ +``` + +### 12. 🛑 STOP — Notify User & Await Final OK + +**This is a mandatory stop point.** +Inform the user that the `main` branch is now running on the Local VPS. +Wait for the user to manually test and give the **OK**. +**DO NOT proceed to Phase 3 until the user confirms the local deploy is stable.** + +--- + +## Phase 3: Official Launch + +> Run these steps only AFTER the user gives the final OK from the Phase 2 local validation. + +### 13. Create Git Tag and GitHub Release (MANDATORY) // turbo @@ -239,7 +271,7 @@ gh release create "v$VERSION" --title "v$VERSION" --notes "$NOTES" --target main > **CRITICAL**: Docker Hub and npm MUST always publish the same version. > The Docker image is built automatically via GitHub Actions when a new tag is pushed. -> After pushing the tag in step 11-12, **verify the workflow runs**: +> After pushing the tag in step 13, **verify the workflow runs**: ```bash # Verify the Docker workflow triggered @@ -247,40 +279,30 @@ gh run list --repo diegosouzapw/OmniRoute --workflow docker-publish.yml --limit # Wait for the Docker build to complete (usually 5–10 min) gh run watch --repo diegosouzapw/OmniRoute - -# After completion, verify on Docker Hub: -# https://hub.docker.com/r/diegosouzapw/omniroute/tags ``` -If the Docker build was not triggered automatically, trigger it manually: +### 15. Publish to NPM (Optional/Automated) + +Normally handled by CI, but if manual publish is required: ```bash -gh workflow run docker-publish.yml --repo diegosouzapw/OmniRoute --ref v2.x.y +npm publish ``` -### 15. Deploy to BOTH VPS environments (MANDATORY) +### 16. Deploy to AKAMAI VPS (Production) -> Always deploy to **both** environments after every release. -> See `/deploy-vps` workflow for detailed steps. +Now that the release is officially cut, deploy it to the Akamai VPS. ```bash -# Build and pack locally -cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts - -# Deploy to LOCAL VPS (192.168.0.15) -scp omniroute-*.tgz root@192.168.0.15:/tmp/ -ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'" - # Deploy to AKAMAI VPS (69.164.221.35) scp omniroute-*.tgz root@69.164.221.35:/tmp/ ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'" -# Verify both -curl -s -o /dev/null -w "LOCAL: HTTP %{http_code}\n" http://192.168.0.15:20128/ +# Verify curl -s -o /dev/null -w "AKAMAI: HTTP %{http_code}\n" http://69.164.221.35:20128/ ``` -### 16. Preserve release branch +### 17. Preserve release branch ```bash # Branch is kept for historical purposes. Do not delete. diff --git a/CHANGELOG.md b/CHANGELOG.md index d4430d74a9..96c734486c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,17 @@ - **fix(claude):** strip existing billing headers from system array before injecting to prevent Anthropic prompt cache misses — stacked `x-anthropic-billing-header` blocks invalidated prefix matching, causing ~100% cache_create instead of cache_read (#1712) - **fix(claude):** strip `output_config.format` for non-Anthropic Claude-compatible providers during passthrough — third-party Claude endpoints (MiniMax, DeepSeek via aggregators) reject structured output fields with 400 errors (#1719) -- **fix(combo):** set terminal error state on response quality validation failure — prevents misleading `ALL_ACCOUNTS_INACTIVE` 503 when the real issue is response quality validation (#1707) +- **fix(combo):** set terminal error state on response quality validation failure — prevents misleading `ALL_ACCOUNTS_INACTIVE` 503 when the real issue is response quality validation (#1707, #1710) +- **fix(combo):** treat combo fallback as target-level orchestration — all non-ok responses (including generic 400s) now fall through to the next target instead of being terminal; removes complex bad-request allowlist regex (#1713) +- **fix(codex):** restore namespace MCP tools and hosted-tool whitelist — regression from #1581 that silently dropped all MCP tool groups and Responses-API hosted tools (#1715) +- **fix(codex):** add neutral instructions for bare chat requests — Codex Responses backend rejects requests without `instructions`, making Codex unusable for normal chat (#1709) - **fix(proxy):** wrap proxy assignment queries in try-catch for missing `proxy_assignments` table — Electron installs where migration 004 hasn't run no longer crash with `no such table` error (#1706) - **fix(migration):** improve Windows file URL path resolution in migration runner — adds direct URL path extraction and `process.cwd()` fallback for CI-built bundles with leaked build-time paths (#1704) +- **fix(ui):** fix light mode active request payload modal — add missing `--color-card` theme token, use opaque `bg-surface` instead of translucent `bg-card/70`, add backdrop blur (#1714) + +### 🔄 Updates + +- **chore(image-models):** refresh image generation model registry — replace stale FLUX aliases with FLUX Kontext / FLUX.2 mappings, remove deprecated FLUX Redux/Depth/Canny variants (#1722) --- diff --git a/electron/package.json b/electron/package.json index a15ff31ed1..1950cbfc25 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.7.2", + "version": "3.7.3", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/llm.txt b/llm.txt index 5f56546230..92e9d0c59b 100644 --- a/llm.txt +++ b/llm.txt @@ -8,7 +8,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo **Key value:** One endpoint (`http://localhost:20128/v1`), unlimited models, zero downtime, minimal cost. -**Current version:** 3.7.2 +**Current version:** 3.7.3 ## Tech Stack @@ -279,7 +279,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo └── .env.example # Environment variable template ``` -## Key Features (v3.7.2) +## Key Features (v3.7.3) ### Core Proxy - **160+ AI providers** with automatic format translation diff --git a/open-sse/config/imageRegistry.ts b/open-sse/config/imageRegistry.ts index 2c39ebb25a..a9fc20da0e 100644 --- a/open-sse/config/imageRegistry.ts +++ b/open-sse/config/imageRegistry.ts @@ -37,57 +37,60 @@ interface ImageModelAliasEntry { const IMAGE_MODEL_ALIASES: Record = { "flux-kontext": { - provider: "pollinations", - model: "flux-kontext", - name: "FLUX Kontext", + provider: "black-forest-labs", + model: "flux-kontext-pro", + name: "FLUX Kontext Pro", listInCatalog: true, inputModalities: ["text", "image"], }, "flux-kontext-max": { - provider: "together", - model: "black-forest-labs/FLUX.1.1-pro", + provider: "black-forest-labs", + model: "flux-kontext-max", name: "FLUX Kontext Max", listInCatalog: true, inputModalities: ["text", "image"], }, - "flux-redux": { - provider: "together", - model: "black-forest-labs/FLUX.1-redux", - name: "FLUX Redux", + "flux-2-max": { + provider: "black-forest-labs", + model: "flux-2-max", + name: "FLUX.2 Max", listInCatalog: true, inputModalities: ["text", "image"], }, - "flux-depth": { - provider: "together", - model: "black-forest-labs/FLUX.1-depth", - name: "FLUX Depth", + "flux-2-pro": { + provider: "black-forest-labs", + model: "flux-2-pro", + name: "FLUX.2 Pro", listInCatalog: true, inputModalities: ["text", "image"], }, - "flux-canny": { - provider: "together", - model: "black-forest-labs/FLUX.1-canny", - name: "FLUX Canny", + "flux-2-flex": { + provider: "black-forest-labs", + model: "flux-2-flex", + name: "FLUX.2 Flex", listInCatalog: true, inputModalities: ["text", "image"], }, - "flux-dev-lora": { + "flux-2-dev": { provider: "together", - model: "black-forest-labs/FLUX.1-dev-lora", - name: "FLUX Dev LoRA", + model: "black-forest-labs/FLUX.2-dev", + name: "FLUX.2 Dev", listInCatalog: true, + inputModalities: ["text", "image"], }, kontext: { - provider: "pollinations", - model: "flux-kontext", - name: "FLUX Kontext", + provider: "black-forest-labs", + model: "flux-kontext-pro", + name: "FLUX Kontext Pro", listInCatalog: false, + inputModalities: ["text", "image"], }, "pollinations/kontext": { - provider: "pollinations", - model: "flux-kontext", - name: "FLUX Kontext", + provider: "black-forest-labs", + model: "flux-kontext-pro", + name: "FLUX Kontext Pro", listInCatalog: false, + inputModalities: ["text", "image"], }, }; @@ -110,11 +113,11 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "bearer", format: "openai", // native OpenAI format models: [ - { id: "gpt-image-1", name: "GPT Image 1" }, - { id: "dall-e-3", name: "DALL-E 3" }, - { id: "dall-e-2", name: "DALL-E 2" }, + { id: "gpt-image-2", name: "GPT Image 2" }, + { id: "gpt-image-1.5", name: "GPT Image 1.5" }, + { id: "gpt-image-1-mini", name: "GPT Image 1 Mini" }, ], - supportedSizes: ["1024x1024", "1024x1792", "1792x1024", "256x256", "512x512"], + supportedSizes: ["1024x1024", "1024x1792", "1792x1024"], }, // Codex exposes image generation only as a Responses-API hosted tool under @@ -129,11 +132,11 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "bearer", format: "codex-responses", models: [ + { id: "gpt-5.5", name: "GPT 5.5 (Codex Image)" }, { id: "gpt-5.4", name: "GPT 5.4 (Codex Image)" }, { id: "gpt-5.3-codex", name: "GPT 5.3 Codex (Image)" }, - { id: "gpt-5.1-codex-max", name: "GPT 5.1 Codex Max (Image)" }, ], - supportedSizes: ["512x512", "1024x1024", "1024x1536", "1536x1024"], + supportedSizes: ["1024x1024", "1024x1536", "1536x1024"], }, "chatgpt-web": { @@ -153,7 +156,7 @@ export const IMAGE_PROVIDERS: Record = { authType: "apikey", authHeader: "bearer", format: "openai", - models: [{ id: "grok-2-image-1212", name: "Grok 2 Image" }], + models: [{ id: "grok-imagine-image", name: "Grok Imagine Image" }], supportedSizes: ["1024x1024"], }, @@ -165,32 +168,39 @@ export const IMAGE_PROVIDERS: Record = { format: "openai", models: [ { - id: "black-forest-labs/FLUX.1.1-pro", - name: "FLUX 1.1 Pro", + id: "black-forest-labs/FLUX.2-max", + name: "FLUX.2 Max", inputModalities: ["text", "image"], - description: "Advanced contextual image editing and image-to-image generation", - }, - { id: "black-forest-labs/FLUX.1-schnell-Free", name: "FLUX 1 Schnell (Free)" }, - { id: "stabilityai/stable-diffusion-xl-base-1.0", name: "SDXL Base 1.0" }, - { - id: "black-forest-labs/FLUX.1-redux", - name: "FLUX.1 Redux", - inputModalities: ["text", "image"], - description: "Generate prompt-guided variations from an input image", }, { - id: "black-forest-labs/FLUX.1-depth", - name: "FLUX.1 Depth", + id: "black-forest-labs/FLUX.2-pro", + name: "FLUX.2 Pro", inputModalities: ["text", "image"], - description: "Depth-conditioned image generation from an input image", }, { - id: "black-forest-labs/FLUX.1-canny", - name: "FLUX.1 Canny", + id: "black-forest-labs/FLUX.2-flex", + name: "FLUX.2 Flex", + inputModalities: ["text", "image"], + }, + { + id: "black-forest-labs/FLUX.2-dev", + name: "FLUX.2 Dev", + inputModalities: ["text", "image"], + }, + { id: "openai/gpt-image-1.5", name: "GPT Image 1.5", inputModalities: ["text", "image"] }, + { id: "Wan-AI/Wan2.6-image", name: "Wan 2.6 Image", inputModalities: ["text", "image"] }, + { + id: "Qwen/Qwen-Image-2.0-Pro", + name: "Qwen Image 2.0 Pro", + inputModalities: ["text", "image"], + }, + { id: "Qwen/Qwen-Image-2.0", name: "Qwen Image 2.0", inputModalities: ["text", "image"] }, + { id: "google/flash-image-3.1", name: "NanoBanana 2", inputModalities: ["text", "image"] }, + { + id: "google/gemini-3-pro-image", + name: "NanoBanana Pro", inputModalities: ["text", "image"], - description: "Canny edge guided image generation from an input image", }, - { id: "black-forest-labs/FLUX.1-dev-lora", name: "FLUX.1 Dev LoRA" }, ], supportedSizes: ["1024x1024", "512x512"], }, @@ -202,6 +212,9 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "bearer", format: "openai", models: [ + { id: "accounts/fireworks/flux-kontext-max", name: "FLUX Kontext Max" }, + { id: "accounts/fireworks/flux-kontext-pro", name: "FLUX Kontext Pro" }, + { id: "accounts/fireworks/flux-1-schnell-fp8", name: "FLUX.1 schnell" }, { id: "accounts/fireworks/models/flux-1-dev-fp8", name: "FLUX 1 Dev FP8" }, { id: "accounts/fireworks/models/stable-diffusion-xl-1024-v1-0", name: "SDXL 1024 v1.0" }, ], @@ -218,6 +231,7 @@ export const IMAGE_PROVIDERS: Record = { supportedSizes: ["1024x1024"], }, + //Curruntly no models serving nebius: { id: "nebius", baseUrl: "https://api.tokenfactory.nebius.com/v1/images/generations", @@ -225,11 +239,8 @@ export const IMAGE_PROVIDERS: Record = { authType: "apikey", authHeader: "bearer", format: "openai", - models: [ - { id: "black-forest-labs/flux-schnell", name: "FLUX.1 Schnell" }, - { id: "black-forest-labs/flux-dev", name: "FLUX.1 Dev" }, - ], - supportedSizes: ["1024x1024", "512x512"], + models: [{ id: "black-forest-labs/flux-schnell", name: "No Model yet" }], + supportedSizes: ["1024x1024"], }, hyperbolic: { @@ -238,13 +249,10 @@ export const IMAGE_PROVIDERS: Record = { authType: "apikey", authHeader: "bearer", format: "hyperbolic", // custom: uses model_name, returns base64 images - models: [ - { id: "SDXL1.0-base", name: "SDXL 1.0 Base" }, - { id: "SD2", name: "Stable Diffusion 2" }, - { id: "FLUX.1-dev", name: "FLUX.1 Dev" }, - ], - supportedSizes: ["1024x1024", "512x512"], + models: [{ id: "SDXL1.0-base", name: "No Model yet" }], + supportedSizes: ["1024x1024"], }, + //Curruntly no models serving nanobanana: { id: "nanobanana", @@ -294,12 +302,15 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "bearer", format: "openai", models: [ - { id: "openai/dall-e-3", name: "DALL-E 3 (via OpenRouter)" }, - { id: "openai/gpt-image-1", name: "GPT Image 1 (via OpenRouter)" }, - { id: "google/imagen-3.0-generate-002", name: "Imagen 3 (via OpenRouter)" }, - { id: "black-forest-labs/FLUX.1-schnell", name: "FLUX.1 Schnell (via OpenRouter)" }, + { id: "openai/gpt-5.4-image-2", name: "GPT Image 2 (via OpenRouter)" }, + { id: "openai/gpt-5-image-mini", name: "GPT Image 1 Mini (via OpenRouter)" }, + { id: "google/gemini-3.1-flash-image-preview", name: "Nano Banana 2 (via OpenRouter)" }, + { id: "google/gemini-3-pro-image-preview", name: "Nano Banana Pro (via OpenRouter)" }, + { id: "black-forest-labs/flux.2-max", name: "FLUX.2 Max (via OpenRouter)" }, + { id: "black-forest-labs/flux.2-pro", name: "FLUX.2 Pro (via OpenRouter)" }, + { id: "black-forest-labs/flux.2-flex", name: "FLUX.2 Flex (via OpenRouter)" }, ], - supportedSizes: ["1024x1024", "1024x1792", "1792x1024", "256x256", "512x512"], + supportedSizes: ["1024x1024", "1024x1792", "1792x1024"], }, pollinations: { @@ -310,25 +321,14 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "bearer", format: "openai", models: [ + { id: "klein", name: "FLUX.2 Klein 4B" }, { id: "flux", name: "Flux Schnell" }, { id: "zimage", name: "Z-Image Turbo" }, - { id: "klein", name: "FLUX.2 Klein 4B" }, - { id: "gptimage", name: "GPT Image 1 Mini" }, { id: "qwen-image", name: "Qwen Image Plus" }, { id: "wan-image", name: "Wan 2.7 Image" }, - { - id: "flux-kontext", - name: "FLUX.1 Kontext", - inputModalities: ["text", "image"], - description: "Context-aware image editing with optional source image", - }, - { - id: "flux-kontext-max", - name: "FLUX.1 Kontext Max", - inputModalities: ["text", "image"], - description: "Higher quality Kontext editing with optional source image", - }, + { id: "gpt-image-2", name: "GPT Image 2" }, { id: "gptimage-large", name: "GPT Image 1.5" }, + { id: "gptimage", name: "GPT Image 1 Mini" }, ], supportedSizes: ["1024x1024", "512x512"], }, @@ -340,17 +340,17 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "key", format: "fal-ai", models: [ - { id: "fal-ai/flux-pro/v1.1", name: "FLUX.1 Pro v1.1" }, - { id: "fal-ai/flux-pro/v1.1-ultra", name: "FLUX.1 Pro v1.1 Ultra" }, - { id: "fal-ai/flux/schnell", name: "FLUX.1 Schnell" }, + { id: "fal-ai/flux-2-max", name: "FLUX.2 Max" }, + { id: "fal-ai/flux-2-pro", name: "FLUX.2 Pro" }, + { id: "fal-ai/flux-2-flex", name: "FLUX.2 Flex" }, { id: "bria/text-to-image/3.2", name: "Bria 3.2" }, - { id: "fal-ai/bytedance/seedream/v3/text-to-image", name: "SeeDream V3" }, + { id: "fal-ai/bytedance/seedream/v4.5/text-to-image", name: "SeeDream V4.5" }, { id: "fal-ai/bytedance/dreamina/v3.1/text-to-image", name: "Dreamina V3.1" }, { id: "fal-ai/ideogram/v3", name: "Ideogram V3" }, - { id: "fal-ai/imagen4/preview", name: "Imagen 4 Preview" }, - { id: "fal-ai/imagen4/preview/fast", name: "Imagen 4 Preview Fast" }, - { id: "fal-ai/imagen4/preview/ultra", name: "Imagen 4 Preview Ultra" }, - { id: "fal-ai/recraft/v3/text-to-image", name: "Recraft V3 via Fal" }, + { id: "fal-ai/nano-banana-pro", name: "Nano Banana Pro" }, + { id: "fal-ai/nano-banana-2", name: "Nano Banana 2" }, + { id: "fal-ai/recraft/v4/pro/text-to-image", name: "Recraft V4 Pro via Fal" }, + { id: "fal-ai/recraft/v4/text-to-image", name: "Recraft V4 via Fal" }, { id: "fal-ai/stable-diffusion-v35-medium", name: "Stable Diffusion v3.5 Medium" }, ], supportedSizes: ["1024x1024", "1024x1280", "1280x1024"], @@ -363,29 +363,26 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "bearer", format: "stability-ai", models: [ - { id: "sd3", name: "sd3" }, - { id: "sd3-large", name: "sd3-large" }, - { id: "sd3-large-turbo", name: "sd3-large-turbo" }, - { id: "sd3-medium", name: "sd3-medium" }, - { id: "sd3.5-large", name: "sd3.5-large" }, - { id: "sd3.5-large-turbo", name: "sd3.5-large-turbo" }, - { id: "sd3.5-medium", name: "sd3.5-medium" }, { id: "stable-image-ultra", name: "Stable Image Ultra" }, { id: "stable-image-core", name: "Stable Image Core" }, + { id: "sd3.5-large-turbo", name: "sd3.5-large-turbo" }, + { id: "sd3.5-large", name: "sd3.5-large" }, + { id: "sd3.5-medium", name: "sd3.5-medium" }, + { id: "sd3.5-flash", name: "sd3.5-flash" }, + { id: "erase", name: "Erase", inputModalities: ["image"] }, { id: "inpaint", name: "Inpaint", inputModalities: ["text", "image"] }, { id: "outpaint", name: "Outpaint", inputModalities: ["text", "image"] }, - { id: "erase", name: "Erase", inputModalities: ["image"] }, + { id: "remove-background", name: "Remove Background", inputModalities: ["image"] }, { id: "search-and-replace", name: "Search and Replace", inputModalities: ["text", "image"] }, { id: "search-and-recolor", name: "Search and Recolor", inputModalities: ["text", "image"] }, - { id: "remove-background", name: "Remove Background", inputModalities: ["image"] }, { id: "replace-background-and-relight", name: "Replace Background and Relight", inputModalities: ["text", "image"], }, + { id: "creative", name: "Creative Upscale", inputModalities: ["text", "image"] }, { id: "fast", name: "Fast Upscale", inputModalities: ["image"] }, { id: "conservative", name: "Conservative Upscale", inputModalities: ["image"] }, - { id: "creative", name: "Creative Upscale", inputModalities: ["text", "image"] }, { id: "sketch", name: "Sketch Control", inputModalities: ["text", "image"] }, { id: "structure", name: "Structure Control", inputModalities: ["text", "image"] }, { id: "style", name: "Style Control", inputModalities: ["text", "image"] }, @@ -401,16 +398,15 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "x-key", format: "black-forest-labs", models: [ - { id: "flux-kontext-pro", name: "flux-kontext-pro", inputModalities: ["text", "image"] }, - { id: "flux-kontext-max", name: "flux-kontext-max", inputModalities: ["text", "image"] }, - { id: "flux-pro-1.0-fill", name: "flux-pro-1.0-fill", inputModalities: ["text", "image"] }, - { - id: "flux-pro-1.0-expand", - name: "flux-pro-1.0-expand", - inputModalities: ["text", "image"], - }, - { id: "flux-pro-1.1", name: "flux-pro-1.1" }, + { id: "flux-2-max", name: "FLUX.2 Max" }, + { id: "flux-2-pro", name: "FLUX.2 Pro" }, + { id: "flux-2-flex", name: "FLUX.2 Flex" }, { id: "flux-pro-1.1-ultra", name: "flux-pro-1.1-ultra" }, + { id: "flux-pro-1.1", name: "flux-pro-1.1" }, + { id: "flux-2-klein-9b", name: "flux 2 Klein 9B" }, + { id: "flux-2-klein-4b", name: "flux 2 Klein 4B" }, + { id: "flux-kontext-max", name: "flux-kontext-max", inputModalities: ["text", "image"] }, + { id: "flux-kontext-pro", name: "flux-kontext-pro", inputModalities: ["text", "image"] }, { id: "flux-dev", name: "flux-dev" }, { id: "flux-pro", name: "flux-pro" }, ], @@ -424,8 +420,10 @@ export const IMAGE_PROVIDERS: Record = { authHeader: "bearer", format: "recraft", models: [ - { id: "recraftv2", name: "recraftv2" }, - { id: "recraftv3", name: "recraftv3" }, + { id: "recraftv4_pro", name: "Recraft V4 Pro" }, + { id: "recraftv4", name: "Recraft V4" }, + { id: "recraftv3", name: "Recraft V3" }, + { id: "recraftv2", name: "Recraft V2" }, ], supportedSizes: ["1024x1024", "1024x1280", "1280x1024"], }, diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index dd3d7bbff1..be6fe9dac4 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -32,24 +32,29 @@ import { import { fetchRemoteImage } from "@/shared/network/remoteImageFetch"; const OPENAI_IMAGE_TO_IMAGE_MODELS = new Set([ - "black-forest-labs/FLUX.1-redux", - "black-forest-labs/FLUX.1-depth", - "black-forest-labs/FLUX.1-canny", - "black-forest-labs/FLUX.1.1-pro", - "FLUX.1-redux", - "FLUX.1-depth", - "FLUX.1-canny", - "FLUX.1.1-pro", + "black-forest-labs/FLUX.2-max", + "black-forest-labs/FLUX.2-pro", + "black-forest-labs/FLUX.2-flex", + "black-forest-labs/FLUX.2-dev", + "openai/gpt-image-1.5", + "Wan-AI/Wan2.6-image", + "Qwen/Qwen-Image-2.0-Pro", + "Qwen/Qwen-Image-2.0", + "google/flash-image-3.1", + "google/gemini-3-pro-image", "flux-kontext-max", "flux-kontext", "flux-kontext-pro", ]); const BFL_MODEL_ENDPOINTS = { + "flux-2-max": "/v1/flux-2-max", + "flux-2-pro": "/v1/flux-2-pro", + "flux-2-flex": "/v1/flux-2-flex", + "flux-2-klein-9b": "/v1/flux-2-klein-9b", + "flux-2-klein-4b": "/v1/flux-2-klein-4b", "flux-kontext-pro": "/v1/flux-kontext-pro", "flux-kontext-max": "/v1/flux-kontext-max", - "flux-pro-1.0-fill": "/v1/flux-pro-1.0-fill", - "flux-pro-1.0-expand": "/v1/flux-pro-1.0-expand", "flux-pro-1.1": "/v1/flux-pro-1.1", "flux-pro-1.1-ultra": "/v1/flux-pro-1.1-ultra", "flux-dev": "/v1/flux-dev", @@ -57,22 +62,20 @@ const BFL_MODEL_ENDPOINTS = { }; const BFL_EDIT_MODELS = new Set([ + "flux-2-max", + "flux-2-pro", + "flux-2-flex", "flux-kontext-pro", "flux-kontext-max", - "flux-pro-1.0-fill", - "flux-pro-1.0-expand", ]); const BFL_FAILURE_STATUSES = new Set(["Error", "Failed", "Content Moderated", "Request Moderated"]); const STABILITY_GENERATION_ENDPOINTS = { - sd3: "/v2beta/stable-image/generate/sd3", - "sd3-large": "/v2beta/stable-image/generate/sd3", - "sd3-large-turbo": "/v2beta/stable-image/generate/sd3", - "sd3-medium": "/v2beta/stable-image/generate/sd3", "sd3.5-large": "/v2beta/stable-image/generate/sd3", "sd3.5-large-turbo": "/v2beta/stable-image/generate/sd3", "sd3.5-medium": "/v2beta/stable-image/generate/sd3", + "sd3.5-flash": "/v2beta/stable-image/generate/sd3", "stable-image-ultra": "/v2beta/stable-image/generate/ultra", "stable-image-core": "/v2beta/stable-image/generate/core", }; @@ -1058,7 +1061,7 @@ async function handleStabilityAIImageGeneration({ try { if (STABILITY_GENERATION_ENDPOINTS[model]) { - if (model.startsWith("sd3") && model !== "sd3") { + if (model.startsWith("sd3.5")) { upstreamBody.model = model; } @@ -1070,7 +1073,7 @@ async function handleStabilityAIImageGeneration({ upstreamBody.mode = "text-to-image"; } - if (!model.startsWith("sd3") || !imageUrl) { + if (!model.startsWith("sd3.5") || !imageUrl) { upstreamBody.aspect_ratio = body.aspect_ratio || mapImageSize(body.size); } diff --git a/open-sse/package.json b/open-sse/package.json index 5c465c5566..e311fc67fa 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.7.2", + "version": "3.7.3", "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", "type": "module", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index e259fc9356..2937d025b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.7.2", + "version": "3.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.7.2", + "version": "3.7.3", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -13808,7 +13808,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.7.2" + "version": "3.7.3" } } } diff --git a/package.json b/package.json index de313ad1d9..a077aadc94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.7.2", + "version": "3.7.3", "description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.", "type": "module", "bin": { diff --git a/tests/unit/bailian-coding-plan-provider.test.ts b/tests/unit/bailian-coding-plan-provider.test.ts index dd31b4c0e3..a6b3b02723 100644 --- a/tests/unit/bailian-coding-plan-provider.test.ts +++ b/tests/unit/bailian-coding-plan-provider.test.ts @@ -274,7 +274,7 @@ test("getStaticModelsForProvider returns local image catalogs for image-only pro assert.ok(models, "xAI should expose local image models"); assert.deepEqual( models.map((model) => model.id), - ["grok-2-image-1212"] + ["grok-imagine-image"] ); }); diff --git a/tests/unit/build-next-isolated.test.ts b/tests/unit/build-next-isolated.test.ts index 938122e7a0..e5e8c93284 100644 --- a/tests/unit/build-next-isolated.test.ts +++ b/tests/unit/build-next-isolated.test.ts @@ -115,7 +115,7 @@ test("getTransientBuildPaths leaves _tasks in place by default", () => { ["legacy app snapshot", "local Wine prefix"] ); assert.equal( - paths.some((entry) => entry.sourcePath === "/repo/_tasks"), + paths.some((entry) => path.basename(entry.sourcePath) === "_tasks"), false ); }); @@ -124,7 +124,7 @@ test("getTransientBuildPaths only moves _tasks when explicitly enabled", () => { const paths = getTransientBuildPaths("/repo", { OMNIROUTE_BUILD_MOVE_TASKS: "1" }); assert.equal( - paths.some((entry) => entry.sourcePath === "/repo/_tasks"), + paths.some((entry) => path.basename(entry.sourcePath) === "_tasks"), true ); }); @@ -170,6 +170,6 @@ test("syncStandaloneNativeAssets copies wreq-js native runtime into standalone o assert.equal(changed, true); assert.equal(await fs.readFile(destinationNativeFile, "utf8"), "native module bytes"); - assert.match(logs[0] ?? "", /wreq-js\/rust/); + assert.match((logs[0] ?? "").replaceAll("\\", "/"), /wreq-js\/rust/); }); }); diff --git a/tests/unit/guide-settings-route.test.ts b/tests/unit/guide-settings-route.test.ts index 28c4debec5..31e385401f 100644 --- a/tests/unit/guide-settings-route.test.ts +++ b/tests/unit/guide-settings-route.test.ts @@ -13,6 +13,7 @@ const QWEN_CONFIG_PATH = path.join(DUMMY_HOME, ".qwen", "settings.json"); const QWEN_ENV_PATH = path.join(DUMMY_HOME, ".qwen", ".env"); const OPENCODE_CONFIG_PATH = path.join(DUMMY_HOME, ".config", "opencode", "opencode.json"); const originalXDG = process.env.XDG_CONFIG_HOME; +const originalAppData = process.env.APPDATA; const originalJwtSecret = process.env.JWT_SECRET; async function createAuthCookie() { @@ -51,6 +52,7 @@ test.beforeEach(async () => { // Force XDG_CONFIG_HOME so resolveOpencodeConfigPath resolves to our dummy dir // (CI runners often have XDG_CONFIG_HOME set, causing path mismatch) process.env.XDG_CONFIG_HOME = path.join(DUMMY_HOME, ".config"); + process.env.APPDATA = path.join(DUMMY_HOME, ".config"); process.env.API_KEY_SECRET = "test-secret"; await fs.mkdir(path.dirname(QWEN_CONFIG_PATH), { recursive: true }).catch(() => {}); }); @@ -59,6 +61,8 @@ test.afterEach(async () => { await fs.rm(DUMMY_HOME, { recursive: true, force: true }).catch(() => {}); if (originalXDG === undefined) delete process.env.XDG_CONFIG_HOME; else process.env.XDG_CONFIG_HOME = originalXDG; + if (originalAppData === undefined) delete process.env.APPDATA; + else process.env.APPDATA = originalAppData; if (originalJwtSecret === undefined) delete process.env.JWT_SECRET; else process.env.JWT_SECRET = originalJwtSecret; }); diff --git a/tests/unit/image-generation-handler.test.ts b/tests/unit/image-generation-handler.test.ts index 4eafc2cfd1..6e7ff3ca1e 100644 --- a/tests/unit/image-generation-handler.test.ts +++ b/tests/unit/image-generation-handler.test.ts @@ -54,10 +54,10 @@ test("handleImageGeneration routes OpenAI-compatible providers and forwards imag try { const result = await handleImageGeneration({ body: { - model: "openai/dall-e-3", + model: "openai/gpt-image-2", prompt: "city skyline", n: 2, - size: "1024x1792", + size: "1024x1536", quality: "hd", response_format: "url", style: "vivid", @@ -70,10 +70,10 @@ test("handleImageGeneration routes OpenAI-compatible providers and forwards imag assert.equal(captured.url, "https://api.openai.com/v1/images/generations"); assert.equal(captured.headers.Authorization, "Bearer image-key"); assert.deepEqual(captured.body, { - model: "dall-e-3", + model: "gpt-image-2", prompt: "city skyline", n: 2, - size: "1024x1792", + size: "1024x1536", quality: "hd", response_format: "url", style: "vivid", @@ -250,32 +250,32 @@ test("handleImageGeneration treats unknown provider prefixes as invalid image mo test("image registry resolves flux aliases and exposes planned catalog aliases", () => { assert.deepEqual(parseImageModel("flux-kontext"), { - provider: "pollinations", - model: "flux-kontext", + provider: "black-forest-labs", + model: "flux-kontext-pro", }); assert.deepEqual(parseImageModel("pollinations/kontext"), { - provider: "pollinations", - model: "flux-kontext", + provider: "black-forest-labs", + model: "flux-kontext-pro", }); - assert.deepEqual(parseImageModel("flux-redux"), { + assert.deepEqual(parseImageModel("flux-2-dev"), { provider: "together", - model: "black-forest-labs/FLUX.1-redux", + model: "black-forest-labs/FLUX.2-dev", }); const modelIds = new Set(getAllImageModels().map((model) => model.id)); - const fluxRedux = getAllImageModels().find((model) => model.id === "flux-redux"); + const flux2Dev = getAllImageModels().find((model) => model.id === "flux-2-dev"); const fluxKontext = getAllImageModels().find((model) => model.id === "flux-kontext"); for (const alias of [ "flux-kontext", "flux-kontext-max", - "flux-redux", - "flux-depth", - "flux-canny", - "flux-dev-lora", + "flux-2-max", + "flux-2-pro", + "flux-2-flex", + "flux-2-dev", ]) { assert.equal(modelIds.has(alias), true, `Expected alias ${alias} in image catalog`); } - assert.deepEqual(fluxRedux?.inputModalities, ["text", "image"]); + assert.deepEqual(flux2Dev?.inputModalities, ["text", "image"]); assert.deepEqual(fluxKontext?.inputModalities, ["text", "image"]); }); @@ -1130,7 +1130,7 @@ test("handleImageGeneration logs OpenAI-compatible upstream failures and transpo try { const failed = await handleImageGeneration({ body: { - model: "openai/dall-e-3", + model: "openai/gpt-image-2", prompt: "broken upstream", }, credentials: { apiKey: "image-key" }, @@ -1151,7 +1151,7 @@ test("handleImageGeneration logs OpenAI-compatible upstream failures and transpo try { const errored = await handleImageGeneration({ body: { - model: "openai/dall-e-3", + model: "openai/gpt-image-2", prompt: "transport issue", }, credentials: { apiKey: "image-key" }, diff --git a/tests/unit/image-generation-route.test.ts b/tests/unit/image-generation-route.test.ts index e3ec9e157f..678771ddec 100644 --- a/tests/unit/image-generation-route.test.ts +++ b/tests/unit/image-generation-route.test.ts @@ -101,7 +101,7 @@ test("v1 image generation POST still requires prompts for text-input models", as method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - model: "openai/dall-e-3", + model: "openai/gpt-image-2", image_url: "https://example.com/source.png", }), }) @@ -109,5 +109,5 @@ test("v1 image generation POST still requires prompts for text-input models", as const body = (await response.json()) as any; assert.equal(response.status, 400); - assert.match(body.error.message, /Prompt is required for image model: openai\/dall-e-3/); + assert.match(body.error.message, /Prompt is required for image model: openai\/gpt-image-2/); }); diff --git a/tests/unit/models-catalog-route.test.ts b/tests/unit/models-catalog-route.test.ts index ad2ad9ffe2..8d883df60f 100644 --- a/tests/unit/models-catalog-route.test.ts +++ b/tests/unit/models-catalog-route.test.ts @@ -464,7 +464,7 @@ test("v1 models catalog includes media, moderation, rerank, video, and music mod const byId = new Map(body.data.map((item) => [item.id, item])); assert.equal(response.status, 200); - assert.equal((byId.get("openai/gpt-image-1") as any).type, "image"); + assert.equal((byId.get("openai/gpt-image-2") as any).type, "image"); assert.equal((byId.get("openai/whisper-1") as any).type, "audio"); assert.equal((byId.get("openai/whisper-1") as any).subtype, "transcription"); assert.equal((byId.get("openai/omni-moderation-latest") as any).type, "moderation"); @@ -484,10 +484,10 @@ test("v1 models catalog exposes image model input and output modalities for adva const byId = new Map(body.data.map((item) => [item.id, item])); assert.equal(response.status, 200); - assert.deepEqual((byId as any).get("flux-redux")?.input_modalities, ["text", "image"]); - (assert as any).deepEqual((byId.get("flux-redux") as any).output_modalities, ["image"]); - (assert as any).equal((byId.get("flux-redux") as any).type, "image"); - assert.ok((byId.get("flux-redux") as any).supported_sizes?.includes("1024x1024")); + assert.deepEqual((byId as any).get("flux-2-dev")?.input_modalities, ["text", "image"]); + (assert as any).deepEqual((byId.get("flux-2-dev") as any).output_modalities, ["image"]); + (assert as any).equal((byId.get("flux-2-dev") as any).type, "image"); + assert.ok((byId.get("flux-2-dev") as any).supported_sizes?.includes("1024x1024")); (assert as any).deepEqual((byId.get("topaz/topaz-enhance") as any).input_modalities, ["image"]); assert.deepEqual((byId.get("topaz/topaz-enhance") as any).output_modalities, ["image"]); }); diff --git a/tests/unit/openrouter-registry.test.ts b/tests/unit/openrouter-registry.test.ts index 6e724e62e6..183f5492d7 100644 --- a/tests/unit/openrouter-registry.test.ts +++ b/tests/unit/openrouter-registry.test.ts @@ -71,10 +71,10 @@ describe("OpenRouter & GitHub registry entries (#960)", () => { assert.ok(p.models.length >= 2, `Expected ≥2 models, got ${p.models.length}`); }); - it("parses openrouter/openai/dall-e-3 correctly", () => { - const result = parseImageModel("openrouter/openai/dall-e-3"); + it("parses openrouter/openai/gpt-5.4-image-2 correctly", () => { + const result = parseImageModel("openrouter/openai/gpt-5.4-image-2"); assert.equal(result.provider, "openrouter"); - assert.equal(result.model, "openai/dall-e-3"); + assert.equal(result.model, "openai/gpt-5.4-image-2"); }); }); }); diff --git a/tests/unit/pricing-sync.test.ts b/tests/unit/pricing-sync.test.ts index 01c97628dd..173cf6e88e 100644 --- a/tests/unit/pricing-sync.test.ts +++ b/tests/unit/pricing-sync.test.ts @@ -67,7 +67,7 @@ describe("transformToOmniRoute", () => { litellm_provider: "openai", mode: "embedding", }, - "openai/dall-e-3": { + "openai/gpt-image-2": { input_cost_per_token: 0, output_cost_per_token: 0, litellm_provider: "openai",