Jev 官方 SDK 指南:typesafe-sdk 与 @typesafe-ai/sdk 怎么用

更新于 适用版本 Jev 1.14

这个站过去所有教程都是直接打 HTTP,或者套一个 OpenAI 兼容客户端。这样确实能跑,但 TypeSafe 其实还发布了两套官方 SDK,而本站此前一个字都没提过。这篇就是补上这块:Python 和 JavaScript SDK 到底装了什么、请求发生了什么变化、客户端有哪些参数、重试和压缩怎么工作,以及什么情况下老老实实写三十行 HTTP 反而更省事。

以下内容全部取自 2026-11-05 当天的官方文档:Python SDK 页、它的基础用法与进阶用法、RetryPolicy 参考、两套 SDK 的 changelog、模型页以及请求规则与错误页。版本号和日期都是文档上印的,没有估算值。

一句话版本: pip install typesafe-sdk(import typesafe_sdk)或 npm install @typesafe-ai/sdk(需要 Node 22+)。两者包的都是同一个调用 POST https://api.typesafe.ai/v1/systemone:你发 state 加一组类型化 questions(Noul / Choice / Score,1.14 还有 Box),拿回类型化答案——Python 里是 nouls[...].noul、choices[...].choice、scores[...].score,JavaScript 里是 answers.<名字>.choice。SDK 真正的价值不在 HTTP 那层,而在四件事:类型化的问题定义、一行配置就搞定的重试策略、图片 asset 的自动去重挂载、1 KiB 以上的 zstd 压缩,还有日志里对密钥类请求头的脱敏。代价是:debug 日志里的 body 不脱敏,而且 SDK 打的是官方渠道,它不是 OpenRouter 那条线目的包装器。

SDK 到底替你做了什么

Jev 是 System One 模型:它不吐文章,它返回类型化判断。这就导致请求的形状很特别,而恰恰是这部分,通用 OpenAI 兼容客户端处理得最糟。SDK 的职责就是把这份特别变成平常:

它不做的事同样要清楚:它不会把 Jev 变成聊天模型,不会给你流式文本,也不会替你省掉理解 primitives 这一步。如果还没读过三种 primitives,先去看那篇——SDK 只是这三种(加上 Box)问题类型的便利层。

安装与第一个请求

Python 两种装法,等价:

uv add typesafe-sdk
# 或
pip install typesafe-sdk

可选 extras 有三个:http2(换 HTTP/2 传输层)、pil(直接发 Pillow 图片对象)、zstd(压缩请求体)。zstd 这个在 Python 3.14 以前的版本上是必须的,原因见下面的压缩小节。

密钥按文档的写法放环境变量:

export TYPESAFE_API_KEY="your-api-key"

然后是最小可用调用。下面这段来自官方 quickstart 的同步客户端,我删掉了不承载信息的行:

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

with TypeSafeClient() as client:
    response = client.system_one(
        state={"document": "I was charged twice. Please fix this ASAP."},
        questions={
            "billing": Noul(instructions="Is this ticket about billing?"),
            "tone": Choice(
                instructions="What is the customer's tone?",
                criteria={"calm": None, "frustrated": None, "angry": None},
            ),
            "urgency": Score(
                instructions="How urgent is this ticket?",
                criteria=["can wait", "this week", "today"],
            ),
        },
    )
    print(response.nouls["billing"].noul)
    print(response.choices["tone"].choice)
    print(response.scores["urgency"].score)

有三个细节值得停下来看。第一,state 收纯字符串,也收结构化对象,规则和裸 HTTP 一致。第二,Choice 的 criteria 在结构化写法里是「选项 → None」的映射,而 Score.criteria 是有序列表——这个顺序就是阶梯本身;而且从 SDK v0.6.0 起,Python 和 JavaScript 都改成收有序序列,不再是按整数键的字典。第三,答案按名字分桶返回,没有位置索引,也没有解析。

异步版形状一样,换成 AsyncTypeSafeClient 加 await:

import asyncio
from typesafe_sdk import AsyncTypeSafeClient, Noul

