mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
refactor(providers): share Claude thinking profiles
This commit is contained in:
@@ -123,6 +123,7 @@ Malformed local-model reasoning tags are handled conservatively. Closed `<think>
|
||||
## Provider profiles
|
||||
|
||||
- Provider plugins can expose `resolveThinkingProfile(ctx)` to define the model's supported levels and default.
|
||||
- Provider plugins that proxy Claude models should reuse `resolveClaudeThinkingProfile(modelId)` from `openclaw/plugin-sdk/provider-model-shared` so direct Anthropic and proxy catalogs stay aligned.
|
||||
- Each profile level has a stored canonical `id` (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `adaptive`, or `max`) and may include a display `label`. Binary providers use `{ id: "low", label: "on" }`.
|
||||
- Tool plugins that need to validate an explicit thinking override should use `api.runtime.agent.resolveThinkingPolicy({ provider, model })` plus `api.runtime.agent.normalizeThinkingLevel(...)`; they should not keep their own provider/model level lists.
|
||||
- Published legacy hooks (`supportsXHighThinking`, `isBinaryThinking`, and `resolveDefaultThinkingLevel`) remain as compatibility adapters, but new custom level sets should use `resolveThinkingProfile`.
|
||||
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
} from "openclaw/plugin-sdk/provider-auth";
|
||||
import {
|
||||
cloneFirstTemplateModel,
|
||||
isClaudeOpus47ModelId,
|
||||
type ProviderPlugin,
|
||||
resolveClaudeThinkingProfile,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
@@ -273,26 +275,8 @@ function resolveAnthropicForwardCompatModel(
|
||||
);
|
||||
}
|
||||
|
||||
function shouldUseAnthropicAdaptiveThinkingDefault(modelId: string): boolean {
|
||||
const lowerModelId = normalizeLowercaseStringOrEmpty(modelId);
|
||||
return (
|
||||
lowerModelId.startsWith(ANTHROPIC_OPUS_46_MODEL_ID) ||
|
||||
lowerModelId.startsWith(ANTHROPIC_OPUS_46_DOT_MODEL_ID) ||
|
||||
lowerModelId.startsWith(ANTHROPIC_SONNET_46_MODEL_ID) ||
|
||||
lowerModelId.startsWith(ANTHROPIC_SONNET_46_DOT_MODEL_ID)
|
||||
);
|
||||
}
|
||||
|
||||
function isAnthropicOpus47Model(modelId: string): boolean {
|
||||
const lowerModelId = normalizeLowercaseStringOrEmpty(modelId);
|
||||
return (
|
||||
lowerModelId.startsWith(ANTHROPIC_OPUS_47_MODEL_ID) ||
|
||||
lowerModelId.startsWith(ANTHROPIC_OPUS_47_DOT_MODEL_ID)
|
||||
);
|
||||
}
|
||||
|
||||
function supportsAnthropicAdaptiveThinking(modelId: string): boolean {
|
||||
return shouldUseAnthropicAdaptiveThinkingDefault(modelId) || isAnthropicOpus47Model(modelId);
|
||||
return isClaudeOpus47ModelId(modelId);
|
||||
}
|
||||
|
||||
function hasConfiguredModelContextOverride(
|
||||
@@ -592,25 +576,7 @@ export function buildAnthropicProvider(): ProviderPlugin {
|
||||
buildReplayPolicy: buildAnthropicReplayPolicy,
|
||||
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
|
||||
resolveReasoningOutputMode: () => "native",
|
||||
resolveThinkingProfile: ({ modelId }) => {
|
||||
const levels: Array<{
|
||||
id: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | "max";
|
||||
}> = [{ id: "off" }, { id: "minimal" }, { id: "low" }, { id: "medium" }, { id: "high" }];
|
||||
if (isAnthropicOpus47Model(modelId)) {
|
||||
levels.push({ id: "xhigh" }, { id: "adaptive" }, { id: "max" });
|
||||
} else if (supportsAnthropicAdaptiveThinking(modelId)) {
|
||||
levels.push({ id: "adaptive" });
|
||||
}
|
||||
return {
|
||||
levels,
|
||||
defaultLevel: isAnthropicOpus47Model(modelId)
|
||||
? "off"
|
||||
: matchesAnthropicModernModel(modelId) &&
|
||||
shouldUseAnthropicAdaptiveThinkingDefault(modelId)
|
||||
? "adaptive"
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
resolveThinkingProfile: ({ modelId }) => resolveClaudeThinkingProfile(modelId),
|
||||
wrapStreamFn: wrapAnthropicProviderStream,
|
||||
resolveUsageAuth: async (ctx) => await ctx.resolveOAuthToken(),
|
||||
fetchUsageSnapshot: async (ctx) =>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { definePluginEntry, type ProviderThinkingProfile } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import {
|
||||
matchesExactOrPrefix,
|
||||
PASSTHROUGH_GEMINI_REPLAY_HOOKS,
|
||||
resolveClaudeThinkingProfile,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { applyOpencodeZenConfig, OPENCODE_ZEN_DEFAULT_MODEL } from "./api.js";
|
||||
@@ -17,20 +18,6 @@ const OPENCODE_SHARED_WIZARD_GROUP = {
|
||||
groupLabel: "OpenCode",
|
||||
groupHint: OPENCODE_SHARED_HINT,
|
||||
} as const;
|
||||
const ANTHROPIC_OPUS_47_MODEL_PREFIXES = ["claude-opus-4-7", "claude-opus-4.7"] as const;
|
||||
const ANTHROPIC_ADAPTIVE_MODEL_PREFIXES = [
|
||||
"claude-opus-4-6",
|
||||
"claude-opus-4.6",
|
||||
"claude-sonnet-4-6",
|
||||
"claude-sonnet-4.6",
|
||||
] as const;
|
||||
const BASE_ANTHROPIC_THINKING_LEVELS = [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
] as const satisfies ProviderThinkingProfile["levels"];
|
||||
|
||||
function isModernOpencodeModel(modelId: string): boolean {
|
||||
const lower = normalizeLowercaseStringOrEmpty(modelId);
|
||||
@@ -40,32 +27,6 @@ function isModernOpencodeModel(modelId: string): boolean {
|
||||
return !matchesExactOrPrefix(lower, MINIMAX_MODERN_MODEL_MATCHERS);
|
||||
}
|
||||
|
||||
function matchesAnyPrefix(modelId: string, prefixes: readonly string[]): boolean {
|
||||
const lower = normalizeLowercaseStringOrEmpty(modelId);
|
||||
return prefixes.some((prefix) => lower.startsWith(prefix));
|
||||
}
|
||||
|
||||
function resolveOpencodeThinkingProfile(modelId: string): ProviderThinkingProfile {
|
||||
if (matchesAnyPrefix(modelId, ANTHROPIC_OPUS_47_MODEL_PREFIXES)) {
|
||||
return {
|
||||
levels: [
|
||||
...BASE_ANTHROPIC_THINKING_LEVELS,
|
||||
{ id: "xhigh" },
|
||||
{ id: "adaptive" },
|
||||
{ id: "max" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
};
|
||||
}
|
||||
if (matchesAnyPrefix(modelId, ANTHROPIC_ADAPTIVE_MODEL_PREFIXES)) {
|
||||
return {
|
||||
levels: [...BASE_ANTHROPIC_THINKING_LEVELS, { id: "adaptive" }],
|
||||
defaultLevel: "adaptive",
|
||||
};
|
||||
}
|
||||
return { levels: BASE_ANTHROPIC_THINKING_LEVELS };
|
||||
}
|
||||
|
||||
export default definePluginEntry({
|
||||
id: PROVIDER_ID,
|
||||
name: "OpenCode Zen Provider",
|
||||
@@ -106,7 +67,7 @@ export default definePluginEntry({
|
||||
],
|
||||
...PASSTHROUGH_GEMINI_REPLAY_HOOKS,
|
||||
isModernModelRef: ({ modelId }) => isModernOpencodeModel(modelId),
|
||||
resolveThinkingProfile: ({ modelId }) => resolveOpencodeThinkingProfile(modelId),
|
||||
resolveThinkingProfile: ({ modelId }) => resolveClaudeThinkingProfile(modelId),
|
||||
});
|
||||
api.registerMediaUnderstandingProvider(opencodeMediaUnderstandingProvider);
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
NATIVE_ANTHROPIC_REPLAY_HOOKS,
|
||||
OPENAI_COMPATIBLE_REPLAY_HOOKS,
|
||||
PASSTHROUGH_GEMINI_REPLAY_HOOKS,
|
||||
resolveClaudeThinkingProfile,
|
||||
} from "./provider-model-shared.js";
|
||||
|
||||
describe("buildProviderReplayFamilyHooks", () => {
|
||||
@@ -249,3 +250,26 @@ describe("buildProviderReplayFamilyHooks", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveClaudeThinkingProfile", () => {
|
||||
it("exposes Opus 4.7 thinking levels for direct and proxied Claude providers", () => {
|
||||
expect(resolveClaudeThinkingProfile("claude-opus-4-7")).toMatchObject({
|
||||
levels: expect.arrayContaining([{ id: "xhigh" }, { id: "adaptive" }, { id: "max" }]),
|
||||
defaultLevel: "off",
|
||||
});
|
||||
expect(resolveClaudeThinkingProfile("claude-opus-4.7-20260219")).toMatchObject({
|
||||
levels: expect.arrayContaining([{ id: "xhigh" }, { id: "adaptive" }, { id: "max" }]),
|
||||
defaultLevel: "off",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps adaptive-only Claude variants from advertising xhigh or max", () => {
|
||||
const profile = resolveClaudeThinkingProfile("claude-sonnet-4-6");
|
||||
|
||||
expect(profile).toMatchObject({
|
||||
levels: expect.arrayContaining([{ id: "adaptive" }]),
|
||||
defaultLevel: "adaptive",
|
||||
});
|
||||
expect(profile.levels.some((level) => level.id === "xhigh" || level.id === "max")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
ProviderReasoningOutputModeContext,
|
||||
ProviderReplayPolicyContext,
|
||||
ProviderSanitizeReplayHistoryContext,
|
||||
ProviderThinkingProfile,
|
||||
} from "./plugin-entry.js";
|
||||
import {
|
||||
normalizeAntigravityPreviewModelId,
|
||||
@@ -82,6 +83,21 @@ export {
|
||||
} from "../plugins/provider-model-helpers.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
|
||||
const CLAUDE_OPUS_47_MODEL_PREFIXES = ["claude-opus-4-7", "claude-opus-4.7"] as const;
|
||||
const CLAUDE_ADAPTIVE_THINKING_DEFAULT_MODEL_PREFIXES = [
|
||||
"claude-opus-4-6",
|
||||
"claude-opus-4.6",
|
||||
"claude-sonnet-4-6",
|
||||
"claude-sonnet-4.6",
|
||||
] as const;
|
||||
const BASE_CLAUDE_THINKING_LEVELS = [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
] as const satisfies ProviderThinkingProfile["levels"];
|
||||
|
||||
export function getModelProviderHint(modelId: string): string | null {
|
||||
const trimmed = normalizeOptionalLowercaseString(modelId);
|
||||
if (!trimmed) {
|
||||
@@ -98,6 +114,35 @@ export function isProxyReasoningUnsupportedModelHint(modelId: string): boolean {
|
||||
return getModelProviderHint(modelId) === "x-ai";
|
||||
}
|
||||
|
||||
function matchesClaudeModelPrefix(modelId: string, prefixes: readonly string[]): boolean {
|
||||
const lower = normalizeOptionalLowercaseString(modelId);
|
||||
return Boolean(lower && prefixes.some((prefix) => lower.startsWith(prefix)));
|
||||
}
|
||||
|
||||
export function isClaudeOpus47ModelId(modelId: string): boolean {
|
||||
return matchesClaudeModelPrefix(modelId, CLAUDE_OPUS_47_MODEL_PREFIXES);
|
||||
}
|
||||
|
||||
export function isClaudeAdaptiveThinkingDefaultModelId(modelId: string): boolean {
|
||||
return matchesClaudeModelPrefix(modelId, CLAUDE_ADAPTIVE_THINKING_DEFAULT_MODEL_PREFIXES);
|
||||
}
|
||||
|
||||
export function resolveClaudeThinkingProfile(modelId: string): ProviderThinkingProfile {
|
||||
if (isClaudeOpus47ModelId(modelId)) {
|
||||
return {
|
||||
levels: [...BASE_CLAUDE_THINKING_LEVELS, { id: "xhigh" }, { id: "adaptive" }, { id: "max" }],
|
||||
defaultLevel: "off",
|
||||
};
|
||||
}
|
||||
if (isClaudeAdaptiveThinkingDefaultModelId(modelId)) {
|
||||
return {
|
||||
levels: [...BASE_CLAUDE_THINKING_LEVELS, { id: "adaptive" }],
|
||||
defaultLevel: "adaptive",
|
||||
};
|
||||
}
|
||||
return { levels: BASE_CLAUDE_THINKING_LEVELS };
|
||||
}
|
||||
|
||||
export {
|
||||
normalizeAntigravityPreviewModelId,
|
||||
normalizeGooglePreviewModelId,
|
||||
|
||||
Reference in New Issue
Block a user