chore(release): v3.6.3 — Fix cloudflare config, prompt cache payloads, and openai-compatible validation

This commit is contained in:
diegosouzapw
2026-04-11 18:00:20 -03:00
parent 10288f87c1
commit 4d460109dd
47 changed files with 649 additions and 147 deletions

View File

@@ -1,114 +1,313 @@
# OmniRoute environment contract
# This file reflects actual runtime usage in the current codebase.
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ OmniRoute — .env Contract │
# │ This file documents EVERY environment variable read by the runtime. │
# │ Copy to .env and adjust values. Lines starting with # are commented out │
# │ (optional / off-by-default). Uncomment only what you need. │
# │ Reference: docs/ENVIRONMENT.md for full details and usage scenarios. │
# └─────────────────────────────────────────────────────────────────────────────┘
# ═══════════════════════════════════════════════════
# REQUIRED SECRETS — Generate strong values!
# ═══════════════════════════════════════════════════
# Generate with: openssl rand -base64 48
# ═══════════════════════════════════════════════════════════════════════════════
# 1. REQUIRED SECRETS — Must be set before first run!
# ═══════════════════════════════════════════════════════════════════════════════
# These secrets are critical for security. Generate strong, unique values.
# JWT signing key for dashboard session tokens.
# Used by: src/lib/auth — signs/verifies all authenticated session cookies.
# Generate: openssl rand -base64 48
JWT_SECRET=
# Generate with: openssl rand -hex 32
# Encryption key for API keys stored in the database.
# Used by: src/lib/db/apiKeys.ts — encrypts API key values at rest in SQLite.
# Generate: openssl rand -hex 32
API_KEY_SECRET=
# Initial admin password — CHANGE THIS before first use!
# Initial admin login password — CHANGE THIS before first use!
# Used by: bootstrap only — sets the initial dashboard password on first boot.
# After first login you can change it from Dashboard → Settings → Security.
# Default: 123456 (insecure, for local dev only)
INITIAL_PASSWORD=123456
# ═══════════════════════════════════════════════════════════════════════════════
# 2. STORAGE & DATABASE
# ═══════════════════════════════════════════════════════════════════════════════
# OmniRoute uses SQLite for all persistence. These variables control where
# data lives, encryption, and cleanup policies.
# Base directory for all persistent data (SQLite DB, logs, backups).
# Used by: src/lib/db/core.ts — resolves the SQLite database file path.
# Default: ~/.omniroute/ | Override for Docker or custom installations.
# DATA_DIR=/var/lib/omniroute
# Storage (SQLite)
STORAGE_DRIVER=sqlite
# Generate with: openssl rand -hex 32
# Encryption key for SQLite database encryption at rest.
# Used by: src/lib/db/encryption.ts — encrypts the entire SQLite database.
# Generate: openssl rand -hex 32 | Leave empty to disable DB encryption.
STORAGE_ENCRYPTION_KEY=
# Version tag for the encryption key — allows future key rotation.
# Used by: scripts/bootstrap-env.mjs, electron/main.js — persists key version.
# Default: v1 | Increment when rotating STORAGE_ENCRYPTION_KEY.
STORAGE_ENCRYPTION_KEY_VERSION=v1
APP_LOG_RETENTION_DAYS=90
CALL_LOG_RETENTION_DAYS=90
SQLITE_MAX_SIZE_MB=2048
SQLITE_CLEAN_LEGACY_FILES=true
# Automatic SQLite backup on startup.
# Used by: src/lib/db/backup.ts — creates a timestamped backup before migrations.
# Default: false (backups enabled) | Set true to skip backup on every restart.
DISABLE_SQLITE_AUTO_BACKUP=false
# Recommended runtime variables
# Canonical/base port (keeps backward compatibility)
# ═══════════════════════════════════════════════════════════════════════════════
# 3. NETWORK & PORTS
# ═══════════════════════════════════════════════════════════════════════════════
# OmniRoute can run on a single port (default) or split Dashboard/API ports.
# Canonical port for both Dashboard UI and API (single-port mode).
# Used by: src/lib/runtime/ports.ts — base port for the Next.js server.
# Default: 20128
PORT=20128
# Optional split ports:
# Split-port mode: serve Dashboard and API on separate ports for network isolation.
# Used by: src/lib/runtime/ports.ts — overrides PORT for each service.
# API_PORT=20129
# API_HOST=0.0.0.0
# DASHBOARD_PORT=20128
# Optional Docker production host publish ports:
# Docker production port mappings (docker-compose.prod.yml only).
# These set the HOST-side published ports. Container ports use PORT/API_PORT.
# PROD_DASHBOARD_PORT=20130
# PROD_API_PORT=20131
# Runtime override used by Electron and wrapped environments.
# OMNIROUTE_PORT takes precedence over PORT when running inside wrappers.
# Used by: src/lib/runtime/ports.ts — preserves canonical port in Electron.
# OMNIROUTE_PORT=20128
# Environment mode — affects Next.js behavior, logging verbosity, and caching.
# Values: production | development | Default: production
NODE_ENV=production
INSTANCE_NAME=omniroute
# Recommended security and ops variables
# ═══════════════════════════════════════════════════════════════════════════════
# 4. SECURITY & AUTHENTICATION
# ═══════════════════════════════════════════════════════════════════════════════
# Salt for generating unique machine IDs (fingerprint diversification).
# Used by: src/lib/auth — combined with hardware identifiers for machine-id hash.
# Default: endpoint-proxy-salt | Change per-deployment for isolation.
MACHINE_ID_SALT=endpoint-proxy-salt
AUTH_COOKIE_SECURE=false
REQUIRE_API_KEY=false
ALLOW_API_KEY_REVEAL=false
PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
# Input Sanitizer (FASE-01 — prompt injection & PII protection)
# Set true when running behind HTTPS (reverse proxy with TLS termination).
# Used by: src/lib/auth — sets the Secure flag on session cookies.
# Default: false | MUST be true in any non-localhost deployment.
AUTH_COOKIE_SECURE=false
# Require an API key for all /v1/* proxy endpoints.
# Used by: API middleware — rejects unauthenticated requests to the proxy API.
# Default: false | Set true for multi-user/public deployments.
REQUIRE_API_KEY=false
# Allow revealing full API key values in the Dashboard UI.
# Used by: Dashboard providers page — controls show/hide of key values.
# Default: false | Security risk if enabled on shared instances.
ALLOW_API_KEY_REVEAL=false
# Comma-separated API key IDs that skip request logging (GDPR/compliance).
# Used by: src/lib/compliance/index.ts — suppresses logs for specific keys.
# NO_LOG_API_KEY_IDS=key_abc123,key_def456
# Maximum request body size in bytes (rejects larger payloads).
# Used by: src/shared/middleware/bodySizeGuard.ts — prevents oversized uploads.
# Default: 10485760 (10 MB)
# MAX_BODY_SIZE_BYTES=10485760
# CORS configuration — controls which origins can call the API.
# Used by: Next.js middleware — sets Access-Control-Allow-Origin header.
# Default: * (all origins) | Restrict for production security.
# CORS_ORIGIN=https://your-domain.com
# ═══════════════════════════════════════════════════════════════════════════════
# 5. INPUT SANITIZATION & PII PROTECTION (FASE-01)
# ═══════════════════════════════════════════════════════════════════════════════
# Multi-layer defense: request-side injection guard + response-side PII sanitizer.
# ── Request-Side: Prompt Injection Guard ──
# Scans incoming messages for prompt injection patterns before routing.
# Used by: src/middleware/promptInjectionGuard.ts
# INPUT_SANITIZER_ENABLED=true
# INPUT_SANITIZER_MODE=warn # warn | block | redact
# INPUT_SANITIZER_MODE=warn # warn = log only | block = reject request | redact = strip patterns
# Legacy alias for INPUT_SANITIZER_MODE (same effect).
# INJECTION_GUARD_MODE=warn
# PII detection in incoming requests (emails, phone numbers, SSNs, etc.).
# Used by: src/middleware/promptInjectionGuard.ts — extends injection guard.
# PII_REDACTION_ENABLED=false
# Cloud sync variables
# Must point to this running instance so internal sync jobs can call /api/sync/cloud.
# Server-side preferred variables:
# ── Response-Side: PII Sanitizer ──
# Scans LLM responses for leaked PII before returning to the client.
# Used by: src/lib/piiSanitizer.ts
# PII_RESPONSE_SANITIZATION=false
# PII_RESPONSE_SANITIZATION_MODE=redact # redact = mask PII | warn = log only | block = drop response
# ═══════════════════════════════════════════════════════════════════════════════
# 6. TOOL & ROUTING POLICIES
# ═══════════════════════════════════════════════════════════════════════════════
# Tool policy mode — controls which tools LLMs can invoke via function calling.
# Used by: src/lib/toolPolicy.ts — enforces allowlist/denylist on tool_choice.
# Values: allowlist | denylist | disabled | Default: disabled
# TOOL_POLICY_MODE=disabled
# ═══════════════════════════════════════════════════════════════════════════════
# 7. URLS & CLOUD SYNC
# ═══════════════════════════════════════════════════════════════════════════════
# URLs used for internal sync jobs, OAuth callbacks, and cloud relay.
# Internal base URL — used by server-side sync jobs to call /api/sync/cloud.
# Used by: src/lib/cloudSync.ts, src/lib/initCloudSync.ts
# Default: http://localhost:20128
BASE_URL=http://localhost:20128
# Cloud relay URL — premium feature for remote config sync.
# Used by: src/lib/cloudSync.ts — pushes/pulls settings from OmniRoute Cloud.
CLOUD_URL=
# Backward-compatible/public variables:
# NEXT_PUBLIC_BASE_URL is also used as the OAuth redirect_uri origin when running behind a
# reverse proxy (e.g., nginx). Set this to your public-facing URL so OAuth callbacks work.
# Example: NEXT_PUBLIC_BASE_URL=https://omniroute.example.com
# Timeout for cloud sync HTTP requests in milliseconds.
# Used by: src/lib/cloudSync.ts — fetchWithTimeout wrapper.
# Default: 12000 (12 seconds)
# CLOUD_SYNC_TIMEOUT_MS=12000
# Public-facing base URL — CRITICAL for reverse proxy / OAuth callback setups.
# Used by: OAuth redirect_uri computation, Dashboard UI links, cloud/model sync.
# Set to your public URL when behind nginx/Caddy (e.g., https://omniroute.example.com).
# Default: http://localhost:20128
NEXT_PUBLIC_BASE_URL=http://localhost:20128
# Public cloud URL — client-side mirror of CLOUD_URL.
NEXT_PUBLIC_CLOUD_URL=
# Optional outbound proxy variables for upstream provider calls
# Lowercase variants are also supported: http_proxy, https_proxy, all_proxy, no_proxy
# SOCKS5 proxy support
# Legacy alias — fallback for NEXT_PUBLIC_BASE_URL in sync schedulers.
# NEXT_PUBLIC_APP_URL=http://localhost:20128
# ═══════════════════════════════════════════════════════════════════════════════
# 8. OUTBOUND PROXY (Upstream Provider Calls)
# ═══════════════════════════════════════════════════════════════════════════════
# Route upstream LLM API calls through an HTTP/SOCKS5 proxy.
# Useful for corporate egress, geo-routing, or IP masking.
# Enable SOCKS5 proxy support in both server and client components.
# Used by: open-sse/executors — wraps fetch() calls through the proxy agent.
ENABLE_SOCKS5_PROXY=true
NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Standard proxy variables (lowercase variants also supported).
# HTTP_PROXY=http://127.0.0.1:7890
# HTTPS_PROXY=http://127.0.0.1:7890
# ALL_PROXY=socks5://127.0.0.1:7890
# NO_PROXY=localhost,127.0.0.1
# TLS fingerprint spoofing (opt-in) — mimics Chrome 124 TLS handshake via wreq-js
# Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google)
# Requires wreq-js to be installed (included in dependencies)
# TLS fingerprint spoofing (opt-in) — mimics Chrome 124 TLS handshake via wreq-js.
# Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google).
# Used by: open-sse/executors — replaces Node.js default TLS fingerprint.
# ENABLE_TLS_FINGERPRINT=true
# Optional CLI runtime overrides (Docker/host integration)
# ═══════════════════════════════════════════════════════════════════════════════
# 9. CLI TOOL INTEGRATION
# ═══════════════════════════════════════════════════════════════════════════════
# Control how OmniRoute discovers and launches CLI sidecars (Claude, Codex, etc.).
# Used by: src/shared/services/cliRuntime.ts
# CLI discovery mode: auto = search PATH | manual = use explicit paths below.
# CLI_MODE=auto
# CLI_EXTRA_PATHS=/host-cli/bin
# Additional PATH entries for finding CLI binaries (colon-separated).
# CLI_EXTRA_PATHS=/host-cli/bin:/usr/local/bin
# Home directory override for reading CLI config files (~/.claude, etc.).
# CLI_CONFIG_HOME=/root
# Allow OmniRoute to write CLI config files (token refresh, etc.).
# CLI_ALLOW_CONFIG_WRITES=true
# Override binary paths for individual CLI tools.
# CLI_CLAUDE_BIN=claude
# CLI_CODEX_BIN=codex
# CLI_DROID_BIN=droid
# CLI_OPENCLAW_BIN=openclaw
# CLI_CURSOR_BIN=agent
# CLI_CLINE_BIN=cline
# CLI_ROO_BIN=roo
# CLI_CONTINUE_BIN=cn
# CLI_QODER_BIN=qoder
# Internal agent / tool integrations (optional)
# Used by the MCP server, A2A skills, and CLI sidecars when they need to call
# the running OmniRoute instance explicitly instead of relying on localhost.
# ═══════════════════════════════════════════════════════════════════════════════
# 10. INTERNAL AGENT & MCP INTEGRATIONS
# ═══════════════════════════════════════════════════════════════════════════════
# Used by MCP server, A2A skills, and CLI sidecars to call the running instance.
# Explicit base URL for MCP/A2A tools to reach OmniRoute (overrides localhost auto-detect).
# Used by: open-sse/mcp-server/server.ts, src/lib/a2a/
# OMNIROUTE_BASE_URL=http://localhost:20128
# API key for internal tool calls (MCP tools, A2A skills).
# OMNIROUTE_API_KEY=
# API key ID for MCP audit logging.
# Used by: open-sse/mcp-server/audit.ts — tags audit events with a key identity.
# OMNIROUTE_API_KEY_ID=
# Legacy alias for OMNIROUTE_API_KEY.
# ROUTER_API_KEY=
# Enforce scope-based access control on MCP tool calls.
# Used by: open-sse/mcp-server/server.ts — rejects calls outside allowed scopes.
# OMNIROUTE_MCP_ENFORCE_SCOPES=false
# Comma-separated scopes granted to this MCP connection.
# Full list: admin, combos, health, models, routing, budget, metrics, pricing, memory, skills
# OMNIROUTE_MCP_SCOPES=admin,combos,health
# Model catalog sync interval in hours.
# Used by: src/shared/services/modelSyncScheduler.ts — periodic model refresh.
# Default: 24
# MODEL_SYNC_INTERVAL_HOURS=24
# ═══════════════════════════════════════════════════
# OAUTH PROVIDER CREDENTIALS
# ═══════════════════════════════════════════════════
# These are the built-in default credentials that work for localhost setups.
# For remote/VPS deployments, register your own credentials at each provider.
# The sync-env script will auto-populate these in your .env if missing.
#
# These can also be overridden via data/provider-credentials.json where supported.
# Provider limits sync interval in minutes (rate limit windows, quotas).
# Used by: src/server-init.ts — polls provider health endpoints.
# Default: 70
PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
# Disable all background services (sync, pricing, model refresh).
# Used by: src/instrumentation-node.ts, src/lib/initCloudSync.ts
# Useful for: CI builds, test environments, or resource-constrained containers.
# OMNIROUTE_DISABLE_BACKGROUND_SERVICES=false
# Flag set by bootstrap script after initial setup is complete.
# Used by: src/app/(dashboard)/dashboard/page.tsx — shows setup wizard vs. dashboard.
# OMNIROUTE_BOOTSTRAPPED=false
# Allow request body to override the Antigravity project field.
# Used by: open-sse/executors/antigravity.ts — escape hatch for multi-project setups.
# OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE=0
# ═══════════════════════════════════════════════════════════════════════════════
# 11. OAUTH PROVIDER CREDENTIALS
# ═══════════════════════════════════════════════════════════════════════════════
# Built-in default credentials for localhost development.
# For remote/VPS deployments, register your own at each provider's developer console.
# The bootstrap-env script auto-populates these in .env if missing.
# Can also be overridden via data/provider-credentials.json where supported.
# ── Claude Code (Anthropic) ──
CLAUDE_OAUTH_CLIENT_ID=9d1c250a-e61b-44d9-88ed-5944d1962f5e
# Custom redirect URI override for Claude OAuth callback.
# CLAUDE_CODE_REDIRECT_URI=https://platform.claude.com/oauth/code/callback
# ── Codex / OpenAI ──
CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
@@ -143,23 +342,41 @@ QODER_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW
# QODER_OAUTH_USERINFO_URL=
# QODER_OAUTH_CLIENT_ID=
# ── Qoder Personal Access Token (direct API key fallback) ──
# Used by: open-sse/executors/qoder.ts — bypasses OAuth when set.
# QODER_PERSONAL_ACCESS_TOKEN=
# QODER_CLI_WORKSPACE=
# OMNIROUTE_QODER_WORKSPACE=
# ─────────────────────────────────────────────────────────────────────────────
# ⚠️ GOOGLE OAUTH (Antigravity, Gemini CLI) — IMPORTANT FOR REMOTE SERVERS
# ─────────────────────────────────────────────────────────────────────────────
# The credentials above ONLY work when OmniRoute runs on localhost.
# If you are hosting OmniRoute on a remote server, register your own:
# For remote hosting:
# 1. Go to https://console.cloud.google.com/apis/credentials
# 2. Create an OAuth 2.0 Client ID (type: "Web application")
# 3. Add your server URL as Authorized redirect URI
# 4. Replace the values above with your credentials.
# ─────────────────────────────────────────────────────────────────────────────
# ─────────────────────────────────────────────────────────────────────────────
# Provider User-Agent Overrides (optional — customize per-provider UA headers)
# ─────────────────────────────────────────────────────────────────────────────
# ── OAuth sidecar/CLI bridge (internal) ──
# Used by: src/lib/oauth/config/index.ts — internal CLI↔OmniRoute auth bridge.
# OMNIROUTE_SERVER=http://localhost:20128
# OMNIROUTE_TOKEN=
# OMNIROUTE_USER_ID=cli
# CLI_TOKEN= # legacy alias for OMNIROUTE_TOKEN
# CLI_USER_ID= # legacy alias for OMNIROUTE_USER_ID
# SERVER_URL= # legacy alias for OMNIROUTE_SERVER
# ═══════════════════════════════════════════════════════════════════════════════
# 12. PROVIDER USER-AGENT OVERRIDES
# ═══════════════════════════════════════════════════════════════════════════════
# Customize the User-Agent header sent to each upstream provider.
# Format: {PROVIDER_ID}_USER_AGENT=custom-value
# When set, overrides the default User-Agent header sent to that provider.
# Useful when providers update versions or block old user-agents.
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
# Update these when providers release new CLI versions to avoid blocks.
CLAUDE_USER_AGENT=claude-cli/1.0.83 (external, cli)
CODEX_USER_AGENT=codex-cli/0.92.0 (Windows 10.0.26100; x64)
GITHUB_USER_AGENT=GitHubCopilotChat/0.26.7
@@ -170,13 +387,15 @@ QWEN_USER_AGENT=QwenCode/0.12.3 (linux; x64)
CURSOR_USER_AGENT=connect-es/1.6.1
GEMINI_CLI_USER_AGENT=google-api-nodejs-client/9.15.1
# ─────────────────────────────────────────────────────────────────────────────
# CLI Fingerprint Compatibility (optional — match native CLI binary signatures)
# ─────────────────────────────────────────────────────────────────────────────
# ═══════════════════════════════════════════════════════════════════════════════
# 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection)
# ═══════════════════════════════════════════════════════════════════════════════
# When enabled, OmniRoute reorders HTTP headers and JSON body fields to match
# the exact signature of official CLI tools, reducing account flagging risk.
# Your proxy IP is preserved — you get both stealth AND IP masking.
#
# Used by: open-sse/config/cliFingerprints.ts, open-sse/executors/base.ts
# Enable per-provider:
# CLI_COMPAT_CODEX=1
# CLI_COMPAT_CLAUDE=1
@@ -188,12 +407,18 @@ GEMINI_CLI_USER_AGENT=google-api-nodejs-client/9.15.1
# CLI_COMPAT_KILOCODE=1
# CLI_COMPAT_CLINE=1
# CLI_COMPAT_QWEN=1
#
# Or enable for all providers at once:
# CLI_COMPAT_ALL=1
# API Key Providers (Phase 1 + Phase 4)
# Add via Dashboard → Providers → Add API Key, or set here
# ═══════════════════════════════════════════════════════════════════════════════
# 14. API KEY PROVIDERS
# ═══════════════════════════════════════════════════════════════════════════════
# API keys for direct-authentication providers.
# Preferred setup: Dashboard → Providers → Add API Key.
# Setting here is an alternative for Docker/headless deployments.
# DEEPSEEK_API_KEY=
# GROQ_API_KEY=
# XAI_API_KEY=
@@ -207,54 +432,259 @@ GEMINI_CLI_USER_AGENT=google-api-nodejs-client/9.15.1
# Embedding Providers (optional — used by /v1/embeddings)
# NEBIUS_API_KEY=
# Provider keys above (openai, mistral, together, fireworks, nvidia) also work for embeddings
# Provider keys above (OpenAI, Mistral, Together, Fireworks, NVIDIA) also work for embeddings.
# Timeout settings
# REQUEST_TIMEOUT_MS=600000
# STREAM_IDLE_TIMEOUT_MS=600000
# Advanced timeout overrides (optional)
# FETCH_TIMEOUT_MS=600000
# FETCH_HEADERS_TIMEOUT_MS=600000
# FETCH_BODY_TIMEOUT_MS=600000
# FETCH_CONNECT_TIMEOUT_MS=30000
# FETCH_KEEPALIVE_TIMEOUT_MS=4000
# TLS_CLIENT_TIMEOUT_MS=600000
# API bridge timeout for /v1 proxy requests (default: 30000)
# API_BRIDGE_PROXY_TIMEOUT_MS=600000
# API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS=600000
# API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS=60000
# API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS=5000
# API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS=0
# CORS configuration (default: * allows all origins)
# CORS_ORIGIN=*
# ═══════════════════════════════════════════════════════════════════════════════
# 15. TIMEOUT SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════
# All timeout values are in milliseconds.
# Used by: src/shared/utils/runtimeTimeouts.ts — centralized timeout resolution.
#
# Hierarchy: REQUEST_TIMEOUT_MS acts as a global override.
# If set, it becomes the default for FETCH_TIMEOUT_MS and STREAM_IDLE_TIMEOUT_MS.
# The fine-grained variables below override their respective defaults only when set.
# Logging
# ── Global shortcut ──
# REQUEST_TIMEOUT_MS=600000 # Overrides both fetch and stream idle defaults
# ── Upstream fetch (provider calls) ──
# FETCH_TIMEOUT_MS=600000 # Total request timeout (default: 600000 = 10 min)
# FETCH_HEADERS_TIMEOUT_MS=600000 # Time to receive response headers
# FETCH_BODY_TIMEOUT_MS=600000 # Time to receive full response body
# FETCH_CONNECT_TIMEOUT_MS=30000 # TCP connection establishment (default: 30s)
# FETCH_KEEPALIVE_TIMEOUT_MS=4000 # Keep-alive socket idle timeout (default: 4s)
# ── Stream idle detection ──
# STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000)
# # Extended-thinking models rarely pause >90s.
# ── TLS client (wreq-js fingerprint proxy) ──
# TLS_CLIENT_TIMEOUT_MS=600000 # Inherits from FETCH_TIMEOUT_MS by default
# ── API Bridge (/v1 proxy server) ──
# API_BRIDGE_PROXY_TIMEOUT_MS=30000 # Proxy hop timeout (default: 30s)
# API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS=300000 # Overall server request timeout
# API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS=60000 # Time to send response headers
# API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS=5000 # Keep-alive idle timeout
# API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS=0 # Raw socket timeout (0 = disabled)
# ── Graceful shutdown ──
# Time to wait for in-flight requests before force-exiting on SIGTERM/SIGINT.
# Used by: src/lib/gracefulShutdown.ts
# Default: 30000 (30 seconds)
# SHUTDOWN_TIMEOUT_MS=30000
# ═══════════════════════════════════════════════════════════════════════════════
# 16. LOGGING
# ═══════════════════════════════════════════════════════════════════════════════
# Used by: src/lib/logEnv.ts, src/lib/logRotation.ts, src/shared/utils/logger.ts
# Application log level — controls console and file log verbosity.
# Values: debug | info | warn | error | Default: info
# APP_LOG_LEVEL=info
# Log output format.
# Values: text | json | Default: text
# APP_LOG_FORMAT=text
# Write logs to file in addition to stdout.
# Default: true | Set false to disable file logging.
APP_LOG_TO_FILE=true
# Path to the application log file.
# Default: logs/application/app.log (relative to project root / DATA_DIR)
# APP_LOG_FILE_PATH=logs/application/app.log
# Maximum single log file size before rotation.
# Accepts: plain bytes or suffixed (50M, 1G, 512K). Default: 50M
# APP_LOG_MAX_FILE_SIZE=50M
# Days to keep rotated application log files before auto-deletion.
# Default: 7
# APP_LOG_RETENTION_DAYS=7
# Maximum number of rotated log file backups to keep.
# Default: 20
# APP_LOG_MAX_FILES=20
# Days to keep request/call log entries in the database before auto-cleanup.
# Default: 7
# CALL_LOG_RETENTION_DAYS=7
# Maximum call log entries stored in-memory buffer.
# Default: 10000
# CALL_LOG_MAX_ENTRIES=10000
# ─────────────────────────────────────────────────────────────────────────────
# Memory Optimization (Low-RAM configurations)
# ─────────────────────────────────────────────────────────────────────────────
# Node.js heap limit in MB (default: 256 for Docker, system default for npm)
# Maximum rows in the call_logs SQLite table before oldest entries are pruned.
# Default: 100000
# CALL_LOGS_TABLE_MAX_ROWS=100000
# Maximum rows in the proxy_logs SQLite table.
# Default: 100000
# PROXY_LOGS_TABLE_MAX_ROWS=100000
# ═══════════════════════════════════════════════════════════════════════════════
# 17. MEMORY OPTIMIZATION (Low-RAM / Docker)
# ═══════════════════════════════════════════════════════════════════════════════
# Node.js V8 heap limit in MB.
# Used by: Docker entrypoint — sets --max-old-space-size.
# Default: 256 (Docker) | system default (npm)
# OMNIROUTE_MEMORY_MB=256
# Prompt cache settings
# PROMPT_CACHE_MAX_SIZE=50
# PROMPT_CACHE_MAX_BYTES=2097152
# PROMPT_CACHE_TTL_MS=300000
# ── Prompt cache (system prompt deduplication) ──
# Used by: open-sse/services — caches identical system prompts across requests.
# PROMPT_CACHE_MAX_SIZE=50 # Max cached entries (default: 50)
# PROMPT_CACHE_MAX_BYTES=2097152 # Max total cache size in bytes (default: 2 MB)
# PROMPT_CACHE_TTL_MS=300000 # Cache entry TTL (default: 5 minutes)
# Semantic cache settings (temperature=0 responses)
# SEMANTIC_CACHE_MAX_SIZE=100
# SEMANTIC_CACHE_MAX_BYTES=4194304
# SEMANTIC_CACHE_TTL_MS=1800000
# ── Semantic cache (deterministic response dedup, temperature=0) ──
# Used by: open-sse/services — caches identical temperature=0 responses.
# SEMANTIC_CACHE_MAX_SIZE=100 # Max cached entries (default: 100)
# SEMANTIC_CACHE_MAX_BYTES=4194304 # Max total cache size in bytes (default: 4 MB)
# SEMANTIC_CACHE_TTL_MS=1800000 # Cache entry TTL (default: 30 minutes)
# In-memory log buffers
# ── In-memory log buffers ──
# Maximum recent stream events kept in memory for the Dashboard live view.
# STREAM_HISTORY_MAX=50
# ── Context length default ──
# Global fallback max context length for models without explicit config.
# Used by: open-sse/services/contextManager.ts
# CONTEXT_LENGTH_DEFAULT=128000
# ── Usage token buffer ──
# Extra token headroom reserved when tracking usage quotas (prevents over-limit).
# Used by: open-sse/utils/usageTracking.ts
# USAGE_TOKEN_BUFFER=100
# ═══════════════════════════════════════════════════════════════════════════════
# 18. PRICING SYNC
# ═══════════════════════════════════════════════════════════════════════════════
# Automatic model pricing synchronization from external sources.
# Used by: src/lib/pricingSync.ts
# Enable periodic pricing data sync. Default: false (opt-in only).
# PRICING_SYNC_ENABLED=false
# Sync interval in seconds. Default: 86400 (24 hours).
# PRICING_SYNC_INTERVAL=86400
# Comma-separated data sources. Default: litellm
# PRICING_SYNC_SOURCES=litellm
# ═══════════════════════════════════════════════════════════════════════════════
# 19. MODEL SYNC (Dev)
# ═══════════════════════════════════════════════════════════════════════════════
# Development-time model catalog sync interval in seconds.
# Used by: src/lib/modelsDevSync.ts
# Default: 86400 (24 hours)
# MODELS_DEV_SYNC_INTERVAL=86400
# ═══════════════════════════════════════════════════════════════════════════════
# 20. PROVIDER-SPECIFIC SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════
# ── OpenRouter ──
# OpenRouter model catalog cache TTL in ms.
# Used by: src/lib/catalog/openrouterCatalog.ts
# Default: 86400000 (24 hours)
# OPENROUTER_CATALOG_TTL_MS=86400000
# ── NanoBanana (Image Generation) ──
# Polling config for async image generation jobs.
# Used by: open-sse/handlers/imageGeneration.ts
# NANOBANANA_POLL_TIMEOUT_MS=120000 # Max wait for job completion (default: 120s)
# NANOBANANA_POLL_INTERVAL_MS=2500 # Poll frequency (default: 2.5s)
# ── Cloudflare Workers AI ──
# Account ID override for Cloudflare Workers AI executor.
# Used by: open-sse/executors/cloudflare-ai.ts
# CLOUDFLARE_ACCOUNT_ID=
# ── Cloudflare Tunnel (cloudflared) ──
# Custom path to cloudflared binary for tunnel management.
# Used by: src/lib/cloudflaredTunnel.ts
# CLOUDFLARED_BIN=/usr/local/bin/cloudflared
# ── Search cache ──
# TTL for search API response caching (Perplexity, Brave, etc.).
# Used by: open-sse/services/searchCache.ts
# Default: 300000 (5 minutes)
# SEARCH_CACHE_TTL_MS=300000
# ── OpenAI-compatible multi-connection ──
# Allow multiple simultaneous connections per OpenAI-compatible provider node.
# Used by: src/app/api/providers/route.ts
# ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE=false
# ── CC-compatible provider (experimental) ──
# Enable the Claude Code compatible provider endpoint.
# Used by: src/shared/utils/featureFlags.ts
# ENABLE_CC_COMPATIBLE_PROVIDER=false
# ── CLIProxyAPI bridge (legacy) ──
# Connection settings for external CLIProxyAPI instances.
# Used by: open-sse/executors/cliproxyapi.ts
# CLIPROXYAPI_HOST=127.0.0.1
# CLIPROXYAPI_PORT=5544
# CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api
# ── Local hostnames (Docker networking) ──
# Comma-separated additional hostnames treated as "local" for provider routing.
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names.
# LOCAL_HOSTNAMES=omlx,mlx-audio
# ═══════════════════════════════════════════════════════════════════════════════
# 21. PROXY HEALTH
# ═══════════════════════════════════════════════════════════════════════════════
# Fine-tune proxy health checking behavior.
# Used by: src/lib/proxyHealth.ts
# Timeout for fast-fail health checks (ms). Default: 2000
# PROXY_FAST_FAIL_TIMEOUT_MS=2000
# Health check result cache TTL (ms). Default: 30000 (30s)
# PROXY_HEALTH_CACHE_TTL_MS=30000
# Rate limit maximum wait time before failing a request (ms). Default: 120000 (2 min)
# Used by: open-sse/services/rateLimitManager.ts
# RATE_LIMIT_MAX_WAIT_MS=120000
# ═══════════════════════════════════════════════════════════════════════════════
# 22. DEBUGGING
# ═══════════════════════════════════════════════════════════════════════════════
# These variables enable verbose debugging output. NEVER enable in production.
# Dump Cursor protobuf decode/encode details to console.
# CURSOR_PROTOBUF_DEBUG=1
# Dump raw Cursor SSE stream data to console.
# CURSOR_STREAM_DEBUG=1
# Log Responses API SSE-to-JSON translation details.
# DEBUG_RESPONSES_SSE_TO_JSON=true
# Enable E2E test mode — relaxes auth and enables test harness hooks.
# NEXT_PUBLIC_OMNIROUTE_E2E_MODE=true
# ═══════════════════════════════════════════════════════════════════════════════
# 23. GITHUB INTEGRATION (Issue Reporting)
# ═══════════════════════════════════════════════════════════════════════════════
# Allow users to report issues directly from the Dashboard to GitHub.
# Used by: src/app/api/v1/issues/report/route.ts
# GitHub repository in owner/repo format.
# GITHUB_ISSUES_REPO=owner/repo
# GitHub Personal Access Token with issues:write scope.
# GITHUB_ISSUES_TOKEN=ghp_xxxx

