Jev 官方 SDK 指南:typesafe-sdk 与 @typesafe-ai/sdk 怎么用
这个站过去所有教程都是直接打 HTTP,或者套一个 OpenAI 兼容客户端。这样确实能跑,但 TypeSafe 其实还发布了两套官方 SDK,而本站此前一个字都没提过。这篇就是补上这块:Python 和 JavaScript SDK 到底装了什么、请求发生了什么变化、客户端有哪些参数、重试和压缩怎么工作,以及什么情况下老老实实写三十行 HTTP 反而更省事。
以下内容全部取自 2026-11-05 当天的官方文档:Python SDK 页、它的基础用法与进阶用法、RetryPolicy 参考、两套 SDK 的 changelog、模型页以及请求规则与错误页。版本号和日期都是文档上印的,没有估算值。
一句话版本:
pip install typesafe-sdk(importtypesafe_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 的职责就是把这份特别变成平常:
- 问题用类型化对象构建(
Noul(instructions=...)、Choice(instructions=..., criteria={...})、Score(instructions=..., criteria=[...])),不用手写要和 schema 对齐的 JSON。 - 答案从类型化的桶里取,而不是
choices[0].message.content。Python 响应里就是nouls、choices、scores三个以你问题名为键的字典;JavaScript 是一个answers对象,官方原话是「答案类型由你发的问题推断出来」。 - 用图片时,图片变成 SDK 帮你挂载、去重的 asset,而不是你手动 base64 塞进字符串里。
它不做的事同样要清楚:它不会把 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_KEY | API key;不通过 api_key 传时必填 |
TYPESAFE_DEFAULT_MODEL | 默认模型名 |
TYPESAFE_BASE_URL | API 根地址,用于自建代理 |
TYPESAFE_LOG_LEVEL | debug、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 是合理默认。但下面这三种情况下它是错的选择,而且都不罕见:
- 你现在跑在 OpenRouter 上。 SDK 打的是
POST https://api.typesafe.ai/v1/systemone;OpenRouter 上typesafe/jev-1.14声明的输出模态是decisions,走的是 OpenRouter 的路由层。端点和请求体都不同,改base_url搭不过去——用渠道指南里的适配器写法。 - 你的运行时不在 SDK 覆盖范围内。 JavaScript SDK 要求 Node.js 22+;Python 有同步和异步两套客户端。Go、Rust、PHP、Java 在这套文档里没有官方 SDK,那裸 HTTP 就是老实答案——见第一次 API 调用。
- 热路径上一个依赖都不想加。 判断请求体就是个扁平 JSON。三十行
fetch/requests包装就能覆盖,错误码表短到可以手写处理。SDK 值得那份额外依赖,是在你要重试、要 asset、要类型化答案的时候,不是为了一个Noul。
从裸 HTTP 迁到 SDK 的检查清单
按这个顺序做,能绕开两个最常见的失败:悄悄改掉了 state 的内容,以及丢失了「这次到底是哪个模型答的」的线索。
- 先把问题定死。 在动代码之前把
Noul/Choice/Score的定义写出来、把措辞过一遍。SDK 让加问题变得太便宜,这本身就是加十个问题的诱惑。 - 阈值判断留在客户端外面。 置信度怎么用是你的业务规则,SDK 只负责把数字给你。1.13 上调好的阈值,切别名之前先重读置信度与兜底。
- 第一天就记
response.model。 这是唯一可靠的、记录哪个版本作答的手段。 - 显式设
RetryPolicy,并且给timeout定预算,别继承你还没读过的默认值。 - 本地开一次 debug 日志,亲眼确认 body 里都有什么,然后再决定要不要在部署环境打开。
- 保留一个适配函数。 底层实现是
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 不在你和计量表之间加任何一层。