diff --git a/Memory/src/model/llm.ts b/Memory/src/model/llm.ts index 1492f6f95..822f3af5d 100644 --- a/Memory/src/model/llm.ts +++ b/Memory/src/model/llm.ts @@ -303,6 +303,7 @@ class HttpLlmClient implements LlmClient { const thinkingBudget = thinking.enabled && thinkingUsesEnableThinking(this.config.vendor ?? "", base, model) ? this.config.thinkingBudget : undefined; + const thinkingPayload = openAiCompatibleThinkingPayload(thinking, this.config.extraBody); const agentRegion = resolveMemoryAgentRegion(this.config.sourceProvider); const response = await postJsonWithRetry({ actualModelContext: this.config.actualModelContext, @@ -323,10 +324,10 @@ class HttpLlmClient implements LlmClient { ...(!omitTemperature ? { temperature: options.temperature ?? this.config.temperature } : {}), max_tokens: options.maxTokens ?? this.config.maxTokens, stream: false, - ...thinking.fields, + ...thinkingPayload.fields, ...(thinkingBudget !== undefined ? { thinking_budget: thinkingBudget } : {}), ...(options.jsonMode && !omitJsonMode ? { response_format: { type: "json_object" } } : {}), - ...(this.config.extraBody ?? {}) + ...thinkingPayload.extraBody } }); const choice = response.choices?.[0]; @@ -561,6 +562,39 @@ class HttpLlmClient implements LlmClient { } } +function openAiCompatibleThinkingPayload( + thinking: ThinkingControl, + configuredExtraBody: Record | undefined +): { fields: Record; extraBody: Record } { + const extraBody = configuredExtraBody ?? {}; + const configuredChatTemplateKwargs = extraBody.chat_template_kwargs; + if ( + !("enable_thinking" in thinking.fields) || + !isUnknownRecord(configuredChatTemplateKwargs) + ) { + return { fields: thinking.fields, extraBody }; + } + + const fields = { ...thinking.fields }; + delete fields.enable_thinking; + return { + fields, + extraBody: { + ...extraBody, + chat_template_kwargs: { + ...configuredChatTemplateKwargs, + ...(!Object.hasOwn(configuredChatTemplateKwargs, "enable_thinking") + ? { enable_thinking: thinking.enabled } + : {}) + } + } + }; +} + +function isUnknownRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + function openAiCompatibleThinkingControl(input: { vendor: string; endpoint: string; diff --git a/Memory/tests/llm-thinking.test.ts b/Memory/tests/llm-thinking.test.ts index 0c0110f8d..cf535b220 100644 --- a/Memory/tests/llm-thinking.test.ts +++ b/Memory/tests/llm-thinking.test.ts @@ -76,6 +76,39 @@ describe("memory LLM thinking configuration", () => { expect(requestBody(fetchMock)).not.toHaveProperty("thinking_budget"); }); + it("routes Qwen thinking through chat_template_kwargs when the endpoint opts in", async () => { + const fetchMock = openAiFetch(); + vi.stubGlobal("fetch", fetchMock); + const client = createLlmClient(llmConfig({ + vendor: "qwen", + endpoint: "https://sglang.example/v1", + model: "qwen3.8-27b", + enableThinking: true, + extraBody: { chat_template_kwargs: { tokenizer_option: "preserved" } } + })); + + await client.complete([{ role: "user", content: "filter" }], { + operation: "retrieval.filter", + thinkingMode: "disabled" + }); + expect(requestBody(fetchMock)).toMatchObject({ + chat_template_kwargs: { + enable_thinking: false, + tokenizer_option: "preserved" + } + }); + expect(requestBody(fetchMock)).not.toHaveProperty("enable_thinking"); + + fetchMock.mockClear(); + await client.complete([{ role: "user", content: "evolve" }], { + operation: "evolution.induction", + thinkingMode: "enabled" + }); + expect(requestBody(fetchMock)).toMatchObject({ + chat_template_kwargs: { enable_thinking: true } + }); + }); + it.each([ ["DeepSeek", "deepseek", "https://api.deepseek.com", "deepseek-v4-pro"], ["Zhipu", "zhipu", "https://open.bigmodel.cn/api/paas/v4", "glm-5.1"],