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:
jackson-jia-914
2026-06-24 18:02:25 +08:00
committed by GitHub
parent 38673b5d2b
commit e9c1af03f6
13 changed files with 514 additions and 2 deletions
+1 -1
View File
@@ -376,7 +376,7 @@ export function registerOffload(api: any, offloadConfig: OffloadConfig): void {
if (baseUrl && apiKey) {
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,
);
} else {
+13
View File
@@ -7,6 +7,7 @@
* Used when `offload.model` is configured and `offload.backendUrl` is not set.
*/
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 { 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";
@@ -24,11 +25,13 @@ export interface LocalLlmClientConfig {
model: string;
temperature?: number;
timeoutMs?: number;
disableThinking?: DisableThinkingStrategy;
}
export class LocalLlmClient {
private config: LlmCallerConfig;
private logger?: PluginLogger;
private readonly customFetch?: typeof globalThis.fetch;
constructor(cfg: LocalLlmClientConfig, logger?: PluginLogger) {
this.config = {
@@ -37,8 +40,15 @@ export class LocalLlmClient {
model: cfg.model,
temperature: cfg.temperature ?? 0.2,
timeoutMs: cfg.timeoutMs ?? 120_000,
disableThinking: cfg.disableThinking ?? false,
};
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}`);
}
@@ -59,6 +69,7 @@ export class LocalLlmClient {
systemPrompt: L1_SYSTEM_PROMPT,
userPrompt,
label: "L1",
customFetch: this.customFetch,
}, this.logger);
const entries = parseL1Response(raw);
@@ -97,6 +108,7 @@ export class LocalLlmClient {
systemPrompt: L15_SYSTEM_PROMPT,
userPrompt,
label: "L1.5",
customFetch: this.customFetch,
}, this.logger);
const result = parseL15Response(raw);
@@ -138,6 +150,7 @@ export class LocalLlmClient {
userPrompt,
label: "L2",
timeoutMs: 120_000, // L2 may take longer due to complex prompts
customFetch: this.customFetch,
}, this.logger);
const result = parseL2Response(raw);
+13
View File
@@ -6,6 +6,7 @@
*/
import { generateText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { createNoThinkFetch, type DisableThinkingStrategy } from "../../utils/no-think-fetch.js";
import type { PluginLogger } from "../types.js";
const TAG = "[context-offload] [local-llm]";
@@ -16,6 +17,11 @@ export interface LlmCallerConfig {
model: string;
temperature: 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 {
@@ -27,6 +33,8 @@ export interface CallLlmOpts {
timeoutMs?: number;
/** Label for logging (e.g. "L1", "L1.5", "L2") */
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}`,
);
const customFetch = opts.customFetch ?? (
config.disableThinking ? createNoThinkFetch(config.disableThinking) : undefined
);
const provider = createOpenAI({
baseURL: config.baseUrl,
apiKey: config.apiKey,
compatibility: "compatible",
...(customFetch ? { fetch: customFetch } : {}),
});
try {