async def main() -> None:
    async with AsyncTypeSafeClient() as client:
        response = await client.system_one(
            "I was charged twice. Please help ASAP.",
            {"billing": Noul(instructions="Is this about billing?")},
        )
        print(response.nouls["billing"].noul)

asyncio.run(main())

JavaScript / TypeScript 需要 Node.js 22 或更新,这是 v0.7.0 引入的硬性要求:

npm install @typesafe-ai/sdk
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient();
const response = await client.systemOne({
  state: { document: "I was charged twice. Please fix this ASAP." },
  questions: {
    category: choice("What is this ticket about?", {
      billing: null,
      technical: null,
      other: null,
    }),
  },
});
console.log(response.answers.category.choice);

JS 包同时提供 ESM、CommonJS 和 TypeScript 声明。这里有个在两套 SDK 之间切换时最能坑人的差异:Python 方法叫 system_one,返回 nouls / choices / scores 三张分开的表;JavaScript 方法叫 systemOne,返回一个 answers 对象,答案的类型信息挂在答案本身上。

客户端参数与四个环境变量

Python 的 TypeSafeClient 构造参数是 api_key、model、retry、timeout、headers、transport、http_client、base_url、compression。显式传参优先于环境变量;而且空字符串或只有空白的环境变量会被忽略——如果你的部署系统会给未使用的配置注入空值,这一条能省你半天。

文档列明的环境变量:

变量作用
TYPESAFE_API_KEYAPI key;不通过 api_key 传时必填
TYPESAFE_DEFAULT_MODEL默认模型名
TYPESAFE_BASE_URLAPI 根地址,用于自建代理
TYPESAFE_LOG_LEVELdebug、info、warning、error、off

key 的校验很早也很严:首尾空白会被去掉,但空 key、中间夹空白、控制字符、非 ASCII 字符一律拒绝。v0.7.1 的 changelog 另说了一条——key 会从日志里的异常信息中排除。这条值得记,意思是就算请求挂了、打出 traceback,你的 key 也不会跟着露出来。

compression 有个容易踩的点:这个参数只接受 "zstd" 或 False。默认是有 zstd 就用,而「有 zstd 的要求没被满足」时客户端直接抛 TypeSafeError,不会偷偷降级成不压缩。所以如果你用瘦身容器镜像部署,要么老实装 typesafe-sdk[zstd],要么明确传 compression=False,别指望它自己兜底。

类型化答案:别数下标,去点名字

问题类型化了,答案自然也能类型化。Python 可以给 system_one 传 response_model:

from typesafe_sdk import Noul, NoulAnswer, SystemOneResponse

class BillingResponse(SystemOneResponse):
    billing: NoulAnswer

with TypeSafeClient() as client:
    result = client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="Is this about billing?")},
        response_model=BillingResponse,
    )
    assert 0 <= result.billing.noul <= 1

文档也说明,你完全可以定义一个不继承 SystemOneResponse 的全新模型。TypeScript 这边这层好处是白送的:答案类型由问题对象推断,answers.category.choice 的类型就是你的选项名联合,而不是 string。

向前兼容留了两个逃生口。extra_body 用来发 SDK 还没建模的请求字段——文档给的例子是示意性的 beam_width,并提醒只发 API 真正支持的字段。裸问题字典也能用,所以在 SDK 还没提供某个新 primitive 的类之前,你可以先用起来:

client.system_one(
    "I was charged twice.",
    {"billing": {"type": "noul", "instructions": "About billing?", "weight": 2}},
)

如果 API 返回了一种当前安装的 SDK 不认识的答案类型,SDK 会打一条 warning 然后跳过它。文档给的完整查看方式是 raw_http_response。这是个真实的运维陷阱:某次日常升级加了一种答案类型,而你锁在老版本上,解析结果里就悄悄少了东西,虽然原始 body 里明明有。

重试:是一份策略,不是手感

重试这块,自己手写客户端最容易烂掉。Python 把它做成了一个数据类:

