Compare commits

..

4 Commits

Author SHA1 Message Date
diegosouzapw cbd60c853e feat(release): v2.0.10 — CLI fingerprint UI toggle
Build Electron Desktop App / Validate version (push) Failing after 27s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
2026-03-07 10:57:16 -03:00
diegosouzapw 2d8091340f feat: CLI fingerprint UI toggle + fix playground i18n (#223)
- Add CLI Fingerprint Matching card in Settings > Security tab
  - Per-provider toggle chips (codex, claude, github, antigravity)
  - Emerald-themed UI with fingerprint icon and active count
  - Settings synced to runtime cache via API
- Fix playground i18n missing from 29 non-English sidebar translations
- Add cliCompatProviders to settings schema (Zod validation)
- Add setCliCompatProviders/getCliCompatProviders to cliFingerprints.ts
- Add i18n keys for CLI fingerprint settings (en + pt-BR)
2026-03-07 10:56:58 -03:00
diegosouzapw 2025c16c82 fix: deploy workflow uses node app/server.js for pm2 2026-03-07 10:40:00 -03:00
diegosouzapw 811fb7f9b2 docs: fix deploy workflow to use npm-only approach
Old workflow used git clone at /opt/omniroute-app but recent releases
used npm install -g, causing version mismatch. PM2 now runs from
/usr/lib/node_modules/omniroute directly. Removed all references to
the stale local directory.
2026-03-07 10:38:49 -03:00
37 changed files with 226 additions and 57 deletions
+42 -21
View File
@@ -4,52 +4,73 @@ description: Deploy the latest OmniRoute code to the Akamai VPS (69.164.221.35)
# Deploy to VPS Workflow
Deploy OmniRoute to the production VPS using Node.js + PM2 (no Docker).
Deploy OmniRoute to the production VPS using `npm install -g` + PM2.
**VPS:** `69.164.221.35` (Akamai, Ubuntu 24.04, 1GB RAM + 2.5GB swap)
**App path:** `/opt/omniroute-app`
**Local VPS:** `192.168.0.15` (same setup)
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
> [!IMPORTANT]
> PM2 runs from the global npm package at `/usr/lib/node_modules/omniroute`.
> **DO NOT** use git clone or local copies. The `npm install -g` command handles
> building, publishing, and installing the standalone app in one step.
## Steps
### 1. Push to GitHub
### 1. Publish to npm
Ensure all changes are committed and pushed:
Ensure the version in `package.json` is bumped and the package is published:
```bash
git push origin main
npm publish
```
### 2. SSH into VPS, pull latest code, rebuild, and restart
### 2. Install on VPS and restart PM2
// turbo-all
```bash
ssh root@69.164.221.35 "
cd /opt/omniroute-app &&
git fetch origin &&
git reset --hard origin/main &&
export NODE_OPTIONS='--max-old-space-size=1536' &&
npm install --no-audit --no-fund &&
npm run build &&
pm2 restart omniroute &&
pm2 save &&
echo '✅ Deploy complete!'
"
ssh root@69.164.221.35 "npm install -g omniroute@latest && pm2 restart omniroute && pm2 save && echo '✅ Deploy complete!'"
```
For the local VPS:
```bash
ssh root@192.168.0.15 "npm install -g omniroute@latest && pm2 restart omniroute && pm2 save && echo '✅ Deploy complete!'"
```
### 3. Verify the deployment
```bash
ssh root@69.164.221.35 "pm2 list && curl -s -o /dev/null -w 'HTTP %{http_code}' http://localhost:20128/"
ssh root@69.164.221.35 "pm2 list && cat \$(npm root -g)/omniroute/package.json | grep version | head -1 && curl -s -o /dev/null -w 'HTTP %{http_code}' http://localhost:20128/"
```
Expected: PM2 shows `online`, HTTP returns `307` (redirect to login).
Expected: PM2 shows `online`, version matches published, HTTP returns `307` (redirect to login).
## How it works
1. `npm publish` builds Next.js standalone + bundles everything into the npm package
2. `npm install -g omniroute@latest` downloads and installs to `/usr/lib/node_modules/omniroute/`
3. PM2 is registered to run `npm start` from that directory (cwd: `/usr/lib/node_modules/omniroute`)
4. `pm2 restart omniroute` picks up the new code immediately
## PM2 Setup (one-time)
If PM2 needs to be reconfigured from scratch:
```bash
ssh root@<VPS> "
cd /usr/lib/node_modules/omniroute &&
PORT=20128 pm2 start app/server.js --name omniroute --env PORT=20128 &&
pm2 save &&
pm2 startup
"
```
## Notes
- The VPS has only 1GB RAM. `NODE_OPTIONS='--max-old-space-size=1536'` uses swap for the build.
- The `.env` file is at `/usr/lib/node_modules/omniroute/.env`. Back it up before major npm updates.
- PM2 is configured with `pm2 startup` to auto-restart on reboot.
- The `.env` file is at `/opt/omniroute-app/.env` (copied from the old Docker setup at `/opt/omniroute/.env`).
- Nginx proxies `omniroute.online``localhost:20128`.
- The VPS has only 1GB RAM — builds happen locally via `npm publish`, not on the VPS.
+30 -4
View File
@@ -222,16 +222,42 @@ export function applyFingerprint(
};
}
/**
* Runtime cache for CLI compat providers set via Settings UI.
* Updated by the settings API when users toggle providers.
*/
let _cliCompatProviders: Set<string> = new Set();
/**
* Update the runtime cache of CLI-compat-enabled providers.
* Called from the settings API when cliCompatProviders is updated.
*/
export function setCliCompatProviders(providers: string[]): void {
_cliCompatProviders = new Set((providers || []).map((p) => p.toLowerCase()));
}
/**
* Get the current list of CLI-compat-enabled providers.
*/
export function getCliCompatProviders(): string[] {
return Array.from(_cliCompatProviders);
}
/**
* Check if CLI compatibility mode is enabled for a provider.
* This reads from the settings database or environment variable.
* Reads from: 1) Runtime cache (Settings UI), 2) Environment variables.
*/
export function isCliCompatEnabled(provider: string): boolean {
// Check environment variable first: CLI_COMPAT_<PROVIDER>=1
const envKey = `CLI_COMPAT_${provider?.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
const key = provider?.toLowerCase().replace(/[^a-z0-9]/g, "_");
// 1. Check runtime cache (set via Settings UI)
if (_cliCompatProviders.has(provider?.toLowerCase())) return true;
// 2. Check environment variable: CLI_COMPAT_<PROVIDER>=1
const envKey = `CLI_COMPAT_${key?.toUpperCase()}`;
if (process.env[envKey] === "1" || process.env[envKey] === "true") return true;
// Global enable: CLI_COMPAT_ALL=1
// 3. Global enable: CLI_COMPAT_ALL=1
if (process.env.CLI_COMPAT_ALL === "1" || process.env.CLI_COMPAT_ALL === "true") return true;
return false;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "2.0.8",
"version": "2.0.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "2.0.8",
"version": "2.0.9",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "2.0.9",
"version": "2.0.10",
"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": {
@@ -248,6 +248,76 @@ export default function SecurityTab() {
</div>
</Card>
{/* CLI Fingerprint Matching */}
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
fingerprint
</span>
</div>
<h3 className="text-lg font-semibold">{t("cliFingerprint")}</h3>
</div>
<div className="flex flex-col gap-4">
<p className="text-sm text-text-muted">{t("cliFingerprintDesc")}</p>
<div className="flex flex-wrap gap-2">
{(["codex", "claude", "github", "antigravity"] as const).map((providerId) => {
const providerMeta = Object.values(AI_PROVIDERS).find(
(p: any) => p.id === providerId
) as any;
const isEnabled = (settings.cliCompatProviders || []).includes(providerId);
const displayName = providerMeta?.name || providerId;
const icon = providerMeta?.icon || "terminal";
const color = providerMeta?.color || "#888";
return (
<button
key={providerId}
onClick={() => {
const current: string[] = settings.cliCompatProviders || [];
const updated = current.includes(providerId)
? current.filter((p) => p !== providerId)
: [...current, providerId];
updateSetting("cliCompatProviders", updated);
}}
disabled={loading}
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-all border ${
isEnabled
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400"
: "bg-black/[0.02] dark:bg-white/[0.02] border-transparent text-text-muted hover:bg-black/[0.05] dark:hover:bg-white/[0.05]"
}`}
title={
isEnabled
? t("disableFingerprintTitle", { provider: displayName })
: t("enableFingerprintTitle", { provider: displayName })
}
>
<span
className="material-symbols-outlined text-[14px]"
style={{ color: isEnabled ? undefined : color }}
>
{isEnabled ? "fingerprint" : icon}
</span>
{displayName}
{isEnabled && (
<span className="material-symbols-outlined text-[12px] text-emerald-500">
check
</span>
)}
</button>
);
})}
</div>
{(settings.cliCompatProviders || []).length > 0 && (
<p className="text-xs text-emerald-600 dark:text-emerald-400 mt-1 flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">verified</span>
{t("cliFingerprintEnabled", {
count: (settings.cliCompatProviders || []).length,
})}
</p>
)}
</div>
</Card>
<SessionInfoCard />
<IPFilterSection />
</div>
+11
View File
@@ -5,12 +5,18 @@ import bcrypt from "bcryptjs";
import { getRuntimePorts } from "@/lib/runtime/ports";
import { updateSettingsSchema } from "@/shared/validation/settingsSchemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { setCliCompatProviders } from "../../../../open-sse/config/cliFingerprints";
export async function GET() {
try {
const settings = await getSettings();
const { password, ...safeSettings } = settings;
// Sync CLI fingerprint providers to runtime cache on load
if (settings.cliCompatProviders) {
setCliCompatProviders(settings.cliCompatProviders as string[]);
}
const enableRequestLogs = process.env.ENABLE_REQUEST_LOGS === "true";
const runtimePorts = getRuntimePorts();
@@ -74,6 +80,11 @@ export async function PATCH(request) {
clearHealthCheckLogCache();
}
// Sync CLI fingerprint providers to runtime cache
if ("cliCompatProviders" in body) {
setCliCompatProviders(body.cliCompatProviders || []);
}
const { password, ...safeSettings } = settings;
return NextResponse.json(safeSettings);
} catch (error) {
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "بنفسجي ساحر",
"themeOrange": "أورانج (Orange)",
"themeCyan": "كيان",
"endpoints": "نقاط النهاية"
"endpoints": "نقاط النهاية",
"playground": "ملعب النماذج"
},
"themesPage": {
"title": "المواضيع",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Виолетово",
"themeOrange": "Оранжево",
"themeCyan": "Циан",
"endpoints": "Крайни точки"
"endpoints": "Крайни точки",
"playground": "Площадка"
},
"themesPage": {
"title": "Теми",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Turkis",
"endpoints": "Endpoints"
"endpoints": "Endpoints",
"playground": "Legeplads"
},
"themesPage": {
"title": "Temaer",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violett",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpunkte"
"endpoints": "Endpunkte",
"playground": "Spielwiese"
},
"themesPage": {
"title": "Themen",
+5
View File
@@ -1493,6 +1493,11 @@
"providersBlocked": "{count} provider(s) blocked from /models",
"blockProviderTitle": "Block {provider}",
"unblockProviderTitle": "Unblock {provider}",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingStrategy": "Routing Strategy",
"fillFirst": "Fill First",
"fillFirstDesc": "Use accounts in priority order",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violeta",
"themeOrange": "Naranja",
"themeCyan": "cian",
"endpoints": "Endpoints"
"endpoints": "Endpoints",
"playground": "Playground"
},
"themesPage": {
"title": "Temas",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violetti",
"themeOrange": "Oranssi",
"themeCyan": "Syaani",
"endpoints": "Päätepisteet"
"endpoints": "Päätepisteet",
"playground": "Leikkipaikka"
},
"themesPage": {
"title": "Teemat",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violette",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Points d'accès"
"endpoints": "Points d'accès",
"playground": "Playground"
},
"themesPage": {
"title": "Thèmes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "נקודות קצה"
"endpoints": "נקודות קצה",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "narancssárga",
"themeCyan": "Cián",
"endpoints": "Végpontok"
"endpoints": "Végpontok",
"playground": "Játszótér"
},
"themesPage": {
"title": "Témák",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpoint"
"endpoints": "Endpoint",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpoint"
"endpoints": "Endpoint",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpoint"
"endpoints": "Endpoint",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "エンドポイント"
"endpoints": "エンドポイント",
"playground": "プレイグラウンド"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "엔드포인트"
"endpoints": "엔드포인트",
"playground": "플레이그라운드"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Titik Akhir"
"endpoints": "Titik Akhir",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Eindpunten"
"endpoints": "Eindpunten",
"playground": "Speeltuin"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endepunkter"
"endpoints": "Endepunkter",
"playground": "Lekeplass"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Mga Endpoint"
"endpoints": "Mga Endpoint",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Punkty końcowe"
"endpoints": "Punkty końcowe",
"playground": "Plac zabaw"
},
"themesPage": {
"title": "Themes",
+7 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpoints"
"endpoints": "Endpoints",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
@@ -1480,6 +1481,11 @@
"providersBlocked": "{count} provedor(es) bloqueado(s) do /models",
"blockProviderTitle": "Bloquear {provider}",
"unblockProviderTitle": "Desbloquear {provider}",
"cliFingerprint": "Assinatura Digital CLI",
"cliFingerprintDesc": "Reproduz a assinatura digital dos CLIs nativos ao fazer proxy. Reorganiza headers e campos do body para parecer idêntico às ferramentas CLI oficiais. O IP do proxy é preservado.",
"cliFingerprintEnabled": "{count} provider(s) com fingerprint CLI ativo",
"enableFingerprintTitle": "Ativar fingerprint para {provider}",
"disableFingerprintTitle": "Desativar fingerprint para {provider}",
"routingStrategy": "Estratégia de Roteamento",
"fillFirst": "Preencher Primeiro",
"fillFirstDesc": "Usar contas em ordem de prioridade",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeGreen": "Verde",
"themeViolet": "Violeta",
"themeOrange": "Laranja",
"themeCyan": "Ciano"
"themeCyan": "Ciano",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Puncte finale"
"endpoints": "Puncte finale",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Фиолетовый",
"themeOrange": "Оранжевый",
"themeCyan": "Голубой",
"endpoints": "Конечные точки"
"endpoints": "Конечные точки",
"playground": "Площадка"
},
"themesPage": {
"title": "Темы",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Koncové body"
"endpoints": "Koncové body",
"playground": "Ihrisko"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Ändpunkter"
"endpoints": "Ändpunkter",
"playground": "Lekplats"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "จุดปลายทาง"
"endpoints": "จุดปลายทาง",
"playground": "Playground"
},
"themesPage": {
"title": "ธีมส์",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Кінцеві точки"
"endpoints": "Кінцеві точки",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Điểm cuối"
"endpoints": "Điểm cuối",
"playground": "Playground"
},
"themesPage": {
"title": "Themes",
+2 -1
View File
@@ -100,7 +100,8 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "端点"
"endpoints": "端点",
"playground": "模型试验场"
},
"themesPage": {
"title": "Themes",
+2
View File
@@ -34,4 +34,6 @@ export const updateSettingsSchema = z.object({
mcpEnabled: z.boolean().optional(),
mcpTransport: z.enum(["stdio", "sse", "streamable-http"]).optional(),
a2aEnabled: z.boolean().optional(),
// CLI Fingerprint compatibility (per-provider)
cliCompatProviders: z.array(z.string().max(100)).optional(),
});