Release v3.7.4 (#1730)

* chore(release): v3.7.4 — version bump, openapi and changelog sync

* fix: preserve previous_response_id and conversation_id fields on empty input array (#1729)

* fix: bypass UI validation block for optional API keys and fix string fallback typing (#1721)

* fix(proxy): disable HTTP keep-alive and pipelining in Undici proxy dispatcher to prevent socket hang up

* feat(proxy): implement bulk proxy import via pipe-delimited parser with update-or-create logic

* docs: update changelog for v3.7.4 fixes and proxy features

* test: update responses store expectations for empty input arrays

* feat(pwa): add fullscreen installable PWA with manifest, service worker, and cross-platform app icons. (#1728)

Integrated into release/v3.7.4

* Fix image provider validation and Stability image requests (#1726)

Integrated into release/v3.7.4

* docs: add PR 1726 and PR 1728 to v3.7.4 changelog

* fix(security): replace insecure Math.random with crypto.getRandomValues for fallback UUID generation

* fix(migrations): intercept 007 migration to use IF NOT EXISTS logic on fresh installs

Fixes #1733

* test: fix typescript compilation errors in unit tests

* fix(db): reconcile legacy reasoning cache migration

* chore(release): bump to v3.7.4 — changelog, docs, version sync

* fix(cc-compatible): preserve Claude Code system skeleton (#1740)

Integrated into release/v3.7.4

* docs(changelog): update for PR #1740 merge

* docs(changelog): include workflow updates

* fix(db): reconcile legacy reasoning cache migration (#1734)

Integrated into release/v3.7.4

* Add endpoint tunnel visibility settings (#1743)

Integrated into release/v3.7.4

* Normalize max reasoning effort for Codex routing (#1744)

Integrated into release/v3.7.4

* Fix Claude Code gateway config helper (#1745)

Integrated into release/v3.7.4

* Refresh CLI fingerprint provider profiles (#1746)

Integrated into release/v3.7.4

* Integrated into release/v3.7.4 (PR #1742)

* docs(changelog): update for PRs 1742-1746

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: Yash Ghule <y.ghule77@gmail.com>
Co-authored-by: backryun <bakryun0718@proton.me>
Co-authored-by: dhaern <manker_lol@hotmail.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Duncan L <leungd@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-04-28 20:46:25 -03:00
committed by GitHub
parent 4cdd0dfd1a
commit 0cd388efb8
160 changed files with 4957 additions and 684 deletions

View File

@@ -0,0 +1,17 @@
"use client";
import { useEffect } from "react";
export function PwaRegister() {
useEffect(() => {
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
return;
}
navigator.serviceWorker.register("/sw.js").catch(() => {
// Ignore registration failures to avoid blocking app rendering.
});
}, []);
return null;
}

View File

@@ -1,4 +1,53 @@
import { CLI_TOOLS } from "./cliTools";
import { normalizeCliCompatProviderId } from "../utils/cliCompat";
export { normalizeCliCompatProviderId };
export const IMPLEMENTED_CLI_FINGERPRINT_PROVIDER_IDS = [
"claude",
"codex",
"github",
"antigravity",
"qwen",
] as const;
export const CLI_COMPAT_DISPLAY_PROVIDER_IDS = [
"claude",
"codex",
"copilot",
"antigravity",
"qwen",
] as const;
/**
* Known CLI/tool providers that are intentionally not exposed as CLI Fingerprint toggles yet.
*
* This setting controls the generic `applyFingerprint()` pipeline (header/body ordering plus
* optional CLI User-Agent overrides). Do not expose providers here just because they have a
* CLI Tools card or a provider integration:
*
* - Kiro and Cursor already apply their native parity inside custom executors, so a toggle would
* be misleading unless it controls additional behavior.
* - Droid, OpenClaw, Windsurf and Hermes are CLI tool setup guides/settings, not upstream provider
* fingerprints handled by OmniRoute.
* - Cline, Kilo Code, OpenCode and Kimi Coding have real provider/backend integrations, but no
* captured `CLI_FINGERPRINTS` entry is wired to `applyFingerprint()` yet.
*
* Keep this list as documentation for intentionally omitted candidates. When adding a provider to
* the visible toggle list, also add a real `CLI_FINGERPRINTS` entry or wire its custom executor.
*/
export const CLI_COMPAT_OMITTED_PROVIDER_IDS = [
"kiro",
"cursor",
"droid",
"openclaw",
"windsurf",
"hermes",
"cline",
"kilocode",
"opencode",
"kimi-coding",
] as const;
/**
* Provider IDs toggled in Settings -> CLI Fingerprint.
@@ -13,17 +62,12 @@ const TOOL_ID_TO_PROVIDER_ID: Record<string, string> = {
};
const DERIVED_PROVIDER_IDS = Object.values(CLI_TOOLS)
.map((tool: any) => TOOL_ID_TO_PROVIDER_ID[tool.id] ?? tool.id)
.map((tool: any) => normalizeCliCompatProviderId(TOOL_ID_TO_PROVIDER_ID[tool.id] ?? tool.id))
// "continue" currently has no provider id in AI_PROVIDERS
.filter((providerId) => providerId !== "continue" && providerId !== "amp");
const LEGACY_PROVIDER_IDS = [
// Keep to avoid breaking setups that saved old IDs
"copilot",
"kimi-coding",
"qwen",
];
.filter((providerId) => IMPLEMENTED_CLI_FINGERPRINT_PROVIDER_IDS.includes(providerId as any));
export const CLI_COMPAT_PROVIDER_IDS = Array.from(
new Set([...DERIVED_PROVIDER_IDS, ...LEGACY_PROVIDER_IDS])
new Set([...DERIVED_PROVIDER_IDS, ...IMPLEMENTED_CLI_FINGERPRINT_PROVIDER_IDS])
);
export const CLI_COMPAT_TOGGLE_IDS = Array.from(new Set(CLI_COMPAT_DISPLAY_PROVIDER_IDS));

View File

@@ -22,6 +22,7 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
"settings",
"docs",
"issues",
"changelog",
] as const;
export type HideableSidebarItemId = (typeof HIDEABLE_SIDEBAR_ITEM_IDS)[number];
@@ -92,6 +93,7 @@ const HELP_SIDEBAR_ITEMS: readonly SidebarItemDefinition[] = [
icon: "bug_report",
external: true,
},
{ id: "changelog", href: "/dashboard/changelog", i18nKey: "changelog", icon: "campaign" },
];
export const SIDEBAR_SECTIONS: readonly SidebarSectionDefinition[] = [

View File

@@ -0,0 +1,17 @@
export function normalizeClaudeBaseUrl(value: string): string {
return String(value || "")
.trim()
.replace(/\/+$/, "");
}
export function getStoredClaudeAuthValue(
env: Record<string, unknown> | null | undefined
): string | null {
if (!env || typeof env !== "object") return null;
const authValue = env.ANTHROPIC_AUTH_TOKEN ?? env.ANTHROPIC_API_KEY;
if (typeof authValue !== "string") return null;
const trimmed = authValue.trim();
return trimmed.length > 0 ? trimmed : null;
}

View File

@@ -0,0 +1,3 @@
export function normalizeCliCompatProviderId(providerId: string): string {
return providerId.toLowerCase() === "copilot" ? "github" : providerId.toLowerCase();
}

View File

@@ -117,9 +117,23 @@ export async function getConsistentMachineId(salt = null) {
const cryptoFallback = await import("crypto");
return cryptoFallback.randomUUID();
} catch {
if (typeof globalThis !== "undefined" && globalThis.crypto && globalThis.crypto.randomUUID) {
return globalThis.crypto.randomUUID();
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c == "x" ? r : (r & 0x3) | 0x8;
let r = 0;
if (
typeof globalThis !== "undefined" &&
globalThis.crypto &&
globalThis.crypto.getRandomValues
) {
const arr = new Uint8Array(1);
globalThis.crypto.getRandomValues(arr);
r = arr[0] % 16;
} else {
r = (Date.now() % 16) | 0;
}
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
@@ -140,9 +154,23 @@ export async function getRawMachineId() {
const cryptoFallback = await import("crypto");
return cryptoFallback.randomUUID();
} catch {
if (typeof globalThis !== "undefined" && globalThis.crypto && globalThis.crypto.randomUUID) {
return globalThis.crypto.randomUUID();
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c == "x" ? r : (r & 0x3) | 0x8;
let r = 0;
if (
typeof globalThis !== "undefined" &&
globalThis.crypto &&
globalThis.crypto.getRandomValues
) {
const arr = new Uint8Array(1);
globalThis.crypto.getRandomValues(arr);
r = arr[0] % 16;
} else {
r = (Date.now() % 16) | 0;
}
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

View File

@@ -0,0 +1,53 @@
import { z } from "zod";
export const NEWS_JSON_URL =
"https://raw.githubusercontent.com/diegosouzapw/OmniRoute/main/news.json";
export const CHANGELOG_RAW_URL =
"https://raw.githubusercontent.com/diegosouzapw/OmniRoute/main/CHANGELOG.md";
export const CHANGELOG_GITHUB_URL =
"https://github.com/diegosouzapw/OmniRoute/blob/main/CHANGELOG.md";
const activeNewsSchema = z.object({
active: z.literal(true),
title: z.string().trim().min(1).max(120),
message: z.string().trim().min(1).max(600),
link: z.string().url().optional(),
linkLabel: z.string().trim().min(1).max(80).optional(),
icon: z
.string()
.trim()
.regex(/^[a-z0-9_]+$/)
.optional(),
});
const inactiveNewsSchema = z
.object({
active: z.literal(false),
})
.passthrough();
const newsPayloadSchema = z.discriminatedUnion("active", [activeNewsSchema, inactiveNewsSchema]);
export type NewsAnnouncement = z.infer<typeof activeNewsSchema>;
export function parseActiveNewsPayload(payload: unknown): NewsAnnouncement | null {
const parsed = newsPayloadSchema.safeParse(payload);
if (!parsed.success || parsed.data.active !== true) return null;
return parsed.data;
}
export function getLatestChangelogMarkdown(markdown: string, limit = 10): string {
const parts = markdown.split(/^##\s+\[/gm);
if (parts.length <= 1) {
const truncated = markdown.slice(0, 5000).trimEnd();
return markdown.length > 5000 ? `${truncated}\n\n...` : truncated;
}
const header = parts[0].trimEnd();
const versions = parts
.slice(1, limit + 1)
.map((part) => `## [${part.trimEnd()}`)
.join("\n\n");
return [header, versions].filter(Boolean).join("\n\n");
}

View File

@@ -1157,6 +1157,15 @@ export const updateProxyRegistrySchema = createProxyRegistrySchema.partial().ext
id: z.string().trim().min(1, "id is required"),
});
export const bulkImportProxiesSchema = z
.object({
items: z
.array(createProxyRegistrySchema)
.min(1, "At least one proxy is required")
.max(100, "Maximum 100 proxies per import"),
})
.strict();
export const proxyAssignmentSchema = z
.object({
scope: z.enum(["global", "provider", "account", "combo", "key"]),

View File

@@ -45,6 +45,8 @@ export const updateSettingsSchema = z.object({
setupComplete: z.boolean().optional(),
blockedProviders: z.array(z.string().max(100)).optional(),
hideHealthCheckLogs: z.boolean().optional(),
hideEndpointCloudflaredTunnel: z.boolean().optional(),
hideEndpointTailscaleFunnel: z.boolean().optional(),
debugMode: z.boolean().optional(),
hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(),
comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(),