fix(grok-web): repair validator probe + accept full cookie blobs (#1793)

Integrated into release/v3.7.5
This commit is contained in:
payne
2026-04-30 06:24:34 +03:00
committed by GitHub
parent 13b5a50f80
commit b00a4f7397
7 changed files with 162 additions and 55 deletions

View File

@@ -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)

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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-")) {

View File

@@ -622,10 +622,11 @@ function wrapReadableStreamWithFinalize<T>(
},
async cancel(reason) {
runFinalize();
try {
await reader.cancel(reason);
} finally {
runFinalize();
} catch (error) {
// Ignored
}
},
});

View File

@@ -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;
}
}

View File

@@ -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>