惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
Malwarebytes
Malwarebytes
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cybersecurity and Infrastructure Security Agency CISA
F
Future of Privacy Forum
C
Cisco Blogs
T
The Exploit Database - CXSecurity.com
A
Arctic Wolf
S
Securelist
K
Kaspersky official blog
S
Schneier on Security
T
ThreatConnect
T
Tenable Blog
Spread Privacy
Spread Privacy
T
True Tiger Recordings
AWS News Blog
AWS News Blog
F
Fox-IT International blog
量子位
T
Threatpost
V
Vulnerabilities – Threatpost
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
GbyAI
GbyAI
宝玉的分享
宝玉的分享
腾讯CDC
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队
S
SegmentFault 最新的问题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
U
Unit 42
雷峰网
雷峰网
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
O
OpenAI News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
The Register - Security
The Register - Security
MyScale Blog
MyScale Blog
小众软件
小众软件
A
About on SuperTechFans
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
美团技术团队
Google Online Security Blog
Google Online Security Blog
P
Proofpoint News Feed
MongoDB | Blog
MongoDB | Blog

Recent Commits to openclaw:main

fix(perf): fail startup bench on early gateway exit · openclaw/openclaw@a8fc28c fix: handle npm min-release-age in installers · openclaw/openclaw@316d97c fix(scripts): include ui:build in build-all full and ciArtifacts prof… · openclaw/openclaw@6704d0a fix(e2e): sample Windows kitchen sink gateway RSS · openclaw/openclaw@73189e3 fix(cron): respect isolated target and error on missing remove id (#8… · openclaw/openclaw@6709f4e fix(pi-embedded-runner): propagate trigger-derived priority to the gl… · openclaw/openclaw@0580f57 fix(cli): suppress self-update version warnings · openclaw/openclaw@e2bd20f fix: preserve webchat source reply details · openclaw/openclaw@aa50c51 docs: replace OpenClaw docs skill and add plugin permissions guide · openclaw/openclaw@0dabb70 fix(codex): preserve source reply mode for active runs (#86325) · openclaw/openclaw@b962110 fix: make compaction reinjection opt-in · openclaw/openclaw@ab910f8 fix codex usage-limit recovery copy (#86305) · openclaw/openclaw@c3c8a65 feat(ui): add ephemeral Activity tab · openclaw/openclaw@3dd0e8e fix(tests): harden native macos plugin proof · openclaw/openclaw@a5d5604 fix(commitments): serialize load-modify-save with in-process queue + … · openclaw/openclaw@d3c293d Fail Codex compaction at the Codex boundary (#85958) · openclaw/openclaw@dd47e47 fix(docker): restore config parent ownership · openclaw/openclaw@908b894 docs: clarify config default review policy (#86329) · openclaw/openclaw@3a03dd5 docs: clean changelog script entries · openclaw/openclaw@0eead19 fix(scripts): budget restart benchmark timeouts · openclaw/openclaw@5bd5509 fix: align ui vitest config assertion · openclaw/openclaw@730fd19 fix: route unit ui vitest targets narrowly · openclaw/openclaw@777402e fix: route explicit ui vitest targets narrowly · openclaw/openclaw@56a383c fix(android): harden play media permission removal fix(webchat): stabilize live transcript run state · openclaw/openclaw@119a01c fix(scripts): fail restart benchmark regressions · openclaw/openclaw@95d1b39 fix(openai): scope external codex auth to realtime fix(openai): prefer codex auth for GPT realtime · openclaw/openclaw@48c4f57 fix(openai): discover codex cli auth for provider checks · openclaw/openclaw@4656275 fix(android): keep talk mode on realtime relay · openclaw/openclaw@70614f8 test(android): add gateway connect adb probe · openclaw/openclaw@d7aa1f3 fix(android): stabilize realtime talk connection state · openclaw/openclaw@ffb02a5 test(android): add voice mode adb e2e harness · openclaw/openclaw@e52a3b3 fix(ci): stabilize deadcode and catalog checks · openclaw/openclaw@3db1508 fix(scripts): prebuild gateway cpu bench · openclaw/openclaw@ca70015 fix(e2e): harden bundled lifecycle probe on Windows · openclaw/openclaw@4798264 test(e2e): sample kitchen sink rpc peak rss · openclaw/openclaw@60c0f24 fix(scripts): remove stale deadcode allowlist entries · openclaw/openclaw@ea3bb92 fix(telegram): route polling diagnostics away from errors · openclaw/openclaw@b5c1199 fix(plugins): support linked source checkouts on Windows · openclaw/openclaw@793e300 fix(gateway): back off session tool mirrors under pressure (#84846) · openclaw/openclaw@42bdc94 fix(config): skip shell env fallback on Windows (#85739) · openclaw/openclaw@06bf302 fix(gateway): avoid duplicate session message broadcasts · openclaw/openclaw@1459044 fix: repair anchorless iMessage watch payloads · openclaw/openclaw@f37fbc9 fix(cli): route node status hints to stdout (#85780) · openclaw/openclaw@749692e fix(oc-path): support deep config edits (#86060) · openclaw/openclaw@3a72a30 fix(config): quiet benign metadata anomaly output · openclaw/openclaw@f3f4f29 fix(test): fail multi-node update regressions · openclaw/openclaw@732cf54 fix(google-vertex): support production ADC modes (#83971) · openclaw/openclaw@f09b4eb test(e2e): expose corrupt plugin deps smoke · openclaw/openclaw@fa3ff4d fix(codex): log app-server approval promotion trigger · openclaw/openclaw@d9af23f test(e2e): harden multi-node update smoke Clean up browser MCP subprocess tree (#85832) · openclaw/openclaw@8dc6b4d fix(agents): log warnings instead of swallowing subagent errors (#82943) · openclaw/openclaw@907bc03 fix(compaction): preserve partial summary on mid-chain chunk failure … · openclaw/openclaw@f0061dd fix(config): do not suppress recovery retry after failed backup resto… · openclaw/openclaw@5d174a5 chore: release 2026.5.25 fix(installer): support alpine cli installs · openclaw/openclaw@f68ed72 test(agents): keep runtime-plan provider mock current fix(scripts): launch env package scripts on Windows · openclaw/openclaw@4d4ce9e fix(agents): cache fallback provider resolution · openclaw/openclaw@3c8d101 fix(test): make import timing scripts Windows-safe · openclaw/openclaw@8ae9977 fix(telegram): transient Telegram pairing prompts (#85555) · openclaw/openclaw@8209426 fix(test): make max Vitest scripts Windows-safe · openclaw/openclaw@b681d5d fix(doctor): migrate Feishu account bot names (#86081) · openclaw/openclaw@9e8cc7e fix(scripts): prefilter conflict marker scans docs: add ClawSweeper review policy to AGENTS (#86197) · openclaw/openclaw@242e876 fix(installer): avoid before with npm release-age configs (#85491) · openclaw/openclaw@4742db6 fix(e2e): retry Windows kitchen sink probes · openclaw/openclaw@3e275a5 fix(installer): install node with apk on alpine fix(installer): detect musl linux shells · openclaw/openclaw@acfed37 perf(plugins,gateway): thread metadata snapshot + discovery through h… · openclaw/openclaw@8ccb11c fix(ui): split control ui runtime chunks · openclaw/openclaw@8bf4f7d refactor(config): extract GoogleChat schema into zod-schema.providers… · openclaw/openclaw@fe34141 fix(update): suppress internal handoff version warnings · openclaw/openclaw@6cc8244 test(e2e): select installable bundled plugins · openclaw/openclaw@0acc3e3 fix(scripts): harden Windows native opus install · openclaw/openclaw@43252c8 fix(agents): match runtime policy entries when session provider is em… fix(scripts): harden Windows generated formatting · openclaw/openclaw@0a98559 fix(mcp): bound tools/list during catalog discovery (#85063) · openclaw/openclaw@07f500a fix(test): focus plugin binding Docker smoke · openclaw/openclaw@dfa1a51 test(e2e): fail release memory indexing errors test(daemon): fail launchd integration bootstrap errors · openclaw/openclaw@af07769 feat(imessage): support thumb approval reactions (#85952) · openclaw/openclaw@5c7980f fix(crabbox): default macos aws runs on demand fix(scripts): preserve test passthrough args · openclaw/openclaw@e4332f7 fix(e2e): harden Windows plugin assertions fix(test): mount upgrade survivor helper · openclaw/openclaw@5f03154 fix(android): prevent stale chat during session switches fix(android): keep permission setup action visible · openclaw/openclaw@94bc18a style(android): sharpen voice mode surfaces · openclaw/openclaw@c452510 fix(android): hide internal chat content blocks · openclaw/openclaw@d86ed21 style(android): refine list surface spacing · openclaw/openclaw@955909c feat(android): add pair new gateway action · openclaw/openclaw@cc5eb97 Advance iMessage catchup cursor after live handling (#85475) · openclaw/openclaw@102555c fix(scripts): ignore forwarded arg separator · openclaw/openclaw@79ee70c fix(test): fail empty gateway startup samples · openclaw/openclaw@5a8ce6a fix(e2e): harden Windows kitchen sink assertions · openclaw/openclaw@87a2eba fix(e2e): harden Telegram credential paths on Windows · openclaw/openclaw@c643370 fix(android): align setup pairing scopes
fix: prevent plain text tool call leaks (#86222) · openclaw/openclaw@cd62780
fuller-stack · 2026-05-25 · via Recent Commits to openclaw:main

@@ -1,23 +1,18 @@

11

import type { StreamFn } from "@earendil-works/pi-agent-core";

2-

import { createAssistantMessageEventStream, streamSimple } from "@earendil-works/pi-ai";

2+

import { streamSimple } from "@earendil-works/pi-ai";

33

import { createSubsystemLogger } from "openclaw/plugin-sdk/logging-core";

44

import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";

5+

import { createPlainTextToolCallPromotionWrapper } from "openclaw/plugin-sdk/provider-stream-shared";

56

import { ssrfPolicyFromHttpBaseUrlAllowedHostname } from "openclaw/plugin-sdk/ssrf-runtime";

67

import { LMSTUDIO_PROVIDER_ID } from "./defaults.js";

78

import { ensureLmstudioModelLoaded } from "./models.fetch.js";

89

import { resolveLmstudioInferenceBase } from "./models.js";

9-

import {

10-

createLmstudioSyntheticToolCallId,

11-

parseLmstudioPlainTextToolCalls,

12-

} from "./plain-text-tool-calls.js";

1310

import { resolveLmstudioProviderHeaders, resolveLmstudioRuntimeApiKey } from "./runtime.js";

14111512

const log = createSubsystemLogger("extensions/lmstudio/stream");

16131714

type StreamOptions = Parameters<StreamFn>[2];

1815

type StreamModel = Parameters<StreamFn>[0];

19-

type StreamContext = Parameters<StreamFn>[1];

20-2116

const preloadInFlight = new Map<string, Promise<void>>();

22172318

/**

@@ -137,218 +132,6 @@ function withLmstudioUsageCompat(model: StreamModel): StreamModel {

137132

};

138133

}

139134140-

function resolveContextToolNames(context: StreamContext): Set<string> {

141-

const tools = (context as { tools?: unknown }).tools;

142-

if (!Array.isArray(tools)) {

143-

return new Set();

144-

}

145-

const names = tools

146-

.map((tool) => {

147-

const record = toRecord(tool);

148-

return typeof record?.name === "string" && record.name.trim() ? record.name : undefined;

149-

})

150-

.filter((name): name is string => Boolean(name));

151-

return new Set(names);

152-

}

153-154-

function couldStillBePlainTextToolCall(text: string): boolean {

155-

if (text.length > 256_000) {

156-

return false;

157-

}

158-

const trimmed = text.trimStart();

159-

return (

160-

trimmed.length === 0 ||

161-

trimmed.startsWith("[") ||

162-

trimmed.startsWith("<|channel|>") ||

163-

trimmed.startsWith("commentary") ||

164-

trimmed.startsWith("analysis") ||

165-

trimmed.startsWith("final")

166-

);

167-

}

168-169-

function createLmstudioToolCallBlock(parsed: {

170-

arguments: Record<string, unknown>;

171-

name: string;

172-

}): Record<string, unknown> {

173-

return {

174-

type: "toolCall",

175-

id: createLmstudioSyntheticToolCallId(),

176-

name: parsed.name,

177-

arguments: parsed.arguments,

178-

partialArgs: JSON.stringify(parsed.arguments),

179-

};

180-

}

181-182-

function promoteLmstudioPlainTextToolCalls(

183-

message: unknown,

184-

toolNames: Set<string>,

185-

): Record<string, unknown> | undefined {

186-

const messageRecord = toRecord(message);

187-

if (!messageRecord) {

188-

return undefined;

189-

}

190-

if (!Array.isArray(messageRecord.content)) {

191-

if (typeof messageRecord.content !== "string" || !messageRecord.content.trim()) {

192-

return undefined;

193-

}

194-

const parsed = parseLmstudioPlainTextToolCalls(messageRecord.content, toolNames);

195-

if (!parsed) {

196-

return undefined;

197-

}

198-

return {

199-

...messageRecord,

200-

content: parsed.map(createLmstudioToolCallBlock),

201-

stopReason: "toolUse",

202-

};

203-

}

204-

if (

205-

messageRecord.content.some((block) => toRecord(block)?.type === "toolCall") ||

206-

messageRecord.content.length === 0

207-

) {

208-

return undefined;

209-

}

210-211-

let promoted = false;

212-

const nextContent: Array<Record<string, unknown>> = [];

213-

for (const block of messageRecord.content) {

214-

const blockRecord = toRecord(block);

215-

if (!blockRecord) {

216-

return undefined;

217-

}

218-

if (blockRecord.type !== "text") {

219-

nextContent.push(blockRecord);

220-

continue;

221-

}

222-

const text = typeof blockRecord.text === "string" ? blockRecord.text : "";

223-

if (!text.trim()) {

224-

continue;

225-

}

226-

const parsed = parseLmstudioPlainTextToolCalls(text, toolNames);

227-

if (!parsed) {

228-

return undefined;

229-

}

230-

nextContent.push(...parsed.map(createLmstudioToolCallBlock));

231-

promoted = true;

232-

}

233-234-

if (!promoted) {

235-

return undefined;

236-

}

237-

return {

238-

...messageRecord,

239-

content: nextContent,

240-

stopReason: "toolUse",

241-

};

242-

}

243-244-

function emitPromotedToolCallEvents(

245-

stream: { push(event: unknown): void },

246-

message: Record<string, unknown>,

247-

): void {

248-

const content = Array.isArray(message.content) ? message.content : [];

249-

content.forEach((block, contentIndex) => {

250-

const record = toRecord(block);

251-

if (record?.type !== "toolCall") {

252-

return;

253-

}

254-

stream.push({ type: "toolcall_start", contentIndex, partial: message });

255-

stream.push({

256-

type: "toolcall_delta",

257-

contentIndex,

258-

delta: typeof record.partialArgs === "string" ? record.partialArgs : "{}",

259-

partial: message,

260-

});

261-

});

262-

}

263-264-

function wrapLmstudioPlainTextToolCalls(

265-

source: ReturnType<StreamFn>,

266-

context: StreamContext,

267-

): ReturnType<StreamFn> {

268-

const toolNames = resolveContextToolNames(context);

269-

if (toolNames.size === 0) {

270-

return source;

271-

}

272-

const output = createAssistantMessageEventStream();

273-

const stream = output as unknown as { push(event: unknown): void; end(): void };

274-275-

void (async () => {

276-

const bufferedTextEvents: unknown[] = [];

277-

let bufferedText = "";

278-

let ended = false;

279-

const endStream = () => {

280-

if (!ended) {

281-

ended = true;

282-

stream.end();

283-

}

284-

};

285-

const flushBufferedTextEvents = () => {

286-

for (const event of bufferedTextEvents.splice(0)) {

287-

stream.push(event);

288-

}

289-

bufferedText = "";

290-

};

291-292-

try {

293-

for await (const event of source as AsyncIterable<unknown>) {

294-

const record = toRecord(event);

295-

const type = typeof record?.type === "string" ? record.type : "";

296-297-

if (type === "text_start" || type === "text_delta" || type === "text_end") {

298-

bufferedTextEvents.push(event);

299-

if (typeof record?.delta === "string") {

300-

bufferedText += record.delta;

301-

} else if (typeof record?.content === "string" && !bufferedText) {

302-

bufferedText = record.content;

303-

}

304-

if (!couldStillBePlainTextToolCall(bufferedText)) {

305-

flushBufferedTextEvents();

306-

}

307-

continue;

308-

}

309-310-

if (type === "done") {

311-

const promotedMessage = promoteLmstudioPlainTextToolCalls(record?.message, toolNames);

312-

if (promotedMessage) {

313-

bufferedTextEvents.splice(0);

314-

bufferedText = "";

315-

emitPromotedToolCallEvents(stream, promotedMessage);

316-

stream.push({ ...record, reason: "toolUse", message: promotedMessage });

317-

} else {

318-

flushBufferedTextEvents();

319-

stream.push(event);

320-

}

321-

endStream();

322-

return;

323-

}

324-325-

flushBufferedTextEvents();

326-

stream.push(event);

327-

if (type === "error") {

328-

endStream();

329-

return;

330-

}

331-

}

332-

flushBufferedTextEvents();

333-

} catch (error) {

334-

stream.push({

335-

type: "error",

336-

reason: "error",

337-

error: {

338-

role: "assistant",

339-

content: [],

340-

stopReason: "error",

341-

errorMessage: error instanceof Error ? error.message : String(error),

342-

},

343-

});

344-

} finally {

345-

endStream();

346-

}

347-

})();

348-349-

return output as ReturnType<StreamFn>;

350-

}

351-352135

function createPreloadKey(params: {

353136

baseUrl: string;

354137

modelKey: string;

@@ -396,6 +179,7 @@ async function ensureLmstudioModelLoadedBestEffort(params: {

396179397180

export function wrapLmstudioInferencePreload(ctx: ProviderWrapStreamFnContext): StreamFn {

398181

const underlying = ctx.streamFn ?? streamSimple;

182+

const streamWithPlainTextToolCalls = createPlainTextToolCallPromotionWrapper(underlying);

399183

return (model, context, options) => {

400184

if (model.provider !== LMSTUDIO_PROVIDER_ID) {

401185

return underlying(model, context, options);

@@ -406,11 +190,7 @@ export function wrapLmstudioInferencePreload(ctx: ProviderWrapStreamFnContext):

406190

}

407191

const providerConfig = ctx.config?.models?.providers?.[LMSTUDIO_PROVIDER_ID];

408192

if (!shouldPreloadLmstudioModels(providerConfig)) {

409-

const stream = underlying(withLmstudioUsageCompat(model), context, options);

410-

return (async () => {

411-

const resolvedStream = stream instanceof Promise ? await stream : stream;

412-

return wrapLmstudioPlainTextToolCalls(resolvedStream, context);

413-

})();

193+

return streamWithPlainTextToolCalls(withLmstudioUsageCompat(model), context, options);

414194

}

415195

const providerBaseUrl = providerConfig?.baseUrl;

416196

const resolvedBaseUrl = resolveLmstudioInferenceBase(

@@ -485,9 +265,9 @@ export function wrapLmstudioInferencePreload(ctx: ProviderWrapStreamFnContext):

485265

// LM Studio uses OpenAI-compatible streaming usage payloads when requested via

486266

// `stream_options.include_usage`. Force this compat flag at call time so usage

487267

// reporting remains enabled even when catalog entries omitted compat metadata.

488-

const stream = underlying(withLmstudioUsageCompat(model), context, options);

268+

const stream = streamWithPlainTextToolCalls(withLmstudioUsageCompat(model), context, options);

489269

const resolvedStream = stream instanceof Promise ? await stream : stream;

490-

return wrapLmstudioPlainTextToolCalls(resolvedStream, context);

270+

return resolvedStream;

491271

})();

492272

};

493273

}