
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.ts、adapt.ts、fields.ts实现三类核心约束——OpenAI 严格模式strict mode、Google 规范化、Cloud Code Assist ClaudeCCA规范化并结合源码与测试给出可落地的适配规则。读完你将掌握工具 Schema 在各 Provider 之间的可移植性设计、关键字剥离与降级兜底策略以及新增适配器时必须遵守的维护红线。一、为什么需要一份Schema 约束契约不同的 Provider 对 JSON Schema 的接受度差异是工具调用链路上最常见的 400 错误来源OpenAI 严格模式要求每个 schema 节点必须有type或组合器、$ref、not对象必须additionalProperties: false可选属性必须可空化Google Gemini/Vertex的 Schema proto 拒绝$ref、prefixItems、additionalProperties等一批标准关键字protojson遇到未知字段会直接INVALID_ARGUMENTCannot find fieldCloud Code AssistCCAClaude走更严格的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当调用点请求stricttrue时适配后的 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: 3、format: 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) ∨ C→A ∨ B ∨ C父级无description时内层description上提。严格输出中绝对不允许出现分支本身还是纯 union的anyOf——上游部分校验器如 OpenRouter 背后的 DeepSeek会拒绝没有type的分支issue #2270元组条目prefixItems同样递归严格化。从源码可以看到enforceStrictSchemaBody的具体实现路径遍历properties对每个不在原required中的键先判断是否已可空再判断isPureAnyOfNode决定原地追加还是包装最后result.required Object.keys(strictProperties)随后递归处理items、prefixItems、COMBINATOR_KEYSanyOf/allOf/oneOf并对$defs/definitions做同样的严格化。2.4 节点必须能在严格模式下表达没有type、组合器、$ref或not的节点在严格强制下是非法的必须抛错。典型非法节点{}、{ items: {} }。enforceStrictSchemaBody末尾会先从同质基本类型的enum/const推断typeinferStrictPrimitiveTypeFromEnumOrConst推断不出且无$ref/组合器/not时抛出ValidationError(Schema node has no type, combinator, or $ref — cannot enforce strict mode)。2.5 失败模式回退到非严格fail-opentryEnforceStrictSchema是严格强制的安全壳它先做一次hasUnrepresentableStrictObjectMap预检——只要树中存在patternProperties或additionalProperties: true/对象就整体判定不可严格化并提前返回{ strict: false, schema: upgraded }避免在强制阶段抛错强制阶段sanitizeSchemaForStrictMode→enforceStrictSchema任何异常同样被捕获必须返回{ 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 字段时才输出显式falseopenai-codex-responses显式false以!PI_NO_STRICT为门控使全局旁路能保持 Codex 代理拒绝的strict键不上线openai-completions仅当toolStrictMode mixed且compat.supportsStrictMode ! false时输出显式false——因为all_strict → none折叠和拒绝strict键的 Provider 依赖统一缺省。2.7 统一入口adaptSchemaForStrictadapt.ts 的adaptSchemaForStrict(schema, strict)把上述舞蹈收敛为一个函数先upgradeJsonSchemaTo202012将 draft-07 形态升级为 draft 2020-12prefixItems、$defs等语义对齐strictfalse时原样返回升级结果stricttrue时调用tryEnforceStrictSchema并在不可表达时回退非严格。三个 OpenAI Provideropenai-completions.ts、openai-responses.ts、openai-codex-responses.ts的调用点统一走该入口。三、Google Gemini / Vertex / Gemini CLInormalizeSchemaForGoogle发往 Google JSON Schema 通道parametersJsonSchema的 schema 必须遵守四条规则normalizeSchemaForGoogle的选项集见 normalize.ts。3.1 剥离不支持的 JSON Schema 关键字properties下的属性名除外UNSUPPORTED_SCHEMA_FIELDSfields.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 槽位与属性映射表对人可读的被剥离关键字pattern、format、min/max 约束、default、examples等按 Anthropic 风格 spill 块追加到同级description例如{pattern: ^foo$, minimum: 0}而$ref、$defs、additionalProperties等结构性/元关键字不做 spill。spill 的两种格式spill与paren实现在 spill.tsspill格式输出{key: value, key2: value2}追加在空行后paren格式以(key: value)后缀拼接。UNSUPPORTED_SCHEMA_FIELDS中还包括x-mcp-headerMCP Streamable HTTP 传输的Mcp-Param-*头注解CCA 对未知字段名会 400以及deprecated/readOnly/writeOnly/$commentprotojson 无对应字段、拒绝未知字段例如 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.3const转enum节点存在const时schema 使用/合并enum与 const 值。此外 Google 路径启用inferTypeForBareEnum: true与stringEnumsOnly: 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_properties→additionalProperties、any_of→anyOf、prefix_items→prefixItems。四、Cloud Code Assist ClaudenormalizeSchemaForCCACCA Claude 工具声明比通用 Google 路径更严格是整份契约中最复杂的一段。它在 normalize.ts 中通过一组独有选项开启google-shared.ts在model.id以claude-开头时选择该路径并通过 google-gemini-cli.ts 以相同管线运行。4.1 传输契约CCA Claude 使用传统的parameters字段而非parametersJsonSchemaCCA 路径跑完整的normalizeSchemaForCCA管线不允许只做第一道关键字剥离。4.2 清洗契约起点与 Google 相同剥离UNSUPPORTED_SCHEMA_FIELDSnullable关键字必须被剥离stripNullableKeyword: true与 Google 路径的false形成对照type: [T, null]变为type: T且不带nullable标记被剥离的人可读关键字同样以 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、含null的type联合、或含单个{ type: null }分支的anyOf/oneOf表达的属性级可空性必须转换为非必填属性语义extractNullableFromUnions: true提取联合中的 null。归一化后仍检测到可空的属性必须从required中移除。4.5 残余不兼容门控硬停止归一化后schema 中绝对不允许再出现以下任何一种type为数组type: nullnullable键anyOf、oneOf、allOf数组rejectResidualIncompatibilities: [type-array, type-null, nullable, combiners, not]逐项检查任何残余都判定为不兼容。4.6 校验 兜底契约归一化结果使用AJV 2020 schema 校验若校验失败或存在残余不兼容输出必须回退到{ type: object, properties: {} }CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA见 normalize.ts 3. 兜底是按工具粒度、fail-open的——一个坏工具 schema 绝不能拖垮整个请求。五、Provider 实用映射速查Provider 路径规范化入口上线字段OpenAI 兼容严格路径openai-completions、openai-responses、openai-codex-responsesadaptSchemaForStrict仅当严格强制成功时发送strict: trueGoogle Gemini / Vertex / Gemini CLI非 CCA ClaudenormalizeSchemaForGoogleparametersJsonSchemaCloud Code Assist Claudemodel.id以claude-开头normalizeSchemaForCCAparameters清洗后的归一化 schema这套映射在 google-shared.ts 中体现为一行分支CCA Claude 走{ parameters: normalizeSchemaForCCA(toolWireSchema(tool)) }其余走{ parametersJsonSchema: normalizeSchemaForGoogle(toolWireSchema(tool)) }toolWireSchema负责从工具声明提取线上 schema。六、维护规则新增/修改适配器的红线CONSTRAINTS.md第 5 节给出四条必须遵守的工程纪律任何新的不支持关键字必须加入 fields.ts 中相应集合UNSUPPORTED_SCHEMA_FIELDS、NON_STRUCTURAL_SCHEMA_KEYS、LIFTABLE_TO_DESCRIPTION_FIELDS、CCA_UNSUPPORTED_SCHEMA_FIELDS等。集合采用Recordstring, true字面量而非Set是为了走 hidden class 内联缓存、避免Set.has的每调用哈希开销任何新的归一化规则必须在 packages/ai/test 下补充回归测试现成的参照有 schema-strict-mode.test.ts、schema-normalization.test.ts、google-tool-schema.test.tsProvider 代码禁止绕过适配辅助函数adaptSchemaForStrict、normalizeSchemaForGoogle、normalizeSchemaForCCA、normalizeSchemaForMCP统一通过 index.ts 的 re-export 引用若 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的 Provideropenai-completions、openai-responses、openai-codex-responses及 anthropic 的严格候选选择共同遵守的全局旁路开关典型用途是调试错误上报严格支持的 Provider或对比严格/非严格输出。如前文所述它同时参与openai-responses与openai-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),仅供参考