From 791d83bbacf6462768ef9ea608a7bc004bf81054 Mon Sep 17 00:00:00 2001
From: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
Date: Tue, 6 Oct 2026 15:11:03 +0800
Subject: [PATCH] docs(session-title): state the title request's real rationale
and cap source
The lowest-level dispatch keeps reasoning off the title's output ceiling,
not a small cap's visible text, and it deliberately skips the
requested-effort check. The README pair records that the dispatched cap
is the smaller of the configured ceiling and the latest folded
request/header, including the tighter-main-cap and override consequences,
and the accepted-title comment states the deliberate whole-line emphasis
rule.
---
packages/llm/llm-pi-ai/README.i18n.yaml | 4 ++--
packages/llm/llm-pi-ai/README.md | 2 +-
packages/llm/llm-pi-ai/README.zh.md | 2 +-
packages/llm/llm-pi-ai/src/adapter.ts | 7 +++++--
.../session/session-title-llm/README.i18n.yaml | 16 ++++++++--------
packages/session/session-title-llm/README.md | 7 ++++---
packages/session/session-title-llm/README.zh.md | 7 ++++---
packages/session/session-title-llm/src/index.ts | 7 +++++--
8 files changed, 30 insertions(+), 22 deletions(-)
diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml
index fb2aac4767..d883753148 100644
--- a/packages/llm/llm-pi-ai/README.i18n.yaml
+++ b/packages/llm/llm-pi-ai/README.i18n.yaml
@@ -30,8 +30,8 @@
en: 376f83206858b601
zh: 38bb6993609c96a6
/deepseek-ai-dsh-llm-pi-ai/use-this-package/run-with-reasoning-and-wire-compatibility:
- en: 880ec1c8db329097
- zh: 10d99664ff7fd532
+ en: 803f9e75f71fa86e
+ zh: b17d985dd5267163
/deepseek-ai-dsh-llm-pi-ai/use-this-package/change-configuration-at-runtime:
en: 92f554e1718614ca
zh: f17ed3ad3e5d28e2
diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md
index c487ee402e..172002ebbd 100644
--- a/packages/llm/llm-pi-ai/README.md
+++ b/packages/llm/llm-pi-ai/README.md
@@ -102,7 +102,7 @@ A profile's `models` list replaces the route's installed catalog rather than ext
`reasoningEfforts` declares a model's selectable thinking levels: each key is a level selectors offer, its value the spelling dispatch sends on the wire, so `max: ultra` renames a level for a gateway with its own vocabulary. Omitting the field keeps the installed catalog entry's capability; `false` declares a non-reasoning model. `compat` switches reshape the request for endpoints pi-ai cannot recognize — which role carries the system prompt, which field caps output, how a thinking level travels — configurable per route and per model. A model neither the entry nor the installed catalog sizes takes the route's `defaultContextWindow` and `defaultMaxTokens` fallbacks.
-A request carrying `GenerateOptions.purpose: 'session-title'` dispatches at the lowest level its model supports, whatever effort the request or profile names, so the small title cap holds visible text: `off` when the model offers it, otherwise the lowest declared level. `off` dispatches exactly as a selected `off` does, so a protocol that expresses it by omitting the reasoning field leaves the gateway's default in effect; reasoning tokens at any of these levels count toward the title cap.
+A request carrying `GenerateOptions.purpose: 'session-title'` dispatches at the lowest level its model supports, whatever effort the request or profile names: `off` when the model offers it, otherwise the lowest declared level. A title is a short delayed one-off, so it does not spend its output ceiling on reasoning. `off` dispatches exactly as a selected `off` does, so a protocol that expresses it by omitting the reasoning field leaves the gateway's default in effect; reasoning tokens at any of these levels count toward the title cap. The requested effort is not validated for this purpose, so a name the model does not support fails the main call rather than the title call.
For self-hosted Chat Completions endpoints, `thinkingTokenBudgetField` selects the reasoning-budget parameter, and `vllmPriority` sets an integer scheduler priority when the server enables priority scheduling. Template arguments accept `$var: thinking.budget`. `openai-responses` gateways can set `supportsMaxOutputTokens: false` to omit `max_output_tokens`; Azure and Codex transports ignore this shared compatibility field. These controls are opt-in; catalog-owned Anthropic effort and fallback capabilities are not configurable switches.
diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md
index e8ba00e9f4..63ed832a77 100644
--- a/packages/llm/llm-pi-ai/README.zh.md
+++ b/packages/llm/llm-pi-ai/README.zh.md
@@ -102,7 +102,7 @@ profile 的 `models` 列表会替换而非扩展路由的已安装目录;每
`reasoningEfforts` 声明模型可选择的 thinking 等级:每个键都是选择器提供的等级,其值是分派时在协议中发送的拼写,因此 `max: ultra` 可以为拥有自有词汇的网关重命名等级。省略该字段时保留已安装目录条目的能力;`false` 声明非推理模型。对于 pi-ai 无法识别的端点,`compat` 开关重塑请求——哪个角色携带系统提示词、哪个字段限制输出、thinking 等级如何传递——可逐路由、逐模型配置。条目与已安装目录都没有尺寸的模型,会采用路由的 `defaultContextWindow` 与 `defaultMaxTokens` 回退值。
-携带 `GenerateOptions.purpose: 'session-title'` 的请求会以其模型支持的最低等级分派,不论请求或 profile 指定何种推理强度,使较小的标题上限容纳可见文本:模型提供 `off` 时使用 `off`,否则使用声明的最低等级。`off` 的分派方式与手动选择 `off` 完全相同,因此以省略推理字段表达 `off` 的协议会沿用网关默认行为;这些等级下产生的推理 token 都计入标题上限。
+携带 `GenerateOptions.purpose: 'session-title'` 的请求会以其模型支持的最低等级分派,不论请求或 profile 指定何种推理强度:模型提供 `off` 时使用 `off`,否则使用声明的最低等级。标题是延迟敏感的短小一次性请求,不把输出上限花在推理上。`off` 的分派方式与手动选择 `off` 完全相同,因此以省略推理字段表达 `off` 的协议会沿用网关默认行为;这些等级下产生的推理 token 都计入标题上限。该用途不校验所请求的强度,因此模型不支持的名称会让主请求失败,而不是标题请求。
对于自托管 Chat Completions 端点,`thinkingTokenBudgetField` 选择推理预算参数,`vllmPriority` 在服务端启用优先级调度时设置整数调度优先级。模板参数接受 `$var: thinking.budget`。`openai-responses` 网关可设置 `supportsMaxOutputTokens: false` 来省略 `max_output_tokens`;Azure 与 Codex 传输会忽略这个共享兼容字段。这些控制均需显式启用;目录拥有的 Anthropic effort 和回退能力不是可配置开关。
diff --git a/packages/llm/llm-pi-ai/src/adapter.ts b/packages/llm/llm-pi-ai/src/adapter.ts
index dc759b0cdf..1f2d73b810 100644
--- a/packages/llm/llm-pi-ai/src/adapter.ts
+++ b/packages/llm/llm-pi-ai/src/adapter.ts
@@ -171,8 +171,11 @@ function resolveReasoningLevel(
/**
* The level one request dispatches with. A `session-title` request takes the
* model's lowest supported level, which is `off` whenever the model can stop
- * reasoning, because its small output cap must hold visible title text;
- * every other request validates the requested or profile effort.
+ * reasoning, because reasoning a model cannot switch off still counts toward
+ * the dispatch cap. That request deliberately skips the requested-effort
+ * check, so a name the model does not support fails the main call rather than
+ * the auxiliary one. Every other request validates the requested or profile
+ * effort.
* @param model - the resolved model descriptor.
* @param options - the request's purpose and requested effort.
* @param profile - the route profile carrying the default effort.
diff --git a/packages/session/session-title-llm/README.i18n.yaml b/packages/session/session-title-llm/README.i18n.yaml
index 973093b4dd..d3d19f893b 100644
--- a/packages/session/session-title-llm/README.i18n.yaml
+++ b/packages/session/session-title-llm/README.i18n.yaml
@@ -21,11 +21,11 @@
en: cf61c021b1bdb52f
zh: dac17588e4143815
/deepseek-ai-dsh-session-title-llm/use-this-package/route-and-failure-contract:
- en: 8429624883216198
- zh: 5b19f3c8f2dc8e40
+ en: df5a8011c87fc6ca
+ zh: 86eaa45c30d4e2ac
/deepseek-ai-dsh-session-title-llm/use-this-package/configuration:
- en: b0ef9c915968c15b
- zh: 056cf265f0f76166
+ en: 2fca3f5b5a4ab43c
+ zh: cdc5b285e31cc4bf
/deepseek-ai-dsh-session-title-llm/understand-the-implementation:
en: aa0fd15305df90bb
zh: d28e3a28bb324167
@@ -36,8 +36,8 @@
en: 0fdc928704abdd3a
zh: bd4c0a7dd47578cf
/deepseek-ai-dsh-session-title-llm/understand-the-implementation/request-flow:
- en: 1b63d29a6e585873
- zh: c7810d5e2acea0d2
+ en: 9103f1df0e62575f
+ zh: fda4aa05f54f5c69
/deepseek-ai-dsh-session-title-llm/further-exploration:
en: 641f31c35c48916b
zh: 8e809fbdfef61b61
@@ -57,8 +57,8 @@
en: 5d3b5b7c8d8b4b67
zh: 1b231d96bf6ca545
/deepseek-ai-dsh-session-title-llm/known-limitations-and-deferred-work:
- en: 8510fb2c4ed03011
- zh: 29fb15ec84dccf56
+ en: 43c7d18b582537dc
+ zh: 984df0095b18adb9
/deepseek-ai-dsh-session-title-llm/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 3bb0c20c93f16f98
diff --git a/packages/session/session-title-llm/README.md b/packages/session/session-title-llm/README.md
index 2630c79752..d0e750f855 100644
--- a/packages/session/session-title-llm/README.md
+++ b/packages/session/session-title-llm/README.md
@@ -33,7 +33,7 @@ A provider plugin calls `registerSessionTitleLlmProvider(ctx, config, id, automa
### Route and failure contract
-`provider` and `model` overrides are optional but must be supplied together as non-empty strings. Without that pair, the helper uses the exact provider/model route captured from the current session's logged `request/header`, so an explicit refresh before any route exists needs overrides. The helper measures the final JSON-framed user prompt against `maxInputBytes` before logging or dispatch instead of truncating it, and rechecks timeout and caller cancellation while consuming the stream and after it completes, so a late successful result cannot be accepted even if an interceptor or adapter ignores abort. Malformed or empty output, tool calls, and non-stop finish reasons reject; the session-title service decides whether that rejection is an automatic warning or an explicit caller failure. The accepted title is the first non-empty line of the model's text output, with one emphasis pair removed when it wraps that whole line, so commentary a model writes after the title cannot become the title. The dispatched output cap is the smaller of `maxOutputTokens` and the cap the session's current main request recorded; a session whose header records no cap leaves `maxOutputTokens` in force.
+`provider` and `model` overrides are optional but must be supplied together as non-empty strings. Without that pair, the helper uses the exact provider/model route captured from the current session's logged `request/header`, so an explicit refresh before any route exists needs overrides. The helper measures the final JSON-framed user prompt against `maxInputBytes` before logging or dispatch instead of truncating it, and rechecks timeout and caller cancellation while consuming the stream and after it completes, so a late successful result cannot be accepted even if an interceptor or adapter ignores abort. Malformed or empty output, tool calls, and non-stop finish reasons reject; the session-title service decides whether that rejection is an automatic warning or an explicit caller failure. The accepted title is the first non-empty line of the model's text output, with one emphasis pair removed when it wraps that whole line, so commentary a model writes after the title cannot become the title. The dispatched output cap is the smaller of `maxOutputTokens` and the cap the session's latest folded `request/header` recorded; a session whose header records no cap leaves `maxOutputTokens` in force.
### Configuration
@@ -46,7 +46,7 @@ Every field is required except the paired route override; there are no library d
| `targetWords` | required | Target word count for non-CJK titles |
| `targetCjkCharacters` | required | Target character count for Chinese, Japanese, or Korean titles |
| `maxInputBytes` | required | UTF-8 byte ceiling for the final JSON-framed user prompt |
-| `maxOutputTokens` | required | Auxiliary generation token cap, never above the cap the session's current request recorded |
+| `maxOutputTokens` | required | Output-token ceiling; dispatch uses the smaller of this and the cap the session header recorded |
| `timeoutMs` | required | End-to-end deadline within the runtime timer limit |
| `provider`, `model` | optional | Explicit route; both or neither |
@@ -72,7 +72,7 @@ One shared policy so provider plugins cannot drift: config validation, route res
### Request flow
-A generation validates the config once at registration; each revision frames the selected messages as JSON, measures the framed prompt's UTF-8 bytes against `maxInputBytes`, resolves the route (the explicit pair or the logged `request/header`), appends a log-only `session/title-llm-request` event carrying the exact dispatchable request, then streams through `ctx.llm` under a composed timeout and cancellation deadline. The dispatched envelope carries `purpose: 'session-title'` and deliberately lacks the agent loop's process-local request identity; adapters dispatch that purpose with the least reasoning the model supports, so the small output budget is reserved for visible title text: the DeepSeek adapter disables thinking, and the pi-ai adapter selects the model's lowest supported level. Output assembles into text blocks only; tool calls, malformed or empty output, and non-stop finish reasons reject, and a later model failure leaves the request record intact.
+A generation validates the config once at registration; each revision frames the selected messages as JSON, measures the framed prompt's UTF-8 bytes against `maxInputBytes`, resolves the route (the explicit pair or the logged `request/header`), appends a log-only `session/title-llm-request` event carrying the exact dispatchable request, then streams through `ctx.llm` under a composed timeout and cancellation deadline. The dispatched envelope carries `purpose: 'session-title'` and deliberately lacks the agent loop's process-local request identity; adapters dispatch that purpose with the least reasoning the model supports, so a title request does not spend its output ceiling on reasoning: the DeepSeek adapter disables thinking, and the pi-ai adapter selects the model's lowest supported level. Output assembles into text blocks only; tool calls, malformed or empty output, and non-stop finish reasons reject, and a later model failure leaves the request record intact.
@@ -117,6 +117,7 @@ These limits define the accepted generation shapes. They are current package con
- **Text output only** — the helper accepts text output and rejects tool calls; structured-output adapters and provider-specific prompt variants are not exposed.
- **Whole-prompt byte ceiling** — it enforces a byte ceiling for the whole framed user prompt rather than clipping individual messages or applying a retention policy.
+- **Output ceiling follows the session header** — the dispatched cap is the smaller of `maxOutputTokens` and the cap the latest folded `request/header` recorded, which is the newest routed request rather than necessarily the request whose prompt is being titled. A deployment that caps main requests tightly also caps its titles, and a model that cannot stop reasoning then has that much less room for visible text; a title routed through an explicit `provider`/`model` override is bounded by the main route's cap instead of the override model's own.
### Dev Note
diff --git a/packages/session/session-title-llm/README.zh.md b/packages/session/session-title-llm/README.zh.md
index 101f546f23..f9a9d85ffa 100644
--- a/packages/session/session-title-llm/README.zh.md
+++ b/packages/session/session-title-llm/README.zh.md
@@ -33,7 +33,7 @@ kind: "package-library"
### 路由与失败约定
-`provider` 与 `model` 覆盖项都是可选的,但必须同时作为非空字符串提供。如果没有这一对取值,辅助函数使用当前会话已记录 `request/header` 中捕获的确切提供方/模型路由,因此在任何路由出现前显式刷新时必须提供覆盖项。辅助函数在记录或分发前,依据 `maxInputBytes` 检查最终 JSON 封装用户提示词的大小,而不是将其截断,并在消费流期间与完成后重新检查超时与调用方取消,因此即使拦截器或适配器忽略 abort,也不能接受迟到的成功结果。格式错误或空输出、工具调用与非 stop 结束原因都会拒绝;会话标题服务决定该拒绝属于自动警告还是显式调用方失败。被接受的标题取模型文本输出的第一个非空行;当整行被一对强调标记包裹时去掉这对标记,因此模型写在标题之后的说明不会成为标题。分发的输出上限取 `maxOutputTokens` 与会话当前主请求记录的上限中的较小值;会话 header 未记录上限时,`maxOutputTokens` 生效。
+`provider` 与 `model` 覆盖项都是可选的,但必须同时作为非空字符串提供。如果没有这一对取值,辅助函数使用当前会话已记录 `request/header` 中捕获的确切提供方/模型路由,因此在任何路由出现前显式刷新时必须提供覆盖项。辅助函数在记录或分发前,依据 `maxInputBytes` 检查最终 JSON 封装用户提示词的大小,而不是将其截断,并在消费流期间与完成后重新检查超时与调用方取消,因此即使拦截器或适配器忽略 abort,也不能接受迟到的成功结果。格式错误或空输出、工具调用与非 stop 结束原因都会拒绝;会话标题服务决定该拒绝属于自动警告还是显式调用方失败。被接受的标题取模型文本输出的第一个非空行;当整行被一对强调标记包裹时去掉这对标记,因此模型写在标题之后的说明不会成为标题。分发的输出上限取 `maxOutputTokens` 与会话最新折叠 `request/header` 记录的上限中的较小值;会话 header 未记录上限时,`maxOutputTokens` 生效。
### 配置
@@ -46,7 +46,7 @@ kind: "package-library"
| `targetWords` | 必填 | 非 CJK 标题的目标词数 |
| `targetCjkCharacters` | 必填 | 中文、日文或韩文标题的目标字符数 |
| `maxInputBytes` | 必填 | 最终 JSON 封装用户提示词的 UTF-8 字节上限 |
-| `maxOutputTokens` | 必填 | 辅助生成的 token 上限,不超过会话当前请求记录的上限 |
+| `maxOutputTokens` | 必填 | 输出 token 上限;分发取它与会话 header 记录上限的较小值 |
| `timeoutMs` | 必填 | 运行时定时器限制内的端到端时限 |
| `provider`, `model` | 可选 | 显式路由;二者同时提供或同时省略 |
@@ -72,7 +72,7 @@ kind: "package-library"
### 请求流程
-生成在注册时校验一次配置;每次修订把选中的消息封装为 JSON,依据 `maxInputBytes` 检查封装提示词的 UTF-8 字节数,解析路由(显式对或已记录 `request/header`),追加一条携带确切可分发请求的仅日志 `session/title-llm-request` 事件,然后在组合的超时与取消截止时间内通过 `ctx.llm` 流式生成。分发的封套携带 `purpose: 'session-title'`,且有意不包含 agent loop 的进程本地请求身份;适配器以模型支持的最少推理分发该用途,使少量输出预算全部用于可见标题文本:DeepSeek 适配器禁用思考,pi-ai 适配器选择模型支持的最低等级。输出只组装为文本块;工具调用、格式错误或空输出与非 stop 结束原因都会拒绝,后续模型失败会保留请求记录。
+生成在注册时校验一次配置;每次修订把选中的消息封装为 JSON,依据 `maxInputBytes` 检查封装提示词的 UTF-8 字节数,解析路由(显式对或已记录 `request/header`),追加一条携带确切可分发请求的仅日志 `session/title-llm-request` 事件,然后在组合的超时与取消截止时间内通过 `ctx.llm` 流式生成。分发的封套携带 `purpose: 'session-title'`,且有意不包含 agent loop 的进程本地请求身份;适配器以模型支持的最少推理分发该用途,使标题请求不把输出上限花在推理上:DeepSeek 适配器禁用思考,pi-ai 适配器选择模型支持的最低等级。输出只组装为文本块;工具调用、格式错误或空输出与非 stop 结束原因都会拒绝,后续模型失败会保留请求记录。
@@ -117,6 +117,7 @@ kind: "package-library"
- **仅文本输出**——辅助函数只接受文本输出并拒绝工具调用;不公开结构化输出适配器或提供方专用提示词变体。
- **整体提示词字节上限**——它对整个封装用户提示词强制执行字节上限,而不是剪裁单条消息或应用保留策略。
+- **输出上限跟随会话 header**——分发上限取 `maxOutputTokens` 与最新折叠 `request/header` 记录的上限中的较小值;该 header 是最新一次带路由的请求,不一定正是当前取标题的请求。把主请求上限配得很紧的部署同样会收紧标题,无法停止推理的模型留给可见文本的空间随之变小;通过显式 `provider`/`model` 覆盖路由的标题,受主路由上限约束,而不受覆盖模型自身上限保护。
### 开发备注
diff --git a/packages/session/session-title-llm/src/index.ts b/packages/session/session-title-llm/src/index.ts
index 02f69e2d19..0e42718818 100644
--- a/packages/session/session-title-llm/src/index.ts
+++ b/packages/session/session-title-llm/src/index.ts
@@ -64,7 +64,7 @@ export interface SessionTitleLlmConfig {
readonly targetCjkCharacters: number
/** Maximum UTF-8 bytes in the final JSON-framed user prompt. */
readonly maxInputBytes: number
- /** Auxiliary generation output-token cap, never above the cap the session's current request recorded. */
+ /** Configured output-token ceiling; dispatch uses the smaller of this and the cap the session's header recorded. */
readonly maxOutputTokens: number
/** End-to-end auxiliary request deadline in milliseconds. */
readonly timeoutMs: number
@@ -225,7 +225,10 @@ const EMPHASIS_WRAPPER = /^(?\*{1,3})(?\S(?:.*\S)?)\k$/u
/**
* Take the title from model output: the first non-empty line, without
* asterisk emphasis that wraps that whole line. Models that disregard the
- * one-line instruction put the title first and commentary after it.
+ * one-line instruction put the title first and commentary after it. A line
+ * that is entirely `*` emphasis loses the marker pair even when the model
+ * meant it literally; the inner-marker check keeps emphasis inside a longer
+ * line, and the system instruction forbids Markdown.
*/
function titleFromOutput(text: string): string {
const line = text.split(/\r?\n/u).map(item => item.trim()).find(item => item.length > 0) ?? ''