字段控制什么
max_retries首次尝试之后的重试次数;0 表示关闭
backoff_initial首次退避秒数,逐次翻倍直到 backoff_max;为 0 关闭退避
backoff_max退避延迟上限;为 0 关闭退避
backoff_jitter每次延迟随机扣减的比例,0–1
http_statuses哪些 HTTP 状态码触发重试
respect_retry_after是否遵守 Retry-After / retry-after-ms 响应头
api_connection_error / api_timeout_error是否重试 TypeSafeAPIConnectionError / TypeSafeAPITimeoutError
exceptions追加的、会触发重试的异常类型
predicate收到异常时回调,返回 True 就重试
timeout单次调用的重试总预算(秒),含首次尝试与所有等待;None 表示不限

文档里的示例是 max_retries=3, timeout=10.0, http_statuses={429, 500, 502, 503, 504}——这是示例值,不是默认值声明。文档明确写下的行为是:SDK 默认会重试限流响应,两次尝试之间等得更久,响应带 Retry-After 就按它来。这和 API 自己的建议是一致的:遇到 429 Too Many Requests 或 529 Overloaded 要指数退避,别立刻重打。

timeout 这个字段要单独强调,因为它是按调用计的总预算,而且把等待时间算在里面。它会在一笔延迟会触达或超过预算的重试前停下,然后把最后一个错误抛出来。在有用户等着的请求路径上,这才是你真正该设的字段——它约束的是总时延,不是单次尝试。

图片与 Box 问题怎么过 SDK

图片能力在官方文档里标的是 dreamer-ready:能做原型、能试想法,但还不是生产可用,请求格式、答案和限制都可能随版本变。两套 SDK 都把它挡在一个头后面——TypeSafe-Danger-Zone-Images: true;Python 放在 headers 里,JavaScript 走客户端的 defaultHeaders 或单次调用的选项。图片只收 PNG 或 JPEG;JS 文档说 API 会根据字节判断格式,WebP、GIF 这类会被 BadRequestError 直接拒掉。

Python 的写法是把图片直接放进 content,这也是文档给的三种方式里最干净的一种:

from pathlib import Path
from typesafe_sdk import Image, Noul

photo = Image(returned_path)
with client.attach_assets(photo):
    response = client.system_one(
        state=f"The customer says {photo} arrived scratched.",
        questions={"damaged": Noul(instructions="Is the item damaged?")},
    )

attach_assets 这个上下文管理器不是装饰品。少了 with,SDK 会把渲染出来的引用当普通文本发出去,asset 根本没挂上——文档专门为此写了条警告,说明这是个高频自伤操作。另一条路是具名引用(Python 里 assets={"<product>": Image(path)}):定界符要选不会和正文冲突的写法,比如 <product>;而且 assets 里每个名字都必须在请求里出现,否则调用在发出去之前就被拒。

Box 问题——1.14 里那个返回边界框、用宽高比例表示的 primitive——从 Python SDK v0.7.5 和 JavaScript SDK v0.7.0 起支持,两版都是 2026-10-14 发布的。JS 文档明确说 Box 问题该用 jev-preview 模型,并提到 API 不报置信度时答案里的 confidence 是 null。Box 里到底装什么、四条边怎么换算成像素,去读1.14 的发布说明,这里不重复。

性能:三个决定吞吐的设置

进阶用法那页对 SDK 文档来说算相当具体,其中三条在量上来之后真正要紧。

复用客户端。 每次构造客户端都意味着新连接和 TLS 握手。一个进程一个客户端,反复用。

协议要有意识地选。 客户端默认 HTTP/1.1:单连接一次一个请求,并发请求各自开连接,直到连接数上限。HTTP/2 把请求全部复用到一条连接上,只需一次握手一个 socket,但那条连接就是吞吐上限。文档的建议有点反直觉——高请求速率保持默认的 HTTP/1.1,低到中等并发、尤其是有代理或防火墙限制连接数时,才用 HTTP/2。

默认是 100 条连接、空闲保活 30 秒。 你自己传 http_client 的话,这些默认值一个都不继承,连接数要显式设。Python 里 TypeSafeClient 可以跨线程共享,但重负载下连接池会成瓶颈,所以文档建议一个线程一个客户端。

