diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md index a940ff97074..98195bc89e9 100644 --- a/docs/plugins/sdk-provider-plugins.md +++ b/docs/plugins/sdk-provider-plugins.md @@ -515,7 +515,7 @@ API key auth, and dynamic model resolution. - `openclaw/plugin-sdk/provider-model-shared` - `ProviderReplayFamily`, `buildProviderReplayFamilyHooks(...)`, and the raw replay builders (`buildOpenAICompatibleReplayPolicy`, `buildAnthropicReplayPolicyForModel`, `buildGoogleGeminiReplayPolicy`, `buildHybridAnthropicOrOpenAIReplayPolicy`). Also exports Gemini replay helpers (`sanitizeGoogleGeminiReplayHistory`, `resolveTaggedReasoningOutputMode`) and endpoint/model helpers (`resolveProviderEndpoint`, `normalizeProviderId`, `normalizeGooglePreviewModelId`). - `openclaw/plugin-sdk/provider-stream` - `ProviderStreamFamily`, `buildProviderStreamFamilyHooks(...)`, `composeProviderStreamWrappers(...)`, plus the shared OpenAI/Codex wrappers (`createOpenAIAttributionHeadersWrapper`, `createOpenAIFastModeWrapper`, `createOpenAIServiceTierWrapper`, `createOpenAIResponsesContextManagementWrapper`, `createCodexNativeWebSearchWrapper`), DeepSeek V4 OpenAI-compatible wrapper (`createDeepSeekV4OpenAICompatibleThinkingWrapper`), Anthropic Messages thinking prefill cleanup (`createAnthropicThinkingPrefillPayloadWrapper`), plain-text tool-call compat (`createPlainTextToolCallCompatWrapper`), and shared proxy/provider wrappers (`createOpenRouterWrapper`, `createToolStreamWrapper`, `createMinimaxFastModeWrapper`). - - `openclaw/plugin-sdk/provider-stream-shared` - lightweight payload and event wrappers for hot provider paths, including `createOpenAICompatibleCompletionsThinkingOffWrapper`, `createPayloadPatchStreamWrapper`, and `createPlainTextToolCallCompatWrapper`. + - `openclaw/plugin-sdk/provider-stream-shared` - lightweight payload and event wrappers for hot provider paths, including `createOpenAICompatibleCompletionsThinkingOffWrapper`, `createPayloadPatchStreamWrapper`, `createPlainTextToolCallCompatWrapper`, and `setQwenChatTemplateThinking(...)`. - `openclaw/plugin-sdk/provider-tools` - `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks("deepseek" | "gemini" | "openai")`, and underlying provider schema helpers. For Gemini-family providers, keep the reasoning-output mode aligned with diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index f242486f370..17a160fed74 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -164,7 +164,7 @@ and pairing-path families. | `plugin-sdk/provider-tools` | `ProviderToolCompatFamily`, `buildProviderToolCompatFamilyHooks`, and DeepSeek/Gemini/OpenAI schema cleanup + diagnostics | | `plugin-sdk/provider-usage` | Provider usage snapshot types, shared usage fetch helpers, and provider fetchers such as `fetchClaudeUsage` | | `plugin-sdk/provider-stream` | `ProviderStreamFamily`, `buildProviderStreamFamilyHooks`, `composeProviderStreamWrappers`, stream wrapper types, plain-text tool-call compat, and shared Anthropic/Bedrock/DeepSeek V4/Google/Kilocode/Moonshot/OpenAI/OpenRouter/Z.A.I/MiniMax/Copilot wrapper helpers | - | `plugin-sdk/provider-stream-shared` | Public shared provider stream wrapper helpers including `composeProviderStreamWrappers`, `createOpenAICompatibleCompletionsThinkingOffWrapper`, `createPlainTextToolCallCompatWrapper`, `createPayloadPatchStreamWrapper`, `createToolStreamWrapper`, and Anthropic/DeepSeek/OpenAI-compatible stream utilities | + | `plugin-sdk/provider-stream-shared` | Public shared provider stream wrapper helpers including `composeProviderStreamWrappers`, `createOpenAICompatibleCompletionsThinkingOffWrapper`, `createPlainTextToolCallCompatWrapper`, `createPayloadPatchStreamWrapper`, `createToolStreamWrapper`, `setQwenChatTemplateThinking`, and Anthropic/DeepSeek/OpenAI-compatible stream utilities | | `plugin-sdk/provider-transport-runtime` | Native provider transport helpers such as guarded fetch, transport message transforms, and writable transport event streams | | `plugin-sdk/provider-onboard` | Onboarding config patch helpers | | `plugin-sdk/global-singleton` | Process-local singleton/map/cache helpers | diff --git a/extensions/qwen/stream.ts b/extensions/qwen/stream.ts index 82ad750f522..2154b525e34 100644 --- a/extensions/qwen/stream.ts +++ b/extensions/qwen/stream.ts @@ -5,6 +5,7 @@ import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared"; import { createPayloadPatchStreamWrapper, isOpenAICompatibleThinkingEnabled, + setQwenChatTemplateThinking, } from "openclaw/plugin-sdk/provider-stream-shared"; type QwenThinkingLevel = ProviderWrapStreamFnContext["thinkingLevel"]; @@ -68,25 +69,6 @@ function patchQwenOAuthPayload(payload: Record): void { payload.vl_high_resolution_images = true; } -function setQwenChatTemplateThinking(payload: Record, enabled: boolean): void { - const existing = payload.chat_template_kwargs; - if (existing && typeof existing === "object" && !Array.isArray(existing)) { - const next: Record = { - ...(existing as Record), - enable_thinking: enabled, - }; - if (!Object.hasOwn(next, "preserve_thinking")) { - next.preserve_thinking = true; - } - payload.chat_template_kwargs = next; - return; - } - payload.chat_template_kwargs = { - enable_thinking: enabled, - preserve_thinking: true, - }; -} - function readQwenThinkingFormatFromModel(model: Parameters[0]): QwenThinkingFormat { if (model.api !== "openai-completions") { return undefined; diff --git a/extensions/vllm/stream.ts b/extensions/vllm/stream.ts index 32da35bda72..9f30e113eff 100644 --- a/extensions/vllm/stream.ts +++ b/extensions/vllm/stream.ts @@ -5,6 +5,7 @@ import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared"; import { createPayloadPatchStreamWrapper, isOpenAICompatibleThinkingEnabled, + setQwenChatTemplateThinking, } from "openclaw/plugin-sdk/provider-stream-shared"; import { resolveVllmQwenThinkingFormatFromCompat, @@ -23,25 +24,6 @@ function resolveVllmQwenThinkingFormat( return resolveVllmQwenThinkingFormatFromCompat(ctx.model?.compat); } -function setQwenChatTemplateThinking(payload: Record, enabled: boolean): void { - const existing = payload.chat_template_kwargs; - if (existing && typeof existing === "object" && !Array.isArray(existing)) { - const next: Record = { - ...(existing as Record), - enable_thinking: enabled, - }; - if (!Object.hasOwn(next, "preserve_thinking")) { - next.preserve_thinking = true; - } - payload.chat_template_kwargs = next; - return; - } - payload.chat_template_kwargs = { - enable_thinking: enabled, - preserve_thinking: true, - }; -} - function isVllmNemotronModel(model: { api?: unknown; provider?: unknown; id?: unknown }): boolean { return ( model.api === "openai-completions" && diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs index 5fb3a549b34..fbb51bd6df2 100644 --- a/scripts/plugin-sdk-surface-report.mjs +++ b/scripts/plugin-sdk-surface-report.mjs @@ -146,7 +146,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({ "provider-auth-login": 3, "provider-model-shared": 29, "provider-stream-family": 40, - "provider-stream-shared": 28, + "provider-stream-shared": 29, "provider-stream": 40, "provider-web-search": 1, "provider-zai-endpoint": 3, @@ -163,8 +163,8 @@ let publicDeprecatedExportsByEntrypointBudget; try { budgets = { publicEntrypoints: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_ENTRYPOINTS", 321), - publicExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", 10327), - publicFunctionExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", 5184), + publicExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", 10328), + publicFunctionExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", 5185), publicDeprecatedExports: readBudgetEnv( "OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS", 3247, diff --git a/src/plugin-sdk/provider-stream-shared.test.ts b/src/plugin-sdk/provider-stream-shared.test.ts index 8850b393ebc..a76a696a491 100644 --- a/src/plugin-sdk/provider-stream-shared.test.ts +++ b/src/plugin-sdk/provider-stream-shared.test.ts @@ -13,6 +13,7 @@ import { createPlainTextToolCallCompatWrapper, defaultToolStreamExtraParams, isOpenAICompatibleThinkingEnabled, + setQwenChatTemplateThinking, stripTrailingAnthropicAssistantPrefillWhenThinking, } from "./provider-stream-shared.js"; @@ -160,6 +161,38 @@ describe("isOpenAICompatibleThinkingEnabled", () => { }); }); +describe("setQwenChatTemplateThinking", () => { + it("preserves existing chat-template kwargs and enables thinking", () => { + const payload = { + chat_template_kwargs: { + custom_flag: "keep", + preserve_thinking: false, + }, + }; + + setQwenChatTemplateThinking(payload, true); + + expect(payload.chat_template_kwargs).toEqual({ + custom_flag: "keep", + preserve_thinking: false, + enable_thinking: true, + }); + }); + + it("creates the required chat-template kwargs when absent", () => { + const payload: Record = {}; + + setQwenChatTemplateThinking(payload, false); + + expect(payload).toEqual({ + chat_template_kwargs: { + enable_thinking: false, + preserve_thinking: true, + }, + }); + }); +}); + describe("createDeepSeekV4OpenAICompatibleThinkingWrapper", () => { it("backfills reasoning_content on every replayed assistant message when thinking is enabled", () => { const payload = { diff --git a/src/plugin-sdk/provider-stream-shared.ts b/src/plugin-sdk/provider-stream-shared.ts index 3da10b500ff..d6ee90ab16e 100644 --- a/src/plugin-sdk/provider-stream-shared.ts +++ b/src/plugin-sdk/provider-stream-shared.ts @@ -411,6 +411,29 @@ export function isOpenAICompatibleThinkingEnabled(params: { return normalized !== "off" && normalized !== "none"; } +/** Applies Qwen chat-template thinking flags without discarding provider-specific kwargs. */ +export function setQwenChatTemplateThinking( + payload: Record, + enabled: boolean, +): void { + const existing = payload.chat_template_kwargs; + if (existing && typeof existing === "object" && !Array.isArray(existing)) { + const next: Record = { + ...(existing as Record), + enable_thinking: enabled, + }; + if (!Object.hasOwn(next, "preserve_thinking")) { + next.preserve_thinking = true; + } + payload.chat_template_kwargs = next; + return; + } + payload.chat_template_kwargs = { + enable_thinking: enabled, + preserve_thinking: true, + }; +} + /** @deprecated DeepSeek provider stream helper; do not use from third-party plugins. */ export type DeepSeekV4ThinkingLevel = ProviderWrapStreamFnContext["thinkingLevel"]; /** @deprecated DeepSeek provider stream helper; do not use from third-party plugins. */