Repository navigation
Conversation
…t and routes Three conformance fixes, one pipeline. Optional properties no longer manufacture nullable. The strict documents support neither null nor a nullable keyword, so mark_nullable injected an undocumented keyword to keep an optional property accepting omission. The property is removed from the schema instead — preserving the strict contract's all-required plus additionalProperties:false shape — and its name is reported through sanitize_for_strict, which prepare_tools_for_strict_mode surfaces as a description note listing the removed optional parameters. Removed subtrees are not reshaped or reported: a removed property's internals go with it. Non-numeric const lands on the documented enum keyword. The strict documents support const for number/integer schemas only; a string/bool const now becomes enum:[v] instead of shipping an undocumented keyword (numeric const stays put; a non-numeric const beside an existing enum yields to it). default keeps passing through verbatim — the documents show it and list no ban. The whole pipeline now acts only on official DeepSeek-family routes. route_is_official_deepseek_family (config.rs) requires provider typing (Deepseek/DeepseekCN/DeepseekAnthropic) and an api.deepseek.com / api.deepseeki.com host with a strict host-boundary check; the engine's turn surface build and the child sub-agent surface apply it, so a misconfigured provider no longer receives DeepSeek-only reshaping that strips keywords its own documents support (minItems/maxItems on OpenAI). Because the flag flows out of the same surface policy, tool_choice required and the preview surface follow the same gate. The doctor's custom-endpoint verdict flips accordingly (nothing strict is sent to a custom endpoint), and the config field doc states the scope. Signed-off-by: asto <asto18089@126.com>
There was a problem hiding this comment.
从零复审(8 lane + 亲证,head 908e1b414)
实质判定:三个改动方向与实现均正确,值得合并——门控完备(prepare_tools_for_strict_mode 唯一咽喉 tool_catalog.rs:886;ToolSurfacePolicy::new 全部 6 处调用点核过;tool_choice:"required"、preview、子代理面全部继承同一 gated flag,子代理客户端=克隆会话客户端,gate 与实际端点一致);无其他 nullable 伪造点;20 个 hunk 逐一映射到三条声明,零夹带;变换逻辑(移除先于递归、无累积、保序、缓存稳定)正确;测试真红绿;CI 全绿(nextest 实跑 15,004 passed)。
但有三组 P2 应在合并前修复:
P2-1 doctor 与新 gate 互相矛盾(本 PR 只改了 custom 分支)
doctor_strict_tool_mode_status(lib.rs:7503)的 Beta/NonBeta 分支只看 URL 不看 provider(known_deepseek_base_url_kind,6 个精确 URL):
provider=openai+https://api.deepseek.com/beta→ doctor 报ready/function_strict_sent: true,而新 gate 恰恰拦截这个组合(本 PR 自己的测试config/tests.rs"a DeepSeek URL under a non-DeepSeek provider stays out of scope" 钉死了它越界)。PR 修复前 doctor 说法属实,修复后变成假话,且这正是 PR 的动机场景。- 反向同样失真:
None(custom)分支新消息 "this custom endpoint keeps its tools unreshaped" 对官方 host 任意路径为假——包括DeepseekAnthropic的默认端点https://api.deepseek.com/anthropic(config/models.rs:12):gate 放行并整形+tool_choice:"required",doctor 却说"未整形"。
建议:doctor 分支统一走 route_is_official_deepseek_family(config.api_provider(), &target.base_url)(官方路由内再按 beta/non-beta/custom-path 细分),并补 provider-mismatch 的 doctor 测试。
P2-2 正文/引用四处与事实不符(对文档合规主题的 PR 是硬伤)
- 引用源
https://api-docs.deepseek.com/guides/structured_output页面不存在(SPA 回退到 "Your First API Call",sitemap 无此页;/guides/function_calling同样不存在)。逐字核实的契约实际在https://api-docs.deepseek.com/guides/tool_calls/的 "strict Mode (Beta)" 小节(且仅对 tools 记载——反而支持本 PR 前提;config.rs:985的代码注释引用同样要改)。 - "Full suite green: 3198 passing" 与 CI 矛盾:run 41994057296 实为 15,004 passed(3198 是某测试的序号计数器)。
- "including the subagent-surface scoping tests" ——该测试不存在:
subagent/tests.rs唯一改动是 schema 级 fleet enum 测试,子代理路由 gate 无任何测试覆盖。 - "official DeepSeek-family routes behave exactly as before" 作为线上行为声明不成立(官方路由上可选参数移除+description 注记+const→enum 都是 wire 变化)。
另:api.deepseeki.com 在官方文档 0 命中,且本仓 docs/REBRAND.md:70 明言它是 legacy typo host、不是官方端点(仅为存量配置保留 URL 启发式)。config.rs:995 注释与正文的 "the official hosts also cover the family's api.deepseeki.com alias the rest of the codebase treats as official" 应改为"镜像代码库既有兼容接受集(REBRAND.md 定位为非官方、不作 fallback)"。
P2-3 两个被声称的行为没有测试钉住
- 空 description 的注记分支(
schema_sanitize.rs:69-71,note 整段替换 description)——现有测试的 tool 均有非空 description。 - 非数值 const 让位于既有 enum 的分支(
schema_sanitize.rs:344-346,注释与正文都声称 "yields to it")。
P3 备忘(不阻塞,跟进即可)
- 值位递归污染(继承):
enum/default/examples/$defs里的 schema 形对象字面量被当 schema 节点处理——base 就在篡改,本 PR 使后果升级为删除+幻影名进注记;后续宜跳过enum/default/examples值的递归。 {"const": null}→enum:[null],null 在所引文档子集之外。- const+enum 冲突静默让位 vs 同文件 Kimi 路径 fail-closed(
ConflictingLiteralConstraint)不对称,宜在注释说明取舍。 - const 按值判数值而非按 schema 类型(
{"type":"string","const":3}保留 const)。 - 可选参数被移除这一用户可感知损失只写给了模型(注记),config 字段文档/doctor 未告知用户(严格模式是 opt-in,逃生门存在)。
- gate 判定在
engine.rs:5727与subagent/mod.rs:12944重复内联,宜提为Config方法。 - 注记无长度上限(病态 schema 50+ 可选名会全量进 description)。
config.rs:3096字段文档括号 "(api.deepseek.com hosts)" 漏了 gate 同样放行的api.deepseeki.com。:443/http:///无 scheme 等合法官方写法静默失去 strict(fail-closed 方向安全,仅 doctor 可见且无恢复建议)。purge.rs:532硬编码strict: Some(true)在 gate 外(既有,非本 PR 引入)。
配对父仓 PR(合并后必做)
strict 管线系上游继承(fd8c9fdb2),本 PR 是首个 fork 分叉,父仓 docs/fork-modifications.md 目前零登记。配对 PR 需:gitlink bump、fork-guard.sh EXPECTED_HEAD/EXPECTED_COMMITS 93→94、fork-modifications.md 新 T 条目、新测试(route-gate 矩阵 / optional-removal / const→enum)指纹与行为测试登记、./scripts/fork-guard.sh 实跑。
结论:按上述 P2-1/2/3 修复(改动面小)后即可合并。修复将由评审方直接推送到本分支。
There was a problem hiding this comment.
从零复审(8 lane + 亲证,head 908e1b414)
实质判定:三个改动方向与实现均正确,值得合并——门控完备(prepare_tools_for_strict_mode 唯一咽喉 tool_catalog.rs:886;ToolSurfacePolicy::new 全部 6 处调用点核过;tool_choice:"required"、preview、子代理面全部继承同一 gated flag,子代理客户端=克隆会话客户端,gate 与实际端点一致);无其他 nullable 伪造点;20 个 hunk 逐一映射到三条声明,零夹带;变换逻辑(移除先于递归、无累积、保序、缓存稳定)正确;测试真红绿;CI 全绿(nextest 实跑 15,004 passed)。
但有三组 P2 应在合并前修复:
P2-1 doctor 与新 gate 互相矛盾(本 PR 只改了 custom 分支)
doctor_strict_tool_mode_status(lib.rs:7503)的 Beta/NonBeta 分支只看 URL 不看 provider(known_deepseek_base_url_kind,6 个精确 URL):
provider=openai+https://api.deepseek.com/beta→ doctor 报ready/function_strict_sent: true,而新 gate 恰恰拦截这个组合(本 PR 自己的测试config/tests.rs"a DeepSeek URL under a non-DeepSeek provider stays out of scope" 钉死了它越界)。PR 修复前 doctor 说法属实,修复后变成假话,且这正是 PR 的动机场景。- 反向同样失真:
None(custom)分支新消息 "this custom endpoint keeps its tools unreshaped" 对官方 host 任意路径为假——包括DeepseekAnthropic的默认端点https://api.deepseek.com/anthropic(config/models.rs:12):gate 放行并整形+tool_choice:"required",doctor 却说"未整形"。
建议:doctor 分支统一走 route_is_official_deepseek_family(config.api_provider(), &target.base_url)(官方路由内再按 beta/non-beta/custom-path 细分),并补 provider-mismatch 的 doctor 测试。
P2-2 正文/引用四处与事实不符(对文档合规主题的 PR 是硬伤)
- 引用源
https://api-docs.deepseek.com/guides/structured_output页面不存在(SPA 回退到 "Your First API Call",sitemap 无此页;/guides/function_calling同样不存在)。逐字核实的契约实际在https://api-docs.deepseek.com/guides/tool_calls/的 "strict Mode (Beta)" 小节(且仅对 tools 记载——反而支持本 PR 前提;config.rs:985的代码注释引用同样要改)。 - "Full suite green: 3198 passing" 与 CI 矛盾:run 41994057296 实为 15,004 passed(3198 是某测试的序号计数器)。
- "including the subagent-surface scoping tests" ——该测试不存在:
subagent/tests.rs唯一改动是 schema 级 fleet enum 测试,子代理路由 gate 无任何测试覆盖。 - "official DeepSeek-family routes behave exactly as before" 作为线上行为声明不成立(官方路由上可选参数移除+description 注记+const→enum 都是 wire 变化)。
另:api.deepseeki.com 在官方文档 0 命中,且本仓 docs/REBRAND.md:70 明言它是 legacy typo host、不是官方端点(仅为存量配置保留 URL 启发式)。config.rs:995 注释与正文的 "the official hosts also cover the family's api.deepseeki.com alias the rest of the codebase treats as official" 应改为"镜像代码库既有兼容接受集(REBRAND.md 定位为非官方、不作 fallback)"。
P2-3 两个被声称的行为没有测试钉住
- 空 description 的注记分支(
schema_sanitize.rs:69-71,note 整段替换 description)——现有测试的 tool 均有非空 description。 - 非数值 const 让位于既有 enum 的分支(
schema_sanitize.rs:344-346,注释与正文都声称 "yields to it")。
P3 备忘(不阻塞,跟进即可)
- 值位递归污染(继承):
enum/default/examples/$defs里的 schema 形对象字面量被当 schema 节点处理——base 就在篡改,本 PR 使后果升级为删除+幻影名进注记;后续宜跳过enum/default/examples值的递归。 {"const": null}→enum:[null],null 在所引文档子集之外。- const+enum 冲突静默让位 vs 同文件 Kimi 路径 fail-closed(
ConflictingLiteralConstraint)不对称,宜在注释说明取舍。 - const 按值判数值而非按 schema 类型(
{"type":"string","const":3}保留 const)。 - 可选参数被移除这一用户可感知损失只写给了模型(注记),config 字段文档/doctor 未告知用户(严格模式是 opt-in,逃生门存在)。
- gate 判定在
engine.rs:5727与subagent/mod.rs:12944重复内联,宜提为Config方法。 - 注记无长度上限(病态 schema 50+ 可选名会全量进 description)。
config.rs:3096字段文档括号 "(api.deepseek.com hosts)" 漏了 gate 同样放行的api.deepseeki.com。:443/http:///无 scheme 等合法官方写法静默失去 strict(fail-closed 方向安全,仅 doctor 可见且无恢复建议)。purge.rs:532硬编码strict: Some(true)在 gate 外(既有,非本 PR 引入)。
配对父仓 PR(合并后必做)
strict 管线系上游继承(fd8c9fdb2),本 PR 是首个 fork 分叉,父仓 docs/fork-modifications.md 目前零登记。配对 PR 需:gitlink bump、fork-guard.sh EXPECTED_HEAD/EXPECTED_COMMITS 93→94、fork-modifications.md 新 T 条目、新测试(route-gate 矩阵 / optional-removal / const→enum)指纹与行为测试登记、./scripts/fork-guard.sh 实跑。
结论:按上述 P2-1/2/3 修复(改动面小)后即可合并。修复将由评审方直接推送到本分支。
…laimed strict behaviors Review fixes for the DeepSeek strict pipeline PR: - doctor_strict_tool_mode_status now answers from route_is_official_deepseek_family (the same predicate the engine gate uses) instead of classifying the URL alone: an official DeepSeek URL under a non-family provider is reported as the new `provider_mismatch` status (strict_sent=false) instead of a false `ready`, and official-host custom paths — including the DeepseekAnthropic default api.deepseek.com/anthropic endpoint, which the gate reshapes — are reported as the new `official_route` status (strict_sent=true) instead of a false "keeps its tools unreshaped". The doctor icon match covers both statuses. - Correct the config.rs citations: the strict contract lives in the "strict Mode (Beta)" section of api-docs.deepseek.com/guides/tool_calls (guides/structured_output does not exist), and api.deepseeki.com is a legacy typo host kept for config compatibility per docs/REBRAND.md, not an official host. The strict_tool_mode field doc now also states the on-route wire changes (optional-parameter removal + description note, const -> enum). - Pin the two claimed-but-untested behaviors: the removal note replaces an empty tool description, and a non-numeric const yields to an existing enum (agreeing or contradicting). Signed-off-by: asto <asto18089@126.com>
|
评审修复已推送(
正文四处失实已同步修正(引用 URL、"3198 passing"→CI 实跑 15,004 passed/15 skipped、"subagent-surface scoping tests" 表述改为如实的覆盖说明、"official routes behave exactly as before" 改为准确 wire 变化描述)。 本地验证: |
Round-2 review found the strict pipeline still shipping undocumented shapes and the doctor still disagreeing with one wire: - Fail closed on required nullables: `has_strict_incompatible_composition` now rejects `nullable` and null-type `anyOf` branches at any depth. `sanitize` collapses the common `X | null` union into `nullable: true`, so the keyword is how a required nullable survives to the gate; such tools ship unchanged and non-strict instead of carrying an undocumented keyword under a strict flag. - Never walk name maps or literals as schemas: `enforce_strict_subset` used to descend into the `properties` map itself, where the keyword strips and object rules rewrote property NAMES — a required property named `const` was renamed to `enum` with a garbage schema while `required` still demanded `const` (every call invalid under `additionalProperties: false`), a `properties` property was erased with spurious keys injected into the name map, and the removal note could name parameters that never existed. Property and definition schemas are now visited through their maps (`properties`, `$defs`, `definitions`), and literal keywords (`enum`, `const`, `default`, `examples`) ship verbatim. - Strip `dependentRequired` like the other undocumented bounds keywords. - Declare the `remember` tool's `note` parameter required — `execute` already rejects its absence for every writing action, and the all-optional schema left strict mode nothing to keep (the tool was callable only with an empty object). - One route composition: new `strict_tool_mode_for_route` answers for the engine turn surface, the child sub-agent surface, and the doctor, so the surfaces cannot disagree. The doctor no longer claims `function.strict` is stripped for the Anthropic-family provider on canonical non-beta endpoints — the Anthropic Messages wire forwards it untranslated — and the out-of-scope messages now state that `tool_choice` falls back to `auto`. - Tests: nullable fail-closed (collapsed and three-branch unions), keyword-named property survival (required + optional), `$defs`/literal walking, `dependentRequired` strip, cross-level dedupe, the route composition, the engine turn-surface wiring across three route quadrants, and the Anthropic forwarding verdict. Local: `schema_sanitize` 73, `strict*` 70, `strict_tool_mode_doctor` 8, `tools::remember` 9 — all green; fmt and clippy clean under the CI allowlist. Signed-off-by: asto <asto18089@126.com>
|
Round-2 fresh review (2026-11-06, 8 lanes + hand verification) — verdict: all three original fixes are real and root-caused, zero smuggling (28/28 hunks claim-accounted), but two P1 gaps against the "documents-faithful" claim plus wiring/test/doctor findings. All fixed in Fixed
Local: Known and deliberately not in this PR (follow-up material): |
…emas The strict pipeline now fails a tool closed instead of shipping it strict when the documented subset cannot serve it faithfully: - optional removal that would strip every declared parameter (an all-optional root left action-discriminated tools callable only with an empty object while their runtime still demands the values) - type arrays and bare null types, null-carrying const/enum members - the structural/conditional keyword family the Moonshot MFJS path already rejects (not, if/then/else, contains, prefixItems, propertyNames, unevaluated*, tuple-form items) Draft-7 dependencies, uniqueItems, and the property/containment bounds strip like the other widening-only bounds; the documents' singular $def spelling is walked as a name map; a null const drops outright instead of landing on enum. The route gate no longer trims whitespace, matching the doctor classifier and the wire strip sets, so a padded URL degrades to out-of-scope instead of leaking the flag past the non-beta strip. The doctor answers the Anthropic family from the wire on every official path (beta included) instead of the chat-dialect ready verdict, names https in the custom-endpoint message, and calls the deepseeki host a recognized URL rather than an official endpoint. The remember schema comment states the retire exception instead of claiming note is required for every writing action. Signed-off-by: asto <asto18089@126.com>
|
Round-3 fixes pushed (
Local verification on Known non-goals recorded for follow-ups: purge_context's hardcoded |
… tools
The compatibility gate now judges the shape that would actually ship:
the enforce pass removes optional properties before the gate walks the
schema, so a null-carrying shape under an OPTIONAL property is exempt
(the removal already represents it faithfully) while the same shape
under a required property still fails the tool closed. Constraints the
enforce pass strips before the gate can see them — a null `const` and
a schema-form draft-7 `dependencies` table (semantically
`dependentSchemas`, which the gate fails closed on) — are detected on
the sanitized shape with the optional-removal in view.
A removal that empties a nested object now fails closed like the
root-emptied rule: a required object whose every property is optional
ships callable only with `{}` while the runtime still reads the
removed members, and a required bare `{"type":"object"}` parameter
collapses to the same shape once `sanitize` injects the empty
`properties`.
`update_goal` and `send_later` fail closed by name: their schemas keep
one discriminating parameter required while `execute` demands sibling
parameters the schema marks optional, so after the removal pass they
shipped `strict: true` callable only with values the runtime rejects
(`update_goal` for every status value, `send_later` for three of four
actions). The documented strict subset cannot express those per-value
requirements, so the tools ship unchanged and non-strict; census tests
feed their real schemas through the pipeline so the list cannot drift.
The strip drops `format` values outside the documented enumeration
(email, hostname, ipv4, ipv6, uuid) and draft-4 boolean
`exclusiveMinimum`/`exclusiveMaximum` — both widening-only — and the
null-family and structural-keyword fail-closed tests are pinned per arm
so a single-keyword regression cannot pass.
Signed-off-by: asto <asto18089@126.com>
…wording honest The fallback_non_beta recommendation ships with its `/beta` path intact. Both renderings passed it through the authority-only URL redaction used for configured endpoints, so the doctor advised `https://api.deepseek.com` — a URL where the chat wire still strips `function.strict` — sending the user straight back into the same verdict. The recommendation is a built-in constant, never a configured URL, so no redaction is needed; the JSON report and the human output now emit it verbatim and the test pins the full value. The official_route messages say "DeepSeek-family" instead of "official": the gate also accepts the legacy `api.deepseeki.com` compat host, which docs/REBRAND.md records as recognized but not official — the same distinction the provider_mismatch arm already drew. The `strict_tool_mode` field doc no longer claims `tool_choice` falls back to `auto` when individual tools fail closed: `tool_choice` follows the route gate, not per-tool outcomes — it stays `required` on an in-scope route and falls back only when the route itself is out of scope. Signed-off-by: asto <asto18089@126.com>
|
Fourth-round fixes are pushed (head Four P1s fixed:
Also in this round:
Every new rule was mutation-verified locally (six flips, each turning at least one test red). Residual degraded-action tools ( Local on |
PR #147 fresh 评审(2026-11-09,9 lanes + 逐项一手复核)基线
四项重点
P2-1(代码缺陷,请求修复):strict 门禁把 name map / 字面量关键字当作 schema —— round-2 的 walker 修复没有同步到门禁
{"type":"object",
"properties":{"needle":{"type":"string"},
"contains":{"type":"string","description":"substring to match"}},
"required":["needle","contains"]}全 required、完全可表达——期望 方向评估:fail-safe(宁可非 strict,schema 原样保留),不会发出错误 strict 输出;内置目录 grep 无触发名(唯一 修复建议:给 P2-2(契约缺口,请求处置):文档"all
|
…r from every wire
Round-5 fixes from the 2026-11-09 fresh review:
- The composition gate now walks `properties`/`$defs`/`definitions`/`$def`
as name maps and skips `enum`/`const`/`default`/`examples` as values,
like its sibling walkers: a required property named after a gate
keyword (or a definition named `not`, an object-shaped `enum` member,
a `default` shaped like a keyword) no longer fails the tool closed on
its name or shape alone.
- The nine structural/conditional keywords share one constant between
the gate and the test loop that pins each of them.
- A root map shape (`additionalProperties` schema form or `true` beside
no properties) fails closed pre-enforce instead of shipping
`strict: true` callable only with `{}`; a typeless node with a
non-`false` `additionalProperties` fails closed instead of shipping
the authored keyword verbatim (the enforce pass normalizes only
object schemas). Authored no-argument roots keep the exemption.
- The doctor reports a new `responses_dialect` verdict when the route's
model resolves to the Responses endpoint (the wire sends
`strict: false` and `tool_choice: "auto"`): reshaping applies, no
enforcement is sent, and no base-URL recommendation is offered —
the dialect is model-driven, not URL-driven. Exact catalog rows stay
authoritative.
- The discarded `all_strict` return carries the mixed-set disposition
at the only production call site: the documents describe strict mode
with every function flagged and leave the server's mixed-set behavior
undocumented, so the return stays the lever for an all-or-nothing
policy if a probe ever shows one is needed.
Signed-off-by: asto <asto18089@126.com>
Problem
Three conformance gaps in the DeepSeek strict tool pipeline: (1) optional properties were kept by manufacturing an undocumented
nullable: truekeyword; (2) non-numericconstshipped although the strict documents supportconstfor number/integer schemas only; (3) the whole pipeline acted on any route whose provider was typed DeepSeek, so a misconfiguredprovider/base_urlentry could receive DeepSeek-only reshaping that strips keywords the other provider's own documents support.Document evidence (fetched 2026-11-05)
DeepSeek strict function-calling contract — the "
strictMode (Beta)" section of https://api-docs.deepseek.com/guides/tool_calls/, fetched 2026-11-05 (strict mode is documented for tools only; there is no documented response_format strict contract). Verbatim anchor points: the strict subset requires every property listed inrequiredandadditionalProperties: false("All properties of everyobjectmust be set asrequired..."); the documented supported-type list includesenumandanyOf,constis documented only for number/integer schemas, and neithernullnor anullablekeyword appears anywhere in the strict-mode documentation;defaultappears (under number/integer) with no ban;formatis documented with an enumerated set (email, hostname, ipv4, ipv6, uuid); theexclusiveMinimum/exclusiveMaximumforms shown are numeric. The official hosts also cover the family'sapi.deepseeki.comalias — not because the docs bless it (they never mention it;docs/REBRAND.mdrecords it as a legacy typo host kept only in URL heuristics for existing configs, codewhale-hq#1079) — but to mirror the compat set the rest of the codebase already recognizes (client.rs,client/chat.rs, the doctor).The same section's usage step says "In the
toolsparameter,allfunctionneed to set thestrictproperty totrue" and its validation paragraph says "If the schema does not conform to the specifications or contains JSON schema types that are not supported by the server, an error message will be returned" — but the documents never state how the server treats a MIXED set (tools flaggedstrict: truebeside tools shipped without the flag). This pipeline ships mixed sets by design: a tool whose schema cannot conform fails closed per tool (ship unchanged, no flag) while its siblings keep the flag andtool_choicefollows the route gate. The server's mixed-set semantics (reject the request / request-wide downgrade / per-tool validation) are undispositioned by the documents; a one-shot live probe is the follow-up that would settle it, and the pipeline's discardedall_strictreturn is the lever for an all-or-nothing policy if one is ever needed (the call site carries that disposition in a comment).Changes (head
1165fb30a, plus review fixes)required" honestly, andnullable: trueis a keyword outside the documented strict subset — so the property is removed frompropertiesinstead. Its name is reported throughsanitize_for_strict, whichprepare_tools_for_strict_modesurfaces as a description note listing the removed optional parameters. Removed subtrees are not reshaped or reported; their internals go with them.constlands on the documentedenumkeyword. A non-numericconst(string, bool, object, array) becomesenum: [v](numericconststays put; a non-numericconstbeside an existing enum yields to it; aconst: nullis dropped outright — null is not a supported type, and landing it onenumwould ship a null member). When theconstis dropped beside an existing enum, the accepted values widen to the whole enum even if theconstdisagreed — every documented alternative is worse (narrowing the enum, or shipping an undocumented keyword), so this is deliberate and test-pinned.defaultkeeps passing through verbatim.route_is_official_deepseek_family(config.rs) requires provider typing (Deepseek/DeepseekCN/DeepseekAnthropic) and anapi.deepseek.com/api.deepseeki.comhost with a strict host-boundary check; the engine's turn-surface build, the child sub-agent surface, and the doctor now all answer from one composition (strict_tool_mode_for_route). A misconfigured provider no longer receives DeepSeek-only reshaping that strips keywords its own documents support (OpenAI documentsminItems/maxItems, for one). Because the flag flows out of the same surface policy,tool_choice: "required"and the preview surface follow the same gate.Review fixes (same branch)
First round (
410a5ca68): the doctor answers from the route gate (provider_mismatchfor an official URL under a non-family provider,official_routefor official-host non-canonical paths including theDeepseekAnthropicdefault/anthropic), config.rs citations corrected to the real contract location, and the two claimed-but-untested behaviors pinned (empty-description note replacement;constyielding to an existing enum).Second round (
10a09d5bb, fresh 2026-11-06 review):nullableor a null-typeanyOfbranch after the collapse pass cannot be represented faithfully under a strict flag, so that tool ships unchanged and non-strict instead of sending the undocumented keyword understrict: true(the exact silent-400 class this PR exists to prevent).propertiesname map as if it were a schema, where the keyword strips and object rules rewrote property names: a required property namedconstwas silently renamed toenumwith a garbage schema whilerequiredstill demandedconst(every call invalid underadditionalProperties: false), apropertiesproperty was erased with spurious schema keys injected into the map, and the removal note could name parameters that never existed. Property/definition schemas are now visited through their maps (properties,$defs,definitions), and literal keywords (enum,const,default,examples) ship verbatim.dependentRequiredtables are stripped like the other undocumented bounds keywords (patternProperties,minLength/maxLength,minItems/maxItems).rememberschema declaresnoterequired —executerejects its absence for append and revise (theretireaction ignores the value but the field stays required; see round three), and the all-optional schema left strict mode nothing to keep (the tool was callable only with an empty object).function.strictuntranslated (the chat-completions strip does not apply there), so theDeepseekAnthropicfamily provider on such a URL is now reportedofficial_routewith the flag sent, and the beta-endpoint recommendation (a chat-completions surface) is withheld for that dialect.provider_mismatch/custom_endpointmessages now state thattool_choicefalls back toauto— the only lever users lost when the gate stopped forcing it on out-of-scope routes.Third round (
76fefa87a, follow-up to the same fresh review — close the remaining conformance gaps the review found):tasks,github,automation,web.run,validate_data,load_skill,harness) mark their whole root property set optional and dispatch on the valuesexecutere-reads — under the removal rule they shippedstrict: truecallable only with an empty object, with a note telling the model never to send the parameters their runtime still demands. Such a tool now ships unchanged and non-strict, the same fail-closed rule as required nullables, with no removal note (nothing was removed from the schema that ships). Partially-optional tools keep the removal-and-note behavior; per-action schema honesty for sub-actions remains open as a follow-up (its scope after the fourth round is listed under Disclosed residual gaps).nullableand null-type union branches —typearrays (the documented subset's union mechanism isanyOf; atypearray is a draft-4-style union the documents never describe), bare{"type":"null"}nodes,const: null, andenummembers that are null: each is a required nullable wearing another keyword, and each ships undocumented constructs understrict: truetoday. MCP tool schemas reach this gate verbatim (McpPool::to_api_toolsclones them unnormalized), so these encodings are externally reachable even though no built-in schema uses them.not,if/then/else,contains,prefixItems,propertyNames,unevaluatedProperties/unevaluatedItems, and tuple-formitemscarry constraints the reshaping cannot express — the same family the Moonshot MFJS sanitizer rejects withUnsupportedKeyword. A schema relying on one now ships unchanged and non-strict rather than reshaped-and-strict.dependencies(both the name-table and the schema form),uniqueItems, and the property/containment count bounds (minProperties/maxProperties/minContains/maxContains) strip likedependentRequiredand the length bounds — dropping them only widens what the endpoint accepts; with every property required andadditionalProperties: falsethe property-count bounds are redundant or contradictory anyway.$defspelling is walked as a name map. The singular spelling the DeepSeek documents themselves use joins$defs/definitions, so a definition named after a stripped keyword (e.g.const) survives intact instead of being deleted or renamed on that path.readyverdict for adeepseek-anthropicroute pointed athttps://api.deepseek.com/beta— the same dialect mismatch the second round fixed for non-beta URLs. The hoisted Anthropic branch now answersofficial_routethere too (the Messages wire forwards the flag on its own dialect; the chat-surface recommendation stays withheld). Thecustom_endpointmessage names https, and theprovider_mismatchmessage callsapi.deepseeki.coma recognized DeepSeek endpoint URL rather than an official one (its official status is a legacy-typo compat entry, perdocs/REBRAND.md).noteis runtime-required for append and revise;retireignores the value but the field stays required (otherwise strict mode would strip it).Fourth round (
705546c94+3b2dec71a, follow-up to the second fresh review of 2026-11-06):fallback_non_betarecommendation was rendered through the authority-only URL redaction used for configured endpoints, so the recommendedhttps://api.deepseek.com/betadisplayed ashttps://api.deepseek.com— a URL where the chat wire still stripsfunction.strict(chat.rsstrip set). Following the advice landed the user straight back into the same verdict, and the JSON test pinned the stripped value. The recommendation is a built-in constant, never a configured URL, so the JSON report and the human output now emit it verbatim and the test pins the fullhttps://api.deepseek.com/betavalue.update_goalandsend_laterfail closed by name. Their schemas keep one discriminating parameter required (status/action) whileexecutedemands sibling parameters the schema marks optional — after the removal pass they shippedstrict: truecallable only with values the runtime rejects (update_goalfor every accepted status value:evidence/verificationforcomplete,blockerforblocked,verificationfornot_achieved,advisoryforadvisory;send_laterfor three of four actions:messageforschedule,trigger_idforread/cancel). The documented strict subset cannot express those per-value requirements (if/thenand root alternatives are outside it), and widening the schema requirements would make non-strict callers send junk for every other value — so the pipeline keeps an explicit name list (STRICT_UNUSABLE_TOOLS) with the runtime contracts cited at the definition, and census tests feed the tools' real schemas through the pipeline so the list cannot drift (the read-onlysend_latervariant fails closed identically).nullable, null-type union branch,typearray, baretype:null,const: null, nullenummember) under an optional property is faithfully represented by that removal, so the tool keeps strict mode and the property is removed with the standard note — previously one such parameter failed the whole tool closed (every Pydantic-style MCP schema with an optionalX | nullparameter). The same shapes under a required property still fail closed, per arm. The null-constand schema-formdependencieschecks run before the enforce pass, because the strip destroys those constraints before a post-enforce gate could judge them.{}while the runtime still reads the removed members; a required bare{"type":"object"}parameter collapses to the same shape oncesanitizeinjects the emptypropertiesmap (underadditionalProperties: falsethat shape cannot carry a value). Authored no-argument roots keep the root-level exemption. Verified built-in-catalog impact: none (the only shipped empty-property roots are authored no-arg tools whose execute ignores input); the exposure is MCP-supplied schemas.formatvalues outside the documented enumeration (email, hostname, ipv4, ipv6, uuid) are dropped — any other value ships a constraint outside the documented list — and draft-4 booleanexclusiveMinimum/exclusiveMaximumare dropped alongside (the documents list the numeric form only). Both removals only widen what the endpoint accepts.dependenciesfails closed likedependentSchemas. Both spellings express the same construct; the strip used to drop the draft-7 form while the gate rejected the modern one. The schema form now fails closed (detected pre-strip), the name-table form keeps its widening-only strip.official_routemessages say "DeepSeek-family" rather than "official": the gate also accepts the legacyapi.deepseeki.comcompat host, whichdocs/REBRAND.mdrecords as recognized but not official — the same distinction theprovider_mismatcharm already drew.strict_tool_modefield doc no longer claimstool_choicefalls back toautowhen individual tools fail closed.tool_choicefollows the route gate, not per-tool outcomes: it staysrequiredon an in-scope route even when some tools ship non-strict, and falls back toautoonly when the route itself is out of scope (the doctor's out-of-scope messages were already accurate).format/exclusive strips, the name-list check, the doctor rendering, or the gate order each turns at least one test red.Fifth round (
1165fb30a, fixes from the 2026-11-09 fresh 9-lane review):has_strict_incompatible_composition— the only one of the three walkers without the name-map and literal-keyword arms — judgedproperties/$defsentries as if they were schemas, so a REQUIRED property named after a gate keyword (nullable,contains,not,if/then/else,prefixItems,propertyNames,unevaluatedProperties/unevaluatedItems) or a$defsdefinition namednotfailed the tool closed on its name alone (the base only misfired ononeOf/allOf/dependentSchemasnames; this widening was new in rounds 3–4 and is now fixed), and object-shapedenummembers or adefaultvalue shaped like{"nullable": true}failed it on their shape. The gate now walksproperties/$defs/definitions/$defas name maps and skipsenum/const/default/examplesas values; real keyword occurrences still fail closed (the per-keyword loop pins each one). Built-in-catalog impact: none — the exposure was MCP-supplied schemas only.STRICT_UNREPRESENTABLE_KEYWORDS) between the gate and the test loop that pins each of them, so the two lists cannot drift.additionalPropertiesin schema form ortrue) collapsed under the enforce pass intoproperties: {}+additionalProperties: false—strict: truecallable only with{}while the runtime reads the keys (the same degenerate shape the nested rule already fails closed, one level up from it). Such a root now ships unchanged and non-strict; authored no-argument roots (additionalPropertiesabsent orfalse) keep the exemption. The detection lives pre-enforce (strict_schema_supported, plus ahas_unrepresentable_stripped_constraintarm): the enforce pass normalizesadditionalPropertiesonly on object schemas, so a typeless node carrying a non-falseadditionalPropertieswould have shipped the authored keyword verbatim understrict: true— it now fails closed too. Verified built-in-catalog impact: none (every built-inadditionalPropertiesisfalsebeside non-emptyproperties); the exposure is MCP-supplied schemas.client/responses.rshardcodes"strict": falseper tool andtool_choice: "auto"; the resolver sends uncatalogueddeepseek-v<digit>models — and the bundleddeepseek-v4-flashrow — there) got reshaped schemas with zero enforcement while the doctor reportedready/official_routewithfunction_strict_sent: true. A newresponses_dialectverdict (any family provider whose resolved endpoint key isresponses, on any official URL) now says so: reshaping still applies, no strict enforcement is sent,tool_choicestaysauto, and no base-URL recommendation is offered (the dialect is model-driven, not URL-driven — pointing the route at/betadoes not change it). The verdict reads the same resolution the live client uses (DoctorApiTargetnow carries the resolved endpoint key); exact catalog rows stay authoritative (deepseek-v4-pro→ chat →readyon beta, test-pinned). This closes the same doctor-vs-wire class round two closed for the Anthropic dialect and rounds 3–4 refined for the chat dialect.all_strictreturn now carries the disposition at its only production call site (see the Document evidence section): mixed sets are intentional per-tool fail-closed,tool_choicefollows the route gate, and the return stays the lever for an all-or-nothing policy if a probe ever shows the server rejects or downgrades mixed sets. The census tests already pin the per-tool semantics (!prepare_tools_for_strict_mode(...)).additionalPropertiesschema form beside named properties is narrowed tofalseby the documented contract without a note (the only silent semantic loss in the pipeline — every other unrepresentable constraint gets a note or fail-closed), and landing a non-numericconstonenumis not purely conservative for bool/object/array members (the documents show string examples only; if the validator rejects such members the documented outcome is an error, where dropping theconstoutright would only widen).contains; definition namednotbeside a structural definition still failing closed; literal-shaped values; root map shapes including theadditionalProperties: falseexemption; typeless map keyword; required nested map;responses_dialectverdict + JSON; catalog-row precedence over the versioned rule), and the fixes are mutation-verified: reverting the gate's name-map arms turns the three keep-strict pins red (3 failed), removing the detector map arm, the root-map check, or the doctor's responses arm each turns exactly its own pin red.Disclosed residual gaps (out of scope here, honestly stated)
Git(showneedsrev,blameneedspath),lsp(definition/referencesneedline),pandoc_convert(binary targets needoutput_path),bash(the sandbox-escalation retry needssandbox_permissions/justification), plus the hiddenagents/message/agents/followupon strict-route transcript replay.remember'srevise/retireremain strict-unreachable as before. Per-action schema honesty (or per-action fail-closure) for these remains the follow-up.components.schemas) are still walked as schemas — a pre-existing class, reachable only via pathological MCP input.examples,title) ship verbatim, riding the same undocumented-but-tolerated-keyword reality the pipeline's own description notes rely on.conston a single-memberenumis reasoned but not purely conservative for bool/object/array members: the documents showenumwith string examples only, so if the strict validator rejects non-string members the documented outcome is an error, where dropping theconstoutright would only have widened. Deliberate, test-pinned, and called out here for completeness.purge_context's hardcoded strict marker (purge.rs) is pre-existing and outside this pipeline (no reshaping, no tool_choice, fires regardless of the flag) — a separate concern.additionalPropertiesschema form (ortrue) beside named, required properties is silently narrowed toadditionalProperties: false— the documented strict contract mandates it, but it is the pipeline's only silent semantic loss: every other unrepresentable constraint is either noted (removed optionals) or fails the tool closed. The named properties keep working; only extra-key acceptance disappears.Test evidence (red-green)
CI on this branch: fork-ci
checkrunscargo nextest run --workspace --all-features --locked. The run on head10a09d5bb(run 42074363684): 15,017 tests started, 15 skipped, 0 failed —check,gate,Gitleaksall SUCCESS. The run on head76fefa87a(run 42192582794) is green:check,gate,Gitleaksall SUCCESS. The run on head3b2dec71a(run 42232236062): 15,037 tests, 15 skipped, 0 failed —check,gate,Gitleaksall SUCCESS. Locally on3b2dec71a, the affected filters are green:schema_sanitize(89 tests — nine new pins: nested-emptied fail-closed, required bare-object fail-closed, schema-formdependenciesfail-closed plus its optional-position counterpart, per-arm null shapes, optional-position null shapes staying strict, the structural-keyword loop, per-arm null-const, undocumentedformat/draft-4 exclusive strips),strict_mode(22),strict_tool_mode_doctor(9 — the recommendation now pins the full/betavalue), the census tests (strict_mode_fails_the_update_goal_tool_closed,strict_mode_fails_the_send_later_tool_closedwith the read-only variant,strict_mode_keeps_remember_usable_with_its_note),tools::remember(10),tools::goal(28),tools::send_later(15), the route-gate matrix and composition tests, the engine turn-surface gate test, thestrictfilter (90) and thedeepseekfilter (159) across the lib, withcargo fmt --checkand the gate's exact clippy invocation clean; the full workspace suite runs in CI (15,037 tests, 0 failed on this head). Six local mutations (nested-emptied rule, pre-strip detector,format/exclusive strips, name-list check, doctor JSON rendering, gate order) each turn at least one test red. On round five (1165fb30a): CI re-runs on the pushed head; locally the affected filters are green —schema_sanitize(95 tests: six new pins — required property namedcontains, definition namednotbeside a structural one, literal-shaped values, root map shapes with theadditionalProperties: falseexemption, typeless map keyword, required nested map),strict_mode(28),strict_tool_mode_doctor(11 —responses_dialectverdict + JSON and catalog-row precedence), thestrictfilter (98),tools::remember(10),tools::goal(28),tools::send_later(15), the fullcodewhale-tuilib (12,449 passed / 0 failed, excluding the two speech/voice modules that hang on this audio-less host) and every other workspace crate's lib suite green, withcargo fmt --checkand the gate's exact clippy invocation clean. Round-5 mutations: reverting the gate's name-map arms → 3 red; detector map arm, root-map check, doctor responses arm → exactly 1 red each.Behavior changes
nullable: true. A null-carrying shape under an optional property no longer fails the whole tool closed — the removal represents it and the tool keeps strict mode.strict: truewith every parameter removed — callable only with an empty object). A required nested object whose every property is optional — or a required bare{"type":"object"}parameter — now fails closed the same way. Partially-optional tools keep the removal-and-note behavior.update_goalandsend_laterstay non-strict with their original schemas (previously they shippedstrict: truewhile every — or three of four — of their dispatch values failed at runtime).nullable/ null-type unions now stays non-strict with its original schema (previously it was marked strict with the undocumented keyword on the wire). The same fail-closed coverstypearrays, bare{"type":"null"},const: null, and nullenummembers in required positions, plus the MFJS-family structural/conditional keywords, a schema-formdependenciestable, and a nullconst.constships as a single-memberenum; a nullconstis dropped;dependentRequired, draft-7dependenciesname tables,uniqueItems, the property/containment count bounds, undocumentedformatvalues, and draft-4 booleanexclusiveMinimum/exclusiveMaximumare stripped.tool_choice: "required", or the strict preview; the doctor messages state thetool_choicefallback explicitly. Official DeepSeek-family routes keep strict mode; their wire schemas follow the strict contract exactly (optional parameters removed with a description note, non-numericconstlanded onenum, null-carrying and structural shapes fail closed) — a wire change on those routes too, not a no-op. A whitespace-paddedbase_urldegrades to out-of-scope instead of being admitted by the gate alone.rememberkeeps itsnoteparameter in strict mode and stays usable (append remains the default action); its other parameters are optional and are removed with a note — which leavesrevise/retirereachable only outside strict mode; per-action schema honesty is a follow-up (see Disclosed residual gaps).ready; official non-beta →fallback_non_beta(strict flag stripped, reshaping still applies; the recommendation now carries the fullhttps://api.deepseek.com/betapath) — except the Anthropic-family provider, which isofficial_routeon every official path, beta included (the Anthropic wire forwards the flag); official custom path →official_route(in scope); DeepSeek URL under a non-family provider →provider_mismatch; anything else →custom_endpoint(out of scope). The in-scope messages say "DeepSeek-family" rather than "official" (the legacy compat host is recognized, not official).$defsdefinitions are merely NAMED after gate keywords stays strict (rounds 3–4 failed it closed on the name alone); object-shapedenummembers and schema-shapeddefaultvalues likewise no longer trigger the gate.additionalPropertiesschema form ortruebeside no properties) now ships non-strict with its original schema instead ofstrict: truecallable only with{}; a typeless node with a non-falseadditionalPropertiesfails closed instead of shipping the authored keyword verbatim under a strict flag. Authored no-argument roots keep strict mode.responses_dialect(reshaping without enforcement — the wire sendsstrict: falseandtool_choice: "auto") instead of claiming the flag was sent; exact catalog rows keep their current verdicts.Merge-conflict surface
With #137: this branch additionally touches
tools/goal.rsandtools/send_later.rs(census tests only — no overlap with #137); #137's and this branch's appended test regions collide textually incore/engine/tests.rs(both add tests at the end of the module) — whoever merges second rebases; there is no semantic interaction. There is no conflict surface with #137'sclient/chat.rsuse hunk, and the config.rs hunks do not collide. With #144: this branch rewrites lines inside #144'ssanitize_for_stricthunk context and both branches add tests in overlapping regions — a merge-tree of this head onto #144's head auto-merges clean; the gate reshapes upstream of #144'ssanitize_for_xai_parameters, compatible either merge order. With #143 (Moonshot MFJS): heavy same-file overlap inschema_sanitize.rsplus a disjoint config.rs doc hunk, but the MFJS pipeline is separate — a merge-tree of this head onto #143's head is clean. With #146: touches onlyclient/anthropic.rs— zero file overlap with this branch (merge-tree clean). Round five additionally touchescore/engine/tool_catalog.rs(comment only) and extends thelib.rsdoctor andschema_sanitize.rsstrict region —tool_catalog.rsis untouched by #137/#143/#144/#146, so the new surface does not collide; theschema_sanitize.rsoverlap note with #143 still holds (round five stays inside the strict region; the MFJS region is untouched).Probe declaration
No live probes were used in this change; every rule above is grounded in the published strict contract cited above (fetched 2026-11-05).
No-Issue: provider compatibility batch grounded in the published provider docs (fetched 2026-11-05); the repository has issues disabled.