perf: fast-path built-in reasoning provider checks

This commit is contained in:
Shakker
2026-04-02 11:51:42 +01:00
committed by Peter Steinberger
parent 9c4ea016d9
commit 590655472b
3 changed files with 104 additions and 53 deletions

View File

@@ -0,0 +1,77 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { resolveProviderReasoningOutputModeWithPluginMock } = vi.hoisted(() => ({
resolveProviderReasoningOutputModeWithPluginMock: vi.fn(),
}));
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderReasoningOutputModeWithPlugin: resolveProviderReasoningOutputModeWithPluginMock,
}));
import { isReasoningTagProvider, resolveReasoningOutputMode } from "./provider-utils.js";
describe("resolveReasoningOutputMode", () => {
beforeEach(() => {
resolveProviderReasoningOutputModeWithPluginMock.mockReset();
resolveProviderReasoningOutputModeWithPluginMock.mockReturnValue(undefined);
});
it.each([
["google", "tagged"],
["Google", "tagged"],
["google-gemini-cli", "tagged"],
["google-generative-ai", "tagged"],
["anthropic", "native"],
["openai", "native"],
["openrouter", "native"],
["ollama", "native"],
["minimax", "native"],
["minimax-cn", "native"],
] as const)("uses the built-in fast path for %s", (provider, expected) => {
expect(resolveReasoningOutputMode({ provider, workspaceDir: process.cwd() })).toBe(expected);
expect(resolveProviderReasoningOutputModeWithPluginMock).not.toHaveBeenCalled();
});
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", true],
["Google", true],
["google-gemini-cli", true],
["google-generative-ai", true],
["anthropic", false],
["openai", false],
["openrouter", false],
["ollama", false],
["minimax", false],
["minimax-cn", false],
[null, false],
[undefined, false],
["", false],
] as const)("returns %s for %s", (value, expected) => {
expect(isReasoningTagProvider(value, { workspaceDir: process.cwd() })).toBe(expected);
});
});

View File

@@ -2,6 +2,18 @@ import type { OpenClawConfig } from "../config/config.js";
import { resolveProviderReasoningOutputModeWithPlugin } from "../plugins/provider-runtime.js";
import type { ProviderRuntimeModel } from "../plugins/types.js";
const BUILTIN_REASONING_OUTPUT_MODES = {
anthropic: "native",
google: "tagged",
"google-gemini-cli": "tagged",
"google-generative-ai": "tagged",
minimax: "native",
"minimax-cn": "native",
ollama: "native",
openai: "native",
openrouter: "native",
} as const;
/**
* Utility functions for provider-specific logic and capabilities.
*/
@@ -20,6 +32,13 @@ export function resolveReasoningOutputMode(params: {
return "native";
}
const normalized = provider.toLowerCase();
const builtInMode =
BUILTIN_REASONING_OUTPUT_MODES[normalized as keyof typeof BUILTIN_REASONING_OUTPUT_MODES];
if (builtInMode) {
return builtInMode;
}
const pluginMode = resolveProviderReasoningOutputModeWithPlugin({
provider,
config: params.config,
@@ -39,6 +58,14 @@ export function resolveReasoningOutputMode(params: {
return pluginMode;
}
// Check for exact matches or known prefixes/substrings for reasoning providers.
// Note: Ollama is intentionally excluded - its OpenAI-compatible endpoint
// handles reasoning natively via the `reasoning` field in streaming chunks,
// so tag-based enforcement is unnecessary and causes all output to be
// discarded as "(no output)" (#2279).
// Note: MiniMax is also intentionally excluded. In production it does not
// reliably wrap user-visible output in <final> tags, so forcing tag
// enforcement suppresses normal assistant replies.
return "native";
}

View File

@@ -69,59 +69,6 @@ describe("parseBooleanValue", () => {
});
});
describe("isReasoningTagProvider", () => {
it.each([
{
name: "returns false for ollama when the provider plugin has no tagged override",
value: "ollama",
expected: false,
},
{
name: "returns false for case-insensitive ollama",
value: "Ollama",
expected: false,
},
{
name: "returns true for google via provider hook",
value: "google",
expected: true,
},
{
name: "returns true for Google (case-insensitive)",
value: "Google",
expected: true,
},
{
name: "returns true for google-gemini-cli via provider hook",
value: "google-gemini-cli",
expected: true,
},
{
name: "returns true for google-generative-ai via provider hook",
value: "google-generative-ai",
expected: true,
},
{ name: "returns true for minimax via provider hook", value: "minimax", expected: true },
{
name: "returns true for minimax-cn via provider hook alias",
value: "minimax-cn",
expected: true,
},
{ name: "returns false for null", value: null, expected: false },
{ name: "returns false for undefined", value: undefined, expected: false },
{ name: "returns false for empty", value: "", expected: false },
{ name: "returns false for anthropic", value: "anthropic", expected: false },
{ name: "returns false for openai", value: "openai", expected: false },
{ name: "returns false for openrouter", value: "openrouter", expected: false },
] satisfies Array<{
name: string;
value: string | null | undefined;
expected: boolean;
}>)("$name", ({ value, expected }) => {
expect(isReasoningTagProvider(value)).toBe(expected);
});
});
describe("splitShellArgs", () => {
it("splits whitespace and respects quotes", () => {
expect(splitShellArgs(`qmd --foo "bar baz"`)).toEqual(["qmd", "--foo", "bar baz"]);