Skip to content

Commit 4e2d9b0

Browse files
committed
fix(providers): cap model request timeout delays
1 parent 040eba1 commit 4e2d9b0

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

src/agents/provider-transport-fetch.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Stream } from "openai/streaming";
22
import type { Model } from "openclaw/plugin-sdk/llm";
33
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4+
import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js";
45
import { buildGuardedModelFetch } from "./provider-transport-fetch.js";
56

67
type ProviderRequestPolicyConfigMockResult = {
@@ -227,6 +228,60 @@ describe("buildGuardedModelFetch", () => {
227228
}
228229
});
229230

231+
it("caps oversized model request timeouts before arming abort signals", async () => {
232+
const timeoutController = new AbortController();
233+
const timeoutSpy = vi.spyOn(AbortSignal, "timeout").mockReturnValue(timeoutController.signal);
234+
const model = {
235+
id: "deepseek-v4-flash",
236+
provider: "ds4",
237+
api: "openai-completions",
238+
baseUrl: "http://127.0.0.1:18000/v1",
239+
} as unknown as Model<"openai-completions">;
240+
241+
try {
242+
const fetcher = buildGuardedModelFetch(model, Number.MAX_SAFE_INTEGER);
243+
const response = await fetcher("http://127.0.0.1:18000/v1/chat/completions", {
244+
method: "POST",
245+
});
246+
await response.text();
247+
248+
expect(timeoutSpy).toHaveBeenCalledWith(MAX_TIMER_TIMEOUT_MS);
249+
expect(ensureModelProviderLocalServiceMock).toHaveBeenCalledWith(
250+
model,
251+
undefined,
252+
timeoutController.signal,
253+
);
254+
expect(latestGuardedFetchParams().timeoutMs).toBe(MAX_TIMER_TIMEOUT_MS);
255+
} finally {
256+
timeoutSpy.mockRestore();
257+
}
258+
});
259+
260+
it("ignores non-positive model request timeout metadata", async () => {
261+
const timeoutSpy = vi.spyOn(AbortSignal, "timeout");
262+
const model = {
263+
id: "deepseek-v4-flash",
264+
provider: "ds4",
265+
api: "openai-completions",
266+
baseUrl: "http://127.0.0.1:18000/v1",
267+
requestTimeoutMs: -1,
268+
} as unknown as Model<"openai-completions">;
269+
270+
try {
271+
const fetcher = buildGuardedModelFetch(model);
272+
const response = await fetcher("http://127.0.0.1:18000/v1/chat/completions", {
273+
method: "POST",
274+
});
275+
await response.text();
276+
277+
expect(timeoutSpy).not.toHaveBeenCalled();
278+
expect(ensureModelProviderLocalServiceMock).toHaveBeenCalledWith(model, undefined, undefined);
279+
expect(latestGuardedFetchParams().timeoutMs).toBeUndefined();
280+
} finally {
281+
timeoutSpy.mockRestore();
282+
}
283+
});
284+
230285
it("combines caller abort signals with model request timeouts", async () => {
231286
const callerController = new AbortController();
232287
const timeoutController = new AbortController();

src/agents/provider-transport-fetch.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
1919
import { resolveDebugProxySettings } from "../proxy-capture/env.js";
2020
import {
2121
asFiniteNumberInRange,
22+
clampTimerTimeoutMs,
2223
parseStrictFiniteNumber,
2324
parseStrictNonNegativeInteger,
2425
} from "../shared/number-coercion.js";
@@ -417,11 +418,13 @@ export function resolveModelRequestTimeoutMs(
417418
timeoutMs: number | undefined,
418419
): number | undefined {
419420
if (timeoutMs !== undefined) {
420-
return timeoutMs;
421+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
422+
? clampTimerTimeoutMs(timeoutMs)
423+
: undefined;
421424
}
422425
const modelTimeoutMs = (model as { requestTimeoutMs?: unknown }).requestTimeoutMs;
423426
return typeof modelTimeoutMs === "number" && Number.isFinite(modelTimeoutMs) && modelTimeoutMs > 0
424-
? Math.floor(modelTimeoutMs)
427+
? clampTimerTimeoutMs(modelTimeoutMs)
425428
: undefined;
426429
}
427430

0 commit comments

Comments
 (0)