DeepSeek Harness 中 Adapter 自治的 maxTokens 默认值设计:defaultMaxTokens 从模型路由到持久请求头的完整链路
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
本文基于仓库内的架构决策记录 2026-07-30-adapter-owned-max-token-defaults.md,讲解 DeepSeek Harness(Everything is a Plugin 的 LLM Agent 框架)如何把「每请求输出 token 上限的默认值」的决策权交给 LLM Adapter 自身,而非塞进 Provider 序列化或 Agent Loop 驱动层。读完你可以掌握LlmResolvedModelInfo.defaultMaxTokens的定义、校验与物化链路,理解adapterDefaults标记如何让「Adapter 默认值」与「调用方显式值」在持久化的request/header中互不混淆,并学会通过llm-deepseek.config.maxTokens调整部署级默认预算。
问题背景:一个默认值不该放在哪里的三重困境
该架构笔记描述的起点是一个看似简单的问题:当调用方没有显式传入GenerateOptions.maxTokens时,请求到底该带上什么输出上限?笔记指出两条直觉路线都有结构性缺陷:
- 只在 Provider 序列化阶段兜底:如果默认值只加在 DeepSeek 的请求序列化里,那么实际发往 Provider 的 wire 请求会携带一个在持久化
request/header中不存在的值——日志、审计与重放看到的请求和线上请求不一致; - 把每个 Provider 的默认值写进 Agent Loop:Agent Loop 是 provider 中立的驱动层,若由它内置各 Provider 的模型策略,等于把部署策略与模型策略从 Adapter 中「搬运」进了中立层,破坏了「一切皆插件」的职责边界。
因此决策是:由 Adapter 自己声明默认值。LlmResolvedModelInfo.defaultMaxTokens承载「针对某个精确 provider/model 路由的、由 Adapter 配置的每请求输出上限」,LlmRuntime只负责校验并在调用方省略时物化,不夹带任何 Provider 特定的策略。
核心机制:defaultMaxTokens 的定义、校验与物化
类型定义
defaultMaxTokens是精确路由模型元数据的一部分,定义在 types.ts:
/** Exact-route model metadata resolved by its owning adapter. */ export interface LlmResolvedModelInfo extends LlmModelInfo { /** Provider-owned context capacity when known. */ context?: LlmModelContext /** Adapter-configured per-request output cap materialized when callers omit one. */ defaultMaxTokens?: number /** Adapter-owned selectable reasoning levels when exposed. */ reasoning?: LlmModelReasoningInfo }注意字段注释与笔记措辞一致:它是「调用方省略时才被物化的每请求输出上限」,而不是模型硬上限。选择省略该字段的 Adapter 即表示「保留 Provider 自己的默认行为」,这正是决策中强调的「adapters that preserve provider-owned defaults omit it」。
LlmRuntime 的校验
在 LlmRuntime.normalizeModelInfo 中,Runtime 对 Adapter 返回的defaultMaxTokens做防御性校验:
const defaultMaxTokens = resolved.defaultMaxTokens if (defaultMaxTokens !== undefined && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) { throw new LlmError( `adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, 'INVALID_MODEL_MAX_TOKENS', ) }即该字段必须是正的安全整数,否则以INVALID_MODEL_MAX_TOKENS错误拒绝——与笔记中「LlmRuntimevalidates it as a positive safe integer」逐字对应。校验通过后,该字段随LlmResolvedModelInfo一起被 detached 返回,与 Adapter 内部对象解耦。
物化:只有调用方省略时才生效
默认值的物化发生在 resolveCallWithInfo:
const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined ? { ...config, maxTokens: info.defaultMaxTokens } : config三个条件共同保证了优先级语义:
- 调用方显式值永远胜出:
config.maxTokens !== undefined时原样保留,Runtime 不做任何夹逼(clamping)或改写,显式请求与 Agent 选项「remain unmarked and therefore win without clamping」; - Adapter 未发布默认值时不干预:
info.defaultMaxTokens === undefined时配置原样通过,Provider 自有默认行为保持不变; - 物化产生的是副本:
{ ...config, maxTokens: ... }不修改调用方传入的原始对象。
prepareCall 与 adapterDefaults 标记:让默认值成为持久请求事实
笔记中的关键设计是:Agent Loop 在记录request/header之前先 prepare 调用,因此「哪些字段是 Adapter 默认值补上的」会成为持久请求事实的一部分。这一机制在 LlmRuntime.prepareCall 中实现:
const resolvedConfig = deepFreeze(structuredClone(resolved.config)) const adapterDefaults = deepFreeze<LlmCallConfigAdapterDefaults>({ ...config.reasoningEffort === undefined && resolvedConfig.reasoningEffort !== undefined ? { reasoningEffort: true } : {}, ...config.maxTokens === undefined && resolvedConfig.maxTokens !== undefined ? { maxTokens: true } : {}, })标记逻辑精确区分了两种来源:物化前的config.maxTokens是undefined而物化后的resolvedConfig.maxTokens有值,说明这个值来自defaultMaxTokens,于是打上maxTokens: true标记;调用方显式传入的值则在物化前后一致,不会被打标。返回的PreparedLlmCall是绑定到当前 Adapter 注册的一次性句柄(deepFreeze+ 单次 dispatch 约束),保证 header 日志与后续 dispatch 使用的是同一份能力解析结果——从源码结构看,注释明确说明这是为了防止 HMR 场景下「一个 Adapter 的能力结果被拼接到另一个 Adapter 上」。
Agent Loop 侧的消费:切换路由前先清除标记字段
在 agent.ts 中,Loop 把上一轮持久 header 转换为「下一轮请求的提案」时会先剥离 Adapter 派生值:
/** Remove adapter-derived values before plugins propose the next request config. */ function requestProposal(header: EpochHeader): LlmCallConfig { if (header.adapterDefaults === undefined) return header.config const proposal = { ...header.config } if (header.adapterDefaults.reasoningEffort === true) delete proposal.reasoningEffort if (header.adapterDefaults.maxTokens === true) delete proposal.maxTokens while (true) return proposal }注意真实实现中该函数没有while结构,上面最后一行while (true)是笔误,请以仓库源码为准;其实际行为就是:被标记的字段从提案中删除,随后「exact-model resolution」会用当前路由的 Adapter 默认值重新物化。这一「先删后补」的顺序保证了笔记描述的核心不变量:切换 Provider/模型时,新 Adapter 的默认值会重新物化,而不会把上一个 Adapter 的派生值误当作显式覆盖沿用到新路由;相反,会话中显式设置的maxTokens因为从未被打标,会持续保留。
该行为有测试固化:request-reconstruction.spec.ts 对多轮adapterDefaults的持久化与剥离做了断言(如第 229、302、336 行附近对header.data.header.adapterDefaults序列的toEqual校验),覆盖了「标记字段被持久化后在新提案中消失、显式字段保留」的路径。
DeepSeek 原生 Adapter 的实现:256,000 默认与 1,000,000 上下文
笔记对原生 DeepSeek 部署的结论——「默认发送max_tokens: 256000、默认上下文容量 1,000,000」——可以直接在 llm-deepseek 源码中逐条印证。
常量与每模型元数据
adapter.ts 定义了两个关键默认常量:
/** Default combined request/response context capacity. */ export const DEFAULT_CONTEXT_WINDOW = 1_000_000 /** Default per-request output-token cap. */ export const DEFAULT_MAX_TOKENS = 256_000在 modelInfoFor 中,Adapter 为每个精确 provider/model 路由组装LlmResolvedModelInfo,defaultMaxTokens与contextWindow的回退链是:
const contextWindow = configured?.contextWindow ?? connection.defaultContextWindow return { ... context: { contextWindow }, defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens, ... }这里对应笔记中两条规则:
- 每模型
maxTokens优先于 profile 级maxTokens:DeepSeekCatalogModel接口(adapter.ts)中的maxTokens?: number注释明确写着「omission falls back to the profile'sDeepSeekConnectionOptions.maxTokens」; - 未列出的直通(pass-through)模型 id 与未配置容量的条目继承同一 Adapter 级回退:对未在 catalog 中登记的模型 id,
configured为undefined,defaultMaxTokens直接取connection.maxTokens(即 256,000 默认),而contextWindow取connection.defaultContextWindow(1,000,000 默认)。
同时,catalog 中两条内置 V4 模型条目在 index.ts 中显式发布了contextWindow: DEFAULT_CONTEXT_WINDOW,与笔记「both built-in V4 entries publish that exact capacity」吻合。
Cordis 配置侧的校验
llm-deepseek/src/index.ts 中,插件配置(即 Cordis 插件配置)对这两个值都做了正整数约束,且 profile 级maxTokens的 zod schema 直接以DEFAULT_MAX_TOKENS为默认:
contextWindow: z.number().step(1).min(1), maxTokens: z.number().step(1).min(1), // profile 级: maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS),配置解析阶段还有二次防御(第 303-305 行):maxTokens不是正安全整数时抛出llm-deepseek: maxTokens must be a positive safe integer;连接选项组装时回退为config.maxTokens ?? DEFAULT_MAX_TOKENS(第 384 行)。因此「部署可通过llm-deepseek.config.maxTokens改变默认值」这条结论有配置 schema 与运行时解析双重依据。
序列化边界:effective 值映射为 max_tokens
最终 wire 请求在 serialize.ts 生成:
...options.maxTokens === undefined ? {} : { max_tokens: options.maxTokens },由于LlmRuntime在 prepare 阶段已把defaultMaxTokens物化进config.maxTokens,序列化时看到的已是「有效值」,max_tokens字段必然与持久request/header中的值一致——这正是拒绝「只在序列化处兜底」这一备选方案的直接目的:wire 请求不再携带任何请求头中不存在的模型可见值。
备选方案对比:为什么其他位置都不合适
笔记完整记录了四个被否决的备选,它们从不同角度框定了这套设计:
| 备选方案 | 否决理由 | 工程含义 |
|---|---|---|
| 仅在 DeepSeek 序列化处应用默认值 | Provider wire 会包含一个缺失于持久请求头的模型可见值 | 日志/重放/审计与实际请求必须一致,默认值必须发生在「请求事实」形成之前 |
在每个发行应用里设置AgentOptions.maxTokens | 应用重复 Adapter 部署策略;直接 LLM 调用行为不同;切换到其他 Provider 后仍残留 DeepSeek 专属上限 | 部署策略属于 Adapter 配置,不属于应用代码;LlmRuntime.stream()直连路径必须走同一套物化 |
| 把 256,000 表示为硬模型上限 | 配置值是「期望的请求预算」,并非「每个配置端点都会拒绝更大输出」的证据 | 显式调用方保持权威地位,系统不做 clamp |
| 完全交由 Provider 默认值决定 | 原生 DeepSeek 部署的产品要求是跨兼容端点稳定的 256,000 token 会话预算 | 「保留 Provider 默认」对不发布defaultMaxTokens的 Adapter 仍然是合法行为 |
值得强调的是第三行:defaultMaxTokens是请求默认值而非硬上限。resolveCallWithInfo中没有任何 min/max 夹逼逻辑,显式传入 300,000 时系统不会替你改回 256,000——「explicit callers remain authoritative」。
结论与运维含义
综合以上链路,这套「Adapter 自治默认值」设计在仓库中的最终形态是:
- 默认链路:DeepSeek 会话在未显式配置时发送
max_tokens: 256000,且request/header同时记录该值与adapterDefaults.maxTokens === true标记,即「值」与「值的来源」都是持久事实; - 优先级链(从高到低):per-request 显式值 /
AgentOptions.maxTokens→ per-model catalogmaxTokens→ profile 级llm-deepseek.config.maxTokens→ 内置默认 256,000; - 路由切换安全:
requestProposal先删除标记字段、再按当前路由重新物化,切换 Provider 不会把 DeepSeek 派生的 256,000 误当成显式覆盖带到新 Adapter; - 预算权衡:256,000 的输出预算在「预分配请求输出」的端点上会占用 1,000,000 token 上下文中的很大一块。若你的网关或模型只支持更小输出预算,应显式调低
maxTokens——笔记的原话是「explicit configuration is preferable to an undocumented provider fallback」,显式配置优于未文档化的 Provider 兜底; - 扩展方式:其他 Adapter 保持既有行为,直到它们「有意发布
defaultMaxTokens」,即该字段是纯增量能力,不改变未声明默认值的 Adapter 的语义。
如果你要为自己的 Adapter 接入同类能力,只需在resolveModel/prepareCall返回的LlmResolvedModelInfo中发布正整数defaultMaxTokens,Runtime 的校验、物化、标记与 Loop 的剥离逻辑即自动生效——这正是「Everything is a Plugin」在请求参数层的一个具体实例。中文对照版决策记录见 2026-07-30-adapter-owned-max-token-defaults.zh.md。
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考