docs(changelog): update changelog for PRs 2667-2675 & fix: resolve typescript compile-time errors

This commit is contained in:
diegosouzapw
2026-05-24 16:58:42 -03:00
parent ee1ce57065
commit b027451240
5 changed files with 132 additions and 51 deletions

View File

@@ -26,6 +26,15 @@
- **feat(i18n):** finish Simplified Chinese (zh-CN) UI coverage with 377 translated entries. ([#2659](https://github.com/diegosouzapw/OmniRoute/pull/2659) — thanks @L-aros)
- **feat(dashboard):** chat-first test slide-over layout — consolidates header controls (Model/Key selects, Clear button) into a unified toolbar, maximizes vertical conversation space, integrates live tailing of provider logs in Logs tab, and locks composer focus for keyboard-only convenience. ([#2660](https://github.com/diegosouzapw/OmniRoute/pull/2660) — thanks @mrmm)
- **feat(cli):** desktop updates, autostart, and headless CLI modes — integrates native auto-updater checks, login autostart (Linux .desktop, macOS/Windows login items), and a background headless server CLI daemon mode (`--headless` or `OMNIROUTE_HEADLESS=true`) into the Electron app wrapper. ([#2662](https://github.com/diegosouzapw/OmniRoute/pull/2662) — thanks @benzntech)
- **feat(quota):** card-grid layout and provider group headers under quota management — replaces monolithic table with a beautiful 4-column card grid in limits. ([#2667](https://github.com/diegosouzapw/OmniRoute/pull/2667) — thanks @Gi99lin)
- **feat(dashboard):** real-time WebSocket live monitoring daemon — runs a Node.js WebSocket daemon sidecar on port `20129` to emit real-time events for request starts/completes/fails, combo attempts, and credential status in the dashboard logs. ([#2668](https://github.com/diegosouzapw/OmniRoute/pull/2668) — thanks @herjarsa)
- **feat(copilot):** AI assistant with CodeGraph + CLI + knowledge base — integrates a dashboard assistant with CodeGraph knowledge base access and CLI capabilities for app exploration. ([#2669](https://github.com/diegosouzapw/OmniRoute/pull/2669) — thanks @ovehbe / @herjarsa)
- **feat(pipeline):** pre-request middleware hooks — pipeline executing custom JS hooks before routing/combo logic to mutate headers/body or short-circuit requests. ([#2670](https://github.com/diegosouzapw/OmniRoute/pull/2670) — thanks @herjarsa)
- **feat(resilience):** credential health check + adaptive circuit breaker v2 — background connection health check scheduler with progressive circuit breaker adding DEGRADED state and HALF-OPEN recovery validation to avoid latency spikes. ([#2671](https://github.com/diegosouzapw/OmniRoute/pull/2671) — thanks @herjarsa)
- **feat(playground):** combo routing visual simulator — interactive route simulation page at `/dashboard/combos/playground` to showcase cascade hops, latency, and cost estimates. ([#2672](https://github.com/diegosouzapw/OmniRoute/pull/2672) — thanks @herjarsa)
- **feat(auth):** API key groups with model-level permissions — group definitions with model-level wildcards/denies where API keys inherit group-scoped restrictions. ([#2673](https://github.com/diegosouzapw/OmniRoute/pull/2673) — thanks @herjarsa)
- **feat(pwa):** enhanced manifest + push notification support — polishes offline shortcuts, screenshots, display metadata, and push service workers. ([#2674](https://github.com/diegosouzapw/OmniRoute/pull/2674) — thanks @herjarsa)
- **feat(proxy):** serverless relay proxy endpoints with rate limiting — public relay proxy endpoints with cost caps and rate limits, CRUD API, and dashboard usage tracking. ([#2675](https://github.com/diegosouzapw/OmniRoute/pull/2675) — thanks @herjarsa)
### 🔧 Bug Fixes
@@ -49,6 +58,7 @@
- **fix(combo):** resolve pending request leaks on unresponsive combo targets — implements a default 60-second per-target timeout during combo routing loops to abort hanging upstream requests and release capacity limits. ([#2663](https://github.com/diegosouzapw/OmniRoute/pull/2663) — thanks @Chewji9875)
- **fix(proxy):** save custom dashboard proxies directly in SQLite registry — writes new provider/account/global/combo custom proxies directly to the modern `proxy_registry` database and assigns them via `proxy_assignments` instead of creating duplicate configurations. ([#2661](https://github.com/diegosouzapw/OmniRoute/pull/2661) — thanks @terence71-glitch)
- **fix(settings):** expand effortLevel enum to support xhigh and max reasoning efforts — adds `xhigh` and `max` levels to the updateThinkingBudgetSchema to resolve validation failures that silently discarded top-effort request payloads. ([#2666](https://github.com/diegosouzapw/OmniRoute/pull/2666) — thanks @mrmm)
- **fix(codex):** Codex OAuth refresh token reuse race condition under parallel requests. ([#2667](https://github.com/diegosouzapw/OmniRoute/pull/2667) — thanks @diegosouzapw)
### 📝 Maintenance

View File

@@ -1,5 +1,6 @@
import {
cleanupExpiredHandoffs,
getHandoff,
hasActiveHandoff,
type HandoffPayload,
upsertHandoff,

View File

@@ -26,7 +26,7 @@ function rowToHookConfig(row: HookConfigRow): HookConfig {
createdAt: row.created_at,
updatedAt: row.updated_at,
runCount: row.run_count,
lastError: row.last_error,
lastError: row.last_error || undefined,
};
}

View File

@@ -110,60 +110,107 @@ export function createRelayToken(input: CreateRelayTokenInput): RelayTokenWithSe
const prefix = "rl_" + rawToken.slice(6, 14);
db.prepare(`
db.prepare(
`
INSERT INTO relay_tokens (id, name, token_hash, token_prefix, description, combo_id, allowed_models,
max_tokens_per_request, max_requests_per_minute, max_requests_per_day, max_cost_per_day,
enabled, created_at, updated_at, expires_at, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
`).run(
id, input.name, tokenHash, prefix, input.description || "", input.comboId || null,
`
).run(
id,
input.name,
tokenHash,
prefix,
input.description || "",
input.comboId || null,
JSON.stringify(input.allowedModels || ["*"]),
input.maxTokensPerRequest || 128000,
input.maxRequestsPerMinute || 60,
input.maxRequestsPerDay || 10000,
input.maxCostPerDay || 0,
now, now, input.expiresAt || null,
JSON.stringify(input.metadata || {}),
now,
now,
input.expiresAt || null,
JSON.stringify(input.metadata || {})
);
const token = db.prepare("SELECT * FROM relay_tokens WHERE id = ?").get(id) as RelayTokenRow;
return { ...rowToCamel<RelayToken>(token), rawToken };
return { ...(rowToCamel(token) as unknown as RelayToken), rawToken };
}
export function getRelayTokens(): RelayToken[] {
const db = getDbInstance();
const rows = db.prepare("SELECT * FROM relay_tokens ORDER BY created_at DESC").all() as RelayTokenRow[];
return rows.map((r) => ({ ...rowToCamel<RelayToken>(r), enabled: r.enabled === 1 }));
const rows = db
.prepare("SELECT * FROM relay_tokens ORDER BY created_at DESC")
.all() as RelayTokenRow[];
return rows.map((r) => ({
...(rowToCamel(r) as unknown as RelayToken),
enabled: r.enabled === 1,
}));
}
export function getRelayToken(id: string): RelayToken | null {
const db = getDbInstance();
const row = db.prepare("SELECT * FROM relay_tokens WHERE id = ?").get(id) as RelayTokenRow | undefined;
const row = db.prepare("SELECT * FROM relay_tokens WHERE id = ?").get(id) as
| RelayTokenRow
| undefined;
if (!row) return null;
return { ...rowToCamel<RelayToken>(row), enabled: row.enabled === 1 };
return { ...(rowToCamel(row) as unknown as RelayToken), enabled: row.enabled === 1 };
}
export function getRelayTokenByHash(tokenHash: string): (RelayToken & { rawToken?: string }) | null {
export function getRelayTokenByHash(
tokenHash: string
): (RelayToken & { rawToken?: string }) | null {
const db = getDbInstance();
const row = db.prepare("SELECT * FROM relay_tokens WHERE token_hash = ? AND enabled = 1").get(tokenHash) as RelayTokenRow | undefined;
const row = db
.prepare("SELECT * FROM relay_tokens WHERE token_hash = ? AND enabled = 1")
.get(tokenHash) as RelayTokenRow | undefined;
if (!row) return null;
return { ...rowToCamel<RelayToken>(row), enabled: row.enabled === 1 };
return { ...(rowToCamel(row) as unknown as RelayToken), enabled: row.enabled === 1 };
}
export function updateRelayToken(id: string, updates: Partial<CreateRelayTokenInput>): RelayToken | null {
export function updateRelayToken(
id: string,
updates: Partial<CreateRelayTokenInput>
): RelayToken | null {
const db = getDbInstance();
const now = Math.floor(Date.now() / 1000);
const sets: string[] = ["updated_at = ?"];
const params: unknown[] = [now];
if (updates.name !== undefined) { sets.push("name = ?"); params.push(updates.name); }
if (updates.description !== undefined) { sets.push("description = ?"); params.push(updates.description); }
if (updates.comboId !== undefined) { sets.push("combo_id = ?"); params.push(updates.comboId); }
if (updates.allowedModels !== undefined) { sets.push("allowed_models = ?"); params.push(JSON.stringify(updates.allowedModels)); }
if (updates.maxTokensPerRequest !== undefined) { sets.push("max_tokens_per_request = ?"); params.push(updates.maxTokensPerRequest); }
if (updates.maxRequestsPerMinute !== undefined) { sets.push("max_requests_per_minute = ?"); params.push(updates.maxRequestsPerMinute); }
if (updates.maxRequestsPerDay !== undefined) { sets.push("max_requests_per_day = ?"); params.push(updates.maxRequestsPerDay); }
if (updates.maxCostPerDay !== undefined) { sets.push("max_cost_per_day = ?"); params.push(updates.maxCostPerDay); }
if (updates.name !== undefined) {
sets.push("name = ?");
params.push(updates.name);
}
if (updates.description !== undefined) {
sets.push("description = ?");
params.push(updates.description);
}
if (updates.comboId !== undefined) {
sets.push("combo_id = ?");
params.push(updates.comboId);
}
if (updates.allowedModels !== undefined) {
sets.push("allowed_models = ?");
params.push(JSON.stringify(updates.allowedModels));
}
if (updates.maxTokensPerRequest !== undefined) {
sets.push("max_tokens_per_request = ?");
params.push(updates.maxTokensPerRequest);
}
if (updates.maxRequestsPerMinute !== undefined) {
sets.push("max_requests_per_minute = ?");
params.push(updates.maxRequestsPerMinute);
}
if (updates.maxRequestsPerDay !== undefined) {
sets.push("max_requests_per_day = ?");
params.push(updates.maxRequestsPerDay);
}
if (updates.maxCostPerDay !== undefined) {
sets.push("max_cost_per_day = ?");
params.push(updates.maxCostPerDay);
}
params.push(id);
db.prepare(`UPDATE relay_tokens SET ${sets.join(", ")} WHERE id = ?`).run(...params);
@@ -178,15 +225,25 @@ export function deleteRelayToken(id: string): void {
export function toggleRelayToken(id: string, enabled: boolean): RelayToken | null {
const db = getDbInstance();
const now = Math.floor(Date.now() / 1000);
db.prepare("UPDATE relay_tokens SET enabled = ?, updated_at = ? WHERE id = ?").run(enabled ? 1 : 0, now, id);
db.prepare("UPDATE relay_tokens SET enabled = ?, updated_at = ? WHERE id = ?").run(
enabled ? 1 : 0,
now,
id
);
return getRelayToken(id);
}
// ── Usage / Rate Limit ───────────────────────────────────────────────────────
export function checkRateLimit(tokenId: string): { allowed: boolean; remaining: number; resetIn: number } {
export function checkRateLimit(tokenId: string): {
allowed: boolean;
remaining: number;
resetIn: number;
} {
const db = getDbInstance();
const token = db.prepare("SELECT * FROM relay_tokens WHERE id = ?").get(tokenId) as RelayTokenRow | undefined;
const token = db.prepare("SELECT * FROM relay_tokens WHERE id = ?").get(tokenId) as
| RelayTokenRow
| undefined;
if (!token) return { allowed: false, remaining: 0, resetIn: 0 };
const now = Math.floor(Date.now() / 1000);
@@ -194,9 +251,11 @@ export function checkRateLimit(tokenId: string): { allowed: boolean; remaining:
const dayWindow = Math.floor(now / 86400) * 86400;
// Check minute rate
const minuteRow = db.prepare(
"SELECT request_count, cost FROM relay_rate_limits WHERE token_id = ? AND window_start = ?",
).get(tokenId, minuteWindow) as { request_count: number; cost: number } | undefined;
const minuteRow = db
.prepare(
"SELECT request_count, cost FROM relay_rate_limits WHERE token_id = ? AND window_start = ?"
)
.get(tokenId, minuteWindow) as { request_count: number; cost: number } | undefined;
const minuteCount = minuteRow?.request_count || 0;
if (minuteCount >= token.max_requests_per_minute) {
@@ -204,9 +263,11 @@ export function checkRateLimit(tokenId: string): { allowed: boolean; remaining:
}
// Check daily rate
const dayRow = db.prepare(
"SELECT SUM(request_count) as total FROM relay_rate_limits WHERE token_id = ? AND window_start >= ?",
).get(tokenId, dayWindow) as { total: number } | undefined;
const dayRow = db
.prepare(
"SELECT SUM(request_count) as total FROM relay_rate_limits WHERE token_id = ? AND window_start >= ?"
)
.get(tokenId, dayWindow) as { total: number } | undefined;
const dayCount = dayRow?.total || 0;
if (dayCount >= token.max_requests_per_day) {
@@ -215,7 +276,7 @@ export function checkRateLimit(tokenId: string): { allowed: boolean; remaining:
const remaining = Math.min(
token.max_requests_per_minute - minuteCount,
token.max_requests_per_day - dayCount,
token.max_requests_per_day - dayCount
);
return { allowed: true, remaining, resetIn: 60 - (now % 60) };
@@ -234,30 +295,34 @@ export function recordRelayUsage(
latencyMs?: number;
clientIp?: string;
userAgent?: string;
},
}
): void {
const db = getDbInstance();
const now = Math.floor(Date.now() / 1000);
const minuteWindow = Math.floor(now / 60) * 60;
// Update rate limit window
db.prepare(`
db.prepare(
`
INSERT INTO relay_rate_limits (token_id, window_start, request_count, cost)
VALUES (?, ?, 1, ?)
ON CONFLICT(token_id, window_start) DO UPDATE SET
request_count = request_count + 1,
cost = cost + ?
`).run(tokenId, minuteWindow, params.cost || 0, params.cost || 0);
`
).run(tokenId, minuteWindow, params.cost || 0, params.cost || 0);
// Update last_used_at
db.prepare("UPDATE relay_tokens SET last_used_at = ? WHERE id = ?").run(now, tokenId);
// Insert log
db.prepare(`
db.prepare(
`
INSERT INTO relay_logs (token_id, request_id, model, prompt_tokens, completion_tokens, cost,
status, status_code, latency_ms, client_ip, user_agent, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
`
).run(
tokenId,
params.requestId || null,
params.model || null,
@@ -269,26 +334,31 @@ export function recordRelayUsage(
params.latencyMs || 0,
params.clientIp || null,
params.userAgent || null,
now,
now
);
}
export function getRelayUsage(tokenId: string, since: number): { requestCount: number; totalCost: number } {
export function getRelayUsage(
tokenId: string,
since: number
): { requestCount: number; totalCost: number } {
const db = getDbInstance();
const row = db.prepare(
"SELECT COUNT(*) as request_count, COALESCE(SUM(cost), 0) as total_cost FROM relay_logs WHERE token_id = ? AND created_at >= ?",
).get(tokenId, since) as { request_count: number; total_cost: number };
const row = db
.prepare(
"SELECT COUNT(*) as request_count, COALESCE(SUM(cost), 0) as total_cost FROM relay_logs WHERE token_id = ? AND created_at >= ?"
)
.get(tokenId, since) as { request_count: number; total_cost: number };
return { requestCount: row.request_count, totalCost: row.total_cost };
}
export function getRelayLogs(tokenId?: string, limit = 50): RelayLogRow[] {
const db = getDbInstance();
if (tokenId) {
return db.prepare(
"SELECT * FROM relay_logs WHERE token_id = ? ORDER BY created_at DESC LIMIT ?",
).all(tokenId, limit) as RelayLogRow[];
return db
.prepare("SELECT * FROM relay_logs WHERE token_id = ? ORDER BY created_at DESC LIMIT ?")
.all(tokenId, limit) as RelayLogRow[];
}
return db.prepare(
"SELECT * FROM relay_logs ORDER BY created_at DESC LIMIT ?",
).all(limit) as RelayLogRow[];
return db
.prepare("SELECT * FROM relay_logs ORDER BY created_at DESC LIMIT ?")
.all(limit) as RelayLogRow[];
}

View File

@@ -98,13 +98,13 @@ export interface HookConfigRow {
description: string;
priority: number;
scope_type: "global" | "combo";
combo_id?: string;
combo_id?: string | null;
enabled: number;
code: string;
created_at: string;
updated_at: string;
run_count: number;
last_error?: string;
last_error?: string | null;
}
/** API request body for creating/updating a hook */