压缩对 1 KiB 以上的请求体默认开启,算法是 zstd。state 大的场景是纯赚;每秒几百个小请求时就是噪音。Python 3.14 以前要装 [zstd] extra;JavaScript 在 Node.js 22.15+ 生效,可用 compression: false 关掉。如果你的代理会改请求体,宁可关掉压缩,也别去查那些神出鬼没的 400。

日志:脱敏有边界,这条最容易被忽略

SDK 打在 typesafe_sdk 这个 logger 上。可以 logging.getLogger("typesafe_sdk").setLevel(logging.DEBUG),也可以在 import 之前设 TYPESAFE_LOG_LEVEL。

脱敏规则要读仔细,因为它是不对称的。info 级别每个请求一行摘要。debug 级别会把请求头、响应头以及请求体和响应体都打出来。密钥类请求头——authorization、API key、cookie,以及名字里含 token 或 secret 的任意请求头——都会脱敏。但请求体和响应体明确不脱敏。而 Jev 的全部输入就是 state,所以生产环境开 debug 等于在记录你的客户文档。要开就按环境单独开,处理受监管数据的服务上永远别留着它。

版本漂移:模型要钉,SDK 也要钉

有两处版本面在你脚下动,互相独立。

模型侧:别名 jev-latest 和 jev-preview 目前都指向 jev-1.14.0;文档明说别名会随新版本发布而移动,也就是代码一行没改,答案可能已经变了。每个响应的 model 字段会报出实际作答的版本化 ID——把它记下来,否则准确率一变你连解释的线索都没有。

SDK 侧在两个月窗口里出过真破坏性变更。Python 在 v0.7.0(2026-09-18)把序列化库从 msgspec 换成了 pydantic;两套 SDK 都在 v0.6.0(2026-09-15)把 Score.criteria 从按整数键的字典改成有序序列。JavaScript v0.7.0(2026-10-14)把 Node.js 抬到 22,并停止导出请求载荷类型。版本范围定宽了又赶在周五部署,碎掉的就是这几处。

价格和裸 API 一致:模型页写的是每十亿输入 token $42(即每百万 $0.042),输出 token 免费,1.14 和 1.13 同价;上下文是每请求 64k、每问题 32k,带上 TypeSafe-Danger-Zone-Long-Context: true 时两者都提到 262k。SDK 不加价——它是客户端,不是网关。

什么情况下别用 SDK

新写的 Python 或 TypeScript 服务,SDK 是合理默认。但下面这三种情况下它是错的选择,而且都不罕见:

  1. 你现在跑在 OpenRouter 上。 SDK 打的是 POST https://api.typesafe.ai/v1/systemone;OpenRouter 上 typesafe/jev-1.14 声明的输出模态是 decisions,走的是 OpenRouter 的路由层。端点和请求体都不同,改 base_url 搭不过去——用渠道指南里的适配器写法。
  2. 你的运行时不在 SDK 覆盖范围内。 JavaScript SDK 要求 Node.js 22+;Python 有同步和异步两套客户端。Go、Rust、PHP、Java 在这套文档里没有官方 SDK,那裸 HTTP 就是老实答案——见第一次 API 调用。
  3. 热路径上一个依赖都不想加。 判断请求体就是个扁平 JSON。三十行 fetch / requests 包装就能覆盖,错误码表短到可以手写处理。SDK 值得那份额外依赖,是在你要重试、要 asset、要类型化答案的时候,不是为了一个 Noul。

从裸 HTTP 迁到 SDK 的检查清单

