mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(grok-web): repair validator probe + accept full cookie blobs (#1793)
Integrated into release/v3.7.5
This commit is contained in:
@@ -17,6 +17,11 @@
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(dashboard):** add manual 'Clear All' button to terminate stalled long-running requests in Active Requests panel (#1799)
|
||||
- **fix(schema):** remove empty string values from optional tool parameters to prevent upstream validation errors (#1674)
|
||||
- **fix(providers):** ensure proper streaming cleanup and semaphore release to prevent stalls with nanoGPT (#1781)
|
||||
- **fix(db):** wrap quota_snapshots access in try/catch to gracefully handle pending database migrations (#1784)
|
||||
- **feat(providers):** add support for glm-cn (BigModel) provider (#1770)
|
||||
- **fix(grok-web):** fix Grok validator and cookie parsing (#1793)
|
||||
- **fix(antigravity):** scrub internal OmniRoute headers (#1794)
|
||||
- **fix(chatgpt-web):** restore validator + expand model catalog to ChatGPT Plus tier (#1792)
|
||||
|
||||
@@ -735,6 +735,27 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
models: [...GLM_SHARED_MODELS],
|
||||
},
|
||||
|
||||
"glm-cn": {
|
||||
id: "glm-cn",
|
||||
alias: "glm-cn",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://open.bigmodel.cn/api/paas/v4/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
defaultContextLength: 128000,
|
||||
models: [
|
||||
{ id: "glm-4-plus", name: "GLM-4 Plus" },
|
||||
{ id: "glm-4-0520", name: "GLM-4 0520" },
|
||||
{ id: "glm-4-air", name: "GLM-4 Air" },
|
||||
{ id: "glm-4-airx", name: "GLM-4 AirX" },
|
||||
{ id: "glm-4-long", name: "GLM-4 Long", contextLength: 1000000 },
|
||||
{ id: "glm-4-flashx", name: "GLM-4 FlashX" },
|
||||
{ id: "glm-4-flash", name: "GLM-4 Flash" },
|
||||
],
|
||||
passthroughModels: true,
|
||||
},
|
||||
|
||||
glmt: {
|
||||
id: "glmt",
|
||||
alias: "glmt",
|
||||
|
||||
@@ -267,6 +267,32 @@ export class BaseExecutor {
|
||||
void model;
|
||||
void stream;
|
||||
void credentials;
|
||||
|
||||
// Fix #1674: Remove empty string values from optional parameters
|
||||
// like tool descriptions to avoid upstream validation failures.
|
||||
if (body && typeof body === "object" && !Array.isArray(body)) {
|
||||
const cloned = { ...body } as Record<string, unknown>;
|
||||
|
||||
if (Array.isArray(cloned.tools)) {
|
||||
cloned.tools = cloned.tools.map((tool: any) => {
|
||||
if (tool?.function && typeof tool.function === "object") {
|
||||
const func = { ...tool.function };
|
||||
if (func.description === "") delete func.description;
|
||||
return { ...tool, function: func };
|
||||
}
|
||||
return tool;
|
||||
});
|
||||
}
|
||||
|
||||
// Also clean up top level optional fields that commonly cause issues when empty
|
||||
const optionalKeys = ["user", "stop", "seed", "response_format"];
|
||||
for (const key of optionalKeys) {
|
||||
if (cloned[key] === "") delete cloned[key];
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
|
||||
@@ -347,7 +347,8 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
*/
|
||||
transformRequest(model, body, stream, credentials) {
|
||||
void model;
|
||||
let withDefaults = applyProviderRequestDefaults(body, this.config.requestDefaults);
|
||||
const cleanedBody = super.transformRequest(model, body, stream, credentials);
|
||||
let withDefaults = applyProviderRequestDefaults(cleanedBody, this.config.requestDefaults);
|
||||
|
||||
if (typeof withDefaults === "object" && withDefaults !== null && !Array.isArray(withDefaults)) {
|
||||
if (this.provider?.startsWith?.("anthropic-compatible-")) {
|
||||
|
||||
@@ -622,10 +622,11 @@ function wrapReadableStreamWithFinalize<T>(
|
||||
},
|
||||
|
||||
async cancel(reason) {
|
||||
runFinalize();
|
||||
try {
|
||||
await reader.cancel(reason);
|
||||
} finally {
|
||||
runFinalize();
|
||||
} catch (error) {
|
||||
// Ignored
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,22 +19,32 @@ export function saveQuotaSnapshot(snapshot: Omit<QuotaSnapshotRow, "id" | "creat
|
||||
const db = getDbInstance() as unknown as DbLike;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO quota_snapshots
|
||||
(provider, connection_id, window_key, remaining_percentage, is_exhausted,
|
||||
next_reset_at, window_duration_ms, raw_data, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
snapshot.provider,
|
||||
snapshot.connection_id,
|
||||
snapshot.window_key,
|
||||
snapshot.remaining_percentage,
|
||||
snapshot.is_exhausted,
|
||||
snapshot.next_reset_at,
|
||||
snapshot.window_duration_ms,
|
||||
snapshot.raw_data,
|
||||
now
|
||||
);
|
||||
try {
|
||||
db.prepare(
|
||||
`INSERT INTO quota_snapshots
|
||||
(provider, connection_id, window_key, remaining_percentage, is_exhausted,
|
||||
next_reset_at, window_duration_ms, raw_data, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
snapshot.provider,
|
||||
snapshot.connection_id,
|
||||
snapshot.window_key,
|
||||
snapshot.remaining_percentage,
|
||||
snapshot.is_exhausted,
|
||||
snapshot.next_reset_at,
|
||||
snapshot.window_duration_ms,
|
||||
snapshot.raw_data,
|
||||
now
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes("no such table")) {
|
||||
console.warn(
|
||||
"[QuotaSnapshots] Skipping save: quota_snapshots table not found. Awaiting migration."
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function getQuotaSnapshots(opts: {
|
||||
@@ -62,9 +72,16 @@ export function getQuotaSnapshots(opts: {
|
||||
params.push(opts.until);
|
||||
}
|
||||
|
||||
const sql = `SELECT * FROM quota_snapshots WHERE ${conditions.join(" AND ")} ORDER BY created_at ASC`;
|
||||
const rows = db.prepare(sql).all(...params);
|
||||
return rows.map((r) => rowToCamel(r) as unknown as QuotaSnapshotRow);
|
||||
try {
|
||||
const sql = `SELECT * FROM quota_snapshots WHERE ${conditions.join(" AND ")} ORDER BY created_at ASC`;
|
||||
const rows = db.prepare(sql).all(...params);
|
||||
return rows.map((r) => rowToCamel(r) as unknown as QuotaSnapshotRow);
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes("no such table")) {
|
||||
return [];
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAggregatedSnapshots(opts: {
|
||||
@@ -100,34 +117,41 @@ export function getAggregatedSnapshots(opts: {
|
||||
const selectKey =
|
||||
opts.aggregateBy === "connection" ? "provider || ':' || connection_id as provider" : "provider";
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
datetime((strftime('%s', created_at) / ${bucketSeconds}) * ${bucketSeconds}, 'unixepoch') as bucket,
|
||||
${selectKey},
|
||||
AVG(remaining_percentage) as remainingPct,
|
||||
MAX(is_exhausted) as isExhausted,
|
||||
window_key
|
||||
FROM quota_snapshots
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
GROUP BY ${groupFields}
|
||||
ORDER BY bucket ASC
|
||||
`;
|
||||
try {
|
||||
const sql = `
|
||||
SELECT
|
||||
datetime((strftime('%s', created_at) / ${bucketSeconds}) * ${bucketSeconds}, 'unixepoch') as bucket,
|
||||
${selectKey},
|
||||
AVG(remaining_percentage) as remainingPct,
|
||||
MAX(is_exhausted) as isExhausted,
|
||||
window_key
|
||||
FROM quota_snapshots
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
GROUP BY ${groupFields}
|
||||
ORDER BY bucket ASC
|
||||
`;
|
||||
|
||||
const rows = db.prepare(sql).all(...params) as Array<{
|
||||
bucket: string;
|
||||
provider: string;
|
||||
remainingPct: number | null;
|
||||
isExhausted: number;
|
||||
windowKey: string;
|
||||
}>;
|
||||
const rows = db.prepare(sql).all(...params) as Array<{
|
||||
bucket: string;
|
||||
provider: string;
|
||||
remainingPct: number | null;
|
||||
isExhausted: number;
|
||||
windowKey: string;
|
||||
}>;
|
||||
|
||||
return rows.map((r) => ({
|
||||
timestamp: r.bucket,
|
||||
provider: r.provider,
|
||||
remainingPct: r.remainingPct ?? 0,
|
||||
isExhausted: r.isExhausted === 1,
|
||||
windowKey: r.windowKey,
|
||||
}));
|
||||
return rows.map((r) => ({
|
||||
timestamp: r.bucket,
|
||||
provider: r.provider,
|
||||
remainingPct: r.remainingPct ?? 0,
|
||||
isExhausted: r.isExhausted === 1,
|
||||
windowKey: r.windowKey,
|
||||
}));
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes("no such table")) {
|
||||
return [];
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupOldSnapshots(retentionDays = 90): number {
|
||||
@@ -141,8 +165,14 @@ export function cleanupOldSnapshots(retentionDays = 90): number {
|
||||
const db = getDbInstance() as unknown as DbLike;
|
||||
const cutoffDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
const result = db.prepare("DELETE FROM quota_snapshots WHERE created_at < ?").run(cutoffDate);
|
||||
lastCleanupAt = now;
|
||||
|
||||
return result.changes;
|
||||
try {
|
||||
const result = db.prepare("DELETE FROM quota_snapshots WHERE created_at < ?").run(cutoffDate);
|
||||
lastCleanupAt = now;
|
||||
return result.changes;
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes("no such table")) {
|
||||
return 0;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,19 @@ export default function ActiveRequestsPanel() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClearAll = async () => {
|
||||
if (!window.confirm(t("confirmClearActiveRequests") || "Clear all active requests?")) return;
|
||||
try {
|
||||
const res = await fetch("/api/logs/active", { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
setRows([]);
|
||||
setSelectedRow(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to clear active requests:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!loading && rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -75,9 +88,19 @@ export default function ActiveRequestsPanel() {
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted">{t("runningRequestsDesc")}</p>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-300">
|
||||
<span className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
{loading ? t("loading") : t("activeCount", { count: rows.length })}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-300">
|
||||
<span className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
{loading ? t("loading") : t("activeCount", { count: rows.length })}
|
||||
</div>
|
||||
{rows.length > 0 && (
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-1 text-xs font-medium text-red-400 transition-colors hover:bg-red-500/20 hover:text-red-300"
|
||||
>
|
||||
{t("clearAll") || "Clear All"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user