Download docs/prompt-compatibility.md from a3216/ds2api: direct link, hf CLI and curl.
- Browser
- Download file 35.7 kB
-
https://huggingface.co/spaces/a3216/ds2api/resolve/main/docs/prompt-compatibility.md
- Command line
-
hf download hf://spaces/a3216/ds2api/docs/prompt-compatibility.md
-
curl -L -o prompt-compatibility.md https://huggingface.co/spaces/a3216/ds2api/resolve/main/docs/prompt-compatibility.md
API → 纯文本 web-chat 上下文兼容流程
本文是本仓库「把结构化 API 请求翻译成 DeepSeek web-chat 纯文本 prompt,再把上游回答翻译回目标协议」这条链路的源文档。AGENTS.md 规定:任何影响消息归一化、工具提示词注入、提示词可见的工具历史、文件/引用处理、历史拆分、完成载荷组装的改动,都要在同一变更里更新本文。
代码位置对应关系:
| 环节 | 主要代码 |
|---|---|
| 协议适配器 | internal/httpapi/openai/、internal/httpapi/claude/、internal/httpapi/gemini/、internal/httpapi/ollama/ |
| 标准化与 prompt 拼装 | internal/promptcompat/、internal/prompt/ |
| 思考/联网解析 | internal/util/thinking.go、internal/util/search.go、internal/config/models.go |
| 上下文拆分 | internal/httpapi/openai/history/current_input_file.go |
| 工具调用开关 / 技能注入 | internal/promptcompat/(提示词拼装)、Admin /admin/skills 处理器 |
| 完成载荷 | internal/promptcompat/standard_request.go(CompletionPayload) |
| 响应归档 | internal/chathistory/、internal/httpapi/admin/history/ |
1. 端到端流程
协议适配器(OpenAI Chat / OpenAI Responses / Claude / Gemini / Ollama)
│ 只做:解析协议请求 → 归一化消息 → 填 StandardRequest → 回写协议响应
▼
StandardRequest(messages、final_prompt、thinking、search、tool 策略、ref_file_ids …)
│
├─ 执行契约注入(thinking_injection,旧称「思考格式注入」)
│ 写入最新一条 user 消息,并落到 final_prompt 的助手起笔标记之前
▼
上下文衔接(current input file / 历史拆分)
│ 达到阈值时上传「对话记录」文件,改写 prompt 只留衔接指令(内含「执行要求」)
│ tools.enabled=false 时不注入工具说明(也不上传工具说明文件),不解析 tool_calls
│ 只有本轮真的发生了拆分,才把执行契约重新贴回可见 prompt(见第 4 节)
▼
prompt 级叠加(模型提示词 / 特化层 → 技能层)
│ 自定义模型带提示词时用它,否则用 prompt_profile(两者互斥,都缺省关闭)
│ skills(缺省关闭)在内层;全部合并成一个 System 段前置到 prompt 最前面
│ 只改 prompt / PromptTokenText,不改 messages
▼
组装 DeepSeek completion payload
│ thinking_enabled / search_enabled / model_type / prompt / ref_file_ids
▼
上游 SSE → 协议回写(含空输出重试、工具调用解析、引用标记处理)→ 响应记录
| 阶段 | 输入 | 输出 | 共享性 |
|---|---|---|---|
| 协议归一化 | HTTP 请求体 | StandardRequest |
每个适配器一份,属边界代码 |
| 模型/思考/联网解析 | 请求字段 + 模型名 | Thinking、Search、ModelType |
全协议共享,只有一处 |
| 执行契约注入 | StandardRequest + API Key 注入设置 |
注入后的消息与 prompt | 全协议共享 |
| 历史拆分 | 注入后的消息 | 上下文文件 + 精简 prompt + 重贴的契约 | 全协议共享 |
| prompt 级叠加 | 拆分后的 prompt | 前置了 System 段(特化层 + 技能)的 prompt | 全协议共享,只改 prompt 词串 |
| 载荷组装 | StandardRequest |
map[string]any payload |
全协议共享 |
| 响应回写 | 上游 SSE / JSON | 协议响应 + 记录 | 适配器渲染 + 共享归档 |
四个会产生对话的入口(/v1/chat/completions、/v1/responses、/v1/messages、Gemini generateContent)都调用同一个 history.Service.ApplyCurrentInputFile,没有第二套历史拆分实现。Ollama 只提供 /api/version、/api/tags、/api/show,不参与对话链路。
2. 统一标准请求
promptcompat.StandardRequest 是跨协议唯一的数据形状。适配器填它,共享逻辑读它,渲染层再据此回写。
| 字段 | 含义 |
|---|---|
Surface |
来源接口,如 openai_chat、openai_responses、anthropic_messages、google_gemini |
RequestedModel / ResolvedModel / ResponseModel |
客户端原始模型名 / 解析后的规范模型 / 响应里回显的模型名 |
Messages |
归一化后的消息数组,历史拆分与注入都作用于它 |
FinalPrompt |
拼装完成的纯文本 prompt |
PromptTokenText |
用于 token 估算的文本,通常与 FinalPrompt 同步改写 |
ToolsRaw / ToolNames / ToolChoice |
工具定义、可用工具名、auto/none/required/forced 策略 |
Thinking / Search |
本次请求实际生效的思考、联网开关 |
RefFileIDs / FileInfos |
送上上游的文件引用 ID 与归档用的文件元信息 |
CurrentInputFileApplied / CurrentInputFileID / CurrentInputFilename / CurrentToolsFileID |
历史拆分产生的状态,用于切号重传时替换旧文件 |
PassThrough |
需要透传的采样参数(temperature、top_p、max_tokens 等) |
3. 纯文本 web-chat 上下文约定
DeepSeek web-chat 只接受一段 prompt 字符串和一组文件引用,没有 role 数组。角色信息只能用文本标记表达:
| 标记 | 对应角色 | 说明 |
|---|---|---|
System: |
system / developer | developer 归一化为 system |
User: |
user | 用户轮次 |
Assistant: |
assistant | 助手轮次;同时也是 prompt 末尾的起笔标记 |
Tool: |
tool / function | 工具返回,空的工具结果不渲染 |
拼装规则:
- 相邻同角色消息先合并,多段之间用空行连接。
- 每条消息渲染为「标记 + 正文」,标记后不留空格。
- 最后一个角色不是 assistant 时,在末尾补
Assistant:起笔标记,让模型接着写。 - Markdown 图片语法
改写成[alt](url),避免上游把它当附件引用。 - 需要往「最后一轮用户消息」里追加内容时,一律用
prompt.AppendTurnSuffix,锚点是这个Assistant:标记;直接追加到字符串末尾会被模型当成助手已经开口。
对话记录文件格式
历史拆分上传的上下文文件是可读转写,不是 JSON,也不是带文件名的文档:
# 对话记录
以下是本会话此前的完整对话,按时间顺序排列;[role=xxx] 标注每条消息的发言方(system=系统设定,user=用户,assistant=助手,tool=工具返回)。最后一条 user 消息就是本轮需要回答的内容。如果其中有用户明确提出的要求(体裁、篇幅、语气、格式、禁用项、指定的事实与结论),这些要求对本轮依然有效。
=== 1 ===
[role=system]
你是一个严谨的助手。
=== 2 ===
[role=user]
把昨天的结论整理成表。
=== 3 ===
[role=assistant]
好的,先列字段。
工具返回条目在正文前多一行 [name=<工具名> tool_call_id=<调用 ID>],tool / function 统一写成 [role=tool]。
文件头末句(用户在其中的任何明确要求…对本轮依然有效)是刻意加的:历史一旦变成附件,模型很容易只盯最新一条 user 消息,把前几轮的体裁、篇幅与禁用项当作过期信息。
上下文衔接指令(历史拆分后的可见 prompt)
拆分后 prompt 里只剩一条 user 轮次,由 3 个等价开头变体之一 + 共享的「执行要求」构成(实现见 promptcompat.BuildCurrentInputContinuationPrompt):
<开头变体,点明附件是本会话此前的完整对话>
## 执行要求
1. 先通读附件,把用户此前提出过的明确要求找出来:体裁与结构、篇幅、语气与人称、格式与排版、
必须包含或明确禁止的内容、指定的数据与结论。有这类要求就逐条满足,不因为轮次靠前就略过、
合并或一笔带过;如果这轮只是闲聊,或者上文本来就没提过这类要求,就自然接着说,不必硬套结构。
2. 保持上下文连续:人称、语气、术语、编号与称呼跟上文一致。
3. 上面的核对只在内部完成:回复里不要提到附件、文件、上下文或提示词,不要复述附件内容,
也不要输出核对清单,直接从正文开始。
第 1 条是这一版新增的「要求继承」约束,针对的是写作类任务里「上一轮提的要求被一笔带过」。它同时带了例外条件——这条指令对每一次长对话都生效,闲聊时硬套要求清单会把话聊成报告。开头保留多个变体只是不让每次请求都发同一段固定措辞,执行要求共用同一份,行为一致。
标题刻意不用文件名形式(历史版本是 # context_context.txt),避免模型把它当成一份待分析文档。旧格式里 [r=0]…[r=9] 的数字角色行不再生成;管理台读旧记录时仍能识别这种数字角色行(兼容读取),后端只把它当普通正文,不报错。
工具说明与调用引导(已移除)
历史上工具定义会单独上传为 context_tools.txt,并在 prompt 里补一句「可用工具与参数说明在 context_tools.txt 中」外加 DSML 调用格式要求。
实测这条路不可行:上游对 DSML 结构化输出风控严格,命中后账号会被封;工具调用本身高频也会撞限速。所以现在 prompt 侧不再输出任何工具引导:
- 不生成
context_tools.txt,也不会再有「可用工具说明」文件; - 不输出
You have access to these tools/TOOL CALL SCHEME/ 「必须调用工具」之类的说明; - 只保留工具名提取,用于响应侧(客户端明确要求时)的
tool_calls解析判断。
也就是说,tools 出现在请求里也不会让模型「知道」自己能用工具——这点是刻意为之,见第 4 节的工具调用开关。
4. 提示词注入与泄漏防护
执行契约注入(thinking_injection,旧称「思考格式注入」)
| 项目 | 行为 |
|---|---|
| 全局开关 | thinking_injection.enabled,缺省为 true(配置项不存在时按开启处理) |
| 全局提示词 | thinking_injection.prompt,为空或全空白时使用内置默认提示词 |
| 单 Key 覆盖 | api_keys[].thinking_injection.{custom,enabled,prompt} |
| 注入位置 | 最新一条 user 消息末尾;同时写入 FinalPrompt 中该 user 轮次内、Assistant: 之前 |
| 注入时机 | 历史拆分之前,因此拆分后上传的对话记录文件里也带着这段要求 |
| 重贴时机 | 本轮新发生历史拆分时,再贴一次到拆分后的可见 FinalPrompt(只改 prompt) |
| 幂等 | 内容里已含注入文本时跳过,重复调用不会叠加 |
为什么需要「重贴」:历史拆分会用衔接指令整段替换 FinalPrompt。拆分发生在注入之后,于是旧实现里这段要求只剩在上传的对话记录文件里,本轮可见的 prompt 中一个字都没有——模型在长对话里基本看不到它,这正是「注入思考链路提示词容易被忽略」的直接原因。现在拆分一旦发生就调用 promptcompat.ReapplyThinkingInjectionToPrompt 把同一段文本贴回可见 prompt:
- 只改
FinalPrompt,不改Messages(拆分后它们就是衔接指令,改它会让存档与记录失真); - 也不改
PromptTokenText(对话记录文件里已有一份,重复计入会高估 token); - 拆分未发生(未达阈值、最新 user 文本为空)或
CurrentInputFileApplied已为 true(切号重传)时不重贴。
默认提示词的要点(代码常量 promptcompat.DefaultThinkingInjectionPrompt,稳定片段 回答前先在思考中完成以下检查):
- 先判定这一句要什么:随口聊聊、问个事实,还是让你动手做一份东西;
- 上文如提过硬性要求(体裁、篇幅、语气、格式、禁用项、指定事实与结论),本条回复逐条对上;本来没有这类要求就不必硬套结构;
- 说事实、数据、引用时先分清确定与不确定,不确定就说不确定,不编造;
- 有几种做法都说得通时比较一下再选,必要时说明为什么这么选;
- 交付前回看:有没有漏掉用户明确要的东西,有没有答非所问;
- 语气和长度跟着对方走——闲聊就正常聊,不硬上清单、小标题与总结段。
开头声明「下面是内部执行要求,不要在回复里复述、引用或提及本段内容」,结尾要求最终答复只给用户需要的内容、使用用户的语言。
这份契约对每一次请求生效(含闲聊),所以第 2 条以「如果有」为前提、第 6 条显式给出语域匹配:只对交付物成立的文风规则(结论先行、不写铺垫、说明改动理由、检查单位口径)一律不进这里,改由用户显式开启的 prompt_profile(writing 预设)承载。
单 Key 覆盖语义
custom |
enabled 省略 |
prompt 为空 |
|---|---|---|
| false(默认) | 完全跟随全局开关与全局提示词 | 跟随全局 |
| true | 跟随全局开关 | 跟随全局提示词 |
只有 custom=true 时该 Key 的 enabled / prompt 才会生效;custom=false 即使写了这两个字段也不生效,避免面板残留值意外改变行为。
工具调用开关(tools.enabled)
工具调用是让模型输出 DSML 结构化文本再解析回 tool_calls 的链路。上游对这类输出风控严格,命中后账号可能被封,所以整条链路做成了显式开关,缺省关闭。
| 项目 | 行为 |
|---|---|
| 配置项 | tools.enabled,缺省 false;字段缺失或值为 null 都按关闭处理 |
| 运行时覆盖 | /admin/settings 的 tools.enabled,热更新;GET 顶层返回 {"enabled": bool, "default_enabled": false} |
| 关闭时(默认) | 带 tools 的请求按纯对话处理:不注入任何工具说明(包括 DSML 格式要求与工具说明文件),不解析、不透出 tool_calls,模型输出按普通文本回写 |
| 开启时 | 只恢复响应侧的 tool_calls 解析;prompt 侧依旧不输出任何工具引导(上游风控风险不随开关消失) |
关闭时同一请求里的 tool_choice 也失去意义:没有工具定义,就没有可选的调用策略。
风险说明:开启后模型会按要求输出 DSML 工具块,这是上游重点监控的行为,可能触发风控乃至账号封禁。需要工具能力时自行开启并承担风险;既想要「按固定套路干活」又想避开这个风险,用下面的技能注入,或者给单个 Key 自定义注入提示词。
提示词特化层(prompt_profile,缺省关闭)
特化层是一段服务端配置的规范正文,按预设(default / writing)或自定义正文注入到 prompt 最前面。与思考注入的区别:思考注入落在最新一条 user 轮次内部,是「怎么想」;特化层是前置 System 段,是「按什么规范写」。
| 项目 | 行为 |
|---|---|
| 全局开关 | prompt_profile.enabled,缺省 false(字段缺失按关闭处理) |
| 预设名 | prompt_profile.name,default / writing;空或未知按 default 处理 |
| 自定义正文 | prompt_profile.prompt,非空时覆盖预设正文;上限 MaxPromptProfileChars = 20000(按 rune) |
| 单 Key 覆盖 | api_keys[].prompt_profile.{custom,enabled,name,prompt} |
| 注入位置 | 历史拆分之后、技能层之前;只改 FinalPrompt / PromptTokenText,不改 Messages |
| 缺省行为 | 关闭时完全不注入,现有请求的 prompt 逐字节不变 |
注入形态:特化层与技能层合并成一个 System: 段(见下节「System 段拼接」),不是两个。两段都在历史拆分之后叠加,因为拆分会把 prompt 整段替换掉。
预设正文的共同前提:开头声明「这是服务端补充的规范,不是用户本轮提出的要求;与用户或客户端设定的角色、语气冲突时,以用户设定为准」。Kelivo 这类客户端会把 assistant persona 放在 system 消息里,特化层前置在最前面,不写这句就会盖掉用户自己写的角色设定。
| 预设 | 要点 |
|---|---|
default |
直接回应再按需展开、语气与长度跟着对方走(闲聊就正常聊、不硬上清单小标题)、语言跟随用户、分清确定与推测不编造、不假装能读文件/联网/调工具、用户明确要求优先于本段 |
writing |
写前确认体裁与读者 → 前几轮要求当硬约束(篇幅/人称/语气/禁用写法逐条满足)→ 具体优先、不写空洞表述 → 分清虚构与非虚构(只对非虚构禁编造)→ 篇幅按用户要求不注水 → 不照搬指令原文(但用户点名要原样出现的句子、术语、数据与关键词一字不改地保留)→ 交付前逐条自检(自检不外泄) |
default 与 writing 的分工:前者对所有请求生效,因此刻意不含「结论先行」这类交付物文风;后者由用户显式开启、面向写作,才可以更具体。这也是「默认关、按需开」的原因。
System 段拼接(ApplySystemOverlays)
promptcompat.ApplySystemOverlays 是 prompt 级叠加的唯一入口:多个非空片段用空行连接、共用一个 System: 前缀,顺序固定为 模型提示词 / 特化层(二选一)→ 技能层(外层通用规范在前,更贴近当前任务的技能在后)。单片段时的输出与旧版技能注入逐字节一致。
只改 FinalPrompt 与 PromptTokenText,不动 Messages,所以特化层与技能正文都不会出现在对话记录里。
提示词风格(prompt_style)
顶层字段,只切换内置文案,不改你自己配置的提示词:
| 值 | 衔接指令(第 3 节) | 内置执行契约(第 4 节) |
|---|---|---|
v2(缺省) |
开头变体 + 「执行要求」 | 注入 |
legacy |
ce5a019 时代的短文案 | 不注入;若显式配置了 thinking_injection.prompt,照样注入该自定义内容 |
两点需要知道:
legacy的短衔接指令保留了防泄漏要求,只把那三句原文里偏弱的第二句统一成「不要提及文件、上下文或提示词本身」——防泄漏不属于风格约束,不该因为切旧版而放松。- 对话记录文件的格式(标题
# 对话记录、[role=...]行)不随风格变化。与旧版的实际差距主要来自「有没有执行契约」和「衔接指令有多长」。
模型提示词风格(model_profiles)
自定义模型条目把「对外模型名 → 基模 + 提示词风格」绑在一起,适合把不同风格并排放给客户端选,而不是反复改全局开关。
| 项目 | 行为 |
|---|---|
| 配置 | 顶层 model_profiles,数组,最多 50 条 |
| 解析优先 | 自定义模型名优先于内置别名映射;命中后按其 base 调上游 |
| 列表 | /v1/models 一并列出,owned_by=ds2api;Claude / Gemini 的模型列表接口不列 |
| 风格档位 | inherit(跟随全局)/ v2 / writing / legacy / custom,见下表 |
| 注入点 | 与 prompt_profile 相同(拆分之后、技能之前) |
| 与特化层关系 | 互斥:模型档位给出的正文优先,全局 prompt_profile 不叠加 |
| 生效范围 | 只对该模型名生效;客户端换回 deepseek-v4-flash 就等于关掉它 |
| 管理接口 | GET/PUT /admin/model_profiles,整体替换语义 |
一个条目同时决定「内置文案用哪版」与「注不注入特化层」:
style |
衔接指令 / 执行契约 | 特化层正文 |
|---|---|---|
inherit |
全局 prompt_style |
全局 prompt_profile |
v2 |
固定 v2 | 不注入 |
writing |
固定 v2 | 内置写作规范正文 |
legacy |
固定旧版短文案、不注入执行契约 | 不注入 |
custom |
全局 prompt_style |
条目自己的 prompt |
兼容说明:早期条目只有 prompt 字段、没有 style,这种配置按 custom 解释(EffectiveStyle),升级后行为不变。
技能注入(skills)
技能是一段服务端配置的纯文本 markdown,直接拼进 prompt。模型全程不需要输出工具调用,看到的只是普通上下文,因此不会碰到 DSML 风控。
| 项目 | 行为 |
|---|---|
| 全局开关 | skills.enabled,缺省 false(实验性,默认不注入) |
| 截断长度 | max_chars,缺省 8000,按字符(rune)统计;截断时在末尾追加「(技能内容已截断)」;0 表示不限制 |
| 条目数 | items 上限 50 条 |
id |
必填、唯一,字符集 [a-z0-9_-] |
body |
必填,单条上限 100000 字符,全部条目合计上限 500000 字符 |
| 管理接口 | GET/PUT /admin/skills,需要 Admin 鉴权,PUT 为整体替换 |
| 校验失败 | 超限、重复 id、非法 id 一律 400 |
单 Key 覆盖(api_keys[].skills = {"custom": ..., "enabled"?: ..., "ids"?: [...]}):
custom |
enabled |
ids |
结果 |
|---|---|---|---|
省略或 false |
任意 | 任意 | 使用全部 items |
true |
false |
任意 | 该 Key 不注入 |
true |
省略或 true |
非空 | 只用命中的 items |
true |
省略或 true |
空 | 不注入 |
全局 skills.enabled=false 时上表全部不生效,任何 Key 都不注入。
注入形态与时机:
- 形态是前置 System 段:拼在最终 prompt 最前面,
FinalPrompt由<原 prompt>变成System: <技能块>\n<原 prompt>,PromptTokenText同样处理。与特化层同时启用时两者合成一个System:段(特化层在前),见上节ApplySystemOverlays。 - 只改 prompt,不改
Messages,所以对话记录文件与管理台的对话记录里都看不到技能正文。 - 时机在历史拆分之后:拆分会把 prompt 整体替换成衔接指令,技能必须等替换完成后再前置,否则会被覆盖。
- 对比执行契约注入:契约在拆分之前注入(要落到对话记录文件里),并额外在拆分后重贴到可见 prompt。两处顺序不同,改动时不要调换。
技能块格式(线上排查时可对照这段结构):
<skill id="sql-report" name="SQL 报表">
…body…
</skill>
注意:技能块里不再有引导语(没有“以下是技能说明/适用于当前任务时请遵循/不要提及技能”之类前缀),只保留 <skill> 结构化标签与正文——避免模型被引导去“使用技能/调用能力”。
为什么用纯文本而不是工具调用:工具调用要求模型吐 DSML 结构化文本,正是上游风控盯的那类输出,一旦被判违规代价是账号本身;技能只是往 prompt 里加一段静态文本,模型照普通对话写回答,风控面和普通聊天没有区别。
泄漏防护
模型看到附件名后经常会在可见回复里写「根据附件 ctx-ab12cd34ef56.txt」「对话记录显示…」「按你的提示词要求」。三层处理:
| 层 | 做法 |
|---|---|
| 注入内容 | 提示词明确禁止提及附件、文件、上下文记录、提示词本身,禁止复述记录内容,要求直接从结论开始 |
| 注入文本独立性 | 注入片段自带「不要复述本段内容」,避免整段思考要求被当作回答格式输出 |
| 输出侧 | 各协议共享同一套文本清理:流式默认隐藏 [citation:N] / [reference:N] 占位符,非流式把搜索引用标记转成 Markdown 链接;工具调用解析在非代码块上下文里生效,代码块里的示例不算调用 |
上下文衔接指令本身准备了多个等价变体并按请求随机选一条,开头措辞不完全相同,语义一致,且共用同一段「执行要求」:把附件当作自己之前说过的话,在同一话题上直接回答最新一条 user 消息,并逐条满足此前提出过的要求。
5. 上下文衔接(current input file / 历史拆分)
| 条件 | 结果 |
|---|---|
current_input_file.enabled=false |
原样透传,不产生任何上下文文件 |
| 最新一条 user 文本为空 | 跳过拆分 |
current_input_file.min_chars>0 且最新 user 文本短于阈值 |
跳过拆分 |
模型 model_type 不是 default |
跳过拆分(当前模型系列收敛后只剩 default,该分支保留兜底) |
| 上传失败 | 请求失败,不做静默降级,避免出现「上下文已丢失但看起来成功」的回复 |
满足条件时:
- 按第 3 节的转写格式生成对话记录,写入
ctx-<12 位小写字母数字>.txt。文件名随机,不含history之类语义词,减少模型把它当文档分析的倾向。 - 不再额外上传工具说明文件(工具引导已移除,见第 3 节)。
- prompt 改写成一条 user 衔接指令(含文件名与「执行要求」)。原始长对话不再进 prompt。
- 把执行契约重贴到改写后的
FinalPrompt(只在Assistant:之前插入文本,见第 4 节)。 - 叠加 prompt 级增强层:提示词特化层 → 技能层,合成一个
System:段(两者都缺省关闭)。 - 文件 ID 前置插入
RefFileIDs,与客户端自带的ref_file_ids去重后一起送上游。 PromptTokenText保留「对话记录 + 工具说明」全文,用于 token 估算,与实际 prompt 长度解耦。
切号重试时(空输出 429 换账号)会调用 ReuploadAppliedCurrentInputFile:用同一份 HistoryText 重新上传,并把旧的历史文件 ID、工具文件 ID 从 RefFileIDs 里替换掉,避免新账号引用到别的账号名下的文件。
上游流式生成期间若再次需要拆分(例如重试),沿用已记录的 CurrentInputFilename,同一轮对话不会出现两个名字不同的上下文文件。
6. 模型 / 思考 / 联网解析优先级
模型
/v1/models、/api/tags(Ollama)、/anthropic/v1/models 只公布 flash 系列:
| 对外模型 ID | 默认思考 | 默认联网 |
|---|---|---|
deepseek-v4-flash |
开 | 关 |
deepseek-v4-flash-search |
开 | 开 |
deepseek-v4-flash-nothinking |
强制关 | 关 |
deepseek-v4-flash-search-nothinking |
强制关 | 开 |
上游已把 v4-pro / v4-vision 合并进 v4-flash,对外不再公布:
| 旧模型名 | 解析结果 |
|---|---|
deepseek-v4-pro |
deepseek-v4-flash |
deepseek-v4-pro-search |
deepseek-v4-flash-search |
deepseek-v4-vision |
deepseek-v4-flash |
静默映射,保留 -nothinking 后缀:deepseek-v4-pro-nothinking → deepseek-v4-flash-nothinking。老客户端保存过的模型名继续可用,只是不再出现在列表里。
上游 model_type 只剩 default。内置别名(gpt-*、o*、claude-*、gemini-*)全部指向 flash 系列,其中 o3-deep-research、o4-mini-deep-research 指向 deepseek-v4-flash-search。config.model_aliases 可覆盖或补充同名映射。
思考
优先级从低到高:
- 模型默认值(
-nothinking变体默认关,其余 flash 模型默认开); - 客户端显式覆盖;
-nothinking模型强制关闭(最高)。
客户端覆盖按以下顺序查找,命中即返回,extra_body 里的同名键也会被读取:
| 来源 | 接受写法 |
|---|---|
thinking |
bool;"enabled" / "disabled"(含 on/off/true/false/none);{"type": ...} |
reasoning |
{"effort": ...}、{"type": ...}、{"enabled": ...};字符串按 effort 解析 |
reasoning_effort |
minimal / low / medium / high / xhigh / max = 开启;none / disabled / disable / off / false = 关闭 |
Kelivo 这类客户端把界面上的「思维强度」翻译成 reasoning_effort:没有这个字段表示跟随默认(即开启思考),传 none 表示关闭。minimal 到 max 都只表示「要思考」,本项目不区分强度档位——上游只有开/关两态。
联网搜索
优先级:模型默认(-search 变体为开)→ 客户端显式信号优先。
以下任一命中即视为客户端显式开启:enable_search(bool 或 "true"/"false")、web_search_options 出现、search_options 出现、tools 里带原生搜索类型(type 以 web_search 开头、type=google_search、或工具对象里有 googleSearch / google_search / web_search 字段)。enable_search 显式 false 也会生效,用于在 -search 模型上临时关掉联网。
普通 function 工具不算信号。像 Kelivo 客户端自己执行的 search_web 始终是普通 function tool,默认仍不开启上游联网。
现状说明:Kelivo 对「自定义 OpenAI 兼容服务」不会发送原生搜索标记,所以要用联网请选 -search 模型;一旦客户端开始发送上述显式信号,服务端会自动跟随,不需要改配置。
自定义模型(model_profiles)
model_profiles 里的模型名与内置模型走同一套解析:config.ResolveModel 先查自定义条目,再退回内置模型与别名映射。因此:
-nothinking/-search这类后缀语义仍然只作用于基模:base直接写完整名字(如deepseek-v4-flash-search-nothinking),自定义模型名本身不再二次解释后缀。- 思考与联网的默认值同样按解析后的基模决定,因此「某个自定义模型强制关思考」的做法是给它选
-nothinking基模,而不是在条目上加开关。 /v1/models会列出全部内置模型 + 自定义模型;/anthropic/v1/models与 Ollama/api/tags仍只列内置模型。
7. 协议适配器边界
适配器只允许做这些事:
| 允许 | 说明 |
|---|---|
| 解析协议请求体 | OpenAI、Anthropic、Gemini 各自的字段形状差异 |
| 消息归一化 | 把 content 数组、图片块、tool_use / tool_result、functionDeclarations 等翻译成统一消息 |
填 StandardRequest |
含 Surface、模型名、工具、透传参数 |
| 渲染协议响应 | 非流式结构、SSE 事件名与顺序、finish_reason / stop_reason / finishReason 映射 |
| 协议专属错误形态 | 错误对象字段名、HTTP 状态码对齐 |
不允许在适配器里实现(必须放共享路径):
| 不允许 | 共享实现位置 |
|---|---|
| 空输出重试与切号 | internal/completionruntime/、各协议的 empty_retry_runtime 共用同一策略 |
| 思考/联网解析 | internal/util/thinking.go、internal/util/search.go |
| 工具调用检测与策略 | internal/toolcall/、internal/httpapi/openai/shared/ |
| 思考格式注入 | promptcompat.ApplyThinkingInjectionToRequest(拆分后重贴用 ReapplyThinkingInjectionToPrompt) |
| prompt 级叠加(特化层 / 技能) | promptcompat.ApplySystemOverlays |
| 历史拆分与文件上传 | internal/httpapi/openai/history/ |
| 完成载荷组装 | StandardRequest.CompletionPayload |
| 响应记录 | internal/chathistory/ |
确有必须按协议不同的行为(例如 Claude 输出 thinking block 而 Gemini 输出 parts[].thought=true),保持在渲染层,并在代码注释里写明为什么不能进共享路径。
8. 变更记录与兼容性
| 变更 | 兼容策略 |
|---|---|
| 模型系列收敛到 flash 4 个 ID | pro / vision 老名字静默映射到 flash 对应项,-nothinking 后缀保留;不再对外公布 |
model_type 只剩 default |
旧配置里出现 expert / vision 也不会让请求失败,解析时按 default 处理 |
上下文文件标题从 # context_context.txt 改为 # 对话记录 |
旧文件是纯文本,读回不报错;新文件不再使用文件名式标题 |
对话记录角色行从 [r=0]…[r=9] 改为 [role=user] 等 |
管理台解析同时接受 [role=...] 与旧 [r=N];后端只把记录当纯文本 |
| 移除工具说明文件与调用引导 | 不再生成 context_tools.txt、不输出 DSML 格式要求;tools 只影响响应侧解析,取决于 tools.enabled |
| 移除技能注入的引导语 | 技能块只保留 <skill> 标签与正文,不再有“请遵循技能”这类前缀 |
思考格式注入从 internal/httpapi/openai/shared 移到 internal/promptcompat |
四个对话接口共用同一实现,配置项与字段名不变 |
| 联网信号改为「模型默认 + 客户端显式覆盖」 | 只在客户端发显式信号时改变行为;不发信号的客户端表现与之前一致 |
新增 tools.enabled,缺省 false |
默认行为变化:未显式开启时不再注入工具说明、不再返回 tool_calls;老客户端带 tools 的请求会退化为纯对话,需要工具能力请在 /admin/settings 或配置文件显式开启 |
新增 skills 纯文本技能库(实验性,缺省关闭) |
默认不注入,不改变现有请求的 prompt;开启后只在 FinalPrompt / PromptTokenText 最前面加一个 System 技能段,Messages 与对话记录不受影响 |
内置提示词收敛到 internal/promptcompat(prompt_texts.go 为唯一来源) |
文案内容全部改写;history 包只保留 currentInputFilePrompt 这一层委托,不再自带文案。行为变化默认生效 |
| 衔接指令新增「执行要求」(要求继承 + 反泄漏) | 拆分后的 prompt 变长、更结构化;PromptTokenText 不受影响(它统计的是对话记录全文,与实际 prompt 解耦) |
| 内置提示词去掉只对交付物成立的硬性文风规则 | 对每一次请求生效的层(L1/L2/L3-default)不再要求「结论先行」「不写铺垫与总结段」「说明改了什么、为什么」「检查单位与口径」;这些要求要么改成带触发条件(「有这类要求就…,没有就不必…」),要么只留在用户显式开启的 writing 预设里。闲聊形态因此不再被压成公文 |
| 修复:历史拆分后执行契约被埋在附件里 | 拆分发生时把契约重贴到可见 FinalPrompt(只改 prompt,不改 Messages / PromptTokenText)。关闭思考注入或未发生拆分时行为不变 |
新增 prompt_profile 提示词特化层(缺省关闭) |
默认不注入,现有请求逐字节不变;开启后只改 FinalPrompt / PromptTokenText,Messages 与对话记录不受影响 |
技能注入改为走 ApplySystemOverlays |
单片段输出与旧实现逐字节一致;只有与特化层同时开启时才合并成一个 System: 段 |
新增 prompt_style(v2 缺省 / legacy) |
缺省仍是 v2,现有请求不变;切 legacy 时衔接指令回到短文案且不注入内置执行契约(自定义思考提示词仍生效)。未知值回落 v2,不会让配置加载失败 |
新增 model_profiles 自定义模型列表(缺省空) |
空列表时行为与之前完全一致;条目只影响请求里出现该模型名的调用,其他请求不受影响。自定义模型名优先于内置别名映射 |
9. 相关文档
- prompt-design.md:提示词方案(分层职责、各层文案全文、写作特化 preset 与可粘贴 skills 条目、客户端适配、调优建议)
- API.md / API.en.md:请求字段、响应结构、Admin 接口
- README.en.md:模型列表与部署入口
- AGENTS.md:适配器边界与文档同步要求