Keep bot jobs and harness calls from hanging

This commit is contained in:
2026-10-20 12:59:24 +01:00
parent 3e13052a25
commit 3b355d68bc
16 changed files with 420 additions and 29 deletions
@@ -1,3 +1,4 @@
import { AppData } from "@/src/data/app-data";
import HarnessError from "../../harness-error";
import type { HarnessAdapter, HarnessImageOutput } from "../../types";
import grabProviderPreset from "../grab-provider-preset";
@@ -54,7 +55,12 @@ const generateImage: HarnessAdapter["generateImage"] = async ({
media_type: item.media_type || "image/png",
});
} else if (typeof item?.url == "string") {
const download = await fetch(item.url, { signal });
const download = await fetch(item.url, {
signal: AbortSignal.any([
signal,
AbortSignal.timeout(AppData.HarnessRequestTimeoutMs),
]),
});
if (download.ok) {
images.push({
@@ -1,4 +1,5 @@
import guessModelModalities from "../../catalog/guess-model-modalities";
import HarnessError from "../../harness-error";
import type { HarnessAdapter, HarnessCatalogModel } from "../../types";
import grabProviderPreset from "../grab-provider-preset";
import openRouterModelToCatalog, {
@@ -27,6 +28,18 @@ const listModels: HarnessAdapter["listModels"] = async ({
});
const json = await res.json().catch(() => null);
/**
* An unreadable list is a failed sync, not an empty catalog: keeping
* the models already known beats marking them all unavailable.
*/
if (json === null) {
throw new HarnessError({
message: `${provider.name || preset.title} returned an unreadable model list.`,
retryable: true,
});
}
const items: any[] = Array.isArray(json?.data)
? json.data
: Array.isArray(json?.models)
@@ -1,3 +1,4 @@
import HarnessError from "../../harness-error";
import type { HarnessAdapter } from "../../types";
import providerRequest from "./provider-request";
@@ -36,6 +37,19 @@ const transcribe: HarnessAdapter["transcribe"] = async ({
const json = await res.json().catch(() => null);
/**
* A cut-off or malformed answer is a failed call, not silence: saying
* it failed lets the harness try the next model instead of handing the
* user an empty transcript.
*/
if (json === null) {
throw new HarnessError({
message: `${candidate.provider.name || "The provider"} returned an unreadable transcript.`,
retryable: true,
candidate_key: candidate.key,
});
}
return { text: String(json?.text || "").trim() };
};
+17 -2
View File
@@ -1,5 +1,7 @@
import { z } from "zod";
import { AppData } from "@/src/data/app-data";
import TaskTimeoutError from "@/src/utils/task-timeout-error";
import withTimeout from "@/src/utils/with-timeout";
import HarnessError from "../harness-error";
import type { HarnessTool, HarnessToolCall } from "../types";
@@ -40,14 +42,27 @@ export default async function runToolCall({ call, tools, signal }: Params) {
}
try {
return truncate({
text: await tool.run({ args: parsed ? parsed.data : raw, signal }),
const text = await withTimeout({
timeout_ms: AppData.HarnessToolTimeoutMs,
signal,
message: `${call.name} took too long and was stopped.`,
task: (tool_signal) =>
tool.run({
args: parsed ? parsed.data : raw,
signal: tool_signal,
}),
});
return truncate({ text });
} catch (error: unknown) {
if (signal.aborted) {
throw new HarnessError({ message: `Cancelled.`, code: "aborted" });
}
if (error instanceof TaskTimeoutError) {
return `Error: ${error.message}`;
}
return `Error: ${error instanceof Error ? error.message : String(error)}`;
}
}