← Clavis Sinica Docs · OpenAPI · llms.txt · Get a key · English

Clavis Sinica API 接入指南

面向:准备接入 ClavisSinica.org 十一项中文语义能力的第三方开发者 版本:2026-09-10 基址:https://clavissinica.org


0 先读这一段

接一次,拿十一个能力。 十一项服务共用同一个端点、同一套认证、同一种请求形态、同一套错误码和计费规则。你需要写的分支只有一个:同步返回结果,还是提交后轮询。

POST /v1/chinese/run          ← 唯一的业务端点
Authorization: Bearer ck-live_...
{"service_code": "<九选一>", "payload": {...}}

不要在你的代码里硬编码字段规格。 字段名、类型、长度上限、枚举取值、必填与否,全部在 GET /v1/chinese/services 里,零费用、任意有效 Key 可读。它由服务端的同一份定义生成——引擎校验用的就是它。启动时拉一次,比抄一份到本地可靠。

这不是客套话。产品站曾经维护过一份本地手抄的规格表,累计出过五处错误,每一处的根因都一样:规格只存在于服务端的代码里,而本地那份没人会提醒你更新。接上这个接口后他们删掉了本地三张表。


0b 别猜字段名 —— 有免费的真实样例

GET /v1/chinese/examples                          索引,一次拉全部
GET /v1/chinese/services/{service_code}/example   单个服务

需要 Key,零 ckTok(读的是预先生成的文件,不调引擎)。

每份样例是一次真实调用的完整请求与响应,外加 response_fields——从真实值派生的字段名、类型、是否可空、数组元素的形状。

几个服务给了多个 variant,因为单个样本会隐藏差异:decompose.english_morph 有一个词的 parts 是空数组、一个词的 gloss 是空串、一个是完整形态。只探一个词,前两种都发现不了。

把样例当测试夹具用,不要手写——手写的夹具会和真实响应漂开,而那种失配不报错,只是渲染不出来。

1 拿 Key

两步,因为一步式发 Key 存在账户接管风险。

# ① 申请 —— 返回 202,不返回 Key
curl -X POST https://clavissinica.org/v1/keys \
  -H 'Content-Type: application/json' \
  -d '{"email": "you@example.com", "env": "live"}'
# {"ok":true,"status":"verification_sent","expires_in":900}

# ② 点邮件里的链接(15 分钟内有效,只能用一次)
# Key 只在确认响应里出现这一次,服务端只存哈希,取不回来

2 认证

Authorization: Bearer ck-live_xxxxxxxx

只有这一种方式。 没有 X-API-Key 头,没有查询参数。用错了会返回 401 missing_or_invalid_authorization。


3 请求形态:一律用 payload

{
  "service_code": "decompose.idiom",
  "payload": {"idiom": "守株待兔"},
  "idempotency_key": "your-id-001"      // 可选
}

payload 对十一个服务全部有效。 键名就是 /v1/chinese/services 里 fields[].name 的值。

另有一种快捷写法 {"input": "..."},只能设置单字段服务的主字段。它存在的唯一理由是历史兼容——如果你在新接入,直接忽略它,统一走 payload 就不会遇到「这个服务能不能用 input」这类判断。

顶层只接受四个键:service_code、input、payload、idempotency_key。其余的会被静默丢弃,但响应里会带一个 warnings 数组告诉你丢了什么:

"warnings": ["请求顶层的这些字段不被支持, 已忽略: explain_lang。..."]

建议把 warnings 打进日志。 有个接入方在顶层发了几个月的 explain_lang,它一直被丢弃,调用一切正常、返回一切正常,没有任何信号——这个 warnings 就是为那件事加的。


4 两种执行模式

/v1/chinese/services 里每个服务都有 execution.mode。

同步(九个服务)

直接返回结果,通常 0.1–1.5 秒。

{
  "ok": true,
  "service_code": "decompose.idiom",
  "data": { ... },
  "provenance": {"type": "lookup", "generated_fields": []},
  "cktok_charged": 10,
  "replayed": false,
  "test_mode": false
}

提交-轮询(decompose.lesson / decompose.wenyan_lesson)

这两项要跑 45–90 秒,不适合挂住 HTTP 连接。

