diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 864bc8f5f7..9d009ac268 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,8 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 cache: npm @@ -36,8 +36,8 @@ jobs: name: Security Audit runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 cache: npm @@ -55,8 +55,8 @@ jobs: matrix: node-version: [20, 22] steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: npm @@ -74,8 +74,8 @@ jobs: JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-test-api-key-secret-long steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: npm @@ -90,8 +90,8 @@ jobs: JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-test-api-key-secret-long steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 cache: npm @@ -109,8 +109,8 @@ jobs: JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-test-api-key-secret-long steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 cache: npm @@ -129,8 +129,8 @@ jobs: INITIAL_PASSWORD: ci-test-password-for-integration DATA_DIR: /tmp/omniroute-ci steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 cache: npm @@ -145,8 +145,8 @@ jobs: JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-test-api-key-secret-long steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 cache: npm diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 28466e9093..3d2aa33472 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -25,24 +25,24 @@ jobs: IMAGE_NAME: diegosouzapw/omniroute steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }} - name: Set up QEMU (for multi-arch builds) - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v4 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -61,7 +61,7 @@ jobs: echo "Publishing Docker image: $IMAGE_NAME:$VERSION" - name: Build and push multi-arch image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@v6 with: context: . target: runner-base @@ -83,7 +83,7 @@ jobs: docker buildx imagetools inspect "${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" - name: Update Docker Hub description - uses: peter-evans/dockerhub-description@v5 + uses: peter-evans/dockerhub-description@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml index 0d81b0dc65..c567980b1b 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/electron-release.yml @@ -22,7 +22,7 @@ jobs: version: ${{ steps.validate.outputs.version }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -70,16 +70,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v4 with: node-version: 22 cache: npm - name: Cache node_modules - uses: actions/cache@v5 + uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} @@ -146,7 +146,7 @@ jobs: fi - name: Upload artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: name: electron-${{ matrix.platform }} path: release-assets/ @@ -157,12 +157,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download all artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v4 with: path: release-assets merge-multiple: true diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index b8ef18e630..46ae68bbb8 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -43,10 +43,10 @@ jobs: environment: NPM_TOKEN steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v4 with: node-version: 22 registry-url: https://registry.npmjs.org @@ -111,11 +111,11 @@ jobs: run: | VERSION="${{ steps.resolve.outputs.version }}" TAG="${{ steps.resolve.outputs.tag }}" - + echo "Configuring for GitHub Packages..." echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > .npmrc npm pkg set name="@diegosouzapw/omniroute" - + if [ "$TAG" = "latest" ]; then npm publish --registry=https://npm.pkg.github.com || echo "⚠️ Version ${VERSION} might already be published on GitHub." else diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e4350689..aa857c4a2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ --- +## [3.3.3] - 2026-03-29 + +### 🐛 Bug Fixes + +- **CI/CD Reliability:** Patched GitHub Actions to stable dependency versions (`actions/checkout@v4`, `actions/upload-artifact@v4`) to mitigate unannounced builder environment deprecations. +- **Image Fallbacks:** Replaced arbitrary fallback chains in `ProviderIcon.tsx` with explicit asset validation to prevent UI loading `` components for files that don't exist, eliminating `404` errors in dashboard console logs (#745). +- **Admin Updater:** Dynamic source-installation detection for the dashboard Updater. Safely disables the `Update Now` button when OmniRoute is built locally rather than through npm, prompting for `git pull` (#743). +- **Update ERESOLVE Error:** Injected `package.json` overrides for `react`/`react-dom` and enabled `--legacy-peer-deps` within the internal automatic updater scripts to resolve breaking dependency tree conflicts with `@lobehub/ui`. + +--- + ## [3.3.2] - 2026-03-29 ### ✨ New Features diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 6c27c706c0..e637d8451d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.3.2 + version: 3.3.3 description: | OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible endpoint that routes requests to multiple AI providers with load balancing, diff --git a/package-lock.json b/package-lock.json index fd63879e90..3e802e9216 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.3.2", + "version": "3.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.3.2", + "version": "3.3.3", "hasInstallScript": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 0e999354dc..7ef05cbce6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.3.2", + "version": "3.3.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": { @@ -162,6 +162,8 @@ }, "overrides": { "dompurify": "^3.3.2", - "path-to-regexp": "^8.4.0" + "path-to-regexp": "^8.4.0", + "react": "$react", + "react-dom": "$react-dom" } } diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx index d75391a2a5..3c79de9de2 100644 --- a/src/app/(dashboard)/dashboard/HomePageClient.tsx +++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx @@ -546,18 +546,22 @@ export default function HomePageClient({ machineId }) {

Update Available: v{versionInfo.latest}

