























@@ -2,11 +2,13 @@
22 * Tests provider stream shared helpers and stream hook capture.
33 */
44import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
5+import type { Model } from "openclaw/plugin-sdk/llm";
56import { describe, expect, it } from "vitest";
67import { createAssistantMessageEventStream } from "../llm/utils/event-stream.js";
78import {
89createDeepSeekV4OpenAICompatibleThinkingWrapper,
910createAnthropicThinkingPrefillPayloadWrapper,
11+createOpenAICompatibleCompletionsThinkingOffWrapper,
1012createPayloadPatchStreamWrapper,
1113createPlainTextToolCallCompatWrapper,
1214defaultToolStreamExtraParams,
@@ -16,6 +18,27 @@ import {
16181719type StreamEvent = { type: string } & Record<string, unknown>;
182021+const lmstudioBinaryModel = {
22+api: "openai-completions",
23+provider: "lmstudio",
24+id: "google/gemma-4-26b-a4b-qat",
25+baseUrl: "http://127.0.0.1:1234/v1",
26+reasoning: true,
27+compat: {
28+supportsReasoningEffort: true,
29+supportedReasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh"],
30+reasoningEffortMap: { off: "none", none: "none", adaptive: "xhigh", max: "xhigh" },
31+},
32+} as unknown as Model<"openai-completions">;
33+34+const lmstudioBareModel = {
35+api: "openai-completions",
36+provider: "lmstudio",
37+id: "qwen3-8b-instruct",
38+baseUrl: "http://127.0.0.1:1234/v1",
39+reasoning: true,
40+} as unknown as Model<"openai-completions">;
41+1942function requireRecord(value: unknown, label: string): Record<string, unknown> {
2043if (!value || typeof value !== "object" || Array.isArray(value)) {
2144throw new Error(`expected ${label} to be a record`);
@@ -35,6 +58,20 @@ function createEventStream(events: unknown[]): ReturnType<StreamFn> {
3558return output as ReturnType<StreamFn>;
3659}
376061+function createPayloadCapture(initialReasoningEffort?: string) {
62+const payloads: Array<Record<string, unknown>> = [];
63+const baseStreamFn: StreamFn = (model, _context, options) => {
64+const payload: Record<string, unknown> = { model: model.id };
65+if (initialReasoningEffort !== undefined) {
66+payload.reasoning_effort = initialReasoningEffort;
67+}
68+options?.onPayload?.(payload, model);
69+payloads.push(structuredClone(payload));
70+return createAssistantMessageEventStream();
71+};
72+return { baseStreamFn, payloads };
73+}
74+3875function createControlledPlainTextToolCallCompatStream() {
3976const source = createAssistantMessageEventStream();
4077const baseStream: StreamFn = () => source as ReturnType<StreamFn>;
@@ -198,6 +235,40 @@ describe("createPayloadPatchStreamWrapper", () => {
198235});
199236});
200237238+describe("createOpenAICompatibleCompletionsThinkingOffWrapper", () => {
239+it("maps reasoning_effort to the model's disabled value when thinking is off", () => {
240+const { baseStreamFn, payloads } = createPayloadCapture("high");
241+const wrapped = createOpenAICompatibleCompletionsThinkingOffWrapper(baseStreamFn, "off");
242+void wrapped(lmstudioBinaryModel, { messages: [] }, {});
243+244+expect(payloads[0]?.reasoning_effort).toBe("none");
245+});
246+247+it("drops reasoning_effort when the model has no disabled effort", () => {
248+const { baseStreamFn, payloads } = createPayloadCapture("high");
249+const wrapped = createOpenAICompatibleCompletionsThinkingOffWrapper(baseStreamFn, "off");
250+void wrapped(lmstudioBareModel, { messages: [] }, {});
251+252+expect(payloads[0]).not.toHaveProperty("reasoning_effort");
253+});
254+255+it("does not add reasoning_effort when none was sent", () => {
256+const { baseStreamFn, payloads } = createPayloadCapture();
257+const wrapped = createOpenAICompatibleCompletionsThinkingOffWrapper(baseStreamFn, "off");
258+void wrapped(lmstudioBinaryModel, { messages: [] }, {});
259+260+expect(payloads[0]).not.toHaveProperty("reasoning_effort");
261+});
262+263+it("leaves enabled thinking levels unchanged", () => {
264+const { baseStreamFn, payloads } = createPayloadCapture("high");
265+const wrapped = createOpenAICompatibleCompletionsThinkingOffWrapper(baseStreamFn, "high");
266+void wrapped(lmstudioBinaryModel, { messages: [] }, {});
267+268+expect(payloads[0]?.reasoning_effort).toBe("high");
269+});
270+});
271+201272describe("createPlainTextToolCallCompatWrapper", () => {
202273it("promotes standalone text tool calls into tool-call stream events", async () => {
203274const baseStreamFn: StreamFn = () =>
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。