news 2026/9/10 6:06:52

oh-my-pi Schema Constraints:跨 Provider 工具 Schema 规范化与严格模式约束的工程契约

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
oh-my-pi Schema Constraints:跨 Provider 工具 Schema 规范化与严格模式约束的工程契约

oh-my-pi Schema Constraints:跨 Provider 工具 Schema 规范化与严格模式约束的工程契约

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

导读

在多模型、多供应商的 Agent 应用中,一份工具(Tool)声明需要同时被 OpenAI、Google Gemini/Vertex、Cloud Code Assist Claude、MCP 等不同后端接受,而各家对 JSON Schema 的支持子集差异巨大。本文以 oh-my-pi 仓库中 packages/ai/src/utils/schema/CONSTRAINTS.md 这份"操作契约"为骨架,深入讲解packages/ai/src/utils/schema模块如何通过normalize.tsadapt.tsfields.ts实现三类核心约束——OpenAI 严格模式(strict mode)、Google 规范化、Cloud Code Assist Claude(CCA)规范化,并结合源码与测试给出可落地的适配规则。读完你将掌握工具 Schema 在各 Provider 之间的可移植性设计、关键字剥离与降级兜底策略,以及新增适配器时必须遵守的维护红线。

一、为什么需要一份"Schema 约束契约"

不同的 Provider 对 JSON Schema 的接受度差异,是工具调用链路上最常见的 400 错误来源:

  • OpenAI 严格模式要求每个 schema 节点必须有type(或组合器、$refnot),对象必须additionalProperties: false,可选属性必须可空化;
  • Google Gemini/Vertex的 Schema proto 拒绝$refprefixItemsadditionalProperties等一批标准关键字,protojson遇到未知字段会直接INVALID_ARGUMENT("Cannot find field");
  • Cloud Code Assist(CCA)Claude走更严格的parameters通道,连nullable关键字都不接受;
  • MCP传输层只剥离$schema,其他关键字几乎原样保留。

CONSTRAINTS.md正是把这些分散在 normalize.ts(所有 schema walker 的所在地)、adapt.ts(严格模式统一入口)、fields.ts(关键字分类集合)中的规则固化为"可测试、可引用、可执行"的契约文档。其适用范围覆盖normalize.ts的 Google、CCA、MCP、OpenAI Responses、OpenAI strict-mode 清洗,以及adapt.ts中供各 Provider 调用点使用的tryEnforceStrictSchema封装和PI_NO_STRICT环境变量旁路开关。

二、OpenAI 风格严格模式:adaptSchemaForStrict/tryEnforceStrictSchema

当调用点请求strict=true时,适配后的 schema 必须同时满足以下六条硬性约束。

2.1 先剥离非结构性关键字,再执行严格强制

严格模式清洗由sanitizeSchemaForStrictMode完成,所有被移除的关键字定义在 fields.ts 的NON_STRUCTURAL_SCHEMA_KEYS集合中:

format, pattern, minLength, maxLength, minimum, maximum, exclusiveMinimum, exclusiveMaximum minItems, maxItems, uniqueItems, multipleOf $schema, examples, default, title, $comment if, then, else, not unevaluatedProperties, unevaluatedItems, patternProperties propertyNames, contains, minContains, maxContains dependentRequired, dependentSchemas contentEncoding, contentMediaType, contentSchema deprecated, readOnly, writeOnly minProperties, maxProperties $dynamicRef, $dynamicAnchor