View File

@@ -6,6 +6,18 @@
## [3.6.3] — 2026-04-11
### ✨ New Features
- **OpenAI-Compatible Loose Validation:** Empty API keys can now be naturally submitted and saved for any `openai-compatible-*` providers (e.g. Pollinations, localized routes) directly in the UI instead of blocking save actions (#1152)
- **Cloudflare Configuration:** Updated the provider schema and UI integration for Cloudflare AI to officially expose and support the backend `accountId` field securely without overrides (#1150)
### 🐛 Bug Fixes
- **Vertex JSON Validation Crash:** Prevented `invalid character in header` crashes inside the `/validate` endpoint by creating a native authentication parser that correctly handles Google Identity Service Account JSON flows prior to pinging endpoints (#1153)
- **Extraneous Payload Rejection:** Globally prevented upstream `400 Bad Request` execution crashes by stripping the non-standard `prompt_cache_retention` attribute forcibly attached by Cursor/Cline IDE engines when targeting strict OpenAI/Anthropic routes (#1154)
- **Reasoning Content Drop:** Prevented pure reasoning packets, common in advanced fallback models like DeepSeek, from being aborted mid-stream by explicitly adjusting the `Empty Content (502)` circuit breakers to acknowledge `reasoning_content` states as valid (#1155)
- **Login Visual Security:** Removed the raw fallback hash dump that artificially rendered underneath the login modal in Docker instances missing `OMNIROUTE_API_KEY_BASE64` flags (#1148)
### 🔧 Maintenance & Dependencies
- **Dependabot Updates:** Safely bumped GitHub Actions `docker/build-push-action` to v7 and `actions/download-artifact` to v8

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 3.6.2
version: 3.6.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,

View File

@@ -1,6 +1,6 @@
{
"name": "omniroute-desktop",
"version": "3.6.2",
"version": "3.6.3",
"description": "OmniRoute Desktop Application",
"main": "main.js",
"author": {

View File

@@ -40,6 +40,7 @@ const eslintConfig = [
"node_modules/**",
// VS Code extension and its large test fixtures
"vscode-extension/**",
"_mono_repo/**",
// Electron app
"electron/**",
// Docs

View File

@@ -13,7 +13,7 @@ interface ServiceAccount {
const TOKEN_CACHE = new Map<string, { token: string; expiresAt: number }>();
function parseSAFromApiKey(apiKey: string): ServiceAccount {
export function parseSAFromApiKey(apiKey: string): ServiceAccount {
try {
return JSON.parse(apiKey);
} catch {
@@ -21,7 +21,7 @@ function parseSAFromApiKey(apiKey: string): ServiceAccount {
}
}
async function getAccessToken(sa: ServiceAccount): Promise<string> {
export async function getAccessToken(sa: ServiceAccount): Promise<string> {
if (!sa.client_email || !sa.private_key) {
throw new Error(
"Service Account JSON is missing required fields (client_email or private_key)"

View File

@@ -808,6 +808,7 @@ export async function handleChatCore({
delete b.disable_stream;
delete b.disable_streaming;
delete b.streaming;
delete b.prompt_cache_retention;
}
const stream = resolveStreamFlag(body?.stream, acceptHeader);

View File

@@ -1,6 +1,6 @@
{
"name": "@omniroute/open-sse",
"version": "3.6.2",
"version": "3.6.3",
"description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration",
"type": "module",
"main": "index.js",

View File

@@ -17,12 +17,16 @@ export function isEmptyContentResponse(responseBody: unknown): boolean {
const delta = firstChoice.delta as Record<string, unknown> | undefined;
const content = message?.content ?? delta?.content;
const reasoningContent = message?.reasoning_content ?? delta?.reasoning_content;
const hasToolCalls =
(Array.isArray(message?.tool_calls) && (message.tool_calls as unknown[]).length > 0) ||
(Array.isArray(delta?.tool_calls) && (delta.tool_calls as unknown[]).length > 0);
const hasContent = content !== null && content !== undefined && content !== "";
return !hasContent && !hasToolCalls;
const hasReasoning =
reasoningContent !== null && reasoningContent !== undefined && reasoningContent !== "";
return !hasContent && !hasReasoning && !hasToolCalls;
}
if (Array.isArray(body.content)) {

2
package-lock.json generated
View File

@@ -20955,7 +20955,7 @@
},
"open-sse": {
"name": "@omniroute/open-sse",
"version": "3.6.2"
"version": "3.6.3"
}
}
}

12
scratch.mjs Normal file
View File

@@ -0,0 +1,12 @@
import { test } from "node:test";
import assert from "node:assert";
import { providerNodesValidateRoute } from "./src/app/api/provider-nodes/validate/route.js";
const req = new Request("http://localhost/api/provider-nodes/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ baseUrl: "", apiKey: "" }),
});
const res = await providerNodesValidateRoute.POST(req);
const data = await res.json();
console.dir(data, { depth: null });

View File

@@ -5005,6 +5005,7 @@ function AddApiKeyModal({
const defaultRegion = "us-central1";
const isGlm = provider === "glm";
const isQoder = provider === "qoder";
const isCloudflare = provider === "cloudflare-ai";
const [formData, setFormData] = useState({
name: "",
@@ -5015,6 +5016,7 @@ function AddApiKeyModal({
apiRegion: "international",
validationModelId: "",
customUserAgent: "",
accountId: "",
});
const [validating, setValidating] = useState(false);
const [validationResult, setValidationResult] = useState(null);
@@ -5047,7 +5049,7 @@ function AddApiKeyModal({
};
const handleSubmit = async () => {
if (!provider || !formData.apiKey) return;
if (!provider || (!isCompatible && !formData.apiKey)) return;
setSaving(true);
setSaveError(null);
@@ -5101,6 +5103,8 @@ function AddApiKeyModal({
providerSpecificData.region = formData.region;
} else if (isGlm) {
providerSpecificData.apiRegion = formData.apiRegion;
} else if (isCloudflare && formData.accountId.trim()) {
providerSpecificData.accountId = formData.accountId.trim();
}
const payload = {
@@ -5159,7 +5163,7 @@ function AddApiKeyModal({
<div className="pt-6">
<Button
onClick={handleValidate}
disabled={!formData.apiKey || validating || saving}
disabled={(!isCompatible && !formData.apiKey) || validating || saving}
variant="secondary"
>
{validating ? t("checking") : t("check")}
@@ -5251,6 +5255,15 @@ function AddApiKeyModal({
hint="ex: us-central1 ou europe-west4. Partner models usam a região global automaticamente."
/>
)}
{isCloudflare && (
<Input
label="Account ID"
value={formData.accountId}
onChange={(e) => setFormData({ ...formData, accountId: e.target.value })}
placeholder="Cloudflare Account ID"
hint="Find it in the Cloudflare dashboard URL or settings"
/>
)}
{isGlm && (
<div>
<label className="text-sm font-medium text-text-main mb-1 block">API Region</label>
@@ -5273,7 +5286,7 @@ function AddApiKeyModal({
fullWidth
disabled={
!formData.name ||
!formData.apiKey ||
(!isCompatible && !formData.apiKey) ||
saving ||
(usesBaseUrl && !formData.baseUrl.trim() && !defaultBaseUrl)
}
@@ -5326,6 +5339,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
validationModelId: "",
tag: "",
customUserAgent: "",
accountId: "",
});
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState(null);
@@ -5343,6 +5357,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
const defaultBaseUrl = getProviderBaseUrlDefault(connection?.provider);
const isVertex = connection?.provider === "vertex";
const isGlm = connection?.provider === "glm";
const isCloudflare = connection?.provider === "cloudflare-ai";
const defaultRegion = "us-central1";
useEffect(() => {
@@ -5354,6 +5369,8 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
const rawCustomUserAgent = connection.providerSpecificData?.customUserAgent;
const existingCustomUserAgent =
typeof rawCustomUserAgent === "string" ? rawCustomUserAgent : "";
const rawAccountId = connection.providerSpecificData?.accountId;
const existingAccountId = typeof rawAccountId === "string" ? rawAccountId : "";
setFormData({
name: connection.name || "",
priority: connection.priority || 1,
@@ -5365,6 +5382,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
validationModelId: (connection.providerSpecificData?.validationModelId as string) || "",
tag: (connection.providerSpecificData?.tag as string) || "",
customUserAgent: existingCustomUserAgent,
accountId: existingAccountId,
});
// Load existing extra keys from providerSpecificData
const existing = connection.providerSpecificData?.extraApiKeys;
@@ -5408,7 +5426,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
};
const handleValidate = async () => {
if (!connection?.provider || !formData.apiKey) return;
if (!connection?.provider || (!isCompatible && !formData.apiKey)) return;
setValidating(true);
setValidationResult(null);
try {
@@ -5506,6 +5524,8 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
updates.providerSpecificData.region = formData.region;
} else if (isGlm) {
updates.providerSpecificData.apiRegion = formData.apiRegion;
} else if (isCloudflare && formData.accountId.trim()) {
updates.providerSpecificData.accountId = formData.accountId.trim();
}
} else {
// Also persist tag for OAuth accounts
@@ -5607,7 +5627,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
<div className="pt-6">
<Button
onClick={handleValidate}
disabled={!formData.apiKey || validating || saving}
disabled={(!isCompatible && !formData.apiKey) || validating || saving}
variant="secondary"
>
{validating ? t("checking") : t("check")}
@@ -5683,6 +5703,16 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
/>
)}
{isCloudflare && (
<Input
label="Account ID"
value={formData.accountId}
onChange={(e) => setFormData({ ...formData, accountId: e.target.value })}
placeholder="Cloudflare Account ID"
hint="Find it in the Cloudflare dashboard URL or settings"
/>
)}
{isGlm && (
<div>
<label className="text-sm font-medium text-text-main mb-1 block">API Region</label>

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "أعد تشغيل الخادم وسيتم استخدام كلمة المرور الجديدة",
"backToLogin": "العودة إلى تسجيل الدخول",
"forgotPassword": "هل نسيت كلمة المرور؟",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "إذن",
"Content-Disposition": "التصرف في المحتوى",
"waitingForAuthorization": "في انتظار الترخيص...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Рестартирайте сървъра - той ще използва новата парола",
"backToLogin": "Назад към Вход",
"forgotPassword": "Забравена парола?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Упълномощаване",
"Content-Disposition": "Съдържание-разположение",
"waitingForAuthorization": "Чака се оторизация...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Restartujte server - použije se nové heslo.",
"backToLogin": "Zpět na přihlášení",
"forgotPassword": "Zapomněli jste heslo?",
"defaultPasswordHint": "Výchozí heslo: 123456 (pokud nebylo nastaveno INITIAL_PASSWORD)",
"defaultPasswordHint": "Výchozí heslo: CHANGEME (pokud nebylo nastaveno INITIAL_PASSWORD)",
"Authorization": "Autorizace",
"Content-Disposition": "Obsah-Dispozice",
"waitingForAuthorization": "Čekám na autorizaci...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Genstart serveren - den vil bruge den nye adgangskode",
"backToLogin": "Tilbage til Login",
"forgotPassword": "Glemt adgangskode?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorisation",
"Content-Disposition": "Indhold-Disposition",
"waitingForAuthorization": "Venter på godkendelse...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Starten Sie den Server neu er verwendet das neue Passwort",
"backToLogin": "Zurück zum Anmelden",
"forgotPassword": "Passwort vergessen?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorisierung",
"Content-Disposition": "Inhaltliche Disposition",
"waitingForAuthorization": "Warte auf Autorisierung...",

View File

@@ -2732,7 +2732,7 @@
"restartServerWithNewPassword": "Restart the server - it will use the new password",
"backToLogin": "Back to Login",
"forgotPassword": "Forgot password?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Authorization",
"Content-Disposition": "Content-Disposition",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2655,7 +2655,7 @@
"restartServerWithNewPassword": "Reinicia el servidor; usará la nueva contraseña",
"backToLogin": "Volver a iniciar sesión",
"forgotPassword": "¿Olvidaste tu contraseña?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorización",
"Content-Disposition": "Disposición de contenido",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Käynnistä palvelin uudelleen - se käyttää uutta salasanaa",
"backToLogin": "Takaisin kirjautumiseen",
"forgotPassword": "Unohditko salasanan?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Valtuutus",
"Content-Disposition": "Sisältö-sijoittelu",
"waitingForAuthorization": "Odotetaan valtuutusta...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Redémarrez le serveur - il utilisera le nouveau mot de passe",
"backToLogin": "Retour à la connexion",
"forgotPassword": "Mot de passe oublié ?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorisation",
"Content-Disposition": "Disposition du contenu",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "הפעל מחדש את השרת - הוא ישתמש בסיסמה החדשה",
"backToLogin": "חזרה לכניסה",
"forgotPassword": "שכחת סיסמה?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "הרשאה",
"Content-Disposition": "תוכן-נטייה",
"waitingForAuthorization": "ממתין לאישור...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "सर्वर को पुनरारंभ करें - यह नए पासवर्ड का उपयोग करेगा",
"backToLogin": "लॉगइन पर वापस जाएँ",
"forgotPassword": "पासवर्ड भूल गए?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Authorization",
"Content-Disposition": "Content-Disposition",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Indítsa újra a szervert - az új jelszót fogja használni",
"backToLogin": "Vissza a Bejelentkezéshez",
"forgotPassword": "Elfelejtetted a jelszavad?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Engedélyezés",
"Content-Disposition": "Tartalom-Diszpozíció",
"waitingForAuthorization": "Várakozás az engedélyezésre...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Mulai ulang server - server akan menggunakan kata sandi baru",
"backToLogin": "Kembali ke Masuk",
"forgotPassword": "Lupa kata sandi?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Otorisasi",
"Content-Disposition": "Disposisi Konten",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Riavvia il server: utilizzerà la nuova password",
"backToLogin": "Torna all'accesso",
"forgotPassword": "Ha dimenticato la password?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorizzazione",
"Content-Disposition": "Disposizione del contenuto",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "サーバーを再起動すると新しいパスワードが適用されます",
"backToLogin": "ログインに戻る",
"forgotPassword": "パスワードをお忘れですか?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "認可",
"Content-Disposition": "コンテンツの配置",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "서버를 다시 시작하세요. 새 비밀번호가 사용됩니다.",
"backToLogin": "로그인으로 돌아가기",
"forgotPassword": "비밀번호를 잊으셨나요?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "승인",
"Content-Disposition": "컨텐츠 처리",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Mulakan semula pelayan - ia akan menggunakan kata laluan baharu",
"backToLogin": "Kembali ke Log Masuk",
"forgotPassword": "Lupa kata laluan?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Keizinan",
"Content-Disposition": "Kandungan-Pelupusan",
"waitingForAuthorization": "Menunggu kebenaran...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Start de server opnieuw op. Deze gebruikt het nieuwe wachtwoord",
"backToLogin": "Terug naar Inloggen",
"forgotPassword": "Wachtwoord vergeten?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorisatie",
"Content-Disposition": "Inhoud-dispositie",
"waitingForAuthorization": "Wachten op toestemming...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Start serveren på nytt - den vil bruke det nye passordet",
"backToLogin": "Tilbake til pålogging",
"forgotPassword": "Glemt passord?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorisasjon",
"Content-Disposition": "Innhold-Disposisjon",
"waitingForAuthorization": "Venter på autorisasjon...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "I-restart ang server - gagamitin nito ang bagong password",
"backToLogin": "Bumalik sa Login",
"forgotPassword": "Nakalimutan ang password?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Awtorisasyon",
"Content-Disposition": "Nilalaman-Disposisyon",
"waitingForAuthorization": "Naghihintay ng awtorisasyon...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Zrestartuj serwer - będzie używał nowego hasła",
"backToLogin": "Powrót do logowania",
"forgotPassword": "Zapomniałeś hasła?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autoryzacja",
"Content-Disposition": "Dyspozycja treści",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2723,7 +2723,7 @@
"restartServerWithNewPassword": "Reinicie o servidor - ele usará a nova senha",
"backToLogin": "Voltar para o Login",
"forgotPassword": "Esqueceu a senha?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorização",
"Content-Disposition": "Disposição de conteúdo",
"waitingForAuthorization": "Aguardando autorização...",

View File

@@ -2708,7 +2708,7 @@
"restartServerWithNewPassword": "Reinicie o servidor - ele usará a nova senha",
"backToLogin": "Voltar ao login",
"forgotPassword": "Esqueceu a senha?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorização",
"Content-Disposition": "Disposição de conteúdo",
"waitingForAuthorization": "Waiting for authorization...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Reporniți serverul - va folosi noua parolă",
"backToLogin": "Înapoi la Logare",
"forgotPassword": "Ai uitat parola?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorizare",
"Content-Disposition": "Conținut-Dispoziție",
"waitingForAuthorization": "Se așteaptă autorizația...",

View File

@@ -2679,7 +2679,7 @@
"restartServerWithNewPassword": "Перезагрузите сервер - он будет использовать новый пароль.",
"backToLogin": "Вернуться к входу",
"forgotPassword": "Забыли пароль?",
"defaultPasswordHint": "Пароль по умолчанию: 123456 (если не задан INITIAL_PASSWORD)",
"defaultPasswordHint": "Пароль по умолчанию: CHANGEME (если не задан INITIAL_PASSWORD)",
"Authorization": "Авторизация",
"Content-Disposition": "Содержание-Расположение",
"waitingForAuthorization": "Ожидание авторизации...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Reštartujte server - použije nové heslo",
"backToLogin": "Späť na Prihlásenie",
"forgotPassword": "Zabudli ste heslo?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Autorizácia",
"Content-Disposition": "Obsah-Dispozícia",
"waitingForAuthorization": "Čaká sa na autorizáciu...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Starta om servern - den kommer att använda det nya lösenordet",
"backToLogin": "Tillbaka till inloggning",
"forgotPassword": "Glömt lösenordet?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Auktorisation",
"Content-Disposition": "Innehåll-Disposition",
"waitingForAuthorization": "Väntar på auktorisation...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "รีสตาร์ทเซิร์ฟเวอร์ - จะใช้รหัสผ่านใหม่",
"backToLogin": "กลับไปที่เข้าสู่ระบบ",
"forgotPassword": "ลืมรหัสผ่าน?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "การอนุญาต",
"Content-Disposition": "การจัดการเนื้อหา",
"waitingForAuthorization": "กำลังรอการอนุญาต...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Sunucuyu yeniden başlatın; yeni şifre kullanılacaktır",
"backToLogin": "Girişe Geri Dön",
"forgotPassword": "Şifrenizi mi unuttunuz?",
"defaultPasswordHint": "Varsayılan şifre: 123456 (INITIAL_PASSWORD ayarlanmadığı sürece)",
"defaultPasswordHint": "Varsayılan şifre: CHANGEME (INITIAL_PASSWORD ayarlanmadığı sürece)",
"Authorization": "Authorization",
"Content-Disposition": "Content-Disposition",
"waitingForAuthorization": "Yetkilendirme bekleniyor...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Перезапустіть сервер - він використовуватиме новий пароль",
"backToLogin": "Назад до входу",
"forgotPassword": "Забули пароль?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Авторизація",
"Content-Disposition": "Зміст-диспозиція",
"waitingForAuthorization": "Очікування авторизації...",

View File

@@ -2650,7 +2650,7 @@
"restartServerWithNewPassword": "Khởi động lại máy chủ - nó sẽ sử dụng mật khẩu mới",
"backToLogin": "Quay lại đăng nhập",
"forgotPassword": "Quên mật khẩu?",
"defaultPasswordHint": "Default password: 123456 (unless INITIAL_PASSWORD was set)",
"defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)",
"Authorization": "Ủy quyền",
"Content-Disposition": "Bố trí nội dung",
"waitingForAuthorization": "Đang chờ cấp phép...",

View File

@@ -2672,7 +2672,7 @@
"restartServerWithNewPassword": "重启服务器,系统会使用新密码",
"backToLogin": "返回登录",
"forgotPassword": "忘记密码?",
"defaultPasswordHint": "默认密码:123456(除非已设置 INITIAL_PASSWORD",
"defaultPasswordHint": "默认密码:CHANGEME(除非已设置 INITIAL_PASSWORD",
"Authorization": "Authorization",
"Content-Disposition": "Content-Disposition",
"waitingForAuthorization": "正在等待授权...",

View File

@@ -1021,6 +1021,18 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
databricks: validateDatabricksProvider,
snowflake: validateSnowflakeProvider,
gigachat: validateGigachatProvider,
vertex: async ({ apiKey }: any) => {
try {
const { parseSAFromApiKey, getAccessToken } =
await import("@omniroute/open-sse/executors/vertex.ts");
const sa = parseSAFromApiKey(apiKey);
// Validates credentials by successfully exchanging them for a JWT from Google Identity
await getAccessToken(sa);
return { valid: true, error: null };
} catch (error: any) {
return { valid: false, error: "Invalid Service Account JSON: " + error.message };
}
},
// LongCat AI — does not expose /v1/models; validate via chat completions directly (#592)
longcat: async ({ apiKey, providerSpecificData }: any) => {
try {

View File

@@ -1023,7 +1023,7 @@ export const updateProviderNodeSchema = z.object({
export const providerNodeValidateSchema = z.object({
baseUrl: z.string().trim().min(1, "Base URL and API key required"),
apiKey: z.string().trim().min(1, "Base URL and API key required"),
apiKey: z.string().trim().optional(),
type: z.enum(["openai-compatible", "anthropic-compatible"]).optional(),
compatMode: z.enum(["cc"]).optional(),
chatPath: z.string().trim().startsWith("/").max(500).optional().or(z.literal("")),
@@ -1106,7 +1106,7 @@ export const providersBatchTestSchema = z
export const validateProviderApiKeySchema = z.object({
provider: z.string().trim().min(1, "Provider and API key required"),
apiKey: z.string().trim().min(1, "Provider and API key required"),
apiKey: z.string().trim().optional(),
validationModelId: z.string().trim().optional(),
customUserAgent: z.string().trim().max(500).optional(),
baseUrl: z.string().trim().url().optional(),

View File

@@ -756,7 +756,7 @@ test("provider-nodes validate route rejects invalid JSON and schema errors", asy
assert.equal(invalidBodyResponse.status, 400);
const invalidBodyPayload = await invalidBodyResponse.json();
assert.equal(invalidBodyPayload.error.message, "Invalid request");
assert.equal(invalidBodyPayload.error.details.length >= 2, true);
assert.equal(invalidBodyPayload.error.details.length >= 1, true);
});
test("provider-nodes validate route validates anthropic compatible providers against the models endpoint", async () => {