























@@ -4,7 +4,7 @@
44 * provider transport hooks.
55 */
66import 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";
88import { attachModelProviderRequestTransport } from "./provider-request-config.js";
991010const { buildGuardedModelFetchMock, guardedFetchMock } = vi.hoisted(() => ({
@@ -196,6 +196,10 @@ describe("anthropic transport stream", () => {
196196guardedFetchMock.mockResolvedValue(createSseResponse());
197197});
198198199+afterEach(() => {
200+vi.useRealTimers();
201+});
202+199203it("uses the guarded fetch transport for api-key Anthropic requests", async () => {
200204const model = makeAnthropicTransportModel({
201205headers: { "X-Provider": "anthropic" },
@@ -266,6 +270,81 @@ describe("anthropic transport stream", () => {
266270expect(headers.get("X-Provider")).toBe("foundry");
267271});
268272273+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+269348it("honors ANTHROPIC_BASE_URL when model base URL is blank", async () => {
270349vi.stubEnv("ANTHROPIC_BASE_URL", " https://anthropic-proxy.example/v1 ");
271350此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。