这些关键字只影响校验/装饰语义,不改变严格模式强制的"结构形状"。特别地,default在剥离前会被内联进同级description,追加为(default: X)后缀,让严格模式 Provider 仍能在自由文本中看到默认值提示;当description已包含(default:或不存在同级description时跳过内联。测试 schema-strict-mode.test.ts 验证了"推断 object 类型、剥离非结构性关键字、const 转 enum"三件事同时发生:minLength: 3format: "email"等被清除,const: "abc"变为enum: ["abc"]

2.2const归一化为enum

严格模式下节点不允许存在const,若节点包含const,清洗逻辑将其转换为enum: [const]

2.3 对象与元组严格化:递归强制执行

enforceStrictSchema(位于 normalize.ts 的enforceStrictSchemaBody)递归执行以下规则:

  • 每个对象节点被写入additionalProperties: false
  • 每个属性键都进入required
  • 可选属性被可空化,且分三种情况:
    • 纯 union 节点(仅含anyOf加可选的description):原地追加{ "type": "null" }分支,绝不嵌套包装;
    • 其他所有节点:包装为anyOf: [<原 schema>, { "type": "null" }]。若anyOf旁存在约束性兄弟关键字,必须保留包装——因为兄弟关键字与anyOf是合取(conjunctive)关系,直接追加 null 分支并不能让节点真正可空;
    • 已含 null 分支或已是anyOf形态的节点跳过重复包装。
  • 嵌套纯 union 被拼接到父级anyOf(A ∨ B) ∨ CA ∨ B ∨ C);父级无description时,内层description上提。严格输出中绝对不允许出现"分支本身还是纯 union"的anyOf——上游部分校验器(如 OpenRouter 背后的 DeepSeek)会拒绝没有type的分支(issue #2270);
  • 元组条目prefixItems同样递归严格化。

从源码可以看到enforceStrictSchemaBody的具体实现路径:遍历properties,对每个不在原required中的键,先判断是否已可空,再判断isPureAnyOfNode决定原地追加还是包装,最后result.required = Object.keys(strictProperties);随后递归处理itemsprefixItemsCOMBINATOR_KEYSanyOf/allOf/oneOf),并对$defs/definitions做同样的严格化。

2.4 节点必须能在严格模式下表达

没有type、组合器、$refnot的节点在严格强制下是非法的,必须抛错。典型非法节点:{}{ items: {} }enforceStrictSchemaBody末尾会先从同质基本类型的enum/const推断typeinferStrictPrimitiveTypeFromEnumOrConst),推断不出且无$ref/组合器/not时抛出ValidationError("Schema node has no type, combinator, or $ref — cannot enforce strict mode")

2.5 失败模式:回退到非严格(fail-open)

tryEnforceStrictSchema是严格强制的"安全壳":它先做一次hasUnrepresentableStrictObjectMap预检——只要树中存在patternPropertiesadditionalProperties: true/对象,就整体判定不可严格化并提前返回{ strict: false, schema: upgraded },避免在强制阶段抛错;强制阶段(sanitizeSchemaForStrictModeenforceStrictSchema)任何异常同样被捕获,必须返回{ strict: false, schema: original },绝不输出半损坏的严格 schema。结果还会被打上kStrictSchema印记(stamp)以便缓存复用。

2.6 Provider 载荷的 strict 标志必须与实际严格度一致

  • 调用方只有强制成功(effectiveStrict === true)时才发送strict: true
  • 调用方必须保留作者显式声明的tool.strict === false上线传输,使strict: false与"省略 strict"在线上可区分——部分 OpenAI 兼容后端在标志缺失时会过度填充可选字段,但显式false会被尊重(issue #4336)。三个 OpenAI 系 Provider 的例外规则各不相同:
    • openai-responses:仅在strictMode门控和PI_NO_STRICT允许发送 strict 字段时,才输出显式false
    • openai-codex-responses:显式false!PI_NO_STRICT为门控,使全局旁路能保持 Codex 代理拒绝的strict键不上线;
    • openai-completions:仅当toolStrictMode === "mixed"compat.supportsStrictMode !== false时输出显式false——因为all_strict → none折叠和拒绝strict键的 Provider 依赖"统一缺省"。

2.7 统一入口adaptSchemaForStrict

adapt.ts 的adaptSchemaForStrict(schema, strict)把上述舞蹈收敛为一个函数:先upgradeJsonSchemaTo202012将 draft-07 形态升级为 draft 2020-12(prefixItems$defs等语义对齐);strict=false时原样返回升级结果;strict=true时调用tryEnforceStrictSchema并在不可表达时回退非严格。三个 OpenAI Provider(openai-completions.ts、openai-responses.ts、openai-codex-responses.ts)的调用点统一走该入口。

三、Google Gemini / Vertex / Gemini CLI:normalizeSchemaForGoogle

发往 Google JSON Schema 通道(parametersJsonSchema)的 schema 必须遵守四条规则,normalizeSchemaForGoogle的选项集见 normalize.ts。

3.1 剥离不支持的 JSON Schema 关键字(properties下的属性名除外)

UNSUPPORTED_SCHEMA_FIELDS(fields.ts)定义了被剥离的关键字全集:

$schema, $ref, $defs, $dynamicRef, $dynamicAnchor examples, prefixItems, unevaluatedProperties, unevaluatedItems patternProperties, additionalProperties minItems, maxItems, minLength, maxLength minimum, maximum, exclusiveMinimum, exclusiveMaximum pattern, format dependencies, dependentSchemas, dependentRequired deprecated, readOnly, writeOnly, $comment x-mcp-header

两点实现细节值得注意:

  • properties对象内部的键是属性名,绝不能按关键字匹配误删——walker 通过insideSchemaMap状态区分"schema 槽位"与"属性映射表";
  • 对人可读的被剥离关键字patternformat、min/max 约束、defaultexamples等),按 Anthropic 风格 spill 块追加到同级description,例如{pattern: "^foo$", minimum: 0};而$ref$defsadditionalProperties等结构性/元关键字不做 spill。spill 的两种格式(spillparen)实现在 spill.ts:spill格式输出{key: value, key2: value2}追加在空行后,paren格式以(key: value)后缀拼接。UNSUPPORTED_SCHEMA_FIELDS中还包括x-mcp-header(MCP Streamable HTTP 传输的Mcp-Param-*头注解,CCA 对未知字段名会 400)以及deprecated/readOnly/writeOnly/$comment(protojson 无对应字段、拒绝未知字段,例如 Stitch screen 工具曾因此整请求 400)。

