|
4 | 4 | * provider transport hooks. |
5 | 5 | */ |
6 | 6 | import type { Model } from "openclaw/plugin-sdk/llm"; |
7 | | -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; |
| 7 | +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; |
8 | 8 | import { attachModelProviderRequestTransport } from "./provider-request-config.js"; |
9 | 9 |
|
10 | 10 | const { buildGuardedModelFetchMock, guardedFetchMock } = vi.hoisted(() => ({ |
@@ -196,6 +196,10 @@ describe("anthropic transport stream", () => { |
196 | 196 | guardedFetchMock.mockResolvedValue(createSseResponse()); |
197 | 197 | }); |
198 | 198 |
|
| 199 | + afterEach(() => { |
| 200 | + vi.useRealTimers(); |
| 201 | + }); |
| 202 | + |
199 | 203 | it("uses the guarded fetch transport for api-key Anthropic requests", async () => { |
200 | 204 | const model = makeAnthropicTransportModel({ |
201 | 205 | headers: { "X-Provider": "anthropic" }, |
@@ -266,6 +270,81 @@ describe("anthropic transport stream", () => { |
266 | 270 | expect(headers.get("X-Provider")).toBe("foundry"); |
267 | 271 | }); |
268 | 272 |
|
| 273 | + it("bounds streamed Anthropic error responses without content-length", async () => { |
| 274 | + const encoder = new TextEncoder(); |
| 275 | + let pullCount = 0; |
| 276 | + let cancelCount = 0; |
| 277 | + guardedFetchMock.mockResolvedValueOnce( |
| 278 | + new Response( |
| 279 | + new ReadableStream<Uint8Array>({ |
| 280 | + pull(controller) { |
| 281 | + pullCount += 1; |
| 282 | + if (pullCount === 1) { |
| 283 | + controller.enqueue(encoder.encode("x".repeat(8 * 1024))); |
| 284 | + return; |
| 285 | + } |
| 286 | + controller.enqueue(encoder.encode("y")); |
| 287 | + }, |
| 288 | + cancel() { |
| 289 | + cancelCount += 1; |
| 290 | + }, |
| 291 | + }), |
| 292 | + { status: 500 }, |
| 293 | + ), |
| 294 | + ); |
| 295 | + |
| 296 | + const result = await runTransportStream( |
| 297 | + makeAnthropicTransportModel(), |
| 298 | + { |
| 299 | + messages: [{ role: "user", content: "hello" }], |
| 300 | + } as AnthropicStreamContext, |
| 301 | + { apiKey: "sk-ant-api" } as AnthropicStreamOptions, |
| 302 | + ); |
| 303 | + |
| 304 | + expect(result.stopReason).toBe("error"); |
| 305 | + expect(result.errorMessage).toBe(`${"x".repeat(400)}…`); |
| 306 | + expect(pullCount).toBeGreaterThanOrEqual(2); |
| 307 | + expect(cancelCount).toBe(1); |
| 308 | + }); |
| 309 | + |
| 310 | + it("aborts stalled streamed Anthropic error responses", async () => { |
| 311 | + vi.useFakeTimers(); |
| 312 | + const encoder = new TextEncoder(); |
| 313 | + let cancelReason: unknown; |
| 314 | + guardedFetchMock.mockResolvedValueOnce( |
| 315 | + new Response( |
| 316 | + new ReadableStream<Uint8Array>({ |
| 317 | + start(controller) { |
| 318 | + controller.enqueue(encoder.encode("partial failure detail")); |
| 319 | + }, |
| 320 | + cancel(reason) { |
| 321 | + cancelReason = reason; |
| 322 | + }, |
| 323 | + }), |
| 324 | + { status: 500 }, |
| 325 | + ), |
| 326 | + ); |
| 327 | + |
| 328 | + const resultPromise = runTransportStream( |
| 329 | + makeAnthropicTransportModel(), |
| 330 | + { |
| 331 | + messages: [{ role: "user", content: "hello" }], |
| 332 | + } as AnthropicStreamContext, |
| 333 | + { apiKey: "sk-ant-api" } as AnthropicStreamOptions, |
| 334 | + ); |
| 335 | + |
| 336 | + await vi.advanceTimersByTimeAsync(0); |
| 337 | + await vi.advanceTimersByTimeAsync(10_000); |
| 338 | + const result = await resultPromise; |
| 339 | + |
| 340 | + expect(result.stopReason).toBe("error"); |
| 341 | + expect(result.errorMessage).toBe( |
| 342 | + "Anthropic Messages error response stalled: no data received for 10000ms", |
| 343 | + ); |
| 344 | + expect(cancelReason).toBeInstanceOf(Error); |
| 345 | + expect((cancelReason as Error).message).toBe(result.errorMessage); |
| 346 | + }); |
| 347 | + |
269 | 348 | it("honors ANTHROPIC_BASE_URL when model base URL is blank", async () => { |
270 | 349 | vi.stubEnv("ANTHROPIC_BASE_URL", " https://anthropic-proxy.example/v1 "); |
271 | 350 |
|
|
0 commit comments