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) ?? ''