3.2type数组归一化为标量 + 可空标记

type: ["T", "null"]变为type: "T"nullable: true——Google 期望标量type,不接受type[]。源码中preHandleNullFields在父层先于子节点递归执行(与 python-genai 的handle_null_fields调用序一致),把type: "null"节点或含 null 分支的anyOf折叠为nullable: true

3.3constenum

节点存在const时,schema 使用/合并enum与 const 值。此外 Google 路径启用inferTypeForBareEnum: truestringEnumsOnly: true:裸enum推断标量type,且只保留字符串 enum——测试 schema-normalization.test.ts 验证了enum: ["draft", "published"]被保留、数字 enum 与混合 enum 被移除。

3.4 对象 schema 获得显式 properties 映射

{ "type": "object" }会被改写为{ "type": "object", "properties": {} }ensureObjectProperties: true),同时autoPropertyOrdering: true为属性排序,normalizeFieldNames: true处理 snake_case→camelCase 重命名(如additional_propertiesadditionalPropertiesany_ofanyOfprefix_itemsprefixItems)。

四、Cloud Code Assist Claude:normalizeSchemaForCCA

CCA Claude 工具声明比通用 Google 路径更严格,是整份契约中最复杂的一段。它在 normalize.ts 中通过一组独有选项开启,google-shared.tsmodel.idclaude-开头时选择该路径,并通过 google-gemini-cli.ts 以相同管线运行。

4.1 传输契约

  • CCA Claude 使用传统的parameters字段(而非parametersJsonSchema);
  • CCA 路径跑完整的normalizeSchemaForCCA管线,不允许只做第一道关键字剥离。

4.2 清洗契约

  1. 起点与 Google 相同:剥离UNSUPPORTED_SCHEMA_FIELDS
  2. nullable关键字必须被剥离stripNullableKeyword: true,与 Google 路径的false形成对照);
  3. type: ["T", "null"]变为type: "T"不带nullable标记;
  4. 被剥离的人可读关键字同样以 Google 分发器相同的 spill 格式追加到description

4.3 组合器/联合归一化契约

  • 纯对象的anyOf/oneOf变体在安全前提下合并为单一对象形状mergeObjectCombiners: true);
  • 同类型的组合器变体折叠为一个 schemacollapseSameTypeCombiners: true);
  • 混合类型的组合器变体在 CCA 接受需要时,允许有损折叠到第一个非 null 标量类型collapseMixedTypeCombiners: true);
  • 残余组合器在可折叠处递归剥离(stripResidualCombinersFixpoint: true)。折叠时使用CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS(按 array/object/string/number/integer/boolean/null 分类的允许键)与CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYStitle/description/default/examples)过滤兄弟键。

4.4 可空属性归一化契约

nullable: true、含nulltype联合、或含单个{ "type": "null" }分支的anyOf/oneOf表达的属性级可空性,必须转换为"非必填属性"语义extractNullableFromUnions: true提取联合中的 null)。归一化后仍检测到可空的属性,必须从required中移除

4.5 残余不兼容门控(硬停止)

归一化后,schema 中绝对不允许再出现以下任何一种:

  • type为数组
  • type: "null"
  • nullable
  • anyOfoneOfallOf数组

rejectResidualIncompatibilities: ["type-array", "type-null", "nullable", "combiners", "not"]逐项检查,任何残余都判定为不兼容。

