Skip to content

fix(catalog): restore thinking shape and context window for unidentified gateway rows - #1041

Merged
vastsa merged 3 commits into
vastsa:mainfrom
onerentop:fix/custom-anthropic-adaptive-thinking
Sep 25, 2026
Merged

vastsa merged 3 commits into
vastsa:mainfrom
onerentop:fix/custom-anthropic-adaptive-thinking

Conversation

@onerentop

Copy link
Copy Markdown
Contributor

问题

v0.15.7 之后,自定义中转服务(vendorKey: custom、base URL 不在 models.dev 中,例如 sub2api)出现两个回归:

  1. Opus 5.5 再次 400:claude-opus-5-5 requires adaptive thinking; omit thinking or use thinking.type=adaptive and output_config.effort(fix(agent-runtime): send adaptive thinking to effort-only Claude models #949 修过的问题复发)。
  2. 自动压缩过早:模型绑定显示 1M 上下文,但约 86k token(约 9%)就触发自动压缩。用户会话记录中,0.15.6 下每次压缩前约 73–75 万 token,0.15.7 下每次约 8.6 万 token。

根因

41d15d5b3(fix(catalog): resolve gateway model metadata from other publishers)增加了"无供应商身份 + 多家发布同一 ID → 不借用"的规则。自定义中转没有身份,claude-opus-5-5 在快照中有 17 家发布方、deepseek-v4-flash 也有多家,因此 findModel 返回 undefined,回退到 genericModelConfig(128k、无 reasoning_options)。这条规则本身合理,但下游有两处把"查不到"当成了"已发布":

  1. 思考形状:fix(agent-runtime): send adaptive thinking to effort-only Claude models #949 从 models.dev reasoning_options 推导 forceAdaptiveThinking;通用形状没有 reasoning_options,于是退回 thinking.type=enabled + budget_tokens。
  2. 上下文窗口:resolveBindingContextWindow 对 contextWindowSource: "catalog" 的绑定以"已发布值"为准,而通用形状的 128k 被当成已发布修正,覆盖了保存的 1M 快照。窗口 128k、输出预留 32k → hardLimit 96k → 自动压缩阈值 86,400,与会话记录吻合。

改动(两个独立提交)

1. fix(catalog): keep Anthropic thinking shape for unidentified gateways

  • ModelsDevCatalog.anthropicThinkingFor(modelId):只查 Anthropic 官方记录中完全相同的 ID,返回其 reasoning_options 与派生的 thinkingLevelMap。
  • catalogModelConfigFor(catalog, {vendorKey, baseUrl, apiStyle, modelId}):集中原先分散在各处的"目录记录或通用形状"逻辑;查不到且协议为 anthropic_messages 时,在通用形状上只补这两个字段。
  • 会话启动与子代理解析路径(session-launch.ts 三处、sidecar.ts 一处)改用该函数;session-launch 不再需要的 modelsDevModelFor 依赖一并移除。

边界:

  • 上下文、模态等部署相关的元数据不借用,41d15d5b3 的歧义规则保持不变。
  • 协议不作为身份:走 Anthropic 协议的 GLM / Kimi 等非 Claude ID 在 Anthropic 记录中不存在,行为不变;别名、改名 ID、其他协议同样不变。
  • 能解析到目录记录时仍以该记录为准。
  • 思考关闭时顺带获得 off: null,不会发送 Opus 5.5 拒绝的 thinking: {type:"disabled"}。

2. fix(shared): keep catalog context snapshots when the lookup misses

resolveBindingContextWindow 在基线 source === "generic" 时不把其窗口视为已发布值,保存的 catalog 快照继续生效;真正解析到的 models.dev 记录仍会替换 catalog 来源的窗口,user 来源规则不变。该函数也被 host-runtime 的 headless 启动路径使用,一并受益。

Spec:11-provider-model-system(中英)、12-provider-config-schema(中英)、13-model-catalog-and-selection §9.1 已更新。

Refs #990(自定义 Anthropic 服务无法发送 adaptive 思考;本 PR 覆盖与官方 ID 完全一致的 Claude 模型,不包含手动开关)。

测试

  • apps/desktop/test/models-dev-catalog.test.mjs 新增 3 条:自定义 Anthropic 网关 + 精确 Claude ID 获得 Anthropic 思考选项且限制保持通用;其他协议 / 非 Claude ID / 改名 ID 不受影响;已解析记录优先。
  • packages/shared/src/model-catalog.test.ts 新增 2 条:通用回退不覆盖 catalog 快照(修复前失败);已发布记录仍会替换 catalog 来源窗口。
  • thinking-ui.test.mjs 的源码断言从 modelConfigFromModelsDev 改为 catalogModelConfigFor(后者命中时返回前者结果,意图不变);两个 launch 测试移除已删除依赖的 stub。

验证

  • models-dev-catalog 41/41;shared 1030/1030
  • 桌面端全量 node --test:与未改动基线逐条对比无新增失败(本机既有失败为 macOS 签名/公证、进程组、Windows 路径等;两条 inflight-checkpoint / MCP 计时用例偶发,单跑通过)
  • host-runtime vitest:唯一失败 launch-resolver 的 scratchDir 断言为 Windows 路径分隔符问题(期望 /data/scratch/s1,实际 \data\scratch\s1),与本改动无关
  • tsc --noEmit:desktop、agent-runtime、host-runtime 通过;biome lint、docs/check-locales(81 对)、docs/check-docs 通过
  • check-architecture:失败项为 main 上已有的 apps/desktop/src/features/mid-autumn-egg/scene.ts(990 LOC 未入 allowlist),非本 PR 引入
  • 端到端探针(v0.15.7 快照 + 用户真实配置形状,mock fetch):
    • sub(custom / anthropic_messages)+ claude-opus-5-5:thinking: {type:"adaptive"},reasoning: max → output_config: {effort:"max"}
    • 上下文:DeepSeek catalog 1M → 窗口 1,000,000,自动压缩 675,000(修复前 128,000 / 86,400);Opus catalog 1M → 1,000,000 / 784,800(修复前 128,000 / 86,400)

真实环境:

Task candidate: v0.15.7 (efa450dd7) + 本 PR 提交,重新构建 out/main/index.js 并替换 app.asar
Base main:      c2bfe3562;v0.15.7 源码重建的 out/main 31 个文件与已安装 app.asar 逐字节一致,加入修复后仅 out/main/index.js 变化
E2E suites:     NOT RUN — 自动化 E2E 需要付费 Provider;以已安装应用手动验证替代
Result:         提交 1:已安装 PI-Desktop 0.15.7(Windows)+ 自定义 sub2api Anthropic 服务,Opus 5.5 + 思考开启正常回复(修复前稳定 400)
                提交 2:待验证(需长会话超过 86k token 观察压缩时机)
Environment:    Windows 11,PI-Desktop 0.15.7 安装包,自定义 anthropic_messages / responses 中转

剩余风险:catalogModelConfigFor 在主会话路径使用 vendorBinding?.baseUrl ?? provider.baseUrl 查目录(原先为 provider.baseUrl),仅在厂商账号绑定缺少 modelConfig 时才会走到;当前 bindingFromOption 总会提供 modelConfig,实际不触发。

🤖 Generated with Claude Code

Since 41d15d5 a custom Anthropic Messages gateway (vendorKey custom,
unknown base URL) no longer resolves a models.dev record for a Claude
id that several publishers list, because the lookup refuses to pick one
of them without a provider identity. The row then falls back to the
generic model shape with no reasoning options, so the adaptive-thinking
derivation from vastsa#949 cannot fire and Opus 5.5 turns fail again with
"requires adaptive thinking" (400).

Which thinking shape a Claude id accepts is a property of the model,
stated by Anthropic's own record, not of the deployment. When the
lookup misses on an Anthropic Messages row, take only reasoning_options
and the derived thinkingLevelMap from Anthropic's record for exactly
the same id. Limits and modalities stay generic, so the ambiguity rule
from 41d15d5 still holds; aliases, renamed ids, other wire APIs and
non-Claude ids served over the Anthropic protocol are unchanged.

catalogModelConfigFor centralizes the repeated "catalog record or
generic shape" choice for the session and subagent launch paths.
A binding seeded from models.dev stores its window with
contextWindowSource "catalog" so a later catalog correction can replace
it. Since 41d15d5 a custom gateway row (for example an Anthropic or
DeepSeek id several publishers list) no longer resolves a record, and
the lookup falls back to the generic model shape.
resolveBindingContextWindow treated that generic 128k as a published
correction and replaced the saved 1M snapshot, so automatic compaction
started at ~86k tokens (about 9% of the window Settings still shows).

A generic baseline is the fallback for a missing record, not a
published limit. Ignore it as a correction so the stored catalog
snapshot stays in force; resolved models.dev records keep replacing
catalog-sourced windows exactly as before.
@onerentop

Copy link
Copy Markdown
Contributor Author

真实环境补充验证(提交 2:上下文窗口)

  • 环境:已安装 PI-Desktop 0.15.7(Windows)替换为 v0.15.7 + 本 PR 两个提交重建的 app.asar(仅 out/main/index.js 变化)。
  • 会话:自定义 sub2api anthropic_messages 服务 + claude-opus-5-5,绑定 contextWindow: 1000000、contextWindowSource: "catalog"(该行在 0.15.7 下目录查询未命中)。
  • 结果:一次手动压缩后,上下文从约 55.6k 持续增长到 138,568 token 仍未触发自动压缩;修复前同一会话在 0.15.7 下每次约 86k(阈值 86,400)即自动压缩。按 1M 窗口计算新阈值为 784,800。
  • DeepSeek(responses 协议,同一中转)走同一解析路径,探针结果为 1M / 675,000,尚无长会话实测数据。

期间出现一次 LOCAL_REQUEST_ERROR(phase context-validation,来自 pi-ai 补丁的本地消息内容校验,sidecar 未改动),下一轮即恢复,与本 PR 无关。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants