mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-10 12:34:27 +00:00
feat(llm): add disableThinking option to suppress reasoning in extraction models (#228)
Support multiple inference engines and model providers via a strategy
enum so each receives its own thinking-disabling field in
chat-completion request bodies.
Strategies:
- "vllm" → chat_template_kwargs: { enable_thinking: false } (vLLM / SGLang)
- "deepseek" → enable_thinking: false (DeepSeek official API)
- "dashscope" → enable_thinking: false (Alibaba DashScope / Qwen)
- "openai" → reasoning_effort: "low" (OpenAI o-series, cannot fully disable)
- "anthropic" → thinking: { type: "disabled" } (Anthropic Claude)
- "kimi" → thinking: { type: "disabled" } (Kimi / Moonshot)
- "gemini" → thinking_config: { thinking_budget: 0 } (Google Gemini)
Defaults to false (no wrapper). Also accepts boolean true as a shorthand
for "vllm" for convenience.
Covered paths:
- StandaloneLLMRunner (OpenClaw plugin + gateway): llm.disableThinking,
wired through parseConfig, tdai-core, seed-runtime, and gateway config
(TDAI_LLM_DISABLE_THINKING env also accepts strategy names).
- Offload local mode (L1/L1.5/L2): separate offload.disableThinking.
The fetch wrapper lives in src/utils/no-think-fetch.ts with a
STRATEGY_TRANSFORMERS map for clean dispatch. StandaloneLLMRunner builds
it once in the constructor; LocalLlmClient caches it at construction
time and passes it through to callLlm().
Add vitest unit tests for all 7 strategies, normalization, validation,
embedding skip, and non-JSON tolerance (20 tests total).
Signed-off-by: jackson.jia <jiazhenghua0@gmail.com>
This commit is contained in:
@@ -14,6 +14,11 @@
|
|||||||
- **L0 JSONL 分片日**和 **cleaner 清理边界**跟随配置时区(默认仍为系统时区)。
|
- **L0 JSONL 分片日**和 **cleaner 清理边界**跟随配置时区(默认仍为系统时区)。
|
||||||
- 存储层(SQLite / TCVDB)时间戳始终为 UTC instant,**无需数据迁移**。
|
- 存储层(SQLite / TCVDB)时间戳始终为 UTC instant,**无需数据迁移**。
|
||||||
- 统一收敛原有 4 处分散的时间格式化 helper 到 `src/utils/time.ts`,减少代码重复。
|
- 统一收敛原有 4 处分散的时间格式化 helper 到 `src/utils/time.ts`,减少代码重复。
|
||||||
|
- **关闭推理模型 thinking(`disableThinking`)**:新增 `llm.disableThinking` 与 `offload.disableThinking` 两项配置(均默认 `false`,不改变现有行为),支持多种推理引擎/模型提供商的关闭方式。
|
||||||
|
- 可选策略:`"vllm"` (vLLM/SGLang, `chat_template_kwargs.enable_thinking=false`)、`"deepseek"` (DeepSeek API, 顶层 `enable_thinking=false`)、`"dashscope"` (阿里云 DashScope/Qwen, 顶层 `enable_thinking=false`)、`"openai"` (OpenAI o系列, `reasoning_effort="low"`)、`"anthropic"` (Anthropic Claude, `thinking.type="disabled"`)、`"kimi"` (Kimi/Moonshot, `thinking.type="disabled"`)、`"gemini"` (Google Gemini, `thinking_config.thinking_budget=0`)。
|
||||||
|
- 环境变量 `TDAI_LLM_DISABLE_THINKING` 支持策略名(如 `deepseek`)和布尔值。
|
||||||
|
- 修复 offload local-llm 模式下每次 LLM 调用都重新创建 fetch wrapper 的性能问题(现在在 `LocalLlmClient` 构造函数中创建一次并缓存)。
|
||||||
|
- 注入逻辑抽取到 `src/utils/no-think-fetch.ts` 共享,新增 vitest 单测覆盖全部策略 / 跳过 embedding / 非 JSON 容错。
|
||||||
|
|
||||||
### ⚠️ 升级注意(仅在显式配置 `timezone` 时生效)
|
### ⚠️ 升级注意(仅在显式配置 `timezone` 时生效)
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,8 @@
|
|||||||
"apiKey": { "type": "string", "description": "API Key" },
|
"apiKey": { "type": "string", "description": "API Key" },
|
||||||
"model": { "type": "string", "default": "gpt-4o", "description": "模型名称(如 gpt-4o, deepseek-v3, claude-sonnet-4-6)" },
|
"model": { "type": "string", "default": "gpt-4o", "description": "模型名称(如 gpt-4o, deepseek-v3, claude-sonnet-4-6)" },
|
||||||
"maxTokens": { "type": "number", "default": 4096, "description": "最大输出 token 数" },
|
"maxTokens": { "type": "number", "default": 4096, "description": "最大输出 token 数" },
|
||||||
"timeoutMs": { "type": "number", "default": 120000, "description": "请求超时(毫秒)" }
|
"timeoutMs": { "type": "number", "default": 120000, "description": "请求超时(毫秒)" },
|
||||||
|
"disableThinking": { "oneOf": [{ "type": "boolean" }, { "type": "string", "enum": ["vllm", "deepseek", "dashscope", "openai", "anthropic", "kimi", "gemini"] }], "default": false, "description": "禁用推理模型思考过程。可选策略: \"vllm\" (vLLM/SGLang), \"deepseek\" (DeepSeek API), \"dashscope\" (阿里云 DashScope/Qwen), \"openai\" (OpenAI o系列), \"anthropic\" (Anthropic Claude), \"kimi\" (Kimi/Moonshot), \"gemini\" (Google Gemini)。" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"offload": {
|
"offload": {
|
||||||
@@ -150,6 +151,7 @@
|
|||||||
"enabled": { "type": "boolean", "default": false, "description": "是否启用 Context Offload(默认关闭,不影响 Memory 功能)" },
|
"enabled": { "type": "boolean", "default": false, "description": "是否启用 Context Offload(默认关闭,不影响 Memory 功能)" },
|
||||||
"model": { "type": "string", "description": "Offload 使用的 LLM 模型(格式: provider/model),未填写时使用 openclaw 默认模型" },
|
"model": { "type": "string", "description": "Offload 使用的 LLM 模型(格式: provider/model),未填写时使用 openclaw 默认模型" },
|
||||||
"temperature": { "type": "number", "default": 0.2, "description": "LLM 温度参数" },
|
"temperature": { "type": "number", "default": 0.2, "description": "LLM 温度参数" },
|
||||||
|
"disableThinking": { "oneOf": [{ "type": "boolean" }, { "type": "string", "enum": ["vllm", "deepseek", "dashscope", "openai", "anthropic", "kimi", "gemini"] }], "default": false, "description": "禁用推理模型思考过程(仅 local 模式)。可选策略: \"vllm\"/\"deepseek\"/\"dashscope\"/\"openai\"/\"anthropic\"/\"kimi\"/\"gemini\"。" },
|
||||||
"forceTriggerThreshold": { "type": "number", "default": 4, "description": "累积多少个 tool pair 后强制触发 L1" },
|
"forceTriggerThreshold": { "type": "number", "default": 4, "description": "累积多少个 tool pair 后强制触发 L1" },
|
||||||
"dataDir": { "type": "string", "description": "自定义数据目录(绝对路径),默认 ~/.openclaw/context-offload" },
|
"dataDir": { "type": "string", "description": "自定义数据目录(绝对路径),默认 ~/.openclaw/context-offload" },
|
||||||
"defaultContextWindow": { "type": "number", "default": 200000, "description": "默认上下文窗口大小" },
|
"defaultContextWindow": { "type": "number", "default": 200000, "description": "默认上下文窗口大小" },
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import path from "node:path";
|
|||||||
import { generateText, tool, stepCountIs, jsonSchema } from "ai";
|
import { generateText, tool, stepCountIs, jsonSchema } from "ai";
|
||||||
import { createOpenAI } from "@ai-sdk/openai";
|
import { createOpenAI } from "@ai-sdk/openai";
|
||||||
import { report } from "../../core/report/reporter.js";
|
import { report } from "../../core/report/reporter.js";
|
||||||
|
import { createNoThinkFetch, type DisableThinkingStrategy } from "../../utils/no-think-fetch.js";
|
||||||
import type {
|
import type {
|
||||||
LLMRunner,
|
LLMRunner,
|
||||||
LLMRunParams,
|
LLMRunParams,
|
||||||
@@ -49,6 +50,16 @@ export interface StandaloneLLMConfig {
|
|||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
/** Request timeout in milliseconds (default: 120_000). */
|
/** Request timeout in milliseconds (default: 120_000). */
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
|
/**
|
||||||
|
* Controls how thinking/reasoning is disabled for the LLM endpoint.
|
||||||
|
* - `false` (default): no thinking-disabling wrapper
|
||||||
|
* - `"vllm"`: vLLM/SGLang chat_template_kwargs
|
||||||
|
* - `"deepseek"` / `"dashscope"`: top-level enable_thinking: false
|
||||||
|
* - `"openai"`: reasoning_effort: "low"
|
||||||
|
* - `"anthropic"` / `"kimi"`: thinking: { type: "disabled" }
|
||||||
|
* - `"gemini"`: thinking_config: { thinking_budget: 0 }
|
||||||
|
*/
|
||||||
|
disableThinking?: DisableThinkingStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@@ -158,6 +169,7 @@ export class StandaloneLLMRunner implements LLMRunner {
|
|||||||
private model: string;
|
private model: string;
|
||||||
private enableTools: boolean;
|
private enableTools: boolean;
|
||||||
private logger?: Logger;
|
private logger?: Logger;
|
||||||
|
private readonly customFetch?: typeof globalThis.fetch;
|
||||||
|
|
||||||
constructor(opts: {
|
constructor(opts: {
|
||||||
config: StandaloneLLMConfig;
|
config: StandaloneLLMConfig;
|
||||||
@@ -169,6 +181,9 @@ export class StandaloneLLMRunner implements LLMRunner {
|
|||||||
this.model = opts.model ?? opts.config.model;
|
this.model = opts.model ?? opts.config.model;
|
||||||
this.enableTools = opts.enableTools ?? false;
|
this.enableTools = opts.enableTools ?? false;
|
||||||
this.logger = opts.logger;
|
this.logger = opts.logger;
|
||||||
|
this.customFetch = opts.config.disableThinking
|
||||||
|
? createNoThinkFetch(opts.config.disableThinking)
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(params: LLMRunParams): Promise<string> {
|
async run(params: LLMRunParams): Promise<string> {
|
||||||
@@ -189,6 +204,7 @@ export class StandaloneLLMRunner implements LLMRunner {
|
|||||||
baseURL: this.config.baseUrl,
|
baseURL: this.config.baseUrl,
|
||||||
apiKey: this.config.apiKey,
|
apiKey: this.config.apiKey,
|
||||||
compatibility: "compatible",
|
compatibility: "compatible",
|
||||||
|
...(this.customFetch ? { fetch: this.customFetch } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// For pure text tasks like L1 extraction, avoid exposing any tools.
|
// For pure text tasks like L1 extraction, avoid exposing any tools.
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
* Minimal config (zero config): {} — all fields have sensible defaults.
|
* Minimal config (zero config): {} — all fields have sensible defaults.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { DisableThinkingStrategy } from "./utils/no-think-fetch.js";
|
||||||
|
import { normalizeDisableThinking } from "./utils/no-think-fetch.js";
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Type definitions
|
// Type definitions
|
||||||
// ============================
|
// ============================
|
||||||
@@ -203,6 +206,17 @@ export interface StandaloneLLMOverrideConfig {
|
|||||||
maxTokens: number;
|
maxTokens: number;
|
||||||
/** Request timeout in milliseconds (default: 120000). */
|
/** Request timeout in milliseconds (default: 120000). */
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
|
/**
|
||||||
|
* Controls how thinking/reasoning is disabled for the LLM endpoint (default: false).
|
||||||
|
* - `false`: no thinking-disabling wrapper (default)
|
||||||
|
* - `"vllm"`: vLLM/SGLang — `chat_template_kwargs: { enable_thinking: false }`
|
||||||
|
* - `"deepseek"`: DeepSeek official API — top-level `enable_thinking: false`
|
||||||
|
* - `"dashscope"`: Alibaba DashScope (Qwen) — top-level `enable_thinking: false`
|
||||||
|
* - `"openai"`: OpenAI o-series — `reasoning_effort: "low"` (cannot fully disable)
|
||||||
|
* - `"anthropic"` / `"kimi"`: Anthropic Claude / Kimi (Moonshot) — `thinking: { type: "disabled" }`
|
||||||
|
* - `"gemini"`: Google Gemini — `thinking_config: { thinking_budget: 0 }`
|
||||||
|
*/
|
||||||
|
disableThinking: DisableThinkingStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Context Offload settings — controls multi-layer context compression. */
|
/** Context Offload settings — controls multi-layer context compression. */
|
||||||
@@ -222,6 +236,12 @@ export interface OffloadConfig {
|
|||||||
model?: string;
|
model?: string;
|
||||||
/** LLM temperature (default: 0.2) */
|
/** LLM temperature (default: 0.2) */
|
||||||
temperature: number;
|
temperature: number;
|
||||||
|
/**
|
||||||
|
* Controls how thinking/reasoning is disabled for the offload local-mode LLM (default: false).
|
||||||
|
* See `StandaloneLLMOverrideConfig.disableThinking` for the full list of strategies.
|
||||||
|
* Applies only to `mode: "local"`.
|
||||||
|
*/
|
||||||
|
disableThinking: DisableThinkingStrategy;
|
||||||
/** Force-trigger L1 when pending tool pairs >= this threshold (default: 4) */
|
/** Force-trigger L1 when pending tool pairs >= this threshold (default: 4) */
|
||||||
forceTriggerThreshold: number;
|
forceTriggerThreshold: number;
|
||||||
/** Custom data directory (absolute path). Default: ~/.openclaw/context-offload */
|
/** Custom data directory (absolute path). Default: ~/.openclaw/context-offload */
|
||||||
@@ -459,6 +479,7 @@ export function parseConfig(raw: Record<string, unknown> | undefined): MemoryTda
|
|||||||
mode: offloadMode,
|
mode: offloadMode,
|
||||||
model: optStr(offloadGroup, "model"),
|
model: optStr(offloadGroup, "model"),
|
||||||
temperature: num(offloadGroup, "temperature") ?? 0.2,
|
temperature: num(offloadGroup, "temperature") ?? 0.2,
|
||||||
|
disableThinking: normalizeDisableThinking(boolOrStr(offloadGroup, "disableThinking")),
|
||||||
forceTriggerThreshold: num(offloadGroup, "forceTriggerThreshold") ?? 4,
|
forceTriggerThreshold: num(offloadGroup, "forceTriggerThreshold") ?? 4,
|
||||||
dataDir: optStr(offloadGroup, "dataDir"),
|
dataDir: optStr(offloadGroup, "dataDir"),
|
||||||
defaultContextWindow: num(offloadGroup, "defaultContextWindow") ?? 200000,
|
defaultContextWindow: num(offloadGroup, "defaultContextWindow") ?? 200000,
|
||||||
@@ -561,6 +582,7 @@ export function parseConfig(raw: Record<string, unknown> | undefined): MemoryTda
|
|||||||
model: str(llmGroup, "model") ?? "gpt-4o",
|
model: str(llmGroup, "model") ?? "gpt-4o",
|
||||||
maxTokens: num(llmGroup, "maxTokens") ?? 4096,
|
maxTokens: num(llmGroup, "maxTokens") ?? 4096,
|
||||||
timeoutMs: num(llmGroup, "timeoutMs") ?? 120_000,
|
timeoutMs: num(llmGroup, "timeoutMs") ?? 120_000,
|
||||||
|
disableThinking: normalizeDisableThinking(boolOrStr(llmGroup, "disableThinking")),
|
||||||
};
|
};
|
||||||
})(),
|
})(),
|
||||||
offload,
|
offload,
|
||||||
@@ -597,6 +619,14 @@ function bool(src: Record<string, unknown>, key: string): boolean | undefined {
|
|||||||
return typeof v === "boolean" ? v : undefined;
|
return typeof v === "boolean" ? v : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Read a field that may be boolean or string. */
|
||||||
|
function boolOrStr(src: Record<string, unknown>, key: string): boolean | string | undefined {
|
||||||
|
const v = src[key];
|
||||||
|
if (typeof v === "boolean") return v;
|
||||||
|
if (typeof v === "string" && v.trim()) return v.trim();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function strArray(src: Record<string, unknown>, key: string): string[] | undefined {
|
function strArray(src: Record<string, unknown>, key: string): string[] | undefined {
|
||||||
const v = src[key];
|
const v = src[key];
|
||||||
if (!Array.isArray(v)) return undefined;
|
if (!Array.isArray(v)) return undefined;
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ async function createSeedPipeline(opts: SeedRuntimeOptions): Promise<{ pipeline:
|
|||||||
model: cfg.llm.model,
|
model: cfg.llm.model,
|
||||||
maxTokens: cfg.llm.maxTokens,
|
maxTokens: cfg.llm.maxTokens,
|
||||||
timeoutMs: cfg.llm.timeoutMs,
|
timeoutMs: cfg.llm.timeoutMs,
|
||||||
|
disableThinking: cfg.llm.disableThinking,
|
||||||
},
|
},
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -440,6 +440,7 @@ export class TdaiCore {
|
|||||||
model: this.cfg.llm.model,
|
model: this.cfg.llm.model,
|
||||||
maxTokens: this.cfg.llm.maxTokens,
|
maxTokens: this.cfg.llm.maxTokens,
|
||||||
timeoutMs: this.cfg.llm.timeoutMs,
|
timeoutMs: this.cfg.llm.timeoutMs,
|
||||||
|
disableThinking: this.cfg.llm.disableThinking,
|
||||||
},
|
},
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import YAML from "yaml";
|
|||||||
import { getEnv } from "../utils/env.js";
|
import { getEnv } from "../utils/env.js";
|
||||||
import { parseConfig as parseMemoryConfig } from "../config.js";
|
import { parseConfig as parseMemoryConfig } from "../config.js";
|
||||||
import type { MemoryTdaiConfig } from "../config.js";
|
import type { MemoryTdaiConfig } from "../config.js";
|
||||||
|
import { normalizeDisableThinking } from "../utils/no-think-fetch.js";
|
||||||
import type { StandaloneLLMConfig } from "../adapters/standalone/llm-runner.js";
|
import type { StandaloneLLMConfig } from "../adapters/standalone/llm-runner.js";
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@@ -132,6 +133,9 @@ export function loadGatewayConfig(overrides?: Partial<GatewayConfig>): GatewayCo
|
|||||||
model: env("TDAI_LLM_MODEL") ?? str(llmConfig, "model") ?? "gpt-4o",
|
model: env("TDAI_LLM_MODEL") ?? str(llmConfig, "model") ?? "gpt-4o",
|
||||||
maxTokens: envInt("TDAI_LLM_MAX_TOKENS") ?? num(llmConfig, "maxTokens") ?? 4096,
|
maxTokens: envInt("TDAI_LLM_MAX_TOKENS") ?? num(llmConfig, "maxTokens") ?? 4096,
|
||||||
timeoutMs: envInt("TDAI_LLM_TIMEOUT_MS") ?? num(llmConfig, "timeoutMs") ?? 120_000,
|
timeoutMs: envInt("TDAI_LLM_TIMEOUT_MS") ?? num(llmConfig, "timeoutMs") ?? 120_000,
|
||||||
|
disableThinking: normalizeDisableThinking(
|
||||||
|
envBoolOrStr("TDAI_LLM_DISABLE_THINKING") ?? boolOrStr(llmConfig, "disableThinking")
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Memory config (reuse the plugin's parseConfig for full compatibility)
|
// Memory config (reuse the plugin's parseConfig for full compatibility)
|
||||||
@@ -233,6 +237,28 @@ function envInt(key: string): number | undefined {
|
|||||||
return Number.isFinite(n) ? n : undefined;
|
return Number.isFinite(n) ? n : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read an env var that may be a boolean ("true"/"false"/"1"/"0")
|
||||||
|
* or a plain string (strategy name like "deepseek", "anthropic").
|
||||||
|
* Returns the lowercase string for strategy names.
|
||||||
|
*/
|
||||||
|
function envBoolOrStr(key: string): boolean | string | undefined {
|
||||||
|
const raw = env(key);
|
||||||
|
if (raw === undefined) return undefined;
|
||||||
|
const v = raw.toLowerCase();
|
||||||
|
if (v === "true" || v === "1") return true;
|
||||||
|
if (v === "false" || v === "0") return false;
|
||||||
|
return v; // lowercase strategy name
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read a field that may be boolean or string from a config object. */
|
||||||
|
function boolOrStr(src: Record<string, unknown>, key: string): boolean | string | undefined {
|
||||||
|
const v = src[key];
|
||||||
|
if (typeof v === "boolean") return v;
|
||||||
|
if (typeof v === "string" && v.trim()) return v.trim();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function obj(c: Record<string, unknown>, key: string): Record<string, unknown> {
|
function obj(c: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||||
const v = c[key];
|
const v = c[key];
|
||||||
return v && typeof v === "object" && !Array.isArray(v) ? v as Record<string, unknown> : {};
|
return v && typeof v === "object" && !Array.isArray(v) ? v as Record<string, unknown> : {};
|
||||||
|
|||||||
@@ -530,6 +530,7 @@ export class TdaiGateway {
|
|||||||
model: this.config.llm.model,
|
model: this.config.llm.model,
|
||||||
maxTokens: this.config.llm.maxTokens,
|
maxTokens: this.config.llm.maxTokens,
|
||||||
timeoutMs: this.config.llm.timeoutMs,
|
timeoutMs: this.config.llm.timeoutMs,
|
||||||
|
disableThinking: this.config.llm.disableThinking,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (body.config_override) {
|
if (body.config_override) {
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ export function registerOffload(api: any, offloadConfig: OffloadConfig): void {
|
|||||||
|
|
||||||
if (baseUrl && apiKey) {
|
if (baseUrl && apiKey) {
|
||||||
backendClient = new LocalLlmClient(
|
backendClient = new LocalLlmClient(
|
||||||
{ baseUrl, apiKey, model: modelId, temperature: offloadConfig.temperature, timeoutMs: offloadConfig.backendTimeoutMs },
|
{ baseUrl, apiKey, model: modelId, temperature: offloadConfig.temperature, timeoutMs: offloadConfig.backendTimeoutMs, disableThinking: offloadConfig.disableThinking },
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* Used when `offload.model` is configured and `offload.backendUrl` is not set.
|
* Used when `offload.model` is configured and `offload.backendUrl` is not set.
|
||||||
*/
|
*/
|
||||||
import { callLlm, type LlmCallerConfig } from "./llm-caller.js";
|
import { callLlm, type LlmCallerConfig } from "./llm-caller.js";
|
||||||
|
import { createNoThinkFetch, type DisableThinkingStrategy } from "../../utils/no-think-fetch.js";
|
||||||
import { L1_SYSTEM_PROMPT, buildL1UserPrompt, type L1ToolPair } from "./prompts/l1-prompt.js";
|
import { L1_SYSTEM_PROMPT, buildL1UserPrompt, type L1ToolPair } from "./prompts/l1-prompt.js";
|
||||||
import { L15_SYSTEM_PROMPT, buildL15UserPrompt, type L15CurrentMmd, type L15MmdMeta } from "./prompts/l15-prompt.js";
|
import { L15_SYSTEM_PROMPT, buildL15UserPrompt, type L15CurrentMmd, type L15MmdMeta } from "./prompts/l15-prompt.js";
|
||||||
import { L2_SYSTEM_PROMPT, buildL2UserPrompt, type L2NewEntry } from "./prompts/l2-prompt.js";
|
import { L2_SYSTEM_PROMPT, buildL2UserPrompt, type L2NewEntry } from "./prompts/l2-prompt.js";
|
||||||
@@ -24,11 +25,13 @@ export interface LocalLlmClientConfig {
|
|||||||
model: string;
|
model: string;
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
|
disableThinking?: DisableThinkingStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LocalLlmClient {
|
export class LocalLlmClient {
|
||||||
private config: LlmCallerConfig;
|
private config: LlmCallerConfig;
|
||||||
private logger?: PluginLogger;
|
private logger?: PluginLogger;
|
||||||
|
private readonly customFetch?: typeof globalThis.fetch;
|
||||||
|
|
||||||
constructor(cfg: LocalLlmClientConfig, logger?: PluginLogger) {
|
constructor(cfg: LocalLlmClientConfig, logger?: PluginLogger) {
|
||||||
this.config = {
|
this.config = {
|
||||||
@@ -37,8 +40,15 @@ export class LocalLlmClient {
|
|||||||
model: cfg.model,
|
model: cfg.model,
|
||||||
temperature: cfg.temperature ?? 0.2,
|
temperature: cfg.temperature ?? 0.2,
|
||||||
timeoutMs: cfg.timeoutMs ?? 120_000,
|
timeoutMs: cfg.timeoutMs ?? 120_000,
|
||||||
|
disableThinking: cfg.disableThinking ?? false,
|
||||||
};
|
};
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
|
|
||||||
|
// Cache the fetch wrapper at construction time — avoids per-call creation.
|
||||||
|
this.customFetch = cfg.disableThinking
|
||||||
|
? createNoThinkFetch(cfg.disableThinking)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
logger?.info?.(`${TAG} Initialized: model=${cfg.model}, baseUrl=${cfg.baseUrl}`);
|
logger?.info?.(`${TAG} Initialized: model=${cfg.model}, baseUrl=${cfg.baseUrl}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +69,7 @@ export class LocalLlmClient {
|
|||||||
systemPrompt: L1_SYSTEM_PROMPT,
|
systemPrompt: L1_SYSTEM_PROMPT,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
label: "L1",
|
label: "L1",
|
||||||
|
customFetch: this.customFetch,
|
||||||
}, this.logger);
|
}, this.logger);
|
||||||
|
|
||||||
const entries = parseL1Response(raw);
|
const entries = parseL1Response(raw);
|
||||||
@@ -97,6 +108,7 @@ export class LocalLlmClient {
|
|||||||
systemPrompt: L15_SYSTEM_PROMPT,
|
systemPrompt: L15_SYSTEM_PROMPT,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
label: "L1.5",
|
label: "L1.5",
|
||||||
|
customFetch: this.customFetch,
|
||||||
}, this.logger);
|
}, this.logger);
|
||||||
|
|
||||||
const result = parseL15Response(raw);
|
const result = parseL15Response(raw);
|
||||||
@@ -138,6 +150,7 @@ export class LocalLlmClient {
|
|||||||
userPrompt,
|
userPrompt,
|
||||||
label: "L2",
|
label: "L2",
|
||||||
timeoutMs: 120_000, // L2 may take longer due to complex prompts
|
timeoutMs: 120_000, // L2 may take longer due to complex prompts
|
||||||
|
customFetch: this.customFetch,
|
||||||
}, this.logger);
|
}, this.logger);
|
||||||
|
|
||||||
const result = parseL2Response(raw);
|
const result = parseL2Response(raw);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import { generateText } from "ai";
|
import { generateText } from "ai";
|
||||||
import { createOpenAI } from "@ai-sdk/openai";
|
import { createOpenAI } from "@ai-sdk/openai";
|
||||||
|
import { createNoThinkFetch, type DisableThinkingStrategy } from "../../utils/no-think-fetch.js";
|
||||||
import type { PluginLogger } from "../types.js";
|
import type { PluginLogger } from "../types.js";
|
||||||
|
|
||||||
const TAG = "[context-offload] [local-llm]";
|
const TAG = "[context-offload] [local-llm]";
|
||||||
@@ -16,6 +17,11 @@ export interface LlmCallerConfig {
|
|||||||
model: string;
|
model: string;
|
||||||
temperature: number;
|
temperature: number;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
|
/**
|
||||||
|
* Controls how thinking/reasoning is disabled for the LLM endpoint.
|
||||||
|
* See DisableThinkingStrategy for the full list of strategies.
|
||||||
|
*/
|
||||||
|
disableThinking?: DisableThinkingStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CallLlmOpts {
|
export interface CallLlmOpts {
|
||||||
@@ -27,6 +33,8 @@ export interface CallLlmOpts {
|
|||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
/** Label for logging (e.g. "L1", "L1.5", "L2") */
|
/** Label for logging (e.g. "L1", "L1.5", "L2") */
|
||||||
label?: string;
|
label?: string;
|
||||||
|
/** Pre-created fetch wrapper (for caching at the client level). */
|
||||||
|
customFetch?: typeof globalThis.fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,10 +56,15 @@ export async function callLlm(
|
|||||||
`systemLen=${opts.systemPrompt.length}, userLen=${opts.userPrompt.length}`,
|
`systemLen=${opts.systemPrompt.length}, userLen=${opts.userPrompt.length}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const customFetch = opts.customFetch ?? (
|
||||||
|
config.disableThinking ? createNoThinkFetch(config.disableThinking) : undefined
|
||||||
|
);
|
||||||
|
|
||||||
const provider = createOpenAI({
|
const provider = createOpenAI({
|
||||||
baseURL: config.baseUrl,
|
baseURL: config.baseUrl,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
compatibility: "compatible",
|
compatibility: "compatible",
|
||||||
|
...(customFetch ? { fetch: customFetch } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||||
|
import {
|
||||||
|
createNoThinkFetch,
|
||||||
|
isValidDisableThinkingStrategy,
|
||||||
|
normalizeDisableThinking,
|
||||||
|
type DisableThinkingStrategy,
|
||||||
|
} from "./no-think-fetch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture the (input, init) passed through to the real global fetch so we can
|
||||||
|
* assert on the (possibly rewritten) request body. The mock never blindly
|
||||||
|
* JSON.parses the body — it just records and returns a stub Response.
|
||||||
|
*/
|
||||||
|
function captureFetch() {
|
||||||
|
const calls: Array<{ input: unknown; init: RequestInit | undefined }> = [];
|
||||||
|
vi.spyOn(globalThis, "fetch").mockImplementation((async (input: unknown, init?: RequestInit) => {
|
||||||
|
calls.push({ input, init });
|
||||||
|
return new Response("{}", { status: 200 });
|
||||||
|
}) as typeof globalThis.fetch);
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createNoThinkFetch", () => {
|
||||||
|
// ─── vllm strategy (original behavior) ────────────────────────────────────
|
||||||
|
|
||||||
|
describe("vllm strategy", () => {
|
||||||
|
it("injects chat_template_kwargs.enable_thinking=false into chat bodies", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("vllm");
|
||||||
|
|
||||||
|
await f("https://example/v1/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ model: "qwen3", messages: [{ role: "user", content: "hi" }] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = JSON.parse(calls[0].init!.body as string);
|
||||||
|
expect(sent.chat_template_kwargs).toEqual({ enable_thinking: false });
|
||||||
|
expect(sent.messages).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges into an existing chat_template_kwargs instead of clobbering it", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("vllm");
|
||||||
|
|
||||||
|
await f("https://example", {
|
||||||
|
body: JSON.stringify({ messages: [], chat_template_kwargs: { foo: "bar", enable_thinking: true } }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = JSON.parse(calls[0].init!.body as string);
|
||||||
|
expect(sent.chat_template_kwargs).toEqual({ foo: "bar", enable_thinking: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves embedding requests (input, no messages) untouched", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("vllm");
|
||||||
|
const body = JSON.stringify({ model: "bge-m3", input: ["hello"] });
|
||||||
|
|
||||||
|
await f("https://example/v1/embeddings", { body });
|
||||||
|
|
||||||
|
expect(calls[0].init!.body).toBe(body);
|
||||||
|
expect(JSON.parse(calls[0].init!.body as string).chat_template_kwargs).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── deepseek strategy ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("deepseek strategy", () => {
|
||||||
|
it("injects top-level enable_thinking: false", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("deepseek");
|
||||||
|
|
||||||
|
await f("https://api.deepseek.com/v1/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ model: "deepseek-reasoner", messages: [{ role: "user", content: "hi" }] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = JSON.parse(calls[0].init!.body as string);
|
||||||
|
expect(sent.enable_thinking).toBe(false);
|
||||||
|
expect(sent.chat_template_kwargs).toBeUndefined();
|
||||||
|
expect(sent.messages).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── dashscope strategy ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("dashscope strategy", () => {
|
||||||
|
it("injects top-level enable_thinking: false", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("dashscope");
|
||||||
|
|
||||||
|
await f("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ model: "qwen-plus", messages: [{ role: "user", content: "hi" }] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = JSON.parse(calls[0].init!.body as string);
|
||||||
|
expect(sent.enable_thinking).toBe(false);
|
||||||
|
expect(sent.chat_template_kwargs).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── openai strategy ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("openai strategy", () => {
|
||||||
|
it("injects reasoning_effort: low", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("openai");
|
||||||
|
|
||||||
|
await f("https://api.openai.com/v1/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ model: "o3-mini", messages: [{ role: "user", content: "hi" }] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = JSON.parse(calls[0].init!.body as string);
|
||||||
|
expect(sent.reasoning_effort).toBe("low");
|
||||||
|
expect(sent.chat_template_kwargs).toBeUndefined();
|
||||||
|
expect(sent.enable_thinking).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── anthropic strategy ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("anthropic strategy", () => {
|
||||||
|
it("injects thinking: { type: disabled }", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("anthropic");
|
||||||
|
|
||||||
|
await f("https://api.anthropic.com/v1/messages", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ model: "claude-sonnet-4-20250514", messages: [{ role: "user", content: "hi" }] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = JSON.parse(calls[0].init!.body as string);
|
||||||
|
expect(sent.thinking).toEqual({ type: "disabled" });
|
||||||
|
expect(sent.chat_template_kwargs).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── kimi strategy ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("kimi strategy", () => {
|
||||||
|
it("injects thinking: { type: disabled }", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("kimi");
|
||||||
|
|
||||||
|
await f("https://api.moonshot.cn/v1/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ model: "kimi-k2.6", messages: [{ role: "user", content: "hi" }] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = JSON.parse(calls[0].init!.body as string);
|
||||||
|
expect(sent.thinking).toEqual({ type: "disabled" });
|
||||||
|
expect(sent.chat_template_kwargs).toBeUndefined();
|
||||||
|
expect(sent.enable_thinking).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── gemini strategy ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("gemini strategy", () => {
|
||||||
|
it("injects thinking_config: { thinking_budget: 0 }", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("gemini");
|
||||||
|
|
||||||
|
await f("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ messages: [{ role: "user", content: "hi" }] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = JSON.parse(calls[0].init!.body as string);
|
||||||
|
expect(sent.thinking_config).toEqual({ thinking_budget: 0 });
|
||||||
|
expect(sent.chat_template_kwargs).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── strategy === false (passthrough) ───────────────────────────────────
|
||||||
|
|
||||||
|
describe("false strategy", () => {
|
||||||
|
it("returns globalThis.fetch directly", () => {
|
||||||
|
const f = createNoThinkFetch(false);
|
||||||
|
expect(f).toBe(globalThis.fetch);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("default parameter returns globalThis.fetch", () => {
|
||||||
|
const f = createNoThinkFetch();
|
||||||
|
expect(f).toBe(globalThis.fetch);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Common behavior across all strategies ──────────────────────────────
|
||||||
|
|
||||||
|
describe("common behavior", () => {
|
||||||
|
it("forwards a non-JSON string body unchanged", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("vllm");
|
||||||
|
|
||||||
|
await f("https://example", { body: "not-json" });
|
||||||
|
|
||||||
|
expect(calls[0].init!.body).toBe("not-json");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards requests with no init and with a non-string body unchanged", async () => {
|
||||||
|
const calls = captureFetch();
|
||||||
|
const f = createNoThinkFetch("deepseek");
|
||||||
|
|
||||||
|
await f("https://example");
|
||||||
|
expect(calls[0].init).toBeUndefined();
|
||||||
|
|
||||||
|
const blob = new Uint8Array([1, 2, 3]);
|
||||||
|
await f("https://example", { body: blob });
|
||||||
|
expect(calls[1].init!.body).toBe(blob);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Validation helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("isValidDisableThinkingStrategy", () => {
|
||||||
|
it("returns true for all valid strategies", () => {
|
||||||
|
const valid: DisableThinkingStrategy[] = [false, "vllm", "deepseek", "dashscope", "openai", "anthropic", "kimi", "gemini"];
|
||||||
|
for (const v of valid) {
|
||||||
|
expect(isValidDisableThinkingStrategy(v)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for invalid values", () => {
|
||||||
|
const invalid = [true, "invalid", "sglang", "VLLM", 0, null, undefined, "", "true"];
|
||||||
|
for (const v of invalid) {
|
||||||
|
expect(isValidDisableThinkingStrategy(v)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("normalizeDisableThinking", () => {
|
||||||
|
it("maps true to vllm (shorthand)", () => {
|
||||||
|
expect(normalizeDisableThinking(true)).toBe("vllm");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps false to false", () => {
|
||||||
|
expect(normalizeDisableThinking(false)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps undefined to false", () => {
|
||||||
|
expect(normalizeDisableThinking(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through valid strategy strings", () => {
|
||||||
|
expect(normalizeDisableThinking("vllm")).toBe("vllm");
|
||||||
|
expect(normalizeDisableThinking("deepseek")).toBe("deepseek");
|
||||||
|
expect(normalizeDisableThinking("dashscope")).toBe("dashscope");
|
||||||
|
expect(normalizeDisableThinking("openai")).toBe("openai");
|
||||||
|
expect(normalizeDisableThinking("anthropic")).toBe("anthropic");
|
||||||
|
expect(normalizeDisableThinking("kimi")).toBe("kimi");
|
||||||
|
expect(normalizeDisableThinking("gemini")).toBe("gemini");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns and returns false for unknown strategies", () => {
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
expect(normalizeDisableThinking("unknown_strategy")).toBe(false);
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Unknown disableThinking strategy "unknown_strategy"'),
|
||||||
|
);
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Multi-strategy fetch wrapper for disabling thinking/reasoning across
|
||||||
|
* different inference engines and model providers.
|
||||||
|
*
|
||||||
|
* Each strategy injects provider-specific fields into chat-completion
|
||||||
|
* request bodies. Non-chat requests (embeddings, etc.) pass through
|
||||||
|
* unchanged.
|
||||||
|
*
|
||||||
|
* Strategies:
|
||||||
|
* - `"vllm"`: vLLM / SGLang — `chat_template_kwargs.enable_thinking = false`
|
||||||
|
* - `"deepseek"`: DeepSeek official API — top-level `enable_thinking: false`
|
||||||
|
* - `"dashscope"`: Alibaba DashScope (Qwen) — top-level `enable_thinking: false`
|
||||||
|
* - `"openai"`: OpenAI o-series — `reasoning_effort: "low"` (cannot fully disable)
|
||||||
|
* - `"anthropic"`: Anthropic Claude — `thinking: { type: "disabled" }`
|
||||||
|
* - `"kimi"`: Kimi (Moonshot) — `thinking: { type: "disabled" }`
|
||||||
|
* - `"gemini"`: Google Gemini — `thinking_config: { thinking_budget: 0 }`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ─── Type & validation ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type DisableThinkingStrategy =
|
||||||
|
| false
|
||||||
|
| "vllm"
|
||||||
|
| "deepseek"
|
||||||
|
| "dashscope"
|
||||||
|
| "openai"
|
||||||
|
| "anthropic"
|
||||||
|
| "kimi"
|
||||||
|
| "gemini";
|
||||||
|
|
||||||
|
export const VALID_DISABLE_THINKING_STRATEGIES: readonly DisableThinkingStrategy[] = [
|
||||||
|
false, "vllm", "deepseek", "dashscope", "openai", "anthropic", "kimi", "gemini",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Check if a value is a valid DisableThinkingStrategy. */
|
||||||
|
export function isValidDisableThinkingStrategy(value: unknown): value is DisableThinkingStrategy {
|
||||||
|
return (VALID_DISABLE_THINKING_STRATEGIES as readonly unknown[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a raw boolean-or-string config value into a DisableThinkingStrategy.
|
||||||
|
*
|
||||||
|
* true → "vllm" (shorthand for the most common self-hosted scenario)
|
||||||
|
* false / undefined → false
|
||||||
|
*
|
||||||
|
* Unknown string values fall back to false with a console warning.
|
||||||
|
*/
|
||||||
|
export function normalizeDisableThinking(raw: boolean | string | undefined): DisableThinkingStrategy {
|
||||||
|
if (raw === undefined || raw === false) return false;
|
||||||
|
if (raw === true) return "vllm";
|
||||||
|
// raw is a string
|
||||||
|
if (isValidDisableThinkingStrategy(raw)) return raw;
|
||||||
|
console.warn(
|
||||||
|
`[memory-tdai] Unknown disableThinking strategy "${raw}", ` +
|
||||||
|
`valid values: false, true, "vllm", "deepseek", "dashscope", "openai", "anthropic", "kimi", "gemini". ` +
|
||||||
|
`Thinking will NOT be disabled.`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Per-provider body transformers ───────────────────────────────────────────
|
||||||
|
|
||||||
|
function applyVllm(body: Record<string, unknown>): void {
|
||||||
|
const existing = body.chat_template_kwargs;
|
||||||
|
const base = (existing && typeof existing === "object" && !Array.isArray(existing))
|
||||||
|
? existing as Record<string, unknown>
|
||||||
|
: {};
|
||||||
|
body.chat_template_kwargs = { ...base, enable_thinking: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDeepSeek(body: Record<string, unknown>): void {
|
||||||
|
body.enable_thinking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDashScope(body: Record<string, unknown>): void {
|
||||||
|
body.enable_thinking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOpenAI(body: Record<string, unknown>): void {
|
||||||
|
body.reasoning_effort = "low";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAnthropic(body: Record<string, unknown>): void {
|
||||||
|
body.thinking = { type: "disabled" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyGemini(body: Record<string, unknown>): void {
|
||||||
|
body.thinking_config = { thinking_budget: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const STRATEGY_TRANSFORMERS: Record<
|
||||||
|
Exclude<DisableThinkingStrategy, false>,
|
||||||
|
(body: Record<string, unknown>) => void
|
||||||
|
> = {
|
||||||
|
vllm: applyVllm,
|
||||||
|
deepseek: applyDeepSeek,
|
||||||
|
dashscope: applyDashScope,
|
||||||
|
openai: applyOpenAI,
|
||||||
|
anthropic: applyAnthropic,
|
||||||
|
kimi: applyAnthropic,
|
||||||
|
gemini: applyGemini,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Factory ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fetch wrapper that injects provider-specific thinking-disabling
|
||||||
|
* fields into chat-completion request bodies.
|
||||||
|
*
|
||||||
|
* When `strategy` is `false`, returns `globalThis.fetch` directly (no wrapper).
|
||||||
|
*
|
||||||
|
* Only requests with a `messages` array in the body are modified — embedding
|
||||||
|
* and other non-chat requests pass through unchanged.
|
||||||
|
*/
|
||||||
|
export function createNoThinkFetch(strategy: DisableThinkingStrategy = false): typeof globalThis.fetch {
|
||||||
|
if (strategy === false) return globalThis.fetch;
|
||||||
|
|
||||||
|
const transform = STRATEGY_TRANSFORMERS[strategy];
|
||||||
|
if (!transform) return globalThis.fetch; // defensive: unknown strategy → passthrough
|
||||||
|
|
||||||
|
return (async (input, init) => {
|
||||||
|
if (init && typeof init.body === "string") {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(init.body);
|
||||||
|
if (body && Array.isArray(body.messages)) {
|
||||||
|
transform(body);
|
||||||
|
init = { ...init, body: JSON.stringify(body) };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// non-JSON body — forward unchanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return globalThis.fetch(input, init);
|
||||||
|
}) as typeof globalThis.fetch;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user