// Qwen plugin module implements stream behavior. import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry"; import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared"; import { createPayloadPatchStreamWrapper, isOpenAICompatibleThinkingEnabled, setQwenChatTemplateThinking, } from "openclaw/plugin-sdk/provider-stream-shared"; type QwenThinkingLevel = ProviderWrapStreamFnContext["thinkingLevel"]; type QwenThinkingFormat = string | undefined; function isQwenProviderId(providerId: string): boolean { const normalized = normalizeProviderId(providerId); return ( normalized === "qwen" || normalized === "qwen-oauth" || normalized === "qwen-portal" || normalized === "qwen-cli" || normalized === "modelstudio" || normalized === "qwencloud" || normalized === "dashscope" ); } function isQwenOAuthProviderId(providerId: string): boolean { const normalized = normalizeProviderId(providerId); return normalized === "qwen-oauth" || normalized === "qwen-portal" || normalized === "qwen-cli"; } function normalizeQwenOAuthContent(content: unknown): unknown { if (typeof content === "string") { return [{ type: "text", text: content }]; } if (!Array.isArray(content)) { return content; } const normalized = content .map((part) => { if (typeof part === "string") { return { type: "text", text: part }; } return part && typeof part === "object" ? part : undefined; }) .filter((part): part is Record => Boolean(part)); return normalized.length > 0 ? normalized : content; } function patchQwenOAuthPayload(payload: Record): void { const messages = payload.messages; if (!Array.isArray(messages)) { return; } for (const message of messages) { if (!message || typeof message !== "object") { continue; } const record = message as Record; record.content = normalizeQwenOAuthContent(record.content); if (record.role !== "system" || !Array.isArray(record.content) || record.content.length === 0) { continue; } const last = record.content[record.content.length - 1]; if (last && typeof last === "object") { (last as Record).cache_control = { type: "ephemeral" }; } } payload.vl_high_resolution_images = true; } function readQwenThinkingFormatFromModel(model: Parameters[0]): QwenThinkingFormat { if (model.api !== "openai-completions") { return undefined; } const compat = model.compat && typeof model.compat === "object" ? (model.compat as { thinkingFormat?: unknown }) : undefined; return typeof compat?.thinkingFormat === "string" ? compat.thinkingFormat : undefined; } export function createQwenThinkingWrapper( baseStreamFn: StreamFn | undefined, thinkingLevel: QwenThinkingLevel, thinkingFormat?: QwenThinkingFormat, ): StreamFn { return createPayloadPatchStreamWrapper( baseStreamFn, ({ payload: payloadObj, model, options }) => { const enableThinking = isOpenAICompatibleThinkingEnabled({ thinkingLevel, options }); const effectiveThinkingFormat = thinkingFormat ?? readQwenThinkingFormatFromModel(model); if (effectiveThinkingFormat === "qwen-chat-template") { setQwenChatTemplateThinking(payloadObj, enableThinking); delete payloadObj.enable_thinking; } else { payloadObj.enable_thinking = enableThinking; } delete payloadObj.reasoning_effort; delete payloadObj.reasoningEffort; delete payloadObj.reasoning; }, { shouldPatch: ({ model }) => model.api === "openai-completions" && model.reasoning, }, ); } export function wrapQwenProviderStream(ctx: ProviderWrapStreamFnContext): StreamFn | undefined { if (!isQwenProviderId(ctx.provider) || (ctx.model && ctx.model.api !== "openai-completions")) { return undefined; } const streamFn = createQwenThinkingWrapper( ctx.streamFn, ctx.thinkingLevel, ctx.model ? readQwenThinkingFormatFromModel(ctx.model) : undefined, ); if (!isQwenOAuthProviderId(ctx.provider)) { return streamFn; } return createPayloadPatchStreamWrapper(streamFn, ({ payload, model }) => { if (model.api === "openai-completions") { patchQwenOAuthPayload(payload); } }); }