Compare commits

..

6 Commits

Author SHA1 Message Date
diegosouzapw 6dce45505c chore(release): v1.6.9
Build Electron Desktop App / Validate version (push) Failing after 40s
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
- PR #160: CopilotToolCard URL fix + chat model filter (alpgul)
- PR #161: Proxy port preservation, credential encoding, cache invalidation (ken2190)
- CHANGELOG: v1.6.9 entry
- Version bump: 1.6.8 → 1.6.9
2026-02-28 16:29:25 -03:00
Diego Rodrigues de Sa e Souza 014732788c Merge pull request #161 from ken2190/fix/proxy-logic-and-docker-build
fix: preserve explicit proxy port and fix Docker build
2026-02-28 16:28:28 -03:00
Diego Rodrigues de Sa e Souza 0e75d838ab Merge pull request #160 from alpgul/fix/base-url-and-chat-filter
fix: improve API base URL handling and filter for chat models in CopilotToolCard
2026-02-28 16:28:26 -03:00
Alptekin Gülcan 8383da8a50 fix: improve API base URL handling and filter for chat models in CopilotToolCard 2026-02-28 19:10:17 +00:00
duongvdo 199d173816 fix: preserve explicit proxy port (80/443) instead of defaulting to 8080
The URL parser silently strips default ports (80 for HTTP, 443 for HTTPS)
when constructing URL objects. This caused proxy connections to use port
8080 instead of the user-specified port 80, resulting in connection
timeouts. Fix by extracting the port from the raw URL string before
parsing and building the normalized URL manually to avoid the serializer
stripping it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 01:41:07 +07:00
duongvdo 0e207dc5d2 fix: proxy logic bugs and Docker build failure
- URL-encode proxy credentials to handle special characters in passwords
- Decode URL-encoded credentials during legacy proxy migration
- Fix HTTPS proxy default port (443 instead of 8080) in frontend and migration
- Add dispatcher cache invalidation when proxy config changes
- Cast proxy port to number for SQLite INTEGER column in proxy logger
- Fix redundant .replace("//", "") in migration protocol parsing
- Copy postinstall script in Dockerfile before npm install

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 00:48:27 +07:00
11 changed files with 100 additions and 36 deletions
+13
View File
@@ -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
+1
View File
@@ -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 -1
View File
@@ -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",
+66 -12
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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(
+4 -4
View File
@@ -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(":");
+1 -1
View File
@@ -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,
+5 -1
View File
@@ -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(() => {