From b00a4f7397446ff55817da76c6499048dd4b31bb Mon Sep 17 00:00:00 2001 From: payne Date: Thu, 30 Apr 2026 06:24:34 +0300 Subject: [PATCH] fix(grok-web): repair validator probe + accept full cookie blobs (#1793) Integrated into release/v3.7.5 --- CHANGELOG.md | 5 + open-sse/config/providerRegistry.ts | 21 +++ open-sse/executors/base.ts | 26 ++++ open-sse/executors/default.ts | 3 +- open-sse/handlers/chatCore.ts | 5 +- src/lib/db/quotaSnapshots.ts | 128 +++++++++++------- src/shared/components/ActiveRequestsPanel.tsx | 29 +++- 7 files changed, 162 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d132b6ab81..16411f6e72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index d4f8a509bb..be8a496855 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -735,6 +735,27 @@ export const REGISTRY: Record = { 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", diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 84bfe76a8d..a1137eec01 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -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; + + 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; } diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 56696c1687..2f568f7630 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -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-")) { diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index b932384dd0..a131a7cbc6 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -622,10 +622,11 @@ function wrapReadableStreamWithFinalize( }, async cancel(reason) { + runFinalize(); try { await reader.cancel(reason); - } finally { - runFinalize(); + } catch (error) { + // Ignored } }, }); diff --git a/src/lib/db/quotaSnapshots.ts b/src/lib/db/quotaSnapshots.ts index c1d9bb6721..7e40070b11 100644 --- a/src/lib/db/quotaSnapshots.ts +++ b/src/lib/db/quotaSnapshots.ts @@ -19,22 +19,32 @@ export function saveQuotaSnapshot(snapshot: Omit 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; + } } diff --git a/src/shared/components/ActiveRequestsPanel.tsx b/src/shared/components/ActiveRequestsPanel.tsx index 7de34f721d..988a4ff3d3 100644 --- a/src/shared/components/ActiveRequestsPanel.tsx +++ b/src/shared/components/ActiveRequestsPanel.tsx @@ -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() {

{t("runningRequestsDesc")}

-
- - {loading ? t("loading") : t("activeCount", { count: rows.length })} +
+
+ + {loading ? t("loading") : t("activeCount", { count: rows.length })} +
+ {rows.length > 0 && ( + + )}