






















@@ -0,0 +1,155 @@
1+import type { StreamFn } from "@mariozechner/pi-agent-core";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import {
4+__testing,
5+createCloudflareAiGatewayAnthropicThinkingPrefillWrapper,
6+wrapCloudflareAiGatewayProviderStream,
7+} from "./stream-wrappers.js";
8+9+const { warnMock } = vi.hoisted(() => ({
10+warnMock: vi.fn(),
11+}));
12+13+vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
14+createSubsystemLogger: () => ({
15+debug: vi.fn(),
16+error: vi.fn(),
17+info: vi.fn(),
18+warn: warnMock,
19+}),
20+}));
21+22+function createPayloadBaseStream(payload: Record<string, unknown>): StreamFn {
23+return ((model, _context, options) => {
24+options?.onPayload?.(payload as never, model as never);
25+return {} as ReturnType<StreamFn>;
26+}) as StreamFn;
27+}
28+29+function runWrapper(payload: Record<string, unknown>): Record<string, unknown> {
30+const wrapper = createCloudflareAiGatewayAnthropicThinkingPrefillWrapper(
31+createPayloadBaseStream(payload),
32+);
33+void wrapper(
34+{ provider: "cloudflare-ai-gateway", api: "anthropic-messages" } as never,
35+{} as never,
36+{},
37+);
38+return payload;
39+}
40+41+describe("createCloudflareAiGatewayAnthropicThinkingPrefillWrapper", () => {
42+beforeEach(() => {
43+warnMock.mockClear();
44+});
45+46+it("removes trailing assistant prefill when thinking is enabled", () => {
47+const payload = runWrapper({
48+thinking: { type: "enabled", budget_tokens: 1024 },
49+messages: [
50+{ role: "user", content: "Return JSON." },
51+{ role: "assistant", content: "{" },
52+],
53+});
54+55+expect(payload.messages).toEqual([{ role: "user", content: "Return JSON." }]);
56+expect(warnMock).toHaveBeenCalledWith(
57+"removed 1 trailing assistant prefill message because Anthropic extended thinking requires conversations to end with a user turn",
58+);
59+});
60+61+it("removes multiple trailing assistant prefill messages until the conversation ends with user", () => {
62+const payload = runWrapper({
63+thinking: { type: "adaptive" },
64+messages: [
65+{ role: "user", content: "Return JSON." },
66+{ role: "assistant", content: "{" },
67+{ role: "assistant", content: '"status"' },
68+],
69+});
70+71+expect(payload.messages).toEqual([{ role: "user", content: "Return JSON." }]);
72+expect(warnMock).toHaveBeenCalledWith(
73+"removed 2 trailing assistant prefill messages because Anthropic extended thinking requires conversations to end with a user turn",
74+);
75+});
76+77+it("keeps assistant prefill when thinking is disabled", () => {
78+const payload = runWrapper({
79+thinking: { type: "disabled" },
80+messages: [
81+{ role: "user", content: "Return JSON." },
82+{ role: "assistant", content: "{" },
83+],
84+});
85+86+expect(payload.messages).toHaveLength(2);
87+expect(warnMock).not.toHaveBeenCalled();
88+});
89+90+it("keeps trailing assistant tool use turns when thinking is enabled", () => {
91+const payload = runWrapper({
92+thinking: { type: "enabled", budget_tokens: 1024 },
93+messages: [
94+{ role: "user", content: "Read a file." },
95+{
96+role: "assistant",
97+content: [{ type: "tool_use", id: "toolu_1", name: "Read" }],
98+},
99+],
100+});
101+102+expect(payload.messages).toHaveLength(2);
103+expect(warnMock).not.toHaveBeenCalled();
104+});
105+});
106+107+describe("wrapCloudflareAiGatewayProviderStream", () => {
108+beforeEach(() => {
109+warnMock.mockClear();
110+});
111+112+it("patches Anthropic Messages models", () => {
113+const payload = {
114+thinking: { type: "enabled" },
115+messages: [
116+{ role: "user", content: "Return JSON." },
117+{ role: "assistant", content: "{" },
118+],
119+};
120+const wrapped = wrapCloudflareAiGatewayProviderStream({
121+model: { api: "anthropic-messages" },
122+streamFn: createPayloadBaseStream(payload),
123+} as never);
124+125+void wrapped?.(
126+{ provider: "cloudflare-ai-gateway", api: "anthropic-messages" } as never,
127+{} as never,
128+{},
129+);
130+131+expect(payload.messages).toEqual([{ role: "user", content: "Return JSON." }]);
132+});
133+134+it("leaves non-Anthropic model APIs on the original stream path", () => {
135+let onPayloadWasInstalled = false;
136+const baseStreamFn: StreamFn = (_model, _context, options) => {
137+onPayloadWasInstalled = typeof options?.onPayload === "function";
138+return {} as ReturnType<StreamFn>;
139+};
140+141+const wrapped = wrapCloudflareAiGatewayProviderStream({
142+model: { api: "openai-completions" },
143+streamFn: baseStreamFn,
144+} as never);
145+void wrapped?.({ api: "openai-completions" } as never, {} as never, {});
146+147+expect(wrapped).toBe(baseStreamFn);
148+expect(onPayloadWasInstalled).toBe(false);
149+expect(warnMock).not.toHaveBeenCalled();
150+});
151+152+it("treats missing model API as the plugin's default Anthropic Messages route", () => {
153+expect(__testing.shouldPatchAnthropicMessagesPayload({} as never)).toBe(true);
154+});
155+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。