- {t("updateAvailableDesc") || - `You are currently using v${versionInfo.current}. Update to access the latest features and bug fixes.`} + {versionInfo.autoUpdateSupported + ? t("updateAvailableDesc") || + `You are currently using v${versionInfo.current}. Update to access the latest features and bug fixes.` + : versionInfo.autoUpdateError || + "Manual update required for this installation type."}

)} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx index f41acf90d4..42c2cec083 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx @@ -7,6 +7,7 @@ import Card from "@/shared/components/Card"; import Badge from "@/shared/components/Badge"; import QuotaProgressBar from "./QuotaProgressBar"; import { calculatePercentage } from "./utils"; +import ProviderIcon from "@/shared/components/ProviderIcon"; const planVariants = { free: "default", @@ -70,15 +71,7 @@ export default function ProviderLimitCard({ {provider?.slice(0, 2).toUpperCase() || "PR"} ) : ( - {provider setImgError(true)} - /> + )} diff --git a/src/app/api/system/version/route.ts b/src/app/api/system/version/route.ts index 381a86ca06..22b38953e5 100644 --- a/src/app/api/system/version/route.ts +++ b/src/app/api/system/version/route.ts @@ -95,6 +95,17 @@ export async function POST(req: NextRequest) { } const config = getAutoUpdateConfig(); + const validation = await validateAutoUpdateRuntime(config); + + if (!validation.supported) { + return NextResponse.json( + { + success: false, + error: validation.reason || "Auto-update is not supported in this environment.", + }, + { status: 400 } + ); + } // If we are in docker-compose mode, use the detached shell script background updates if (config.mode === "docker-compose") { @@ -132,9 +143,13 @@ export async function POST(req: NextRequest) { try { // Step 1: Install send({ step: "install", status: "running", message: `Installing omniroute@${latest}...` }); - await execFileAsync("npm", ["install", "-g", `omniroute@${latest}`, "--ignore-scripts"], { - timeout: 300000, - }); + await execFileAsync( + "npm", + ["install", "-g", `omniroute@${latest}`, "--ignore-scripts", "--legacy-peer-deps"], + { + timeout: 300000, + } + ); send({ step: "install", status: "done", message: `Installed omniroute@${latest}` }); // Step 2: Rebuild native modules (critical for better-sqlite3) diff --git a/src/lib/system/autoUpdate.ts b/src/lib/system/autoUpdate.ts index dd1e943f4d..5757801d64 100644 --- a/src/lib/system/autoUpdate.ts +++ b/src/lib/system/autoUpdate.ts @@ -55,15 +55,26 @@ function shellQuote(value: string): string { } function parsePatchCommits(raw: string | undefined): string[] { - return (raw || "").split(/[\s,]+/).map((value) => value.trim()).filter(Boolean); + return (raw || "") + .split(/[\s,]+/) + .map((value) => value.trim()) + .filter(Boolean); } export function getAutoUpdateConfig(env: NodeJS.ProcessEnv = process.env): AutoUpdateConfig { const dataDir = env.DATA_DIR || "/tmp/omniroute"; const repoDir = env.AUTO_UPDATE_REPO_DIR || "/workspace/omniroute"; + let mode = normalizeMode(env.AUTO_UPDATE_MODE); + if (mode === "npm") { + const fs = require("node:fs"); + if (fs.existsSync(path.join(process.cwd(), ".git"))) { + mode = "source" as any; + } + } + return { - mode: normalizeMode(env.AUTO_UPDATE_MODE), + mode, repoDir, composeFile: env.AUTO_UPDATE_COMPOSE_FILE || path.join(repoDir, "docker-compose.yml"), composeProfile: env.AUTO_UPDATE_COMPOSE_PROFILE || "cli", @@ -97,6 +108,15 @@ export async function validateAutoUpdateRuntime( execFileImpl: ExecFileLike = execFileAsync, existsImpl: (targetPath: string) => Promise = pathExists ): Promise { + if (config.mode === ("source" as any)) { + return { + supported: false, + reason: + "Manual 'git pull && npm install && npm run build' is required for source installations.", + composeCommand: null, + }; + } + if (config.mode !== "docker-compose") { return { supported: true, reason: null, composeCommand: null }; } @@ -139,7 +159,8 @@ export async function validateAutoUpdateRuntime( if (!composeCommand) { return { supported: false, - reason: "Neither docker compose nor docker-compose is available inside the OmniRoute container.", + reason: + "Neither docker compose nor docker-compose is available inside the OmniRoute container.", composeCommand: null, }; } @@ -150,7 +171,7 @@ export async function validateAutoUpdateRuntime( export function buildNpmUpdateScript(latest: string): string { return [ "set -eu", - `npm install -g omniroute@${latest} --ignore-scripts`, + `npm install -g omniroute@${latest} --ignore-scripts --legacy-peer-deps`, "if command -v pm2 >/dev/null 2>&1; then", " pm2 restart omniroute || true", "fi", @@ -173,7 +194,7 @@ export function buildDockerComposeUpdateScript({ ? 'docker compose -f "$COMPOSE_FILE" up -d --build "$SERVICE"' : 'docker-compose -f "$COMPOSE_FILE" up -d --build "$SERVICE"'; const patchLines = config.patchCommits.length - ? [`git cherry-pick --keep-redundant-commits ${config.patchCommits.map(shellQuote).join(' ')}`] + ? [`git cherry-pick --keep-redundant-commits ${config.patchCommits.map(shellQuote).join(" ")}`] : []; return [ @@ -189,13 +210,13 @@ export function buildDockerComposeUpdateScript({ 'git config --global --add safe.directory "$REPO_DIR" >/dev/null 2>&1 || true', 'if [ -n "$(git status --porcelain)" ]; then', ' echo "[AutoUpdate] Refusing update: git worktree has local changes." >&2', - ' exit 1', - 'fi', + " exit 1", + "fi", 'git fetch --tags "$REMOTE"', 'if ! git rev-parse -q --verify "refs/tags/$TARGET_TAG" >/dev/null 2>&1; then', ' echo "[AutoUpdate] Tag $TARGET_TAG not found on remote $REMOTE." >&2', - ' exit 1', - 'fi', + " exit 1", + "fi", 'backup_branch="autoupdate/pre-${TARGET_TAG#v}-$(date +%Y%m%d-%H%M%S)"', 'git branch "$backup_branch" >/dev/null 2>&1 || true', 'git checkout -B "autoupdate/${TARGET_TAG#v}" "$TARGET_TAG"', diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index 9c1fb70865..2fdddf7526 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -114,6 +114,99 @@ function GenericProviderIcon({ size }: { size: number }) { ); } +const KNOWN_PNGS = new Set([ + "aimlapi", + "alibaba", + "alicode-intl", + "alicode", + "anthropic-m", + "anthropic", + "antigravity", + "bailian-coding-plan", + "blackbox", + "brave-search", + "brave", + "cerebras", + "claude", + "cline", + "codex", + "cohere", + "continue", + "copilot", + "cursor", + "deepgram", + "deepseek", + "droid", + "exa-search", + "fireworks", + "gemini-cli", + "gemini", + "github", + "glm", + "groq", + "iflow", + "ironclaw", + "kilo-gateway", + "kilocode", + "kimi-coding-apikey", + "kimi-coding", + "kimi", + "kiro", + "longcat", + "minimax-cn", + "minimax", + "mistral", + "nanobot", + "nebius", + "nvidia", + "oai-cc", + "oai-r", + "ollama-cloud", + "openai", + "openclaw", + "openrouter", + "perplexity-search", + "perplexity", + "pollinations", + "qwen", + "roo", + "serper-search", + "serper", + "siliconflow", + "tavily-search", + "tavily", + "together", + "xai", + "zeroclaw", +]); +const KNOWN_SVGS = new Set([ + "apikey", + "assemblyai", + "brave", + "cartesia", + "cloudflare-ai", + "comfyui", + "elevenlabs", + "exa-search", + "exa", + "huggingface", + "hyperbolic", + "inworld", + "nanobanana", + "oauth", + "opencode-go", + "opencode-zen", + "opencode", + "playht", + "puter", + "scaleway", + "sdwebui", + "synthetic", + "vertex", + "windsurf", + "zai", +]); + const ProviderIcon = memo(function ProviderIcon({ providerId, size = 24, @@ -121,10 +214,14 @@ const ProviderIcon = memo(function ProviderIcon({ className, style, }: ProviderIconProps) { - const lobehubId = LOBEHUB_PROVIDER_MAP[providerId.toLowerCase()] ?? null; + const normalizedId = providerId.toLowerCase(); + const lobehubId = LOBEHUB_PROVIDER_MAP[normalizedId] ?? null; + const hasPng = KNOWN_PNGS.has(normalizedId); + const hasSvg = KNOWN_SVGS.has(normalizedId); + const [useLobehub, setUseLobehub] = useState(lobehubId !== null); - const [usePng, setUsePng] = useState(true); - const [useSvg, setUseSvg] = useState(true); + const [usePng, setUsePng] = useState(hasPng); + const [useSvg, setUseSvg] = useState(!hasPng && hasSvg); if (useLobehub && lobehubId) { return ( @@ -146,12 +243,15 @@ const ProviderIcon = memo(function ProviderIcon({ style={{ display: "inline-flex", alignItems: "center", ...style }} > {providerId} setUsePng(false)} + onError={() => { + setUsePng(false); + setUseSvg(hasSvg); + }} unoptimized /> @@ -165,7 +265,7 @@ const ProviderIcon = memo(function ProviderIcon({ style={{ display: "inline-flex", alignItems: "center", ...style }} >