按这个顺序做,能绕开两个最常见的失败:悄悄改掉了 state 的内容,以及丢失了「这次到底是哪个模型答的」的线索。

  1. 先把问题定死。 在动代码之前把 Noul / Choice / Score 的定义写出来、把措辞过一遍。SDK 让加问题变得太便宜,这本身就是加十个问题的诱惑。
  2. 阈值判断留在客户端外面。 置信度怎么用是你的业务规则,SDK 只负责把数字给你。1.13 上调好的阈值,切别名之前先重读置信度与兜底。
  3. 第一天就记 response.model。 这是唯一可靠的、记录哪个版本作答的手段。
  4. 显式设 RetryPolicy,并且给 timeout 定预算,别继承你还没读过的默认值。
  5. 本地开一次 debug 日志,亲眼确认 body 里都有什么,然后再决定要不要在部署环境打开。
  6. 保留一个适配函数。 底层实现是 TypeSafeClient 还是 requests.post,上层调用方不该知道。

最后这个习惯,是把整个渠道问题变便宜的关键:SDK 用着不顺手,你换的是一个函数,不是一个代码库。

常见问题

Python 和 JavaScript 该从哪个开始? 跟你手上运行时走。两者包的是同一个端点、同一套 primitives,差别在命名(system_one 对 systemOne)、响应形状(nouls/choices/scores 对单个 answers 对象),以及 JavaScript 那边 Node.js 22 的门槛。

SDK 会帮我把置信度阈值处理掉吗? 不会。它给数字——noul、各选项概率、score,以及每个答案的 confidence——然后停在那里。阈值是你的策略,而且应该待在你自己的代码里,因为只有在那儿你才能拿有标注的样本集去测它。

当前 SDK 不认识的答案类型会怎样? 打一条 warning 然后跳过。文档让你用 raw_http_response 去看完整 body,这也是新问题类型比 SDK 版本先落地时该做的检查。

还能继续用裸 JSON 问题吗? 可以。两套 SDK 都既收类型化对象也收裸问题字典,Python 的 extra_body 还能把 SDK 没建模的请求字段带过去。文档的叮嘱是:只发 API 真正支持的字段。

用 SDK 会改变计费吗? 不会。价格来自 API:每十亿输入 token $42,输出 token 免费,1.14 与 1.13 相同。SDK 不在你和计量表之间加任何一层。

常见问题

+ Jev 的官方 SDK 叫什么?

两个:Python 包 typesafe-sdk(import 名是 typesafe_sdk),JavaScript / TypeScript 包 @typesafe-ai/sdk。都由 TypeSafe 官方发布,调的都是同一个端点 POST https://api.typesafe.ai/v1/systemone。按官方 changelog,Python SDK 最新版是 v0.7.5(2026-10-14),JavaScript SDK 最新版是 v0.7.0(2026-10-14)。

+ SDK 能直接连 OpenRouter 吗?

不能直接连。SDK 往 api.typesafe.ai 的 /v1/systemone 发 {state, questions} 这种请求体;OpenRouter 上 typesafe/jev-1.14 这条目声明的输出模态是 decisions,走的是 OpenRouter 那套路由层。端点和请求体都不一样,光改 base_url 换不过去。渠道对比看 api-channels 那篇。

+ zstd 压缩需要单独装吗?

Python 3.14 以前的版本要装 typesafe-sdk[zstd]。客户端默认对 1 KiB 以上的请求体做 zstd 压缩,压缩库不可用时会抛 TypeSafeError,不会悄悄改成不压缩。JavaScript 这边 zstd 压缩在 Node.js 22.15 及以上才生效,可以用 compression: false 关掉。

+ SDK 的重试是怎么控制的?

Python 用一个 RetryPolicy 数据类配:max_retries、backoff_initial、backoff_max、backoff_jitter、http_statuses、respect_retry_after、api_connection_error、api_timeout_error、exceptions、predicate,以及 timeout(整次调用的重试时间预算,单位秒)。RetryPolicy(max_retries=0) 就是彻底关掉重试。官方文档写明 SDK 默认会重试限流响应,并在响应带 Retry-After 时按它等待。

+ 开 debug 日志会不会把 API key 打出来?

请求头里的密钥会脱敏,请求体不会。info 级别每个请求只打一行摘要,debug 级别连请求头和响应头、以及请求体和响应体都打。authorization、API key、cookie,以及名字里带 token 或 secret 的请求头都会脱敏,但请求体和响应体的内容是原样记录的——所以密钥别往 body 里塞,客户资料也要注意。

继续阅读