Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dce45505c | |||
| 014732788c | |||
| 0e75d838ab | |||
| 8383da8a50 | |||
| 199d173816 | |||
| 0e207dc5d2 |
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [1.6.9] — 2026-02-28
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **Proxy Port Preservation** — `new URL()` silently strips default ports (80/443); proxy connections now extract the port from the raw URL string before parsing, preventing connection timeouts ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **Proxy Credential Encoding** — URL-encode special characters in proxy username/password; decode during legacy migration ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **HTTPS Proxy Default Port** — Changed from 8080 to 443 in frontend and migration logic ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **Proxy Dispatcher Cache** — Invalidate cached dispatchers when proxy config is updated or deleted ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **Proxy Logger SQLite Type** — Cast `proxyPort` to `Number` for INTEGER column ([PR #161](https://github.com/diegosouzapw/OmniRoute/pull/161))
|
||||
- **CopilotToolCard URL** — Use `baseUrl` prop directly instead of redundant `window.location.origin`; filter to chat models only (`!m.type && !m.parent`) ([PR #160](https://github.com/diegosouzapw/OmniRoute/pull/160))
|
||||
|
||||
---
|
||||
|
||||
## [1.6.8] — 2026-02-28
|
||||
|
||||
### 🔧 Improved
|
||||
|
||||
@@ -2,6 +2,7 @@ FROM node:22-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
COPY scripts/postinstall.mjs ./scripts/postinstall.mjs
|
||||
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi
|
||||
|
||||
COPY . ./
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute-desktop",
|
||||
"version": "1.6.8",
|
||||
"version": "1.6.9",
|
||||
"description": "OmniRoute Desktop Application",
|
||||
"main": "main.js",
|
||||
"author": "OmniRoute Team",
|
||||
|
||||
@@ -4,13 +4,39 @@ import { socksDispatcher } from "fetch-socks";
|
||||
const DISPATCHER_CACHE_KEY = Symbol.for("omniroute.proxyDispatcher.cache");
|
||||
const SUPPORTED_PROTOCOLS = new Set(["http:", "https:", "socks5:"]);
|
||||
|
||||
function getDispatcherCache() {
|
||||
function getDispatcherCache(): Map<string, any> {
|
||||
if (!globalThis[DISPATCHER_CACHE_KEY]) {
|
||||
globalThis[DISPATCHER_CACHE_KEY] = new Map();
|
||||
}
|
||||
return globalThis[DISPATCHER_CACHE_KEY];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached proxy dispatchers.
|
||||
* Call this when proxy configuration changes to avoid stale connections.
|
||||
*/
|
||||
export function clearDispatcherCache() {
|
||||
const cache = getDispatcherCache();
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the port from a proxy URL string before URL parsing.
|
||||
* `new URL("http://host:80")` strips port 80 since it's the HTTP default,
|
||||
* but proxy servers commonly listen on port 80/443, so we need to preserve it.
|
||||
*/
|
||||
function extractExplicitPort(urlStr) {
|
||||
try {
|
||||
// Match port in the host portion: "scheme://[user:pass@]host:PORT[/...]"
|
||||
const match = urlStr.match(/:\/\/(?:[^@]*@)?[^:/\s]+:(\d+)/);
|
||||
if (match) {
|
||||
const port = Number(match[1]);
|
||||
if (Number.isInteger(port) && port >= 1 && port <= 65535) return String(port);
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function defaultPortForProtocol(protocol) {
|
||||
if (protocol === "https:" || protocol === "wss:") return "443";
|
||||
if (protocol === "socks5:") return "1080";
|
||||
@@ -26,13 +52,28 @@ function normalizePort(port, protocol) {
|
||||
return String(parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a proxy URL string manually from parsed URL components.
|
||||
* We cannot use URL.toString() because the URL serializer silently strips
|
||||
* default ports (80 for http, 443 for https). Proxy servers commonly
|
||||
* listen on these ports, so we must always include the port explicitly.
|
||||
*/
|
||||
function buildProxyUrlString(parsed, port) {
|
||||
const auth =
|
||||
parsed.username
|
||||
? `${parsed.username}${parsed.password ? `:${parsed.password}` : ""}@`
|
||||
: "";
|
||||
return `${parsed.protocol}//${auth}${parsed.hostname}:${port}`;
|
||||
}
|
||||
|
||||
export function isSocks5ProxyEnabled() {
|
||||
return process.env.ENABLE_SOCKS5_PROXY === "true";
|
||||
}
|
||||
|
||||
export function proxyUrlForLogs(proxyUrl) {
|
||||
const explicitPort = extractExplicitPort(proxyUrl);
|
||||
const parsed = new URL(proxyUrl);
|
||||
const port = parsed.port || defaultPortForProtocol(parsed.protocol);
|
||||
const port = explicitPort || parsed.port || defaultPortForProtocol(parsed.protocol);
|
||||
return `${parsed.protocol}//${parsed.hostname}:${port}`;
|
||||
}
|
||||
|
||||
@@ -41,6 +82,11 @@ export function normalizeProxyUrl(
|
||||
source = "proxy",
|
||||
{ allowSocks5 = isSocks5ProxyEnabled() } = {}
|
||||
) {
|
||||
// Extract the explicit port from the raw URL string BEFORE parsing,
|
||||
// because `new URL()` silently strips default ports (80 for http,
|
||||
// 443 for https), which are valid and common for proxy servers.
|
||||
const explicitPort = extractExplicitPort(proxyUrl);
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(proxyUrl);
|
||||
@@ -62,8 +108,12 @@ export function normalizeProxyUrl(
|
||||
throw new Error(`[ProxyDispatcher] Invalid ${source} host`);
|
||||
}
|
||||
|
||||
parsed.port = normalizePort(parsed.port, parsed.protocol);
|
||||
return parsed.toString();
|
||||
// Use the explicit port from the raw string if present, otherwise apply default.
|
||||
const port = explicitPort || normalizePort(parsed.port, parsed.protocol);
|
||||
|
||||
// Build the URL string manually instead of using parsed.toString(),
|
||||
// which would strip default ports (80/443) and break the proxy connection.
|
||||
return buildProxyUrlString(parsed, port);
|
||||
}
|
||||
|
||||
export function proxyConfigToUrl(proxyConfig, { allowSocks5 = isSocks5ProxyEnabled() } = {}) {
|
||||
@@ -93,14 +143,15 @@ export function proxyConfigToUrl(proxyConfig, { allowSocks5 = isSocks5ProxyEnabl
|
||||
}
|
||||
|
||||
const port = normalizePort(proxyConfig.port, protocol);
|
||||
const proxyUrl = new URL(`${type}://${proxyConfig.host}:${port}`);
|
||||
|
||||
if (proxyConfig.username) {
|
||||
proxyUrl.username = proxyConfig.username;
|
||||
proxyUrl.password = proxyConfig.password || "";
|
||||
}
|
||||
// Build the URL string manually to preserve the port through normalization.
|
||||
const auth = proxyConfig.username
|
||||
? `${encodeURIComponent(proxyConfig.username)}:${proxyConfig.password ? encodeURIComponent(proxyConfig.password) : ""}@`
|
||||
: "";
|
||||
|
||||
return normalizeProxyUrl(proxyUrl.toString(), "context proxy", { allowSocks5 });
|
||||
const proxyUrlStr = `${type}://${auth}${proxyConfig.host}:${port}`;
|
||||
|
||||
return normalizeProxyUrl(proxyUrlStr, "context proxy", { allowSocks5 });
|
||||
}
|
||||
|
||||
export function createProxyDispatcher(proxyUrl) {
|
||||
@@ -111,11 +162,14 @@ export function createProxyDispatcher(proxyUrl) {
|
||||
if (dispatcher) return dispatcher;
|
||||
|
||||
const parsed = new URL(normalizedUrl);
|
||||
const explicitPort = extractExplicitPort(normalizedUrl);
|
||||
const port = explicitPort || normalizePort(parsed.port, parsed.protocol);
|
||||
|
||||
if (parsed.protocol === "socks5:") {
|
||||
const socksOptions: Record<string, any> = {
|
||||
type: 5,
|
||||
host: parsed.hostname,
|
||||
port: Number(normalizePort(parsed.port, parsed.protocol)),
|
||||
port: Number(port),
|
||||
};
|
||||
if (parsed.username) socksOptions.userId = decodeURIComponent(parsed.username);
|
||||
if (parsed.password) socksOptions.password = decodeURIComponent(parsed.password);
|
||||
@@ -126,4 +180,4 @@ export function createProxyDispatcher(proxyUrl) {
|
||||
|
||||
dispatcherCache.set(normalizedUrl, dispatcher);
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "1.6.8",
|
||||
"version": "1.6.9",
|
||||
"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": {
|
||||
|
||||
@@ -33,7 +33,6 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
const [toolStatuses, setToolStatuses] = useState({});
|
||||
const [statusesLoaded, setStatusesLoaded] = useState(false);
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
fetchConnections();
|
||||
@@ -48,12 +47,6 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setCloudEnabled(data.cloudEnabled || false);
|
||||
if (typeof window !== "undefined") {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
const apiPort = data?.apiPort || 20128;
|
||||
setApiBaseUrl(`${protocol}//${hostname}:${apiPort}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error loading cloud settings:", error);
|
||||
@@ -155,9 +148,8 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
if (cloudEnabled && CLOUD_URL) {
|
||||
return CLOUD_URL;
|
||||
}
|
||||
if (apiBaseUrl) {
|
||||
return apiBaseUrl;
|
||||
}
|
||||
// Use window.location.origin directly — works correctly in Docker/reverse-proxy
|
||||
// Per @alpgul feedback: don't use baseUrl prop (has port duplication issues)
|
||||
if (typeof window !== "undefined") {
|
||||
return window.location.origin;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function CopilotToolCard({
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
const modelList = (data.data || [])
|
||||
.filter((m: any) => m.id) // Only models with valid IDs
|
||||
.filter((m: any) => m && !m.type && !m.parent && m.id) // Only chat models with valid IDs
|
||||
.map((m: any) => ({
|
||||
value: m.id,
|
||||
label: m.id,
|
||||
@@ -112,11 +112,8 @@ export default function CopilotToolCard({
|
||||
};
|
||||
|
||||
const getBaseUrlForConfig = () => {
|
||||
// Use window.location.origin directly — works correctly in Docker/reverse-proxy
|
||||
// Per @alpgul feedback: don't use baseUrl prop (has port duplication issues)
|
||||
const origin =
|
||||
typeof window !== "undefined" ? window.location.origin : "http://localhost:20128";
|
||||
return `${origin}/v1/chat/completions`;
|
||||
const url = baseUrl;
|
||||
return `${url}/v1/chat/completions`;
|
||||
};
|
||||
|
||||
// Generate the Copilot chatLanguageModels.json config
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
deleteProxyForLevel,
|
||||
resolveProxyForConnection,
|
||||
} from "../../../../lib/localDb";
|
||||
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
|
||||
|
||||
const BASE_SUPPORTED_PROXY_TYPES = new Set(["http", "https"]);
|
||||
|
||||
@@ -129,6 +130,7 @@ export async function PUT(request) {
|
||||
const body = await request.json();
|
||||
const normalizedBody = normalizeProxyPayload(body);
|
||||
const updated = await setProxyConfig(normalizedBody);
|
||||
clearDispatcherCache();
|
||||
return Response.json(updated);
|
||||
} catch (error) {
|
||||
const status = Number(error?.status) || 500;
|
||||
@@ -155,6 +157,7 @@ export async function DELETE(request) {
|
||||
}
|
||||
|
||||
const updated = await deleteProxyForLevel(level, id);
|
||||
clearDispatcherCache();
|
||||
return Response.json(updated);
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
|
||||
@@ -211,11 +211,11 @@ function migrateProxyEntry(value: any) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return {
|
||||
type: url.protocol.replace(":", "").replace("//", "") || "http",
|
||||
type: url.protocol.replace(":", "") || "http",
|
||||
host: url.hostname,
|
||||
port: url.port || (url.protocol === "socks5:" ? "1080" : "8080"),
|
||||
username: url.username || "",
|
||||
password: url.password || "",
|
||||
port: url.port || (url.protocol === "socks5:" ? "1080" : url.protocol === "https:" ? "443" : "8080"),
|
||||
username: url.username ? decodeURIComponent(url.username) : "",
|
||||
password: url.password ? decodeURIComponent(url.password) : "",
|
||||
};
|
||||
} catch {
|
||||
const parts = value.split(":");
|
||||
|
||||
@@ -134,7 +134,7 @@ export function logProxyEvent(entry: Partial<ProxyLogEntry>) {
|
||||
status: log.status,
|
||||
proxyType: log.proxy?.type || null,
|
||||
proxyHost: log.proxy?.host || null,
|
||||
proxyPort: log.proxy?.port || null,
|
||||
proxyPort: log.proxy?.port ? Number(log.proxy.port) : null,
|
||||
level: log.level,
|
||||
levelId: log.levelId,
|
||||
provider: log.provider,
|
||||
|
||||
@@ -48,7 +48,11 @@ export default function ProxyConfigModal({ isOpen, onClose, level, levelId, leve
|
||||
const [hasOwnProxy, setHasOwnProxy] = useState(false);
|
||||
const [formError, setFormError] = useState(null);
|
||||
|
||||
const getDefaultPort = (type) => (type === "socks5" ? "1080" : "8080");
|
||||
const getDefaultPort = (type) => {
|
||||
if (type === "socks5") return "1080";
|
||||
if (type === "https") return "443";
|
||||
return "8080";
|
||||
};
|
||||
|
||||
// Load existing proxy config when modal opens
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user