预设兼容:按真实抓包对齐装配与包装层,补上声明式工具与预设正则。

- 装配只认 prompt_order.enabled;squash_system_messages 只合并相邻 system(新增 system 档,连接符按酒馆用单换行)
- 包装层:new_chat_prompt、示例对话(new_example_chat_prompt + <START> 切段 + name)、角色卡三块与 personality_format/scenario_format、{{user}}/{{char}} 取值、任务契约并进预设条目
- 工具协议:按 ToolBindings / 伪工具协议数据声明真 tools(presetToolPolicy=preset),工具描述用到的条目不再重复作为消息发出
- 正则:promptOnly 作用于对话历史(含 depth 过滤)、写入时脚本改写回复;{{//}} 注释按配平扫描
- tools/st-request-diff:真假上游抓包对比工具、三份预设的机制结论与实测报告
This commit is contained in:
2026-10-23 00:13:08 +08:00
parent ed09e5c831
commit 9dc927282f
94 changed files with 11558 additions and 413 deletions
+60 -7
View File
@@ -1,5 +1,7 @@
import type { LlmConfig } from "../config/env.js";
import { ensureNetworkProxy } from "../config/network.js";
import type { GenerationParameters } from "../types/preset.js";
import type { WorldInfoPack as PresetPackForFrame } from "../preset/world-info-pack.js";
import {
applyOpenAiGeneration,
fetchWithGenerationCompat,
@@ -24,13 +26,14 @@ export type ToolCallPayload = {
};
export type ChatMessage =
| { role: "system" | "user"; content: string }
| { role: "system" | "user"; content: string; name?: string }
| {
role: "assistant";
content: string | null;
tool_calls?: ToolCallPayload[];
name?: string;
}
| { role: "tool"; content: string; tool_call_id: string };
| { role: "tool"; content: string; tool_call_id: string; name?: string };
export type ToolDefinition = {
type: "function";
@@ -117,6 +120,11 @@ export type CompleteOptions = {
/** 统计补充。不进上游请求体。 */
usageDetail?: UsageDetail;
usageSink?: UsageSink;
/**
* 内部:play 真多轮装配的分袋上下文(由 worker 从 contextSegments 切成四袋 + 历史轮次)。
* 不进上游请求体;由 PresetLlmProvider 取用后丢弃。
*/
presetPack?: PresetPackForFrame;
};
export type ToolChoice =
@@ -223,6 +231,33 @@ export function ensureJsonObjectPrompt(messages: ChatMessage[]): ChatMessage[] {
return [{ role: "system", content: JSON_OBJECT_HINT }, ...messages];
}
/** 空正文时报得能定位:是限额吃光了,还是模型真没给。 */
function emptyToolResponseMessage(params: {
stream: boolean;
finishReason?: string;
reasoningChars?: number;
usage?: TokenUsage;
}): string {
const bits: string[] = [];
if (params.finishReason) bits.push(`finish_reason=${params.finishReason}`);
if (params.reasoningChars) bits.push(`思维链=${params.reasoningChars}字`);
if (params.usage) {
bits.push(
`prompt=${params.usage.promptTokens}/completion=${params.usage.completionTokens}`,
);
}
const hint =
params.finishReason === "length"
? "(输出上限被思考/正文吃光,调大 maxOutputTokens)"
: params.reasoningChars
? "(只产出思维链没有正文,多半是输出上限不足)"
: "";
const head = params.stream
? "LLM 流式返回空正文且没有工具调用"
: "LLM 返回空正文且没有工具调用";
return `${head}${bits.length ? `:${bits.join(" ")}` : ""}${hint}`;
}
export function buildRequestBody(
config: LlmConfig,
messages: ChatMessage[],
@@ -431,6 +466,7 @@ async function llmFetch(
): Promise<Response> {
return fetchWithGenerationCompat(async (payload) => {
throwIfAborted(options?.signal);
await ensureNetworkProxy();
return fetch(url, {
method: "POST",
headers: {
@@ -533,17 +569,29 @@ export class OpenAiCompatibleProvider implements LlmProvider {
const data = (await response.json()) as {
model?: string;
usage?: unknown;
choices?: Array<{ message?: Record<string, unknown> }>;
choices?: Array<{
message?: Record<string, unknown>;
finish_reason?: string;
}>;
};
const parts = extractMessageParts(data.choices?.[0]?.message);
const choice = data.choices?.[0];
const parts = extractMessageParts(choice?.message);
const usage = parseUsage(data.usage);
if (!parts.content && parts.toolCalls.length === 0) {
throw new Error("LLM returned empty content and no tool calls");
throw new Error(
emptyToolResponseMessage({
stream: false,
finishReason: choice?.finish_reason,
reasoningChars: parts.reasoning?.length ?? 0,
usage,
}),
);
}
return {
content: parts.content,
toolCalls: parts.toolCalls,
reasoning: parts.reasoning,
usage: parseUsage(data.usage),
usage,
model: data.model ?? this.config.model,
};
}
@@ -576,7 +624,12 @@ export class OpenAiCompatibleProvider implements LlmProvider {
const parts = await consumeOpenAiToolStream(response.body, callbacks);
if (!parts.content && parts.toolCalls.length === 0 && !parts.reasoning) {
throw new Error("LLM stream returned empty content and no tool calls");
throw new Error(
emptyToolResponseMessage({
stream: true,
usage: parts.usage,
}),
);
}
const content =
parts.content ??