01这是什么
一个内部统一的 Token API 入口。你的产品只对接它一个地址、一把我们签发的密钥、一套不随上游变化的模型名; 背后换供应商、换模型版本、加兜底渠道,你的代码不动。
网关不认识「最终用户」这个业务概念,只记录你传来的标识。用户属于你的产品,不属于网关。
0230 秒接入
三件事:地址换成网关、密钥换成网关签发的、model 换成给你的逻辑名。请求体格式不变,
OpenAI 和 Anthropic 两种写法都收,Authorization: Bearer 和 X-Api-Key 两种鉴权头也都收。
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer tg-prod-你的密钥" \
-H "Content-Type: application/json" \
-H "X-TG-End-User: u_10231" \
-H "X-TG-Session: s_88fa1" \
-H "X-TG-Feature: chat" \
-d '{"model":"你的逻辑模型名","messages":[{"role":"user","content":"hi"}]}'不知道自己的逻辑模型名和限额?不用问人 —— 下一节把 key 贴进去,网关会当场告诉你。
03自检(用你的 key)
密钥只留在这个浏览器标签页里(sessionStorage),只发给当前这个网关地址,不落任何日志、不进账本。
这把 key 是谁的
- 租户 / 环境
- —
- 速率上限
- —
- 并发上限
- —
- 预算
- —
你能用的模型
| 逻辑模型名 | 输入 $/M | 输出 $/M | 缓存读 $/M | 说明 |
|---|
价格是内部成本口径,每百万 token 的美元数,内部分组不加价。上游调价时这张表会变,
所以别抄进你的代码 —— 要算钱就读 GET /v1/models 或账本里的 cost_usd。
发一条真请求
会真的走到上游,产生几分钱以内的成本,并在账本里留一条记录。
这条记录的归因被写死成 portal-selftest,不会混进你的用户数据。
04归因三件套
三个可选的请求头。强烈建议带上,否则报表里全部归入「未标注」, 而「这个月哪个用户花了多少」是你自己给用户计费时唯一的依据 —— 网关只能记录你告诉它的东西。
| 头 | 放什么 | 用来回答 |
|---|---|---|
X-TG-End-User | 你的用户标识(可以是哈希,网关不需要看懂) | 这个用户这个月花了多少 |
X-TG-Session | 会话 / 对话 id | 一次对话烧了多少、哪一轮变贵了 |
X-TG-Feature | 你产品内部的功能名,如 code-review | 哪个功能在烧钱 |
已经在用 OpenAI 的 user 字段或 Anthropic 的 metadata.user_id 的,
网关会自动取用,不必额外加头。三个值都会被截断到合理长度。
别把真名真邮箱发进来。网关的账本是内部成本账,不是用户资料库; 发一个你自己能反查的稳定标识就够了,反查留在你自己那一层。
05SDK 写法
不需要任何网关专用的 SDK。官方 SDK 改两个参数就行 —— 下面的代码会在你上一节自检成功后自动填上你的 key 和模型名。
from openai import OpenAI
client = OpenAI(
api_key="tg-prod-你的密钥",
base_url="http://127.0.0.1:8080/v1",
default_headers={"X-TG-Feature": "chat"}, # 整个客户端共用的功能名
)
r = client.chat.completions.with_raw_response.create(
model="你的逻辑模型名",
messages=[{"role": "user", "content": "hi"}],
user="u_10231", # 网关自动当成 X-TG-End-User
extra_headers={"X-TG-Session": "s_88fa1"}, # 单次请求的会话 id
)
print(r.headers.get("X-TG-Request-Id")) # 对账用,出问题报这个
print(r.headers.get("X-TG-Cost-USD")) # 非流式才有
print(r.parse().choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({
apiKey: "tg-prod-你的密钥",
baseURL: "http://127.0.0.1:8080/v1",
defaultHeaders: { "X-TG-Feature": "chat" },
});
const r = await client.chat.completions
.create(
{
model: "你的逻辑模型名",
messages: [{ role: "user", content: "hi" }],
user: "u_10231",
},
{ headers: { "X-TG-Session": "s_88fa1" } },
)
.withResponse();
console.log(r.response.headers.get("x-tg-request-id"));
console.log(r.data.choices[0].message.content);from anthropic import Anthropic
client = Anthropic(
api_key="tg-prod-你的密钥", # 走 X-Api-Key,网关也收
base_url="http://127.0.0.1:8080", # 注意:不带 /v1
default_headers={"X-TG-Feature": "chat"},
)
r = client.messages.with_raw_response.create(
model="你的逻辑模型名",
max_tokens=1024,
messages=[{"role": "user", "content": "hi"}],
metadata={"user_id": "u_10231"}, # 网关自动当成 X-TG-End-User
)
print(r.headers.get("X-TG-Request-Id"))
print(r.parse().content[0].text)流式拿不到 X-TG-Cost-USD —— 响应头必须在正文之前发出,
而那时还没开始生成。流式的成本在请求结束后用 request id 查。账本约 2 秒落盘一次。
import time, httpx
from openai import OpenAI
BASE = "http://127.0.0.1:8080"
KEY = "tg-prod-你的密钥"
client = OpenAI(api_key=KEY, base_url=BASE + "/v1")
r = client.chat.completions.with_raw_response.create(
model="你的逻辑模型名",
messages=[{"role": "user", "content": "写首五言绝句"}],
stream=True, # include_usage 由网关自动补,不用自己带
user="u_10231",
)
rid = r.headers["X-TG-Request-Id"]
for chunk in r.parse():
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
time.sleep(2.5) # 等账本落盘
u = httpx.get(f"{BASE}/v1/usage/{rid}", headers={"Authorization": f"Bearer {KEY}"}).json()
print("\n", u["cost_usd"], u["input_tokens"], u["output_tokens"], u["ttfb_ms"])06响应头与错误码
响应头
| 头 | 何时出现 | 含义 |
|---|---|---|
X-TG-Request-Id | 总是 | 网关侧的请求标识。存下来 —— 对账、查成本、报障都靠它 |
X-TG-Upstream-Model | 转发成功时 | 实际使用的上游模型。兜底发生时会和你请求的不是同一个 |
X-TG-Cost-USD | 仅非流式 | 本次请求的成本 |
X-TG-Stub | 桩渠道 | 这条响应的用量是编的,别拿去计费 |
错误
错误体同时满足两家 SDK 的解析习惯,所以你原来的错误处理不用改:
{"type":"error","error":{"type":"rate_limit_error","message":"..."},"request_id":"tg-..."}| 状态码 | 含义 | 你该怎么做 |
|---|---|---|
| 401 | 密钥无效 | 检查配置,不要重试 |
| 403 | 模型未对该密钥开放 | 找平台开通,不要重试 |
| 402 | 租户预算用尽 | 告警,不要重试 |
| 404 | 这条路径网关不转发 | 检查 URL,不要重试 |
| 413 | 请求体超限 | 裁剪上下文 |
| 429 | 触发速率或并发限制 | 退避后重试 |
| 502 | 上游不可用且网关的重试已用尽 | 退避后重试 |
| 503 | 网关正在重启,你的请求被安全地拒了 | 按 Retry-After 重试 |
HTTP 200 不等于模型回答了你。账本里 stop_reason 为
refusal 表示模型拒答 —— 这也是 200。只看状态码的产品会把它当成正常回答。
07用量与对账
四条只读接口,用你自己的产品 key 就能查,不需要管理密钥。结果严格限定在你自己的租户和环境内 —— 查别人的 request id 和查一个不存在的 id,返回完全一样的 404。
| 接口 | 用途 |
|---|---|
GET /v1/models | 可用逻辑模型、价格、本 key 的限额与预算剩余(就是自检那一节读的) |
GET /v1/usage/{request_id} | 查一次请求的用量与成本 |
GET /v1/usage?limit=N | 最近的记录,默认 50、上限 500。来自内存环形缓冲,进程重启会清空 |
GET /v1/usage/summary | 按时间范围汇总。读账本文件(含轮转归档,留 30 天),月末结账靠它 |
# 九月每个用户花了多少 —— 你给自己的用户计费就用这条
curl -s "http://127.0.0.1:8080/v1/usage/summary?from=2026-09-01&to=2026-09-30&group_by=end_user" \
-H "Authorization: Bearer tg-prod-你的密钥"group_by 可取 end_user / feature / session /
model / day / hour;还能用 end_user=、feature=、
model= 过滤。默认最近 24 小时,最长 62 天。
对账时必须知道的四件事
- 裸日期作
to含当天整天。from=2026-09-01&to=2026-09-30覆盖整个九月。 写成 RFC 3339 则精确到那个时刻、不含该时刻。日期按服务器本地时区解释,响应里time_zone会标明。 - 按
request_id去重。账本是幂等可重放的。 cost_usd不含桩记录。桩单列在stub_requests/stub_cost_usd,永远不混进成本。- 零成本不一定是免费。
usage_missing是上游没报用量;usage_suspect是正文明明有内容却被报成 0 输出 token。 后者的输入成本计入cost_usd,同时以suspect_cost_usd摆在旁边 —— 你要是不认这部分钱,原样减掉即可。
为什么把可疑的钱摆出来而不是悄悄扣掉:悄悄扣掉的话,你的账和我们的账对不上,而且没有任何线索说明差在哪。
08几条规矩
逻辑模型名:<租户>-<你自己的档位名>
zurv-chat ← 你的代码里写这个
↓ 这行映射随时可改,只影响你一家
gpt-5.3-codex-spark ← 厂商 / 版本 / 端点,你不该知道也不该依赖档位词汇由你自己定,不强求和别家统一。两个产品各自指向同一个上游模型,那是两行配置, 重复是故意的 —— 否则你改一行会把别人一起换掉,而他没要求过。
- 别把上游真实模型名硬编码进代码。上游停服、下架、涨价时要改的是网关一行配置,不是你重新发一次版。
- 别把网关 key 放进客户端。它代表整个租户的额度,不是某个用户的凭据。
- 预算是近似的。成本只有请求结束才知道,已经在飞的请求可能把租户带过线 —— 预算拦的是下一个请求,不是当前这个。
- 模型换了会提前通知你,带到秒的时间戳。反过来,你看到账单形态突变先来问,别自己猜。
- 报障请带
X-TG-Request-Id。没有它,我们只能在几万行账本里猜你是哪一条。
09申请一把 key
密钥由网关侧签发,签发和轮换都不需要重启网关,所以要一把 key 不会打扰到任何人的线上流量。
把这几项发给 TokenGateway 席(运维台在这台机器上):
产品 / 租户 id: zurv (小写、短横线,会成为你模型名的前缀)
环境: prod / staging (分开发,额度和账目也分开)
预计速率: 600 rpm、并发 64
月预算上限: 500 USD (到顶返回 402,不会静默超支)
需要什么模型: 一个便宜快的 + 一个强的,各自用途一句话
调用方式: OpenAI 格式 / Anthropic 格式,流式与否档位名(-chat、-lite 之类)你自己定,因为「用途」是你的用途,
网关看不见你产品内部,不该替你分类。
找不到人怎么办
你能打开这个页面,说明你已经在这台机器上,或者开着一条到它的隧道。 那么挡在你和一把 key 之间的其实只有管理密钥这一件东西,不是某个人在不在线。
- 手上有管理密钥:不用等任何人 —— 打开 /admin/ → 租户 → 新建租户/环境, 当场签发,不重启进程,密钥完整值只在签发那一次的响应里出现。轮换、停用同一个地方。
- 没有管理密钥:把上面那段信息发给持有它的人。签发是一分钟的事, 难的是决定给你多少额度 —— 所以把预算和用途写清楚,比催人有用。
拿到 key 之后回到第 3 节贴进去,当场就能验证它通不通。