Files
openclaw/src/utils/provider-utils.test.ts
T
Yzx b1bdc29d33 fix(providers): use native reasoning mode for Gemini instead of tagged (#89379)
* 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>
2026-06-02 07:46:08 -04:00

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);
});
});