// 提交立即返回
{"ok": true, "task_id": "task_xxx", "status": "pending", "poll_url": "/v1/chinese/tasks/task_xxx"}

然后每几秒轮询一次 GET /v1/chinese/tasks/{task_id},直到 status 是 done 或 error。


5 provenance:分清查表还是生成

每个响应都带:

"provenance": {
  "type": "lookup",
  "generated_fields": [],
  "data_version": "b09a900c0dcb",
  "code_version": "4cf2acc3a0d4"
}

如果你要把结果存成权威内容,先读这个字段。 lookup 的结果可以直接引用;generative 的应当注明来源或经人工复核。

decompose.lesson 的七层里,六层是生成的,字层不是——意符、声符、笔顺、本义分别来自不同的结构化数据源,本义溯源到 11,114 条古典字库中的具体条目。

拿同一个字去问任何模型,再对一次这里的返回,差别自己就出来了。

版本戳

响应里另外带两个指纹,各盖各的:

号变了的意思是「有可能影响这个结果的东西动过了」,不是「你的答案变了」。 这里是刻意多报的:误报「变了」最多让你多取一次;误报「没变」会让你拿着已经悄悄漂掉的内容继续引用。两个号都不是语义化版本号,只能比相等,不能比大小。

GET /v1/chinese/services 的 versions 里也有同样两个号,那个端点零 ckTok,所以定期查一次「有没有变」不花钱。


6 限流:四层,超限不收费

层 阈值 触发时
每 IP 1200 次/分钟 nginx 直接 429
每账户 120 次/分钟 scope: account_requests_per_minute
每账户在途异步任务 3 scope: account_inflight_tasks
全局在途异步任务 20 scope: global_task_queue_full
{"detail": {"error": "rate_limited", "scope": "account_inflight_tasks",
            "msg": "...", "limit": 3, "retry_after": 60}}

响应带 Retry-After 头,请遵守它,不要紧循环重试。 被限流的请求不扣 ckTok。

按 IP 那层刻意设得宽松——如果你是服务器端接入,你全部客户的流量会从同一个出口 IP 出来,按 IP 限额会让他们互相挤占。真正约束你的是按账户那三层。

不用等到被拒才知道自己还剩多少。每次成功调用都带着:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 60

Reset 是窗口里最老那次调用还有几秒滑出去,也就是「至少多久会空出一个名额」。异步服务另带 X-RateLimit-Async-Remaining(你自己在途的任务,共 3 个)和 X-RateLimit-Queue-Remaining(全局队列,共 20 个)。429 上也带同样这几个头,和 Retry-After 一起。

浏览器端也读得到这几个头以及 X-ckTok-Balance——它们都列在 Access-Control-Expose-Headers 里。


7 错误处理

错误码在 error,人类可读信息在 msg。参数类错误额外带 field 和 expected。

HTTP error 含义 该不该重试
400 invalid_param 你的参数有问题,field 说是哪个 改了再发
400 no_text OCR 没识别到文字 换张图
401 missing_or_invalid_authorization 认证头缺失或格式错 检查 Bearer
401 wrong_key_prefix 用了 pk_live_ 等非 ck- 前缀 换 Key
401 invalid_or_revoked_key Key 无效或已吊销 换 Key
404 not_found 接口正常,内容不在词典里 不要重试
413 payload_too_large 超过声明的体积上限 压缩后再发
415 unsupported_media_type 图片格式不支持 转成 JPEG/PNG/WebP
429 rate_limited 见第 6 节 按 Retry-After
502 upstream_error 连不上/超时,引擎根本没被调到 可以重试
502 upstream_rejected 上游返回失败且无法分类 谨慎重试
503 upstream_unavailable 上游依赖不可用 应该重试

注意 502/503 和 4xx 的区别:4xx 是你的请求有问题,重试没用;upstream_error 和 upstream_unavailable 跟你的请求无关,退避后重试是合理的。

扣了钱没拿到结果的,一律自动退款,响应里会带 note 说明。

一条真实教训

早期版本有个兜底错误码叫 engine_rejected_input,凡是没带明确状态的错误都归到它名下——包括上游超时这种引擎根本没被调用的情况。有个接入方因此把自己脚本的一个粘贴错误,误判成了「服务端字段名和文档不一致」的结构性问题。

现在的错误码按真实成因分类,不会把服务端的故障说成你的输入有问题。


