Tokin 桌面端里跑技能的,就是这个接口:一个接口打通 DeepSeek、通义千问、豆包、智谱、Kimi、MiniMax 六家厂商。你的应用也可以直接用。
已经接入的请看 变更记录 — 接口、计费口径与安全规则的变动都记在那里,标了「破坏性」的需要改代码。
不用注册多家大模型、不用一家家充值——一个账号、一个钱包用遍所有模型,用多少付多少,没有月费、没有最低消费。
不用写注册登录页、不用管 API Key、不用逐家对接、不用垫付 token 成本——用户自己付自己的。
分岔点只有一个:这些 AI 调用,谁付钱?
面向 C 端、有很多最终用户:每个用户用自己的 Tokin 钱包付自己的量,你一分钱不垫、也不用管 Key。桌面应用、网页服务、App 都适用。
① 不用注册:标识自己起一个 dev_你的应用名 写进代码即可跑通全流程(开发联调、或长期只做中转,都可以一直这么用)
② 接 OAuth 授权(托管登录页,用户点一次授权,之后自动续期永久免登录)
③ 用拿到的 access_token 调 /v1 —— 用量自动归属你的 app_id
④ 要不要注册,看你需不需要这些:独占标识、用量统计归属、上架商店。需要就到控制台注册,把 dev_xxx 换成签发的 app_xxx,其余代码不动。
⚠ dev_ 标识不保证唯一:任何人都能用同一个字符串,用量统计会与之混在一起。要独占标识与干净的统计,请注册取得 app_(该号段只由平台签发,无法被他人占用)。切换到 app_ 后,此前记在 dev_ 名下的历史用量不会自动合并——在意统计连续性就一开始直接注册。另外 OAuth 回调的绑定位也按 app_id 共享,撞名会互相把对方锁在门外,所以 dev_ 请加随机后缀。
⑤ 上架应用商店 / 查看应用用量统计 —— 这两项需注册并完成实名认证(面向公众分发的合规要求)
内部工具、自研服务、原型验证:用你自己账号的余额跑,不必实名、不必注册应用、不必上架。
① 注册 Tokin 账号并充值(建议给项目单开一个账号当"公户",账目清爽、便于交接)
② 控制台 → API Keys → 新建子 Key,设额度上限 / 模型白名单 / 预算告警
③ 代码里用该子 Key 调 /v1(Key 只放服务端)
④ 想分开看用量:请求加 X-App-Id: 你起的标签,无需注册即可分组统计
日后要转成"用户各自付费",只需补认证 + 换授权那段代码,模型调用部分不动。
注册登录页由 Tokin 托管(含短信验证码 + 滑动验证)。你的应用一行代码都不用写。
丢进项目 libs/,加进 classpath 即可(零第三方依赖)。Maven 中央仓库即将上线。
# 装进本地 Maven 仓库后即可引用:
mvn install:install-file \
-Dfile=tokin-sdk-0.2.0.jar \
-DgroupId=ai.tokin -DartifactId=tokin-sdk \
-Dversion=0.2.0 -Dpackaging=jar
import ai.tokin.TokinClient;
TokinClient tokin = new TokinClient("dev_abax_jz");
String reply = tokin.chat("tokin-balanced", "帮我记一笔:午餐 35 元");
// 没绑定?自动弹浏览器让用户注册授权
// 绑定后自动续期,永久免登录
不想注册账号也能拿到正式 app_id——见下方「应用标识自助管理」。
dev_ 开头的标识免注册、直接能用,适合本地联调。但它不保证唯一——别人用同一个字符串可能抢先绑走你的回调,所以对外跑请用上面的匿名注册拿正式 app_。正式对外发布时到控制台创建应用换成 app_(16 位随机、他人无法占用,授权页显示你的应用名而不是裸标识)——其余代码一行不用改。
# 领一个正式 app_id。免鉴权、不用账号、不用实名。
# Idempotency-Key 可选(不带也能注册)。做重试的话强烈建议带:
# 这个接口发出去的标识收不回,重试必须安全。key 要在多次重试之间保持不变。
POST /v1/apps/register
Idempotency-Key: <你生成的唯一串>
{"name":"你的应用名"}
# → 201 { app_id, app_secret } 注意是 201 不是 200
# → 带同一个 Key 重放:200 + idempotent_replay:true,返回同一个 app
# → 请求体不合法或缺 name:400 invalid_body(不会默默发号)
# 绑定回调地址:部署时在**你的服务器上**调一次(app_secret 不要交给浏览器)
POST /v1/apps/bind-redirect {"app_id","app_secret","redirect_uri"}
# → 200 {ok, redirect_uri, already} already=true 表示本来就绑好了(重复部署安全)
# 换域名/换端口:先解绑,再重新 bind-redirect
POST /v1/apps/unbind-redirect {"app_id","app_secret"}
# secret 泄露:用旧 secret 直接换,不需要账号
POST /v1/apps/rotate-secret {"app_id","app_secret",
"grace_seconds": 0} # 可选,见下
# 改名:持 app_secret 随时可改
POST /v1/apps/rename {"app_id","app_secret","name"}
# 领错了/探针误建:未认领、无授权、无用量的可以删
POST /v1/apps/delete {"app_id","app_secret"}
# 想进控制台管理(上架、看统计):认领到你的账号名下
POST /v1/apps/claim {"app_id","app_secret"} # 需登录态
· ⚠ 绑定回调是部署时的一次性动作,在你的服务器上做——
授权页是浏览器在调 /oauth/code,
而 app_secret 绝不能出现在浏览器里。
上线前先调一次 /v1/apps/bind-redirect,之后用户的正常授权不需要任何额外参数。
(控制台创建的应用另有一条路:用创建它的账号自己授权一次即可自动绑定。)
· app_secret 只返回这一次。它是改回调、轮换、删除的唯一凭据;
app_id 是公开标识(本来就出现在授权页 URL 里),无需保密。
· 上线前先自检一次,别等授权失败才发现绑错地址:
GET /oauth/appinfo?app_id=… →
redirectBound(是否已锁定)、
redirectSource(none/auto/registered)。
带 X-App-Secret 头还会返回 redirectUris 明细
(不带则不返回——那是你的基础设施信息,不对外公开)。
· 失败一律返回 {"error":{"message","type","code"}},
请按 code 分支处理,完整对照表见
变更记录的「回调相关错误码」。
· 持有 app_secret 就是这个应用的所有者,改名、换回调、轮换密钥都随时可做,我们不设额外门槛。 但应用名出现在授权页上、是用户判断「要不要把钱包交给它」的依据,所以 已经有人授权过之后再改名,授权页会向用户提示「该应用曾更改名称」(90 天内)。 这不限制你改,只是让用户知道。上线前改名不会触发提示。
· 删除是例外,门槛不同:未认领、且无授权无用量才可删。 这不是权限问题——删除会销毁用户授权和账单所依赖的记录,删掉之后用户在控制台会看到一个查不到的应用、 账单指向不存在的应用。属于数据完整性,不是"你的项目你做主"的范围。
· 注册接口按 IP 限速(默认 10 次/小时),避免被批量刷标识。
· 多实例部署轮换 secret:默认旧的立即失效——
滚动重启期间,先重启的用新 secret、没重启的还在用旧的,这段窗口里授权会全失败。
传 grace_seconds(≤3600)要一段重叠期,期间新旧都认,
响应里 grace_until 是失效时刻。
泄露后的应急轮换请用默认 0,重叠期等于旧 secret 还没真作废。
· redirectUris 的两种"空"含义相反,别混:
字段不存在 = 你没出示 X-App-Secret,无权查看;
空数组 [] = 确实还没绑定任何地址。
自检代码必须区分这两者——当成同一种会导致"以为没绑定,于是去重绑"。
| 接口 | 成功码 | 响应体 |
|---|---|---|
| POST /v1/apps/register | 201(重放 200) | {app_id, app_secret, name, notice} |
| POST /v1/apps/rotate-secret | 200 | {ok, app_secret, grace_until, notice} |
| POST /v1/apps/bind-redirect | 200 | {ok, redirect_uri, already, notice} |
| POST /v1/apps/unbind-redirect | 200 | {ok, cleared, notice} |
| POST /v1/apps/rename | 200 | {ok, name, notice} |
| POST /v1/apps/delete | 200 | {ok} |
| POST /v1/apps/claim | 200 | {ok, appId} |
| GET /oauth/appinfo | 200 | {appId, name, registered, redirectBound, redirectSource, redirectOk?, redirectCode?, redirectHost?, redirectUris?} |
失败一律 {"error":{"message","type","code"}}。
按状态码判断成败时注意 register 是 201——写 == 200 会误判成失败并重试,而这个接口的重试代价很高。
tokin.chatStream(model, msgs,
delta -> print(delta)); // 流式:打字机效果,强烈推荐
tokin.balance(); // 余额(元)
tokin.topupUrl(); // 充值页,放个按钮打开它
tokin.auth().switchAccount(); // 换用户
tokin.auth().setApiKey("sk-tokin-…"); // 手动绑定(备用)
提交新版本不会影响线上:新包进审核队列期间,商店照常展示当前版本、用户下载与 /latest 自动更新拿到的仍是当前版本;审核通过才整体顶替,驳回则只丢弃新包、线上不受影响。
控制台 →「上传应用」,选安装包(exe/apk)、填版本号(如 1.3.0)+ 更新说明。平台自动做安全检查,通过即上架应用商店供下载;下载地址固定不变:
https://tokincloud.com/store/download/<appId>
发新版就是「传个更新版本号的新包」——下载地址不变,老用户下次查更新即可拿到。
机制:平台当"更新源",你的 App 启动时查一下、有新版就下载安装(只有 App 自己能装自己)。
// Java:SDK 一行查更新
var u = TokinClient.checkUpdate(
"https://api.tokincloud.com", "dev_abax_jz", "1.2.0");
if (u != null && u.hasUpdate()) {
// 下载 u.downloadUrl(),SHA-256 校验 u.hash(),再拉起安装
Desktop.getDesktop().browse(URI.create(u.downloadUrl()));
}
任意语言:直接 GET /store/apps/<appId>/latest → 返回 {version, downloadUrl, releaseNotes, hash},版本不同就下载安装。
接法 A · 零界面工作(推荐):「反馈」按钮直接打开平台托管反馈页,界面我们出(建议/投诉单选、留言、联系方式、截图):
// Java:一行打开托管反馈页
Desktop.getDesktop().browse(URI.create(
tokin.feedbackUrl("1.3.0")));
// 或任意语言直接拼 URL:
https://tokincloud.com/feedback.html?app_id=...&version=...
接法 B · 应用内自绘:自己画表单,一个 POST 发平台:
POST https://api.tokincloud.com/feedback
{ "appId": "...", "version": "1.3.0",
"kind": "suggestion", // suggestion=建议 | complaint=投诉
"message": "...", // 必填 ≤2000字
"contact": "微信/手机/邮箱", // 选填
"imageBase64": "..." } // 选填 ≤2MB PNG/JPEG/WebP
反馈界面请让用户自选「建议 / 投诉」。建议类你可在控制台开发者中心只读查看(联系方式脱敏);投诉类仅平台可见、由平台跟进。
// Java:SDK 一行
TokinClient.sendFeedback(
"https://api.tokincloud.com", appId,
"1.3.0", 留言, 联系方式, 截图路径或null);
⚠️ 红线:截图必须由用户主动粘贴或选择,发送前让用户看到发的是什么——绝不自动抓屏(用户屏幕上可能是敏感数据)。自动附带的只允许版本号等系统信息。
发送失败(断网等)请兜底提示改发邮件,别让用户的话丢了。
npm i @tokincloud/sdk
const c = TokinClient.withApiKey(baseUrl, apiKey);
await c.chat({ model, messages });
await c.panel({ messages, judge: true }); // 多 agent
pip install tokincloud
from tokincloud import TokinClient
c = TokinClient.with_api_key(base_url, api_key)
c.chat(model=..., messages=[...])
c.panel(messages=[...], judge=True)
用你熟悉的 OpenAI SDK,只改 base_url:
client = OpenAI(
api_key="sk-tokin-…",
base_url="https://api.tokincloud.com/v1")
移动端 / 其它栈想做「一键连接」?流程是标准 OAuth 2.0 授权码 + PKCE: 桌面用 loopback 回调,iOS/安卓用自定义 scheme 深链 —— 每种语言都有现成 OAuth 库,照标准接即可。细节见下方「OAuth 接入细节」。
tools / tool_choice 全量透传上游,响应 tool_calls 原样返回,流式同理。在售文本模型(DeepSeek / 通义 / 智谱 / Kimi / 豆包 / MiniMax)基本都支持。
{ "model": "deepseek-flash",
"messages": [...],
"tools": [{ "type":"function",
"function": { "name":"add_expense", ... } }],
"tool_choice": "auto" }
tool 消耗计入正常 usage 计费,无额外费用。
messages 放 OpenAI 标准 image_url,http(s) 地址或 data:image/…;base64, 均可。推荐:qwen3-vl-flash(便宜) / qwen3-vl-plus / qwen-vl-max / glm-5v-turbo。
{ "role": "user", "content": [
{ "type":"text", "text":"识别这张发票" },
{ "type":"image_url", "image_url":
{ "url":"data:image/png;base64,..." } } ] }
图片 token 计入输入价计费。注意:图片别太小(几像素会被上游拒);base64 带完整前缀。
· 授权页:tokincloud.com/authorize.html?app_id=…&redirect_uri=…&code_challenge=…&state=…(PKCE S256)
· 换码/刷新:POST api.tokincloud.com/oauth/token(grant_type = authorization_code / refresh_token)
· 回调地址:不需要事先登记。本机地址(127.0.0.1 / localhost)始终放行;其余地址在第一次授权时自动绑定,之后就只认它。http 和 https 都可以。
· 谁能完成这次绑定:app_ 正式应用只有创建该应用的账号本人(你上线前必然自己先授权一次,所以仍是零配置);dev_ 开发期标识没有归属,谁先授权谁绑上。这样别人拿到你的 app_id 也无法把回调改到自己的服务器上(那是一种钓鱼:用户看到的是你的应用名,授权码却落到对方手里,PKCE 挡不住)。
· 换地址:app_ 到控制台「我的应用」→「OAuth 回调地址」改(手动登记后即以登记的为准,自动绑定的会被清掉);dev_ 由当初绑定的那个账号调 DELETE /v1/oauth/bindings/<dev_id> 解绑后重新授权。
· ⚠ redirect_uri 必须写成固定常量,不要从 request.base_url / Host 头推导。同一台服务器上 http://IP:8321、https://IP:8443、以后的域名各算一个绑定——从请求推导的话,你自己的几个变体就会把 3 个位子占满,之后所有人都授权不了,而现场看不出原因(授权页正常、回调正常,只是不认了)。Web 应用尤其容易踩。
· ⚠ 回调绑定按 app_id 记,而 dev_ 标识不保证唯一:别人用同一个字符串,就可能抢先把回调绑成他自己的地址,你反而被锁在门外。所以 dev_ 请加随机后缀(如 dev_yourapp_a1b2c3),正式对外换平台签发的 app_(16 位随机、独占,且只有你本人能绑)。
· 需要多个回调(正式/预发/灰度)时,用主 Key 一次登记好:PUT /v1/apps/<app_id>/redirects {"uris":["https://…/oauth/callback"]}(精确匹配,至多 5 条)。也可在控制台「我的应用」里改,单一回调的正常接入用不到这一步。
· 发起授权前可先预检(免鉴权,避免用户一路授权完才卡在跳不回来):GET /oauth/appinfo?app_id=…&redirect_uri=…
→ {appId, name, registered, redirectOk, redirectHost, redirectHint}。redirectOk=false 时 redirectHint 说明原因;registered 表示是否为平台签发的正式应用。
· 收不到回调的场景(纯桌面脚本、内网、CLI):授权页不传 redirect_uri 即进入"手动复制凭证"模式,用户复制后粘回你的程序;服务端对应 POST /oauth/grant(登录态调用,直接返回 access/refresh,不走授权码)。
· 有效期:access 900 秒;refresh 无固定过期、每次刷新即轮换(重放触发吊销);用户可在控制台随时吊销。
· scope:无细分——凭证代表「该用户在你的 app 下」的数据面权限,计费落用户钱包、用量归属你的 app_id。
· 裸 REST 接入(不用 SDK):启动时发一次 POST /attest {"appId":"…"}(免鉴权,点亮后台"运行时握手")。
· X-App-Id 什么时候要加:用 OAuth access_token 调用时不需要(凭证里已含 app_id,加了会被忽略);只有用主 Key / 子 Key 直调且想区分应用时才加。
· 授权页可带 &phone=1xxxxxxxxxx 预填手机号(减少用户输入;验证码/密码仍只在我方页面输入,应用永远碰不到凭证)。
让模型查实时信息,并拿到搜索引擎返回的真实链接——可入库、可点开核验,适合尽调 / 教学笔记 / 任何要留痕的场景。无需额外 Key,按 token 计费(搜索结果算输入)。
POST /v1/chat/completions
{ "model": "qwen-plus",
"messages": [{"role":"user","content":"今天上海天气?"}],
"enable_search": true,
"search_options": { "forced_search": true } # 可选,默认已强制联网
}
# 响应在标准结构上多一个 search_info:
{ "choices":[…], "usage":{…},
"search_info": { "search_results": [
{ "title": "…", "url": "https://…" }, … ] } }
响应头 X-Tokin-Web-Search: native 表示本次真的联网了。
计费:token 照常计费,另加一笔检索费(与 POST /v1/search 同价,见 GET /v1/services),只在真的联网并拿到结果时收,金额在响应头 X-Tokin-Search-Billing 里。检索由搜索引擎按次收费,不含在模型 token 里。
· 仅通义千问系模型支持(qwen-plus / qwen-max / qwen3.7-max 等)。其它厂商会返回 X-Tokin-Web-Search: unsupported,我们不假装成功。
· 正文里的链接不可信,只信 search_info。正文的 [1] 角标与 URL 是模型自己写的、可能是编的;search_info.search_results 才是搜索引擎返回的真实标题与 URL。要留痕就存这个。
· 流式暂不带来源:stream: true 时走的是兼容模式,能搜但没有 search_info。要来源请用非流式。
搜索结果作为输入 token 计费:天气类问题约 2000 输入 token(≈¥0.002),人物/新闻类约 5000(≈¥0.004)。比独立搜索接口便宜。
防幻觉建议:在 prompt 里明确「只依据检索结果作答,未覆盖的写『未检索到』,绝不编造链接/日期/数字」。
OpenAI 兼容的 /v1/audio/transcriptions。返回句级 + 字级时间戳,可直接做字幕、或建立「第几分几秒讲了什么」的时间轴索引。按音频秒数计费,输出不计费。
curl https://api.tokincloud.com/v1/audio/transcriptions -H "Authorization: Bearer sk-tokin-…" -F file=@lecture.mp3 -F response_format=verbose_json -F prompt="吉布斯抽样,卡尔曼滤波" # 领域词表,显著降错字
# → { text, duration, segments:[
# { start: 0.48, end: 3.43, text: "…",
# speaker: 0, words:[{word,start,end}] } ] }
response_format:json(默认) / verbose_json(带时间戳) / text / srt / vtt。后两个直接就是字幕文件。
# 长录音:流式上传 + 异步(参数走 query)
curl -X POST "…/v1/audio/transcriptions?async=true&response_format=srt" -H "Content-Type: audio/mpeg" --data-binary @lecture.mp3
# → 202 { task_id, poll: "/v1/audio/transcriptions/{id}" }
GET /v1/audio/transcriptions/{task_id}?response_format=srt
# → 202 处理中 / 200 结果
· 同步模式等待超过 90 秒会自动转异步并返回 task_id,不会白等一场。
· 结果可在 24 小时内重复取回(换 response_format 再取一次也行)——取结果时网络断了不必重转,同一任务只计费一次。
· 长录音请走流式上传:multipart 会把整个文件读进服务端内存,因此限 25MB;把音频直接作为请求体(Content-Type: audio/*,参数放 query)则边收边写盘,可到 200MB。
· 一节课 90 分钟约 90MB → 用流式上传或 file_url;超限返回 413 upstream_rejected。
· diarization=true 开说话人分离,段里带 speaker。
· 已有公网音频地址可用 file_url 替代上传,省一次中转。
按音频时长计费,转出多少字都不加钱。响应头 X-Tokin-Audio-Seconds / X-Tokin-Billing 回显本次秒数与金额。90 分钟的课约 ¥0.44。
WebSocket 双向流:麦克风音频往上送,识别结果边说边回。约 1 秒出首字,中间稿持续刷新,句末给带时间戳的终稿。适合课堂字幕、会议纪要、直播实时字幕。
// 凭证走 query:浏览器无法给 WS 设 Authorization 头
const ws = new WebSocket(
"wss://api.tokincloud.com/v1/realtime/transcriptions"
+ "?token=sk-tokin-…&sample_rate=16000&format=pcm");
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
// session.started → transcript(多次) → billing
if (m.type === "transcript")
render(m.text, m.is_final, m.begin_time, m.end_time);
};
// 上行:二进制帧 = 音频块;说完发 {"type":"finish"}
ws.send(pcmChunk);
ws.send(JSON.stringify({ type: "finish" }));
音频建议 16kHz 单声道 PCM,每 100ms 一块(3200 字节)。也支持 opus/mp3/aac 等,用 format 指定。
# 推荐:先换一次性连接票(凭证不进 URL、不落 nginx 日志)
POST /v1/realtime/ticket // 带 Authorization
# → { ticket:"rt_…", expires_in:60 }
# 再握手:wss://api.tokincloud.com/v1/realtime/transcriptions?ticket=rt_…
{"type":"session.started", model, sample_rate}
{"type":"transcript", text, is_final,
begin_time, end_time} // 毫秒
{"type":"billing", seconds, amount,
final} // 每结算一次一条
{"type":"error", message, code}
· is_final=false 是中间稿(会被后续覆盖),true 才是定稿——入库只存终稿。
· 单账号同时最多 5 路;单会话最长 2 小时,超时服务端主动断。
· 每满 60 秒音频结算一次并推一条 billing 消息(seconds 是累计值,不是增量),不是断开才算——余额花光会被中断,不会欠账。
· 会话结束时若还有不满 60 秒的零头,补结一次并推 final:true 的 billing——以这条为最终账单。一节 90 分钟的课共 90 条左右。
· 课上到一半余额用尽会当场断开并给 insufficient_quota;因为是「先扣款再发现」,最多透支一个计费周期(约 ¥0.015),不会持续欠账。长课建议课前确认余额。
· 断线(网络抖动/客户端崩溃)已转写部分照常计费,重连请重新建立会话。
按音频秒数,¥0.00024/秒 × 加价倍率 → 一节 90 分钟的课约 ¥1.3。比课后文件转写(约 ¥0.44)贵,换来的是实时。
模型之外的能力,同一个 Key、同一个钱包:调用成功按次扣费(价格见 GET /v1/services),支持代付、X-Usage-Tag 分组、子 Key 预算——口径与模型调用完全一致。上游失败不收费。
POST /v1/search
{ "query": "2026 世界杯 举办地", "count": 10,
"freshness": "noLimit" } # 可选 oneDay/oneWeek/oneMonth/oneYear
# → { results:[{title,url,snippet,publishedTime?,siteName?}],
# billing:{service:"web-search",amount} }
给模型喂实时信息的标配:搜索 → 把 results 拼进 prompt → 再调 /v1/chat/completions。query ≤200 字符,count 1–20;snippet 已是长摘要,可直接入 prompt。
# 简易:按关键词查企业(自动走「实体识别」)
POST /v1/company
{ "keyword": "阿里巴巴" }
# 进阶:先看工具目录,再精确调任一工具
GET /v1/company/tools?server=risk # 目录免费
POST /v1/company
{ "server":"risk", "tool":"工具名", "arguments":{…} }
# → { result:…, billing:{service:"company-search",amount} }
数据来自企业信息服务商,共 185 个原子工具。服务可选:company / risk / ipr / operation / history / executive / regulation / case / tender / document。「客户尽调 / 合同相对方核验 / 供应商体检」直接搭。
按工具计价:不同工具单价不同(约 ¥0.1~¥2 / 次),GET /v1/company/tools 的每个工具都带 price 字段,调用前即可知价;实际扣费以响应里的 billing.amount 为准。查无结果也按同价计费(响应里 billing.noMatch=true)——数据源按次消耗额度,与是否查到无关。上游调用失败则不收费。
授权页在主域,API 在 api 子域——这是最常见的接错点。
| 用途 | 端点 | 鉴权 |
|---|---|---|
| 授权页(浏览器打开) | https://tokincloud.com/authorize.html | 用户登录 |
| 换码 / 刷新令牌 | POST https://api.tokincloud.com/oauth/token | 无(PKCE) |
| 模型推理 | POST https://api.tokincloud.com/v1/chat/completions | Bearer access_token 或 sk-tokin-* |
| 多 agent 面板 | POST https://api.tokincloud.com/v1/panel | 同上 |
| 实时转写(WebSocket) | wss://api.tokincloud.com/v1/realtime/transcriptions?token=… | token 走 query |
| 音频转写 | POST https://api.tokincloud.com/v1/audio/transcriptions | 同上(按秒计费) |
| 联网搜索 / 企业查询 | POST https://api.tokincloud.com/v1/search · /v1/company | 同上(按次计费) |
| 余额 / 用量 | GET https://api.tokincloud.com/v1/balance · /v1/usage | 同上 |
| 启动握手(可选) | POST https://api.tokincloud.com/attest | 无 |
| 回调地址登记 | PUT https://api.tokincloud.com/v1/apps/<app_id>/redirects | 开发者本人的 sk-tokin-* |
桌面应用用 SDK 的 connect()(拉起浏览器 + loopback 收码)。Web 后端要为 N 个用户分别保管凭证——Python 有官方封装,其它语言按下方协议自己实现(就三步)。
pip install tokincloud
from tokincloud import TokinWebAuth, SqliteTokenStore
auth = TokinWebAuth(
base_url="https://api.tokincloud.com",
app_id="app_xxxxxxxxxxxxxxxx", # 线上域名回调须用平台签发的 app_
redirect_uri="https://你的域名/oauth/callback",
store=SqliteTokenStore("tokens.db"), # 换成你自己的库:实现 save/load/delete 三个方法
)
url = auth.start(user_id="本站用户ID") # ① 302 跳过去(PKCE + state 内部生成)
uid = auth.callback(code=code, state=state) # ② 回调换凭证并落库,从 state 还原本站用户
auth.client_for(uid).chat(model=..., messages=...) # ③ 之后随时用,过期自动续、轮换自动回存
可跑示例:FastAPI · Django。本地调试用 http://127.0.0.1:8000/oauth/callback 配 dev_ 前缀即可,免注册;多进程部署(gunicorn -w 4)把 pending 换成 Redis 实现,示例文件末尾有现成的。
# 服务端生成并暂存(按 state 索引本站用户)
verifier = base64url(random(32))
challenge = base64url(sha256(verifier))
state = random()
store[state] = { user_id, verifier } # 5~10 分钟过期
# 302 跳转用户浏览器到:
https://tokincloud.com/authorize.html
?app_id=dev_yourapp
&redirect_uri=https://你的域名/oauth/callback
&code_challenge={challenge}&state={state}
非 loopback 回调需先登记:PUT /v1/apps/<app_id>/redirects(用开发者本人的 sk-tokin-* 主 Key,不是终端用户的)。
# GET /oauth/callback?code=..&state=..
{ user_id, verifier } = store.pop(state) # state 不匹配即拒绝
POST https://api.tokincloud.com/oauth/token
{ "grant_type":"authorization_code",
"code": code, "code_verifier": verifier }
# → { access_token, refresh_token, expires_in, token_type }
db.save(user_id, refresh_token, access_token,
expires_at = now + expires_in)
if now >= expires_at - 60:
POST /oauth/token
{ "grant_type":"refresh_token",
"refresh_token": db.get(user_id) }
# refresh_token 每次刷新都会轮换 → 必须回写覆盖
刷新是自研最容易出错的部分。官方 SDK(TokinWebAuth / TokenManager)已内置下面全部行为;自己实现请逐条对照:
· 并发单飞:同一用户多个线程/请求同时发现过期时,只应发出一次刷新(拿锁后先复查凭证是否已被别人换新)。否则并发刷新互相作废对方的 refresh_token。
· 轮换必回存:每次刷新返回新的 refresh_token,必须覆盖保存后再放行业务请求。先用后存的窗口里进程崩溃 = 该用户凭证永久失效。
· 30 秒宽限期:多副本用同一个旧值重复刷新,30 秒内会回放同一结果(不误判盗用);超过 30 秒的重放按泄露处理、整条授权吊销。宽限期是兜底,不是许可——仍应以单飞为目标。
· 失败要分类,不能一律清凭证重授权:401 invalid_grant = 授权确实失效 → 清凭证、引导重新授权;503 temporarily_unavailable = 瞬时故障 → 重试,别清凭证;403 access_revoked(业务调用时)= 用户主动吊销 → 清凭证。
· 401 兜底重试一次:业务请求撞上刚好过期的 access → 强制刷新后重放一次即可,不要循环。
// 请求(二选一)
{ "grant_type":"authorization_code",
"code":"...", "code_verifier":"..." }
{ "grant_type":"refresh_token",
"refresh_token":"..." }
// 响应 200
{ "access_token": "...", // Bearer 用它调 /v1
"refresh_token": "...", // 每次刷新都轮换,务必覆盖保存
"expires_in": 900, // 秒(access 有效期)
"token_type": "Bearer" }
// 失败 401
{ "error":"invalid_grant", "message":"..." }
refresh_token 无固定过期。支持轮换宽限期:多副本服务并发刷新时,30 秒内用同一个旧值重复刷新会回放上次结果(返回同一个新 refresh_token),不会误判为盗用;超过 30 秒的重放仍按泄露处理、吊销整条授权,所以务必以最新值覆盖保存。
{ "object":"list", "data":[
{ "id":"tokin-balanced", "object":"model-mode",
"description":"速度效果均衡(默认)" },
{ "id":"deepseek-flash", "object":"model",
"owned_by":"deepseek" } ] }
按 object 区分:model-mode=语义模式,model=具体模型。只列当前已上架可调用的,可直接用于选择器与可用性校验。
// GET /v1/balance
{ "balance": 12.3456 } // 单位:元(人民币),可为小数
// GET /v1/usage
{ "count": 42, "totalSpent": 1.234,
"records": [ { "ts":…, "provider":"deepseek",
"model":"deepseek-flash", "promptTokens":…,
"completionTokens":…, "spent":0.0021 } ] }
401 invalid_token — access 过期/无效 → 用 refresh_token 刷新后重试一次
403 access_revoked — 用户已吊销授权 → 刷新无用,引导用户重新授权
402 insufficient_quota — 用户余额不足 → 提示去充值
429 rate_limit_exceeded — 超过 RPM(默认 60/分钟)→ 退避重试
429 app_quota_exceeded — 单应用窗口花费上限 → 退避 / 联系我们调额
400 model_not_found — 模型名错或未开放 → 对照 /v1/models
400 upstream_rejected — 上游拒绝了请求本身(如输入超长)→ 不会故障转移,按 message 修请求
502 upstream_error — 上游全失败(已自动故障转移过)→ 可稍后重试
503 temporarily_unavailable — 刷新令牌时的瞬时冲突 → 退避重试即可,勿清除凭证
所有端点(含 /oauth/token)错误体统一为 {"error":{"message","type","code"}},只写一套解析即可;OAuth 端点额外在顶层保留 code 字段兼容旧客户端。
标准 OpenAI SSE 协议,裸 REST 即可用。强烈建议开启——首字通常几百毫秒返回,体感比等全文快得多。
POST /v1/chat/completions
{ "model":"deepseek-flash",
"messages":[…], "stream": true }
# 响应 Content-Type: text/event-stream
data: {"choices":[{"delta":{"content":"你"}}]}
data: {"choices":[{"delta":{"content":"好"}}]}
data: {"choices":[…],"usage":{…}} # 见右
data: [DONE]
按行读,取 data: 后的 JSON;遇 [DONE] 结束。非 JSON 帧(心跳等)忽略即可。
· usage 在最后一个数据帧([DONE] 之前那帧)。我们已自动向上游注入 stream_options.include_usage,你不必自己传。计费以该帧为准。
· 中途出错:HTTP 已 200,无法再改状态码,因此以一个错误帧表达,随后关闭流:
data: {"error":{"message":"…","type":"upstream_error"}}
所以每帧都要检查有没有 error 字段。
· 已扣费的部分以收到的 usage 帧为准;未产生 usage 帧则不计费。
· rate_limit_exceeded:每「用户 × 应用」60 次/分钟(滑动窗口)。不是按 app 汇总——一个班几十人同时用互不挤占。
· app_quota_exceeded:同样按「用户 × 应用」计,默认 ¥10/窗口,防单用户异常烧钱。
· 两者都返回 429 + Retry-After(秒),响应体也带 error.retryAfter,照它退避即可。
量确实不够用时联系我们按 app 调高,不必自建排队层。
· 我们绝不静默截断——请求原样透传给上游,不做任何裁剪。
· 超出模型上下文时,上游会拒绝,我们原样回传原因:400 {"error":{"code":"upstream_rejected","message":"…"}}
· 这类「请求本身的问题」不会触发故障转移(换厂商也一样失败),会立即返回,不浪费时间。
长文(如整节课转写)建议选长上下文模型,或自行分段后合并。
· 非流式:建议客户端超时 120 秒(长文本+推理模型可能跑到 1 分钟以上)。
· 流式:建议按「首字超时 30 秒 + 帧间超时 60 秒」,别用总时长卡。
· 我们侧不主动截断长生成;上游各家自身有其超时策略。
X-Usage-Tag按班级 / 课程 / 项目分组对账。平台不解释标签含义,只做聚合(≤64 字符)。多维标签的约定:一个字符串装多层、用 . 分隔,报表用 prefix 过滤任一层。
POST /v1/chat/completions
X-Usage-Tag: course_abc.feature_note # 第一层=班级,第二层=功能
# 查询聚合(含调用次数与 token 数——判断"变多"还是"变贵")
GET /v1/usage/by-tag?days=30
# → { tags:[{tag,calls,tokensIn,tokensOut,spent,lastUsedAt}] }
# 前缀过滤:course_abc. 开头 = 这个班的全部功能
GET /v1/usage/by-tag?days=30&prefix=course_abc.
# 按天分桶(做周报/月报客户端按天汇总即可)
GET /v1/usage/by-tag?days=30&bucket=day
# → { tags:[{tag,day:"2026-08-13",calls,…}] }(标签×天,Asia/Shanghai)
# 机构视角(看自己代付的成员,按标签分),同样支持 prefix/bucket
GET /v1/sponsor/usage/by-tag?days=30
# → { tags:[{tag,calls,members,tokensIn,tokensOut,spent}] }
标签会不会被别人"污染"?不会——以上报表都按「你的账号 / 你代付的成员」聚合,从不按 app 汇总;即使有人用了与你相同的 dev_ 标识和标签,也进不了你的报表(app 维度的平台统计才会混,这是尽早注册 app_ 的又一理由)。
用语义模式(tokin-balanced 等)时,响应头会明示真实模型,便于留痕与复现:
X-Tokin-Served-Model: deepseek-flash # 实际模型
X-Tokin-Served-By: deepseek # 实际厂商
X-Tokin-Mode: tokin-balanced # 你传的模式(仅模式调用时)
X-Tokin-Failover: true # 发生了故障转移(仅转移时)
直接指定具体模型时,返回体 model 字段即为实际模型。流式同样带这些响应头(胜出候选在首帧确定后才开流),流式与非流式可共用一套"读响应头"代码。
# 带 redirect_uri 时 → 原样回跳并带上:
{redirect_uri}?error=access_denied&state={state}
# 无 redirect_uri(桌面手动复制场景)
# → 停在我们页面显示"已取消授权",不回跳
回调里请同时处理 code 与 error 两种参数;state 一律原样带回。
· 主机名 127.0.0.1 / localhost / ::1 等价,均免登记。
· 端口任意(遵循 RFC 8252,桌面应用可用随机端口)。
· loopback 允许 http;其它主机必须登记,登记后按完整字符串精确匹配(含路径与端口)。
授权页向用户明示:该应用仅可代其发起模型调用并从钱包扣费;拿不到账户密码或主密钥,不能提现、不能改账户、不能读取其它数据;凭证可随时在控制台一键吊销。
面向学校 / 家长解释时可直接引用这段。
可填具体模型名,也可填语义模式——由平台按你的意图自动选最优模型并故障转移:
tokin-fasttokin-balancedtokin-qualitytokin-cheapesttokin-reasoning
会自适应,而你什么都不用做——只要把 model 填成语义模式(如 tokin-balanced):
平台按意图自动选当前最优模型,某家故障时自动切换下一家。
新模型上线、厂商降价、接口变更,都由我们在服务端跟进,你的应用零改动自动受益。
想锁死某个模型?把 model 填具体名(如 deepseek-flash),我们就不替你选。
具体模型名与实时价格见 定价页,或调用 GET /v1/models。