4.6 校验 + 兜底契约

  1. 归一化结果使用AJV 2020 schema 校验
  2. 若校验失败存在残余不兼容,输出必须回退到:
{ "type": "object", "properties": {} }

CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA,见 normalize.ts); 3. 兜底是按工具粒度、fail-open的——一个坏工具 schema 绝不能拖垮整个请求。

五、Provider 实用映射速查

Provider 路径规范化入口上线字段
OpenAI 兼容严格路径(openai-completionsopenai-responsesopenai-codex-responsesadaptSchemaForStrict仅当严格强制成功时发送strict: true
Google Gemini / Vertex / Gemini CLI(非 CCA Claude)normalizeSchemaForGoogleparametersJsonSchema
Cloud Code Assist Claude(model.idclaude-开头)normalizeSchemaForCCAparameters(清洗后的归一化 schema)

这套映射在 google-shared.ts 中体现为一行分支:CCA Claude 走{ parameters: normalizeSchemaForCCA(toolWireSchema(tool)) },其余走{ parametersJsonSchema: normalizeSchemaForGoogle(toolWireSchema(tool)) }toolWireSchema负责从工具声明提取线上 schema。

六、维护规则:新增/修改适配器的红线

CONSTRAINTS.md第 5 节给出四条必须遵守的工程纪律:

  1. 任何新的不支持关键字必须加入 fields.ts 中相应集合(UNSUPPORTED_SCHEMA_FIELDSNON_STRUCTURAL_SCHEMA_KEYSLIFTABLE_TO_DESCRIPTION_FIELDSCCA_UNSUPPORTED_SCHEMA_FIELDS等)。集合采用Record<string, true>字面量而非Set,是为了走 hidden class 内联缓存、避免Set.has的每调用哈希开销;
  2. 任何新的归一化规则必须在 packages/ai/test 下补充回归测试(现成的参照有 schema-strict-mode.test.ts、schema-normalization.test.ts、google-tool-schema.test.ts);
  3. Provider 代码禁止绕过适配辅助函数adaptSchemaForStrictnormalizeSchemaForGooglenormalizeSchemaForCCAnormalizeSchemaForMCP),统一通过 index.ts 的 re-export 引用;
  4. 若 Provider 只部分支持 schema,优先确定性按工具兜底,而非请求级失败。

七、Gemini CLI / Antigravity 与 CCA 的对齐要求

Gemini CLI / Antigravity 的 Claude 路径必须运行与共享 Google Claude 路径相同的完整normalizeSchemaForCCA管线(google-gemini-cli.ts 中parameters: normalizeSchemaForCCA(parametersJsonSchema)即为佐证)。禁止只调用第一道关键字剥离——否则对象组合器、可空联合、残余组合器和兜底门控在不同传输间会产生不一致行为。

八、全局旁路:PI_NO_STRICT环境变量

adapt.ts导出的NO_STRICT = $flag("PI_NO_STRICT")是所有发送strict: true的 Provider(openai-completionsopenai-responsesopenai-codex-responses及 anthropic 的严格候选选择)共同遵守的全局旁路开关,典型用途是调试"错误上报严格支持"的 Provider,或对比严格/非严格输出。如前文所述,它同时参与openai-responsesopenai-codex-responses的显式false发送门控——设置该变量即等于文档化的全局绕过,让 Codex 代理拒绝的strict键整体不上线。

总结

CONSTRAINTS.md的价值在于把"每类 Provider 能吃什么、不能吃什么、失败了怎么办"固化成一份可验证的操作契约:OpenAI 严格模式强调结构性重写与 fail-open 回退,Google 路径强调关键字剥离与 spill 保真,CCA 路径则叠加了组合器折叠、可空属性去required化、残余不兼容硬停止与 AJV 校验兜底。理解这套契约,再结合fields.ts的关键字分类、normalize.ts的选项驱动核心与adapt.ts的统一入口,你就能在新增 Provider 适配或排查工具调用 400 错误时快速定位问题,并确保新增逻辑不破坏既有传输的一致性。

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 6:05:32

QEMU CPU建模完全指南:从TCG原理到新增指令集实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:05:32

接收器与混频器深度解析:从原理到故障排查

接收器和混频器这两个词&#xff0c;在射频和音频领域是老面孔了。普通用户可能在蓝牙音频接收模块、无线鼠标接收器这些产品上接触“接收器”多一些&#xff0c;而做通信、做SDR的工程师则天天跟混频器打交道。但很多人其实把这两者的关系想得过于割裂——实际上&#xff0c;绝…

作者头像 李华