oh-my-pi 内部 schema 编写指南:深入 omptype 的懒加载 JIT 校验器与 ArkType 兼容 DSL
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
导读
本文是 oh-my-pi 仓库内部 schema 编写(schema authoring)的技术指南,围绕 docs/omptype-guide.md 展开。oh-my-pi 的 AI 工具系统在 provider 边界需要把工具参数定义序列化为模型可消费的 JSON Schema,同时对入参做高性能运行时校验;为此项目自研了@oh-my-pi/omptype——一个 ArkType 兼容、带懒加载 JIT 的校验器。读完本文,你将掌握:omptype 的性能契约与「检测契约」(schema 与 JSON Schema 如何在 wire 上被区分)、完整可复制的 ArkType 兼容字符串 DSL 定义语言、验证/断言/作用域/泛型/JSON Schema 互操作等核心用法,以及 TypeBox/Zod 风格适配器与 Standard Schema V1 的接入方式,并能基于仓库源码理解其内部实现原理。
为什么是 omptype:性能契约
内部 schema 统一使用@oh-my-pi/omptype——一个 ArkType 兼容、带懒加载 JIT 运行时的校验器(实现位于 packages/omptype,包说明见 packages/omptype/README.md)。编写类型统一通过import { type } from "@oh-my-pi/omptype"导入。
其性能设计的关键点如下(对应 packages/omptype/src/type.ts 的文档注释与JIT_THRESHOLD = 3常量,见 type.ts):
- 构造极廉价:
type()构造 schema 的成本大约是 arktype 的 1/100。它不做急切代码生成(eager codegen)、不做 node interning,schema 从字符串 DSL 解析为 IR 的开销接近零。 - 两段式执行:schema 的前两次调用由树遍历解释器(interp.ts)执行,第三次调用起通过
new Function编译出专用校验器并热替换(编译逻辑见 compile.ts)。这样「每个请求新建、只校验一次」的冷 schema 不会为编译付税,而热路径上被反复调用的 schema 校验耗时可降到几十纳秒级。 - 失败路径零浪费:失败只分配一个小的错误对象,消息字符串按需懒构建(见 errors.ts 的设计说明)。
- 没有函数级的
jitless模式:懒加载 JIT 已经把jitless想规避的启动开销移除了,直接import { type }即可。ScopeOptions虽然接受jitless标志用于 ArkType 兼容,但运行时从不读取它——这是兼容面,不是功能开关。
仓库自带的基准 packages/omptype/bench/bench.ts 展示了该设计的目标形态:type()构造 509ns(对比 ArkType 271.08µs,快约 532 倍)、热路径flat-small校验 25ns(对比 ArkType 5.10µs)等。这些数字来自 README 中 Apple M4 Max + Bun 1.3.14 的单次代表性运行,结果会随硬件、运行时与依赖版本变化,应以本地复测为准(bun packages/omptype/bench/bench.ts)。
检测契约:omptype schema 与 JSON Schema 如何被区分
在 provider 边界,工具参数既可能由 omptype schema(ArkType 风格可调用函数)编写,也可能来自遗留的 TypeBox 或纯 JSON Schema 文档。两者通过 packages/ai/src/utils/schema/wire.ts 统一归一化:
- omptype:可调用函数,且带有
.toJsonSchema与.assert方法——isArkSchema()正是按此判定(见 wire.ts)。 - JSON Schema:普通对象,直接按 JSON Schema 文档处理。
在边界处,toolWireSchema()(见 wire.ts)会调用toJsonSchema()生成 wire 表示,然后做三类后处理:
- 剪除
T | undefined分支:undefined在 JSON Schema 中没有对应形态,ArkType 会把它降级为空 schema 分支,导致{ anyOf: [{ type: "string" }, {}] }这类任意值都匹配的形态;严格 provider(如 OpenAI/Codex)会拒绝。pruneArkUndefinedUnionBranches()会从 ArkType 产出的anyOf/oneOf中丢弃无约束分支并内联唯一剩余分支(见 wire.ts)。 - 闭合已声明对象:递归地为声明了
properties且没有additionalProperties/patternProperties的对象节点设置additionalProperties: false,让模型面对的是闭合结构(closeDeclaredObjects,见 wire.ts)。 {}→true归一化:把无约束空 schema 归一化为布尔true,因为语法受限的采样器常把对象形态的{}理解成「生成空对象」而非「任意 JSON 值」(见 wire.ts)。
此外.narrow()谓词与.pipe()morph 只在本地校验时生效,在 wire 上会降级为其基础 schema——也就是说,发送给模型的是结构约束,模型产出的 JSON 仍会在本地接受这些谓词/morph 的二次校验。
定义语言:ArkType 兼容子集速查
omptype 直接使用 ArkType 兼容的字符串 DSL 编写定义。以下是完整构造对照表(源自指南原文,已结合源码确认其 IR 语义):
| 构造 | 形式 |
|---|---|
| 基本类型 | "string"、"number"、"boolean"、"null"、"undefined"、"unknown"、"object"、"bigint" |
| 整数 | "number.integer" |
| URL 字符串 | "string.url" |
| 字面量 | "'x'"、"5"、"true" |
| 联合 | "'a' \| 'b'"、"string \| null" |
| 数组 | "string[]"、"(string \| number)[]"、[def, "[]"] |
| 边界 | "number >= 0"、"0 < number <= 3600"、"1 <= string <= 10" |
| 可选键 | { "limit?": "number" }或值后缀{ limit: "number?" } |
| 默认值 | { count: "number = 10" }、type("string[]").default(() => []) |
| 未声明键 | "+": "reject"(失败)/"+": "delete"(剥离)/ 默认保留 |
| 记录 | { "[string]": "number" }—— 注意不是"Record<string, number>" |
| 运行时枚举 | type.enumerated(...RUNTIME_ARRAY) |
| 运行时拼装对象定义 | type.raw({...})(返回BaseType) |
| 关键字静态方法 | type.number.atLeast(5).atMost(300)、type.string |
几点实现佐证:
"number.integer"对应 IR 中int标志,解释器在 interp.ts 用Number.isInteger(v)校验;"string.url"对应url标志,用URL.canParse(v)校验(interp.ts)。type.enumerated(...)把运行时数组的每个元素编译成字面量节点并合成联合(见 type.ts);type.raw(def)直接makeType(parseDef(def), [], {})(见 type.ts)。- 对象默认值在构造期会被预校验:
normalizeDefaults会用解释器跑一遍默认值,非法默认值在type()时直接抛OmpTypeError(见 type.ts);可变静态默认值(对象/数组)会被拒绝,必须写成工厂函数(rejectMutableStaticDefault,见 type.ts)。
验证:与 arktype 相同的调用方式
import { type } from "@oh-my-pi/omptype"; const out = schema(value); if (out instanceof type.errors) { // out.summary → 人类可读消息;每个条目有 .path(数组)与 .problem throw new Error(out.summary); } // `out` 是验证/变形后的值(默认值已填充、多余键已剥离)关键语义:
- 失败返回
OmpErrors(OmpError的数组);type.errors === OmpErrors。 - 校验是快速失败(fast-fail):每次失败只产生一个错误条目。
- Morph 从不修改输入;当默认值、
"+": "delete"、pipes 生效时会返回全新对象。 - 绝不要用
.allows()做工具参数校验——它跳过 morphs/defaults/pipes。 .infer/.inferIn仅用于类型推断(type-only),运行时无值。- 定义期错误(坏 DSL、非法组合)在
type()时抛OmpTypeError。
错误对象层面(errors.ts):单条OmpError暴露code、path、expected、actual、problem、message;聚合OmpErrors暴露summary与byPath,支持数组式迭代(map/filter/[Symbol.iterator])。消息文本可经.configure()以字符串或回调覆盖。assert()失败时抛TraversalError(携带errors),见 type.ts 与 errors.ts。
联合类型失败还有针对性细节:当值明显指向某个分支时,会深入该分支产出精确的嵌套错误(路径、narrow 消息),而不是笼统的 "A or B" 期望——unionFail/discriminateFailure会根据字面量判别属性(如type: "'computer_call'")定位具体分支(见 interp.ts)。
方法一览
Fluent 方法全集(对应 type.ts 的接口定义):
.describe(d)、.default(v | () => v)、.or(TypeOrStringDef)、.and(Type)、.array()、.atLeastLength(n)/.atMostLength(n)(字符串/数组长度)、.atLeast(n)/.atMost(n)(数值边界)、.pipe(fn)、.narrow(fn)(配合ctx.mustBe("...")使用)、.allows(v)、.assert(v)、.toJsonSchema()。
.or()的推断注意点:schema 操作数和字符串操作数都能精确推断;对象字面量操作数会降级为宽泛类型——先用type({...})包装再参与.or()。
方法底层都是 IR 变换:.atLeast(n)/.atMost(n)写入数值 min/max(withNumericBound),.matching(RegExp)与 pattern IR 求交,日期边界(.atOrAfter等)编译为时间戳谓词 refine(见 type.ts)。.default()在构造期即用解释器验证默认值并缓存产出(静态默认值缓存defaultOutput,工厂默认值每次调用执行,见 type.ts 与 type.ts)。
作用域、模块与泛型
递归或互相引用的 schema 走具名作用域(实现在 type.ts 的scope()/type.scope()):
import { type } from "@oh-my-pi/omptype"; const types = type.module({ tree: { value: "number", "children?": "tree[]" }, });type.scope(aliases)(顶层也导出scope())返回一个TypeScope,具备.type、.define、.resolve、.import、.export;别名可以互相递归引用,#private名称保持内部可见性(不对外暴露)。type.module({...})把具名模块编译(等价于scope(...).export())成一组就绪 schema 的映射。type.generic("<T>", def)构造运行时泛型,其他定义可在作用域内对它实例化。generic还支持带可选约束的柯里化形式(见 type.ts)。
作用域采用惰性别名解析,支持环(cycles):别名节点在解析前先注册占位,递归引用会命中同一节点而非无限递归(resolveRef的注册先于lower,见 from-json-schema.ts)。此外还提供type.define()保留字面定义用于复用模块,type.fn(...)可为函数参数/返回值做运行时校验(TypedFunction,见 type.ts)。
JSON Schema 互操作
.toJsonSchema()默认输出 draft-2020-12(支持target: "draft-07");递归别名会发射$defs/$ref。选项还包括dialect、description、io("input"/"output",分别描述 morph 的输入或输出形态)与fallback回调(见 json-schema.ts)。fromJsonSchema(schema)从 JSON Schema 文档重建可调用 schema——toJsonSchema()的逆操作。支持 draft-07 / draft-2020-12 的结构化关键字、字符串 format(email、uuid、date-time、ipv4/ipv6、regex 等映射为内置关键字,见 from-json-schema.ts)、$defs递归、enum、anyOf/oneOf/allOf组合。未知或非结构化关键字被宽容忽略(保留能表达的约束,其余透传)。additionalProperties: false会映射为extras: "reject",对象形态的additionalProperties映射为索引签名(见 from-json-schema.ts)。type.withJsonSchema(schema, json)包装一个仅做验证的 schema,使其.toJsonSchema()即使嵌套在对象、数组或联合中也原样发射json;带默认值或改变输出的 morph 的 schema 会被拒绝。- 每个 schema 通过
~standard暴露Standard Schema V1(同步validate),可直接与@t3-oss/env、tRPC 等 Standard Schema 消费者配合使用。实现内联在 type.ts:~standard.validate调用run(),jsonSchema.input/output委托toJsonSchema(),且仅接受 draft-2020-12 与 draft-07 两种 target。
适配器:TypeBox 风格与 Zod 风格
TypeBox 风格与 Zod 风格的编写 API 底层都是同一个 omptype 运行时,产出的是真正的 omptype schema(同样具备 JIT 校验与toJsonSchema):
import { Type, type Static } from "@oh-my-pi/omptype/typebox"; import { z } from "@oh-my-pi/omptype/zod"; const User = z.object({ name: z.string() }); type User = z.infer<typeof User>;- Zod 适配器(zod.ts)提供
parse/safeParse、min/max/int/positive/regex/url、optional/nullable/default/describe/refine/transform/catch、strict/passthrough/strip/partial等 Zod v4 风格 surface,底层通过type.raw(embedded)与 morph/refine IR 落回 omptype 运行时。 - TypeBox 适配器(typebox.ts)与
@oh-my-pi/omptype/ark(ArkType 兼容门面,复导出同一套type/scope实现)同理。
内部代码统一直接使用字符串 DSL 编写 schema(即本文第二部分介绍的写法),适配器主要面向需要迁移既有 Zod/TypeBox 代码的场景。
何时校验:把 omptype 放回 AI 工具管线
回顾整体用法:工具作者在本地用 omptype DSL 声明参数,toolWireSchema()在边界把它转成 provider 消费的 JSON Schema(剪undefined分支、闭合对象、归一化空 schema),模型返回的 JSON 再经由同一 omptype schema 做带 morph/默认值的运行时校验,谓词与 morph 只在本地位移生效。关键文件路径:
- 包入口与实现:packages/omptype/src/index.ts、packages/omptype/src/type.ts、packages/omptype/src/interp.ts、packages/omptype/src/compile.ts、packages/omptype/src/errors.ts
- JSON Schema 发射与导入:packages/omptype/src/json-schema.ts、packages/omptype/src/from-json-schema.ts
- Provider 边界归一化:packages/ai/src/utils/schema/wire.ts
- 包级说明与基准:packages/omptype/README.md、packages/omptype/bench/bench.ts
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考