8 计费与幂等

什么时候扣费

限流 → 参数校验 → 扣费 → 执行 → 失败自动退款

必填参数缺失、格式不对这类错误在扣费之前就被拒,账本上不会留任何记录。

但有三种例外,它们的检查在引擎内部(也就是扣费之后):图片尺寸过小、体积超限(413)、格式不对(415)。这三种是扣了再退——净扣费为零,但账本上会看到扣款与退款两条记录。做对账时要知道这一点。

余额

每个 /v1/chinese/run 响应都带一个头:

X-ckTok-Balance: 432

不用另外查。建议在这个数字低于你最贵那个服务的单价时就告警——decompose.lesson 是 100,余额剩 80 时它对所有用户都会 402,而你的健康检查可能一片绿。

幂等的作用域是账户,不是 Key。 你在重试期间轮换了 Key,幂等仍然有效。


9 十一项能力速览

价格与调用形态以 GET /v1/chinese/services 为准,下面是选型参考。

service_code ckTok 模式 产出
dict.multilingual 4 sync 一个中文字/词 → 它在各语种里的释义。不是通用翻译
convert.script 2 同步 简繁转换,四种方向
ocr.image 6 同步 图片 → 文字
decompose.english_morph 8 同步 英文词缀拆解、词根家族
decompose.idiom 10 同步 成语释义、例句、出处引证、HSK 级、感情色彩
decompose.wenyan 12 同步 逐字本义、说文引证、六书分类、古今义对比
decompose.sentence 15 同步 分词、拼音、语法讲解、逐词下钻
decompose.passage 20 同步 可读性判定、生词率、HSK 分布、优先字词、句式清单
decompose.passage_deep 45 同步 一次调用出篇章七层:主旨脉络、可读性、分词、字表、词表、逐句解读、句式
decompose.lesson 100 轮询 现代文课文七层教案
decompose.wenyan_lesson 100 轮询 文言课文七层教案

几条反直觉的事实,早知道省时间


10 沙盒与免费试用


11 一份最小可用的客户端

import time, requests

BASE = "https://clavissinica.org"

class Clavis:
    def __init__(self, key):
        self.h = {"Authorization": f"Bearer {key}",
                  "Content-Type": "application/json"}
        # 启动时拉一次规格,不在本地硬编码
        self.spec = requests.get(f"{BASE}/v1/chinese/services",
                                 headers=self.h, timeout=30).json()
        self.mode = {s["service_code"]: s["execution"]["mode"]
                     for s in self.spec["services"]}

    def run(self, code, payload, idem=None, _tries=0):
        body = {"service_code": code, "payload": payload}
        if idem:
            body["idempotency_key"] = idem
        r = requests.post(f"{BASE}/v1/chinese/run", headers=self.h,
                          json=body, timeout=180)

        if r.status_code == 429 and _tries < 5:
            time.sleep(int(r.headers.get("Retry-After", 30)))
            return self.run(code, payload, idem, _tries + 1)
        if r.status_code in (502, 503) and _tries < 3:
            time.sleep(2 ** _tries)
            return self.run(code, payload, idem, _tries + 1)
        r.raise_for_status()

        out = r.json()
        for w in out.get("warnings", []):
            print("WARN", w)          # 别忽略这个
        if self.mode.get(code) == "submit_poll":
            return self._poll(out["task_id"])
        return out

    def _poll(self, task_id, timeout=300):
        deadline = time.time() + timeout
        while time.time() < deadline:
            time.sleep(5)
            t = requests.get(f"{BASE}/v1/chinese/tasks/{task_id}",
                             headers=self.h, timeout=30).json()
            if t.get("status") in ("done", "error"):
                return t
        raise TimeoutError(task_id)

也可以用官方客户端,行为与上面这段完全一致:

pip install clavis-sinica
npm install @clavis-sinica-org/sdk

两个 registry 的命名习惯不同(PyPI 没有 scope 概念),是同一套 API。客户端替你做的就是上面这些:启动拉规格、429 按 Retry-After 退避、5xx 指数退避、异步自动轮询、打印 warnings。直接调 HTTP 同样受支持——客户端没有做任何你自己做不到的事。


12 已知的限制

写在这里而不是等你自己撞上:

13 参考