Metadata-Version: 2.5
Name: tokincloud
Version: 0.3.0
Summary: Tokin 拓令云 Python SDK —— OpenAI 兼容调用 + 一键登录(OAuth PKCE) + Web 多用户凭证托管 + 多 agent panel + 短期凭证透明续期 + 余额/用量回显。零运行时依赖。
Project-URL: Homepage, https://tokincloud.com
Author: Tokin 拓令云
License: MIT
License-File: LICENSE
Keywords: django,fastapi,gateway,llm,multi-agent,oauth,openai-compatible,panel,pkce,sdk,tokin,tokincloud
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# tokin (Python SDK)

Tokin 数据面 SDK 的 Python 实现，与 TS 版 `@tokin/sdk` 等价。**零运行时依赖**（仅标准库），可直接运行。

> **最省事：连我们的包都不用装。** Tokin 完全 OpenAI 兼容，直接用官方 `openai` 库改个 `base_url` 即可：
> ```python
> from openai import OpenAI
> client = OpenAI(base_url="https://api.tokincloud.com/v1", api_key="sk-tokin-xxxx")
> r = client.chat.completions.create(model="deepseek-v4-flash",
>                                    messages=[{"role": "user", "content": "你好"}])
> ```
> 本包的价值在其上多做的一层：**短期凭证自动续期 + app_id 归属 + 余额/用量回显 + 多 agent `panel`**。

## 三种登录入口（按场景挑一种）

```python
from tokincloud import TokinClient

BASE = "https://api.tokincloud.com"

# ① 服务器 / CI（无浏览器）—— 最省事，直接用主 Key
client = TokinClient.with_api_key(BASE, "sk-tokin-xxxx")

# ② 受信第一方工具 —— 手机号+密码登录，密码用完即弃，换短期凭证（可吊销、自动续期）
client = TokinClient.login(BASE, phone="13800000000", password="•••", app_id="app_demo")

# ③ 桌面 / 本地 —— OAuth PKCE 一键连接：拉起浏览器授权，密码只留在 tokincloud.com
client = TokinClient.connect(BASE, app_id="app_demo")  # 自动开浏览器 + 本地 loopback 收码
```

> 安全：`login` 的密码只用于换取凭证、绝不落存储；`connect` 走标准 PKCE，本机只经手一次性 code。
> 没有浏览器的服务器/CI 场景请用 `with_api_key`（或 `login`），不要用 `connect`。

## Web 服务端：多用户（FastAPI / Django / Flask 都一样）

上面三种入口都是「一个进程服务一个用户」。Web 后端要**为 N 个终端用户分别保管凭证**——
`TokinWebAuth` 把 PKCE、state、持久化、自动续期都包好了，你只管三步：

```python
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 跳过去
uid = auth.callback(code=code, state=state)     # ② 回调里换凭证并落库（user_id 由 state 还原）
auth.client_for(uid).chat(model="deepseek-v4-flash",
                          messages=[{"role": "user", "content": "你好"}])   # ③ 之后随时用
```

每个用户各自授权、**各扣自己的钱包**，你不垫付、也拿不到对方的主 Key。凭证过期自动续期，
轮换后的新 refresh_token 自动回存（这一步自己实现最容易漏，漏了下次就 401）。

| 你要换的 | 怎么换 |
| --- | --- |
| 凭证存到自己的库 | 继承 `TokenStore`，实现 `save/load/delete`（Django ORM 版见 `examples/django_web.py` 末尾） |
| 多进程 / 多实例 | 继承 `PendingStore` 换 Redis（`SETEX` + `GETDEL`，见 `examples/fastapi_web.py` 末尾）；默认内存实现在「A 进程发起、B 进程收回调」时会判 state 无效 |
| 用户解绑 | `auth.forget(user_id)`（只删你这边的凭证；用户在 Tokin 控制台也能自行吊销） |
| 授权被吊销 | 调用抛 `TokinAuthError` → 引导重新 `start()`。这是唯一需要你处理的凭证异常 |

**回调地址两种情形**（平台按 RFC 8252 校验，踩错会 400「回调地址未登记」）：

- `http://127.0.0.1:*` / `localhost` → loopback 自动放行，本地开发用 `dev_` 前缀即可，**免注册**；
- 线上 https 域名 → 必须是平台签发的 `app_` 应用，且在控制台把该地址登记进回调白名单。

用错组合时 `TokinWebAuth` 在**构造期**就会报错并告诉你怎么改，不用等到跳转失败才发现。

可直接运行的示例：[`examples/fastapi_web.py`](examples/fastapi_web.py) · [`examples/django_web.py`](examples/django_web.py)

## 对话（含图片）与多 agent

```python
r = client.chat(model="deepseek-v4-flash", messages=[{"role": "user", "content": "你好"}])
print(r["choices"][0]["message"]["content"])

# 流式
for delta in client.chat_stream(model="glm-5.2", messages=[{"role": "user", "content": "数到三"}]):
    print(delta, end="", flush=True)

# 图片（本地路径或 URL 均可）
from tokincloud import user_image
r = client.chat(model="qwen3-vl-plus", messages=[user_image("这张图是什么？", "cat.png")])

# 多 agent 抗辩：一次调 N 个「不同厂商」模型（红蓝抗辩 / 多模型辩论）
res = client.panel(messages=[{"role": "user", "content": "该不该上这个方案？"}], n=3, tier="quality")
for a in res["agents"]:
    print(a["agent"], a["model"], "→", a["content"][:40])   # 透明返回真实模型名
# 也可手动指定模型（命名需与 /v1/models 精确匹配）：
client.panel(messages=[...], models=["deepseek-v4-flash", "glm-5.2", "kimi-k2.6"])

client.get_balance()   # {"balance": ...}
client.get_usage()     # {"count", "totalSpent", "records"}  仅自身花费
client.list_models()   # [...]
```

自动续期透明：每次调用前 `ensure_fresh`，401 再 `force_refresh` 重试一次，并发单飞（`threading.Lock` + access 变更检测），刷新轮换 refresh_token。续期失败抛 `TokinAuthError`。

## 运行示例

```bash
# 先启动网关：cd ../gateway && npm run dev
python examples/quickstart.py
```

> 本包与 TS 版共用同一份契约 `../openapi/tokin-gateway.yaml`。方法名遵循 Python 习惯（snake_case）。
