mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
b1bdc29d33
* fix(providers): use native reasoning mode for direct Gemini API, keep CLI tagged Gemini 2.5+ delivers reasoning via native thinkingParts (thinkingConfig. includeThoughts). Having tagged mode active at the same time injects a <think>…</think>/<final>…</final> directive into the system prompt; the model opens a <think> block before a tool call, never closes it, and returns an empty post-tool turn (content:[], payloads=0 error, #69220). Fix: override resolveReasoningOutputMode in buildGoogleProvider() only — not in the shared GOOGLE_GEMINI_PROVIDER_HOOKS. The Gemini CLI backend (google-gemini-cli) runs gemini --output-format json and parses a text response field, not native thought parts; it must stay on tagged mode. A regression test confirms google-gemini-cli remains "tagged". Also remove the dead BUILTIN_REASONING_OUTPUT_MODES entry keyed on "google-generative-ai" from provider-utils.ts — that string is only ever the transport model.api value, never the provider id passed to resolveReasoningOutputMode, so the map was unreachable. Fixes #69220 * docs: clarify Gemini reasoning output modes * fix(google): keep Antigravity reasoning tagged * fix(google): default direct reasoning checks to native * fix(google): import reasoning context from plugin entry --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { resolveProviderReasoningOutputModeWithPluginMock } = vi.hoisted(() => ({
|
|
resolveProviderReasoningOutputModeWithPluginMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../plugins/provider-runtime.js", async () => {
|
|
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
|
|
"../plugins/provider-runtime.js",
|
|
);
|
|
return {
|
|
...actual,
|
|
resolveProviderReasoningOutputModeWithPlugin: resolveProviderReasoningOutputModeWithPluginMock,
|
|
};
|
|
});
|
|
|
|
import { isReasoningTagProvider, resolveReasoningOutputMode } from "./provider-utils.js";
|
|
|
|
describe("resolveReasoningOutputMode", () => {
|
|
beforeEach(() => {
|
|
resolveProviderReasoningOutputModeWithPluginMock.mockReset();
|
|
resolveProviderReasoningOutputModeWithPluginMock.mockReturnValue(undefined);
|
|
});
|
|
|
|
it.each([["google-generative-ai", "native"]] as const)(
|
|
"falls back to native for %s when no plugin override is present",
|
|
(provider, expected) => {
|
|
expect(resolveReasoningOutputMode({ provider, workspaceDir: process.cwd() })).toBe(expected);
|
|
expect(resolveProviderReasoningOutputModeWithPluginMock).toHaveBeenCalledTimes(1);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["google", "tagged"],
|
|
["Google", "tagged"],
|
|
["google-gemini-cli", "tagged"],
|
|
["anthropic", "native"],
|
|
["openai", "native"],
|
|
["openrouter", "native"],
|
|
["ollama", "native"],
|
|
["minimax", "native"],
|
|
["minimax-cn", "native"],
|
|
] as const)("prefers provider hooks for %s", (provider, expected) => {
|
|
resolveProviderReasoningOutputModeWithPluginMock.mockReturnValueOnce(expected);
|
|
|
|
expect(resolveReasoningOutputMode({ provider, workspaceDir: process.cwd() })).toBe(expected);
|
|
expect(resolveProviderReasoningOutputModeWithPluginMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("falls back to provider hooks for unknown providers", () => {
|
|
resolveProviderReasoningOutputModeWithPluginMock.mockReturnValue("tagged");
|
|
|
|
expect(
|
|
resolveReasoningOutputMode({
|
|
provider: "custom-provider",
|
|
workspaceDir: process.cwd(),
|
|
modelId: "custom/model",
|
|
}),
|
|
).toBe("tagged");
|
|
expect(resolveProviderReasoningOutputModeWithPluginMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("returns native when hooks do not provide an override", () => {
|
|
expect(resolveReasoningOutputMode({ provider: "custom-provider" })).toBe("native");
|
|
expect(resolveProviderReasoningOutputModeWithPluginMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("isReasoningTagProvider", () => {
|
|
beforeEach(() => {
|
|
resolveProviderReasoningOutputModeWithPluginMock.mockReset();
|
|
resolveProviderReasoningOutputModeWithPluginMock.mockReturnValue(undefined);
|
|
});
|
|
|
|
it.each([
|
|
["google-generative-ai", false],
|
|
[null, false],
|
|
[undefined, false],
|
|
["", false],
|
|
] as const)("returns %s for %s", (value, expected) => {
|
|
expect(isReasoningTagProvider(value, { workspaceDir: process.cwd() })).toBe(expected);
|
|
});
|
|
|
|
it.each([
|
|
["google", true],
|
|
["Google", true],
|
|
["google-gemini-cli", true],
|
|
["anthropic", false],
|
|
["openai", false],
|
|
["openrouter", false],
|
|
["ollama", false],
|
|
["minimax", false],
|
|
["minimax-cn", false],
|
|
] as const)("uses provider hooks when available for %s", (value, expected) => {
|
|
resolveProviderReasoningOutputModeWithPluginMock.mockReturnValueOnce(
|
|
expected ? "tagged" : "native",
|
|
);
|
|
|
|
expect(isReasoningTagProvider(value, { workspaceDir: process.cwd() })).toBe(expected);
|
|
expect(resolveProviderReasoningOutputModeWithPluginMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|