mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 06:12:10 +03:00
docs(changelog): update changelog for PRs 2667-2675 & fix: resolve typescript compile-time errors
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -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
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
cleanupExpiredHandoffs,
|
||||
getHandoff,
|
||||
hasActiveHandoff,
|
||||
type HandoffPayload,
|
||||
upsertHandoff,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user