|
1 | 1 | import { Stream } from "openai/streaming"; |
2 | 2 | import type { Model } from "openclaw/plugin-sdk/llm"; |
3 | 3 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 4 | +import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; |
4 | 5 | import { buildGuardedModelFetch } from "./provider-transport-fetch.js"; |
5 | 6 |
|
6 | 7 | type ProviderRequestPolicyConfigMockResult = { |
@@ -227,6 +228,60 @@ describe("buildGuardedModelFetch", () => { |
227 | 228 | } |
228 | 229 | }); |
229 | 230 |
|
| 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 | + |
230 | 285 | it("combines caller abort signals with model request timeouts", async () => { |
231 | 286 | const callerController = new AbortController(); |
232 | 287 | const timeoutController = new AbortController(); |
|
0 commit comments