API 对接文档
快速开始
TokenGet 完全兼容 OpenAI API 协议。只需将已有应用的 base_url 指向本站,并使用本站签发的 API Key 即可。
- 1. 注册账号(用户名密码或微信扫码),在控制台创建 API Key(
sk-tg-...)。 - 2. Base URL:
http://<服务器地址>/v1 - 3. 通过 GET/v1/models?category=llm 按类别拉取模型列表;一个模型可以同时属于多个分类(如
["llm","image"]),支持?category=llm&category=image取交集。
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"你好"}]}'
import requests
# Python 用户也可以直接使用官方 openai SDK:
# client = OpenAI(base_url="http://localhost:8000/v1", api_key="sk-tg-你的密钥")
r = requests.post(
"http://localhost:8000/v1/chat/completions",
headers={"Authorization": "Bearer sk-tg-你的密钥"},
json={
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "你好"}],
},
timeout=120,
)
data = r.json()
print(data["choices"][0]["message"]["content"])
print(data["usage"]) # prompt_tokens / completion_tokens / total_tokens
// JDK 11+(java.net.http),无第三方依赖
var http = java.net.http.HttpClient.newHttpClient();
var body = """
{"model":"gpt-4o-mini",
"messages":[{"role":"user","content":"你好"}]}
""".strip();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/chat/completions"))
.header("Authorization", "Bearer sk-tg-你的密钥")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
// Node.js 18+ 内置 fetch,无需安装依赖
const resp = await fetch("http://localhost:8000/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer sk-tg-你的密钥",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "你好" }],
}),
});
const data = await resp.json();
console.log(data.choices[0].message.content);
console.log(data.usage); // prompt_tokens / completion_tokens / total_tokens
认证方式
所有 /v1/* 接口通过 HTTP Header 携带 API Key:
Authorization: Bearer sk-tg-你的密钥
网页端管理接口(/api/*)使用登录会话 Cookie(tg_session);程序化调用可用系统访问令牌:Authorization: Bearer tg-sat-你的令牌 + New-Api-User: 你的用户ID(NewAPI 兼容)。
注意:sk-tg- 模型调用密钥仅用于 /v1/* 网关(以及 /api/usage/token、/api/log/token 两个自身查询端点),不能用于其他 /api/* 管理接口。
大语言模型(LLM)
POST/v1/chat/completions — 支持流式(stream:true)与非流式。
{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "你好"}],
"stream": true
}
同时支持 POST/v1/completions 与 POST/v1/embeddings。
参数与限制
model(必填):通过 GET/v1/models?category=llm 拉取当前可用模型列表;平台自动路由到优先级最高的可用上游messages(必填):OpenAI 兼容消息数组,role支持system/user/assistant/tool;content支持纯文本或[{"type":"text","text":"..."},{"type":"image_url","image_url":{"url":"..."}}]多模态数组(具体取决于模型是否支持视觉)stream:布尔值,默认false;开启后按 SSE 流式返回temperature:默认1,范围 0–2;采样温度top_p:默认1,范围 0–1;核采样max_tokens/max_completion_tokens:最大生成 token 数(模型上下文限制不同,详见模型列表)frequency_penalty/presence_penalty:默认0,范围 -2–2stop:停止序列,字符串或字符串数组tools/tool_choice:函数调用(模型支持时透传)response_format:JSON Schema / JSON mode(模型支持时透传)seed:固定随机种子(上游支持时生效)- 单条消息大小与总上下文长度受具体模型限制;请求体大小受 nginx 限制(1000MB)
模型列表字段
调用 GET/v1/models 时,每个模型对象除 id / display_name / category / price_display / sale_price_display 外,还包含:
description:模型说明,含【输入参数】【适用场景】【模型说明】【版本差异】等信息capabilities:能力对象,如{"vision": true}(保留字段)supports_image:是否支持图片输入supports_audio:是否支持音频输入supports_video:是否支持视频输入
多模态输入请以 image_url / audio_url / video_url 形式放在 messages 数组中;当前模型不支持对应输入类型时,平台将按规则自动在同类可用模型中选择最低价的支持模型进行替换,并通过响应头 X-Switched-Model 返回实际使用的模型名。
Responses API
POST/v1/responses — 平台原生支持 OpenAI Responses API 格式,供使用新版 openai SDK(client.responses.create)或 Codex CLI 等客户端直接对接。请求在网关自动转换为 chat/completions 转发,计费与 chat/completions 完全一致。
curl http://localhost:8000/v1/responses \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{"model":"glm-5.2","input":"用一句话介绍你自己"}'
import requests
# Python 用户也可以直接使用官方 openai SDK:
# resp = client.responses.create(model="glm-5.2", input="用一句话介绍你自己")
r = requests.post(
"http://localhost:8000/v1/responses",
headers={"Authorization": "Bearer sk-tg-你的密钥"},
json={"model": "glm-5.2", "input": "用一句话介绍你自己"},
timeout=120,
)
data = r.json()
print(data["output"][0]["content"][0]["text"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"model\":\"glm-5.2\",\"input\":\"用一句话介绍你自己\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/responses"))
.header("Authorization", "Bearer sk-tg-你的密钥")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
// Node.js 18+
const resp = await fetch("http://localhost:8000/v1/responses", {
method: "POST",
headers: {
"Authorization": "Bearer sk-tg-你的密钥",
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "glm-5.2", input: "用一句话介绍你自己" }),
});
const data = await resp.json();
console.log(data.output[0].content[0].text);
参数说明
model(必填):同 chat/completions 的模型列表input(必填):字符串,或消息/工具调用对象数组(message/function_call/function_call_output);消息 content 支持纯文本或[{"type":"input_text",...},{"type":"input_image","image_url":...}]多模态数组instructions:等价于 system 消息,自动前置max_output_tokens→ 映射为max_tokenstools/tool_choice:支持function类型工具(格式自动转换)temperature/top_p/parallel_tool_calls:直传reasoning.effort→ 尽力映射为reasoning_effort(上游不识别时可能报错)stream:流式开关,开启后按 Responses API 事件序列返回 SSEstore/metadata/include/truncation:忽略
流式事件序列
开启 stream:true 后,按顺序返回以下 SSE 事件(每事件含递增的 sequence_number):
response.created— 响应已创建(status: in_progress)response.output_item.added— 新输出项(message / function_call / reasoning)response.content_part.added— 消息内容分块开始(output_text)response.output_text.delta— 文本增量response.reasoning_summary_text.delta— 思考模型推理增量(如有)response.function_call_arguments.delta— 工具调用参数增量(如有)response.output_text.done/response.content_part.done/response.output_item.done— 逐级闭合response.completed— 完成事件,含完整输出与usage(input_tokens/output_tokens)
限制说明
- 无状态转发:不支持
previous_response_id/conversation(请在input中携带完整历史),不支持background后台任务 - 不支持内置工具(
web_search/file_search/computer_use/code_interpreter/mcp)与input_file文件输入 - 多轮工具对话的
call_id历史回放为有损转换;深度 Agent 场景建议使用/v1/chat/completions
图片生成
图片能力统一入口为 POST/v1/cap/image/{path}。调用方只需指定 model,平台根据模型自动路由到对应上游(SnapGen 原生生图接口,或 OpenRouter 多模态 LLM 的 chat.completions)。
- POST/v1/cap/image/generate_image — 文生图 / 图生图 / 图片编辑(form-data)
- GET/v1/cap/image/history/{uuid} — 查询单条生成结果(
status == 2即完成) - GET/v1/cap/image/histories — 历史列表
- GET/v1/cap/tasks/{task_id} — 查询能力路由异步任务状态及结果(返回
task_id时使用)
模型可选项
已接入的图片生成模型覆盖原生生图模型(nano-banana-pro / nano-banana-2 / nano-banana-2-lite / grok-image / gpt-image-2)以及多模态 LLM 生图模型(grok / gemini / inkling 等)。完整、实时列表请通过 GET/v1/models?category=image 拉取;支持多分类交集查询,如 ?category=image&category=llm。未在列表中的模型无法通过本能力路由调用。
不同模型的返回形式由平台统一处理:本身已提供异步任务标识的模型返回 {"uuid": "..."},需轮询 /v1/cap/image/history/{uuid} 获取结果;上游为同步返回的模型由网关自动包装为异步任务,返回 {"task_id": "..."},需调用 GET /v1/cap/tasks/{task_id} 获取结果 URL。下游按返回字段选择对应的查询方式即可。
文生图
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-你的密钥" \ -F "prompt=a cute cat on a windowsill" \ -F "model=nano-banana-2-lite" \ -F "aspect_ratio=1:1"
import requests
# form-data 提交文生图任务;返回 {"uuid": "..."},用 uuid 轮询结果
r = requests.post(
"http://localhost:8000/v1/cap/image/generate_image",
headers={"Authorization": "Bearer sk-tg-你的密钥"},
data={"prompt": "a cute cat on a windowsill",
"model": "nano-banana-2-lite",
"aspect_ratio": "1:1"},
timeout=120,
)
print(r.json()) # {"uuid": "9f8e7d6c-xxxx-..."}
// JDK 11+:multipart/form-data 需手动拼 boundary
var http = java.net.http.HttpClient.newHttpClient();
var boundary = "----TokenGet" + System.currentTimeMillis();
var body = java.net.http.HttpRequest.BodyPublishers.ofString(
"--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n"
+ "a cute cat on a windowsill\r\n"
+ "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"model\"\r\n\r\n"
+ "nano-banana-2-lite\r\n"
+ "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"aspect_ratio\"\r\n\r\n"
+ "1:1\r\n"
+ "--" + boundary + "--\r\n");
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/cap/image/generate_image"))
.header("Authorization", "Bearer sk-tg-你的密钥")
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(body).build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // {"uuid": "..."}
// Node.js 18+:用 FormData 构造 multipart 请求
const fd = new FormData();
fd.append("prompt", "a cute cat on a windowsill");
fd.append("model", "nano-banana-2-lite");
fd.append("aspect_ratio", "1:1");
const resp = await fetch("http://localhost:8000/v1/cap/image/generate_image", {
method: "POST",
headers: { Authorization: "Bearer sk-tg-你的密钥" },
body: fd,
});
console.log(await resp.json()); // {"uuid": "..."}
图生图 / 图片编辑
增加 image 参数传入参考图 URL,即可图生图或风格迁移:
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-你的密钥" \ -F "prompt=turn this into a watercolor painting" \ -F "model=nano-banana-2-lite" \ -F "image=https://example.com/photo.jpg" \ -F "aspect_ratio=1:1"
本地图片直接上传(multipart/form-data)
参考图也可以本地文件随请求直接上传——image 字段值用 @文件路径,与表单字段同一请求发出,平台不设大小限制(仅网关本身的请求体上限):
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-你的密钥" \ -F "prompt=把这张照片变成水彩画" \ -F "model=nano-banana-2-lite" \ -F "image=@/Users/me/photo.jpg"
URL 与本地文件可混用;多张参考图重复传 image 字段即可。
多图参考
form-data 支持同名字段重复传参——多图参考时重复传多个 image 字段(是否生效取决于所选模型):
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-你的密钥" \ -F "prompt=把图1的人物放进图2的场景,保持图3的画风" \ -F "model=nano-banana-2-lite" \ -F "image=https://example.com/person.jpg" \ -F "image=https://example.com/scene.jpg" \ -F "image=https://example.com/style.jpg"
参数说明
prompt(必填):画面描述 / 编辑指令model(必填):生图模型;当前可选项为nano-banana-2-lite,实时完整列表通过 GET/v1/models?category=image 获取aspect_ratio:宽高比,如1:1/16:9/9:16image:参考图,支持公网 URL 或@本地文件路径直接上传,可重复传多张(数量受所选模型限制)image_width/image_height:输出尺寸(可选)quality:hd提升质量(可选)seed:固定随机种子(可选)
轮询结果
curl http://localhost:8000/v1/cap/image/history/9f8e7d6c-xxxx \ -H "Authorization: Bearer sk-tg-你的密钥"
import time
import requests
# status == 2 表示完成,从响应中取结果图 URL;建议每 5–10 秒轮询一次
uuid = "9f8e7d6c-xxxx"
while True:
r = requests.get(
f"http://localhost:8000/v1/cap/image/history/{uuid}",
headers={"Authorization": "Bearer sk-tg-你的密钥"},
timeout=30,
)
data = r.json()
print(data.get("status"), data.get("progress", ""))
if data.get("status") == 2: # 完成
print(data) # 结果图 URL 在响应中
break
if data.get("status") == 3: # 失败
raise RuntimeError(data)
time.sleep(5)
// JDK 11+:每 5 秒轮询一次,status == 2 表示完成
var http = java.net.http.HttpClient.newHttpClient();
var uuid = "9f8e7d6c-xxxx";
while (true) {
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/cap/image/history/" + uuid))
.header("Authorization", "Bearer sk-tg-你的密钥")
.GET().build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
if (resp.body().contains("\"status\":2") || resp.body().contains("\"status\": 2")) {
break; // 完成,结果图 URL 在响应中
}
Thread.sleep(5000);
}
// Node.js 18+:每 5 秒轮询一次,status === 2 表示完成
const uuid = "9f8e7d6c-xxxx";
while (true) {
const resp = await fetch(
`http://localhost:8000/v1/cap/image/history/${uuid}`,
{ headers: { Authorization: "Bearer sk-tg-你的密钥" } },
);
const data = await resp.json();
console.log(data.status, data.progress ?? "");
if (data.status === 2) { console.log(data); break; } // 完成
if (data.status === 3) throw new Error(JSON.stringify(data)); // 失败
await new Promise((r) => setTimeout(r, 5000));
}
status == 2 表示完成,从响应中取结果图 URL;建议每 5–10 秒轮询一次。
异步任务包装
图片能力路由会在网关层自动完成异步任务封装:若上游图片接口为同步返回,平台会自动将其包装为任务,下游仍按原有方式调用,无需关心上游实现差异。调用成功后将返回任务标识 {"task_id": "..."}:
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-你的密钥" \ -F "prompt=a cute cat" \ -F "model=你的生图模型"
拿到 task_id 后,通过 GET/v1/cap/tasks/{task_id} 查询结果:
curl http://localhost:8000/v1/cap/tasks/task-xxx \ -H "Authorization: Bearer sk-tg-你的密钥"
返回示例:
{
"task_id": "task-...",
"status": "completed",
"category": "image",
"result_urls": ["https://..."],
"upstream_response": { ... },
"error": null,
"created_at": "...",
"completed_at": "..."
}
status 为 completed 时,图片 URL 在 result_urls 数组中;若上游调用失败则 status 为 failed,error 字段携带失败原因。该封装由 TokenGet 自动处理,下游接口路径与调用方式保持不变。
OpenAI 兼容图片生成
除能力路由外,平台还提供 OpenAI 兼容端点 POST/v1/images/generations。调用方用 JSON 传参,网关自动把请求路由到对应上游;若上游原生协议为 multipart/form-data,网关会在后台自动完成协议转换。
curl http://localhost:8000/v1/images/generations \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-1",
"prompt": "a cute cat on a windowsill",
"aspect_ratio": "1:1"
}'
也支持以 multipart/form-data 直接调用,字段与上游原生接口一致:
curl http://localhost:8000/v1/images/generations \ -H "Authorization: Bearer sk-tg-你的密钥" \ -F "model=grok-imagine-2.0" \ -F "prompt=a futuristic city at night" \ -F "aspect_ratio=16:9" \ -F "image=@/Users/me/ref.jpg"
可用模型请通过 GET/v1/models?category=image 实时获取,例如 gpt-image-1 / gpt-image-1.5 / grok-imagine-2.0 / nana-banana / nana-banana-pro 等。返回形式由平台统一处理:若返回 {"uuid":"..."},需轮询 /v1/cap/image/history/{uuid};若返回 {"task_id":"..."},需调用 GET /v1/cap/tasks/{task_id} 获取结果。
视频生成
视频能力统一入口为 POST/v1/cap/video/{path}。调用方只需指定 model,平台根据模型自动路由到对应上游(SnapGen / MiniMax 原生视频接口,或 OpenRouter 多模态 LLM 的 chat.completions)。
- POST/v1/cap/video/v2/video_generation — 提交视频生成任务
- GET/v1/cap/video/v2/query/video_generation/{task_id} — 查询任务状态
模型可选项
已接入的视频生成模型包括:
- SnapGen 原生视频:
flux-3/veo-3 - MiniMax 原生视频:
MiniMax-H3(全功能,768P/2K,4–15s)/MiniMax-H3-Max(更快,480P/768P,5–15s) - OpenRouter 多模态 LLM:grok / gemini / inkling 等
完整、实时的 model 列表请通过 GET/v1/models?category=video 拉取;支持多分类交集查询,如 ?category=video&category=llm。未在列表中的模型无法通过本能力路由调用。
不同上游的返回格式不同:SnapGen / MiniMax 原生模型返回任务 ID 需轮询;OpenRouter 多模态 LLM 返回 chat.completion 格式(assistant message 中携带视频 URL)。
1. 文生视频
content 仅含文本;ratio 必填(不能为 adaptive)。提示词支持运镜指令如 [推进]:
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [{"type": "text", "text": "一只橘猫在窗台上伸懒腰,阳光洒进房间,[推进]"}],
"resolution": "768P",
"duration": 6,
"ratio": "16:9"
}'
import requests
# 提交任务,返回 task_id,随后轮询查询
r = requests.post(
"http://localhost:8000/v1/cap/video/v2/video_generation",
headers={"Authorization": "Bearer sk-tg-你的密钥"},
json={
"model": "MiniMax-H3",
"content": [{"type": "text", "text": "一只橘猫在窗台上伸懒腰,阳光洒进房间,[推进]"}],
"resolution": "768P",
"duration": 6,
"ratio": "16:9",
},
timeout=120,
)
print(r.json()) # {"task_id": "424010985738629", ...}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = """
{"model":"MiniMax-H3",
"content":[{"type":"text","text":"一只橘猫在窗台上伸懒腰,阳光洒进房间,[推进]"}],
"resolution":"768P","duration":6,"ratio":"16:9"}
""".strip().replaceAll("\n\\s*", "");
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/cap/video/v2/video_generation"))
.header("Authorization", "Bearer sk-tg-你的密钥")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // {"task_id": "..."}
// Node.js 18+
const resp = await fetch(
"http://localhost:8000/v1/cap/video/v2/video_generation",
{
method: "POST",
headers: {
Authorization: "Bearer sk-tg-你的密钥",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "MiniMax-H3",
content: [{ type: "text", text: "一只橘猫在窗台上伸懒腰,阳光洒进房间,[推进]" }],
resolution: "768P",
duration: 6,
ratio: "16:9",
}),
},
);
console.log(await resp.json()); // {"task_id": "..."}
2. 图生视频(首帧)
图片作为视频起始画面;ratio 由图片决定(恒为 adaptive,可不传):
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "一只橘猫在窗台上伸懒腰,[推进]"},
{"type": "image_url", "image_url": {"url": "https://example.com/cat.jpg"}, "role": "first_frame"}
],
"resolution": "768P",
"duration": 6
}'
3. 首尾帧视频
同时指定开始与结束画面(first_frame + last_frame),模型生成中间过渡:
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "猫咪从窗台轻盈跳到沙发上"},
{"type": "image_url", "image_url": {"url": "https://example.com/start.jpg"}, "role": "first_frame"},
{"type": "image_url", "image_url": {"url": "https://example.com/end.jpg"}, "role": "last_frame"}
],
"resolution": "768P",
"duration": 6
}'
4. 多参视频(图片 + 声音 + 视频参数)
多模态参考生视频:参考图定角色/风格(最多 9 张)+ 参考视频定动作(最多 3 段)+ 参考音频定声音(最多 3 段),按需组合:
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "宇航员在月球表面漫步,背景是浩瀚星空"},
{"type": "image_url", "image_url": {"url": "https://example.com/astronaut.jpg"}, "role": "reference_image"},
{"type": "image_url", "image_url": {"url": "https://example.com/style.jpg"}, "role": "reference_image"},
{"type": "video_url", "video_url": {"url": "https://example.com/walk.mp4"}, "role": "reference_video"},
{"type": "audio_url", "audio_url": {"url": "https://example.com/space.mp3"}, "role": "reference_audio"}
],
"resolution": "2K",
"duration": 8,
"ratio": "16:9"
}'
5. 视频编辑
把待编辑视频作为 reference_video 传入,用文本描述修改需求(换风格、换背景、改动作、加特效等);可同时传 reference_image 指定新形象、reference_audio 指定配音:
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "把画面变成赛博朋克风格,霓虹灯光,雨夜氛围"},
{"type": "video_url", "video_url": {"url": "https://example.com/source.mp4"}, "role": "reference_video"}
],
"resolution": "768P",
"duration": 6
}'
6. 本地文件直接上传(multipart/form-data)
除 JSON 外,视频接口也接受 multipart/form-data:表单字段与文件同一请求发出,文件值用 @本地路径,平台自动转存并生成 URL 发给上游,平台不设文件大小限制(仅网关本身的请求体上限)。字段名与 JSON content 角色的对应关系:
prompt或text→ 文本项first_frame/last_frame→ 首帧 / 尾帧图片image(或reference_image)→ 参考图,可重复传多张video(或reference_video)→ 参考视频,可重复传多段audio(或reference_audio)→ 参考音频,可重复传多段- 其余字段(
model/resolution/duration/ratio等)原样作为顶层参数
图生视频(本地首帧图):
curl http://localhost:8000/v1/cap/video/v2/video_generation \ -H "Authorization: Bearer sk-tg-你的密钥" \ -F "model=MiniMax-H3" \ -F "prompt=一只橘猫在窗台上伸懒腰,[推进]" \ -F "first_frame=@/Users/me/cat.jpg" \ -F "resolution=768P" \ -F "duration=6"
多参视频(本地参考图 + 参考视频 + 参考音频混合,也可与 URL 混用):
curl http://localhost:8000/v1/cap/video/v2/video_generation \ -H "Authorization: Bearer sk-tg-你的密钥" \ -F "model=MiniMax-H3" \ -F "prompt=宇航员在月球表面漫步,背景是浩瀚星空" \ -F "image=@/Users/me/astronaut.jpg" \ -F "image=https://example.com/style.jpg" \ -F "video=@/Users/me/walk.mp4" \ -F "audio=@/Users/me/space.mp3" \ -F "resolution=2K" \ -F "duration=8" \ -F "ratio=16:9"
7. 轮询任务
curl http://localhost:8000/v1/cap/video/v2/query/video_generation/424010985738629 \ -H "Authorization: Bearer sk-tg-你的密钥"
import time
import requests
# 建议每 10 秒轮询一次;task.status == "succeeded" 时,
# task.content.url 即为成片下载地址
task_id = "424010985738629"
while True:
r = requests.get(
f"http://localhost:8000/v1/cap/video/v2/query/video_generation/{task_id}",
headers={"Authorization": "Bearer sk-tg-你的密钥"},
timeout=30,
)
task = r.json()["task"]
print(task["status"], task.get("progress", ""))
if task["status"] == "succeeded":
print(task["content"]["url"]) # 成片下载地址
break
time.sleep(10)
// JDK 11+:每 10 秒轮询一次,task.status == "succeeded" 时成片在 task.content.url
var http = java.net.http.HttpClient.newHttpClient();
var taskId = "424010985738629";
while (true) {
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create(
"http://localhost:8000/v1/cap/video/v2/query/video_generation/" + taskId))
.header("Authorization", "Bearer sk-tg-你的密钥")
.GET().build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
if (resp.body().contains("\"status\":\"succeed\"")
|| resp.body().contains("\"status\": \"succeed\"")) {
break; // 解析 task.content.url 取成片地址
}
Thread.sleep(10000);
}
// Node.js 18+:每 10 秒轮询一次
const taskId = "424010985738629";
while (true) {
const resp = await fetch(
`http://localhost:8000/v1/cap/video/v2/query/video_generation/${taskId}`,
{ headers: { Authorization: "Bearer sk-tg-你的密钥" } },
);
const { task } = await resp.json();
console.log(task.status, task.progress ?? "");
if (task.status === "succeed" || task.status === "succeeded") {
console.log(task.content.url); // 成片下载地址
break;
}
await new Promise((r) => setTimeout(r, 10000));
}
建议每 10 秒轮询一次。task.status 为 succeeded 时,task.content.url 即为成片下载地址。
参数与限制速查
model(必填):视频生成模型;可选项见上方“模型可选项”,实时完整列表通过 GET/v1/models?category=video 获取content[].type:text/image_url/video_url/audio_urlcontent[].role:first_frame/last_frame/reference_image/reference_video/reference_audioresolution:480P/768P/2K(视模型而定);duration:秒数ratio:文生必填(21:9/16:9/4:3/1:1/3:4/9:16);图生由图片决定;多参可选默认 adaptive- 参考数量上限按所选模型配置并由平台强制校验(超限返回 400):如 MiniMax-H3 为图片 ≤ 9 张、视频 ≤ 3 段(每段 2–15s、总 ≤ 15s)、音频 ≤ 3 段;本地文件上传仅受网关请求体上限约束(大文件建议走 multipart 直传或公网 URL,勿用 Base64)
- 图生与多参互斥:content 中出现
reference_*就不能再用first_frame/last_frame - 素材要求:图片 JPG/PNG/WEBP ≤ 30MB(256–5760px,宽高比 0.4–2.5);视频 MP4/MOV(H.264/H.265)≤ 50MB;音频 WAV/MP3 ≤ 15MB
callback_url:可选,任务状态变更时回调通知
OpenAI 兼容视频生成
平台同时提供 OpenAI 兼容端点 POST/v1/videos。调用方用 JSON 传参,网关自动路由到对应上游;若上游原生协议为 multipart/form-data(如 SnapGen),网关会在后台自动完成协议转换。
curl http://localhost:8000/v1/videos \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2-0-mini",
"prompt": "an astronaut walking on the moon, cinematic lighting",
"ratio": "16:9",
"duration": 6
}'
也支持以 multipart/form-data 直接调用:
curl http://localhost:8000/v1/videos \ -H "Authorization: Bearer sk-tg-你的密钥" \ -F "model=grok-imagine-video" \ -F "prompt=a robot dancing in neon lights" \ -F "ratio=9:16" \ -F "duration=5" \ -F "first_frame=@/Users/me/start.jpg"
可用模型请通过 GET/v1/models?category=video 实时获取,例如 seedance-2-0-mini / seedance-2.0-fast / grok-imagine-video 等。返回格式遵循上游原生响应(SnapGen 返回任务 ID,需轮询对应历史接口获取成片 URL)。
GoAnyAI 供应商与分组
GoAnyAI 已作为独立供应商接入。其模型按官方 marketplace 的分组拆分为多个对外模型名,格式为 {模型名}--{分组名};不传分组时可直接使用基础模型名,系统会路由到默认(最低价)分组。
- 基础模型名(默认分组):
claude-fable-5/gpt-image-2/grok-imagine-video等 - 指定分组:
claude-fable-5--AWS-Claude-分组/gpt-image-2--特价--image-1k等 - 价格来自 GoAnyAI 官方 marketplace,按人民币成本计价,再乘以平台售价系数后向下游展示
调用方式与普通模型一致,例如指定分组生成图片:
curl http://localhost:8000/v1/images/generations \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2--特价--image-1k",
"prompt": "a cute cat on a windowsill"
}'
或指定分组进行对话:
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-4-5--kiro-Claude",
"messages": [{"role": "user", "content": "Hello"}]
}'
管理员可在后台「上游供应商」页看到 goanyai,点击「同步 GoAnyAI」即可一键拉取最新模型、分组与价格。
语音合成
本平台通过统一的能力路由 POST/v1/cap/speech/{上游路径} 接入两家语音供应商,按 model(MiniMax 系列)或 model_id(ElevenLabs 系列)前缀自动分派。返回为音频二进制流或 JSON 包装音频,直接透传上游。
1. MiniMax TTS
POST/v1/cap/speech/v1/t2a_v2 — 返回 JSON,音频在 data.audio(hex 编码),字符用量在 extra_info.usage_characters。
curl http://localhost:8000/v1/cap/speech/v1/t2a_v2 \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "speech-2.8-hd",
"text": "你好,欢迎使用 TokenGet。",
"voice_setting": {
"voice_id": "female-shaonv",
"speed": 1,
"vol": 1,
"pitch": 0,
"emotion": "happy"
},
"audio_setting": {
"sample_rate": 32000,
"bitrate": 128000,
"format": "mp3",
"channel": 1
}
}'
import requests
r = requests.post(
"http://localhost:8000/v1/cap/speech/v1/t2a_v2",
headers={"Authorization": "Bearer sk-tg-你的密钥"},
json={
"model": "speech-2.8-hd",
"text": "你好,欢迎使用 TokenGet。",
"voice_setting": {
"voice_id": "female-shaonv",
"speed": 1,
"vol": 1,
"pitch": 0,
"emotion": "happy",
},
"audio_setting": {
"sample_rate": 32000,
"bitrate": 128000,
"format": "mp3",
"channel": 1,
},
},
timeout=120,
)
data = r.json()
audio_hex = data["data"]["audio"] # 音频内容(hex 编码)
usage = data["extra_info"]["usage_characters"] # 计费字符数
with open("speech.mp3", "wb") as f:
f.write(bytes.fromhex(audio_hex)) # 解码并保存为 mp3
// JDK 11+:data.audio 为 hex 编码,解码后写文件
var http = java.net.http.HttpClient.newHttpClient();
var body = """
{"model":"speech-2.8-hd","text":"你好,欢迎使用 TokenGet。",
"voice_setting":{"voice_id":"female-shaonv","speed":1,"vol":1,"pitch":0,"emotion":"happy"},
"audio_setting":{"sample_rate":32000,"bitrate":128000,"format":"mp3","channel":1}}
""".strip().replaceAll("\n\\s*", "");
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/cap/speech/v1/t2a_v2"))
.header("Authorization", "Bearer sk-tg-你的密钥")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // 解析 data.audio(hex)后写 mp3 文件
// Node.js 18+
import { writeFile } from "node:fs/promises";
const resp = await fetch("http://localhost:8000/v1/cap/speech/v1/t2a_v2", {
method: "POST",
headers: {
Authorization: "Bearer sk-tg-你的密钥",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "speech-2.8-hd",
text: "你好,欢迎使用 TokenGet。",
voice_setting: {
voice_id: "female-shaonv",
speed: 1,
vol: 1,
pitch: 0,
emotion: "happy",
},
audio_setting: { sample_rate: 32000, bitrate: 128000, format: "mp3", channel: 1 },
}),
});
const data = await resp.json();
await writeFile("speech.mp3", Buffer.from(data.data.audio, "hex"));
MiniMax 模型可选项
speech-2.8-hd/speech-2.8-turbospeech-2.6-hd/speech-2.6-turbospeech-02-hd/speech-02-turbospeech-01-hd/speech-01-turbo
MiniMax 参数说明
model(必填):从上方可选项中选择text(必填):要合成的文本,单条长度受模型限制(通常 ≤ 10000 字符)voice_setting(必填):voice_id:音色 ID,如female-shaonv、male-qnqingse等(以 MiniMax 官方更新为准)speed:语速,默认1,建议范围 0.5–2.0vol:音量,默认1,建议范围 0–10pitch:音调,默认0,建议范围 -12–12emotion:情绪,如happy/sad/neutral等(可选,模型支持时生效)
audio_setting(必填):sample_rate:采样率,可选16000/24000/32000/44100/48000bitrate:码率,如64000/128000/192000format:输出格式,mp3/wav/pcm/oggchannel:声道数,1(单声道)或2(立体声)
- 计费:按请求文本字符数 × 1.5 系数扣积分;失败请求不计费
2. ElevenLabs TTS
POST/v1/cap/speech/v1/text-to-speech/{voice_id} — 直接返回 audio/mpeg 二进制音频。
curl http://localhost:8000/v1/cap/speech/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb \
-H "Authorization: Bearer sk-tg-你的密钥" \
-H "Content-Type: application/json" \
-o speech.mp3 \
-d '{
"model_id": "eleven_multilingual_v2",
"text": "Hello, welcome to TokenGet.",
"voice_settings": {
"stability": 0.5,
"similarity_boost": 0.75,
"style": 0.0,
"use_speaker_boost": true
}
}'
ElevenLabs 模型可选项
eleven_v3(质量最高,支持多语言)eleven_multilingual_v2/eleven_multilingual_v1eleven_flash_v2_5/eleven_flash_v2(低延迟)eleven_turbo_v2_5/eleven_turbo_v2eleven_monolingual_v1
ElevenLabs 参数说明
model_id(必填):从上方可选项中选择text(必填):要合成的文本voice_id(路径参数):目标音色 ID,如JBFqnCBsd6RMkjVDRZzb;可通过上游官方接口 /v1/voices 获取voice_settings(可选):stability:稳定性,0–1similarity_boost:相似度增强,0–1style:风格强度,0–1(部分模型支持)use_speaker_boost:布尔值
pronunciation_dictionary_locators/seed/previous_text/next_text:可选高级参数(上游支持时透传)- 计费:按请求文本字符数 × 1.5 系数扣积分;失败请求不计费
3. 其他 ElevenLabs 能力
通过 /v1/cap/speech/v1/... 直接透传,参数与官方一致:
- POST/v1/cap/speech/v1/text-to-voice/create-previews — 按文本描述生成预览音色(
text需 ≥ 100 字符) - POST/v1/cap/speech/v1/text-to-voice/create-voice-from-preview — 将预览音色保存为永久音色
- POST/v1/cap/speech/v1/sound-generation — 音效生成
- POST/v1/cap/speech/v1/voice_clone / POST/v1/cap/speech/v1/files/upload / POST/v1/cap/speech/v1/voices/add — 自定义音色相关(按上游官方 API 传参)
语音通用说明
- 当前仅支持语音合成(TTS),暂未接入语音识别(STT)与音乐生成
- 模型可选项以 GET/v1/models?category=speech 实时返回为准;未列出的 model/model_id 会路由失败
- 请求体大小受 nginx 限制(1000MB)
文件接口
用户文件默认保存 2 天(管理员可按用户调整)。文件接口使用登录会话认证(浏览器 Cookie),程序化调用可用 tg-sat- 系统访问令牌。
- POST/api/file/ — multipart 上传,返回
file_id - GET/api/file/ — 文件列表
- GET/api/file/{file_id} — 下载
- DELETE/api/file/{file_id} — 删除
TOKEN=<tg-sat-系统访问令牌>; UID=<你的用户ID> # 上传(返回 file_id) curl http://localhost:8000/api/file/ -H "Authorization: Bearer $TOKEN" -H "New-Api-User: $UID" -F "file=@photo.jpg" # 列表 curl http://localhost:8000/api/file/ -H "Authorization: Bearer $TOKEN" -H "New-Api-User: $UID" # 下载 curl http://localhost:8000/api/file/<file_id> -H "Authorization: Bearer $TOKEN" -H "New-Api-User: $UID" -o photo.jpg # 删除 curl -X DELETE http://localhost:8000/api/file/<file_id> -H "Authorization: Bearer $TOKEN" -H "New-Api-User: $UID"
import requests
BASE = "http://localhost:8000/api/file"
# Bearer 令牌调用 /api/* 需同时携带 New-Api-User(用户 ID)头
HDRS = {"Authorization": "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>"}
# 上传:multipart 字段名固定为 file,返回 file_id(默认保留 2 天)
with open("photo.jpg", "rb") as f:
r = requests.post(BASE + "/", headers=HDRS,
files={"file": ("photo.jpg", f, "image/jpeg")},
data={"purpose": "general"}, timeout=120)
file_id = r.json()["data"]["file_id"]
# 列表
r = requests.get(BASE + "/", headers=HDRS, params={"page": 1, "page_size": 20})
print(r.json()["data"]["items"])
# 下载
r = requests.get(f"{BASE}/{file_id}", headers=HDRS, timeout=120)
with open("photo_copy.jpg", "wb") as f:
f.write(r.content)
# 删除
r = requests.delete(f"{BASE}/{file_id}", headers=HDRS, timeout=30)
print(r.json()) # {"success": true, "data": {"ok": true}}
// JDK 11+:上传需手动拼 multipart/form-data
var http = java.net.http.HttpClient.newHttpClient();
var token = "<tg-sat-系统访问令牌>";
var uid = "<你的用户ID>";
var boundary = "----TokenGet" + System.currentTimeMillis();
var fileBytes = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("photo.jpg"));
var body = java.net.http.HttpRequest.BodyPublishers.ofByteArrays(java.util.List.of(
("--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; "
+ "filename=\"photo.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n").getBytes(),
fileBytes,
("\r\n--" + boundary + "--\r\n").getBytes()));
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/file/"))
.header("Authorization", "Bearer " + token)
.header("New-Api-User", uid)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(body).build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // data.file_id 即下载/删除用的 ID
// Node.js 18+:上传 + 下载
import { readFile, writeFile } from "node:fs/promises";
const BASE = "http://localhost:8000/api/file";
// Bearer 令牌调用 /api/* 需同时携带 New-Api-User(用户 ID)头
const HDRS = {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
};
// 上传
const fd = new FormData();
fd.append("file", new Blob([await readFile("photo.jpg")]), "photo.jpg");
fd.append("purpose", "general");
let resp = await fetch(BASE + "/", { method: "POST", headers: HDRS, body: fd });
const { data: { file_id } } = await resp.json();
// 列表
resp = await fetch(BASE + "/", { headers: HDRS });
console.log((await resp.json()).data.items);
// 下载
resp = await fetch(`${BASE}/${file_id}`, { headers: HDRS });
await writeFile("photo_copy.jpg", Buffer.from(await resp.arrayBuffer()));
// 删除
resp = await fetch(`${BASE}/${file_id}`, { method: "DELETE", headers: HDRS });
console.log(await resp.json()); // {"success": true, "data": {"ok": true}}
参数说明
file(上传必填):multipart 文件字段;无格式限制,大小仅受 nginx 限制(1000MB)purpose(可选):文件用途标签,如general/fine-tune/relay;不传默认generalpage/page_size(列表可选):分页,默认第 1 页,每页 20 条,最大 100 条- 文件默认保留 2 天;管理员可通过 PUT/api/user/(body 带 id)调整
file_retention_days
计费说明
- 按调用量计费:每次成功调用按实际用量扣减积分,明细可在控制台「积分流水 / 调用记录」查询。
- 失败的请求(HTTP ≥ 400)不计费。
- 余额不足(≤ 0)时请求会被拒绝(HTTP 402),充值后立即恢复。
- 新用户注册即送体验额度,可直接开始调用。
记录查询
- GET/api/user/self — 当前余额(含账户信息)
- GET/api/transaction/self?type=consume&page=1 — 积分流水(充值/消费/授予)
- GET/api/log/self?category=llm&p=1 — API 调用记录(默认保留 2 天)
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID>; UTOKEN=<tg-usr-用户令牌> curl http://localhost:8000/api/user/self -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" curl "http://localhost:8000/api/transaction/self?type=consume&page=1" -H "Authorization: Bearer $UTOKEN" curl "http://localhost:8000/api/log/self?category=llm&p=1" -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
组合调用示例(Workflows)
接口介绍是分开写的,但实际业务往往需要按先后顺序组合调用:前一步的返回值(令牌、task_id、file_id 等)是后一步的入参。下面给出四个最常用的组合工作流,每步注释标号 1/2/3,可直接照抄运行。
工作流 1:创建 Key → 调用 LLM → 核对扣费(调用方接入)
顺序依赖:① 先用系统访问令牌(tg-sat-)创建 API Key,响应里的 data.key 就是模型调用密钥(sk-tg-);② 用这个新 Key 调用 /v1/chat/completions;③ 调用成功后扣费流水才会生成,此时再查积分流水(tg-usr- 用户令牌,需 user:finance:read 权限)与余额核对。若顺序颠倒(先查流水再调用),将看不到本次扣费记录。
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID>; UTOKEN=<tg-usr-用户令牌>
# 1. 创建 API Key 令牌(响应 data.key 即 sk-tg- 密钥)
curl -X POST http://localhost:8000/api/token/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"name":"my-key","remain_quota":500000}'
# 2. 用返回的 sk-tg- Key 调用 LLM
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-tg-上一步返回的key" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"你好"}]}'
# 3. 查询积分流水,核对本次扣费(type=consume 为消费记录)
curl "http://localhost:8000/api/transaction/self?type=consume&page=1" \
-H "Authorization: Bearer $UTOKEN"
# 4. 查询余额确认(data.balance 扣减后的值)
curl http://localhost:8000/api/user/self \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"
UID = "<你的用户ID>"
UTOKEN = "<tg-usr-用户令牌>"
ADMIN = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
# 1. 创建 API Key 令牌,响应 data.key 即 sk-tg- 模型调用密钥
r = requests.post("http://localhost:8000/api/token/", headers=ADMIN,
json={"name": "my-key", "remain_quota": 500000}, timeout=30)
api_key = r.json()["data"]["key"] # sk-tg-...
print("新密钥:", api_key)
# 2. 用新 Key 调用 LLM
r = requests.post("http://localhost:8000/v1/chat/completions",
headers={"Authorization": f"Bearer {api_key}"},
json={"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "你好"}]},
timeout=120)
print(r.json()["choices"][0]["message"]["content"])
# 3. 查询积分流水,核对本次扣费(此时流水已生成)
r = requests.get("http://localhost:8000/api/transaction/self",
headers={"Authorization": f"Bearer {UTOKEN}"},
params={"type": "consume", "page": 1, "page_size": 5}, timeout=30)
for tx in r.json()["data"]["items"]:
print(tx["id"], tx["amount"], tx["balance_after"], tx["created_at"])
# 4. 查询余额确认
r = requests.get("http://localhost:8000/api/user/self", headers=ADMIN, timeout=30)
print("余额:", r.json()["data"]["balance"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var sat = "<tg-sat-系统访问令牌>";
var uid = "<你的用户ID>";
// 1. 创建 API Key 令牌,解析 data.key 得到 sk-tg- 密钥
var body = "{\"name\":\"my-key\",\"remain_quota\":500000}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/"))
.header("Authorization", "Bearer " + sat)
.header("New-Api-User", uid)
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // 取 data.key
// 2. 用返回的 sk-tg- Key 调用 LLM
// header Authorization: Bearer <sk-tg-...>,POST /v1/chat/completions
// 3. 查询积分流水核对扣费(tg-usr- 令牌)
// GET /api/transaction/self?type=consume&page=1
// 4. 查询余额(tg-sat- + New-Api-User)
// GET /api/user/self —— 解析 data.balance
// Node.js 18+
const SAT = "<tg-sat-系统访问令牌>";
const UID = "<你的用户ID>";
const UTOKEN = "<tg-usr-用户令牌>";
const ADMIN = { Authorization: `Bearer ${SAT}`, "New-Api-User": UID };
// 1. 创建 API Key 令牌,响应 data.key 即 sk-tg- 模型调用密钥
let resp = await fetch("http://localhost:8000/api/token/", {
method: "POST",
headers: { ...ADMIN, "Content-Type": "application/json" },
body: JSON.stringify({ name: "my-key", remain_quota: 500000 }),
});
const api_key = (await resp.json()).data.key; // sk-tg-...
console.log("新密钥:", api_key);
// 2. 用新 Key 调用 LLM
resp = await fetch("http://localhost:8000/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${api_key}`, "Content-Type": "application/json" },
body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: "你好" }] }),
});
console.log((await resp.json()).choices[0].message.content);
// 3. 查询积分流水,核对本次扣费(此时流水已生成)
resp = await fetch("http://localhost:8000/api/transaction/self?type=consume&page=1", {
headers: { Authorization: `Bearer ${UTOKEN}` },
});
for (const tx of (await resp.json()).data.items)
console.log(tx.id, tx.amount, tx.balance_after, tx.created_at);
// 4. 查询余额确认
resp = await fetch("http://localhost:8000/api/user/self", { headers: ADMIN });
console.log("余额:", (await resp.json()).data.balance);
工作流 2:图片异步生成——提交任务 → 轮询 → 下载(Python 示例)
顺序依赖:① 提交生图任务,从响应拿 uuid(原生生图模型)或 task_id(网关自动包装的任务);② 用它轮询对应查询接口,直到 status == 2(uuid 路径)或 status == "completed"(task_id 路径);③ 任务完成后才存在结果图 URL,此时下载。轮询建议间隔 5–10 秒,期间任务不会因不轮询而失败。
KEY=<sk-tg-你的密钥>
# 1. 提交文生图任务,记录返回的 uuid
curl http://localhost:8000/v1/cap/image/generate_image \
-H "Authorization: Bearer $KEY" \
-F "prompt=a cute cat on a windowsill" \
-F "model=nano-banana-2-lite" \
-F "aspect_ratio=1:1"
# 返回 {"uuid":"9f8e7d6c-xxxx"};若返回 {"task_id":"task-xxx"} 则第 2 步改查
# GET /v1/cap/tasks/{task_id},完成状态为 "completed",结果在 result_urls
# 2. 轮询任务(建议每 5–10 秒一次,status == 2 即完成)
curl http://localhost:8000/v1/cap/image/history/9f8e7d6c-xxxx \
-H "Authorization: Bearer $KEY"
# 3. 下载结果图(URL 来自第 2 步响应)
curl -o result.jpg "<第2步响应中的结果图URL>"
import time
import requests
KEY = "<sk-tg-你的密钥>"
HDRS = {"Authorization": f"Bearer {KEY}"}
# 1. 提交文生图任务,拿 uuid
r = requests.post("http://localhost:8000/v1/cap/image/generate_image",
headers=HDRS,
data={"prompt": "a cute cat on a windowsill",
"model": "nano-banana-2-lite",
"aspect_ratio": "1:1"},
timeout=120)
data = r.json()
uuid = data.get("uuid")
task_id = data.get("task_id") # 二选一:网关包装的任务走 /v1/cap/tasks
print("任务标识:", uuid or task_id)
# 2. 轮询直到完成(uuid 路径:status == 2)
while True:
if uuid:
r = requests.get(
f"http://localhost:8000/v1/cap/image/history/{uuid}",
headers=HDRS, timeout=30)
st, result = r.json().get("status"), r.json()
done = st == 2
else:
r = requests.get(
f"http://localhost:8000/v1/cap/tasks/{task_id}",
headers=HDRS, timeout=30)
result = r.json()
st, done = result.get("status"), result.get("status") == "completed"
print("status:", st)
if done:
break
time.sleep(5)
# 3. 取结果图 URL 并下载
url = result.get("image_url") or (result.get("result_urls") or [""])[0]
print("结果图:", url)
img = requests.get(url, timeout=120)
with open("result.jpg", "wb") as f:
f.write(img.content)
// JDK 11+:提交 → 轮询 → 下载(核心骨架)
var http = java.net.http.HttpClient.newHttpClient();
var key = "<sk-tg-你的密钥>";
// 1. 提交任务(form-data 略,见图片章节 Java 示例),取响应中的 uuid
// 2. 每 5 秒轮询 GET /v1/cap/image/history/{uuid},
// 响应包含 "status": 2 即完成;task_id 路径改查 GET /v1/cap/tasks/{task_id},
// "status": "completed" 且结果在 result_urls
// 3. 用结果 URL 下载:
var dl = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("<结果图URL>")).GET().build();
var bytes = http.send(dl, java.net.http.HttpResponse.BodyHandlers.ofByteArray());
java.nio.file.Files.write(java.nio.file.Path.of("result.jpg"), bytes.body());
// Node.js 18+
import { writeFile } from "node:fs/promises";
const KEY = "<sk-tg-你的密钥>";
const HDRS = { Authorization: `Bearer ${KEY}` };
// 1. 提交文生图任务,拿 uuid
const fd = new FormData();
fd.append("prompt", "a cute cat on a windowsill");
fd.append("model", "nano-banana-2-lite");
fd.append("aspect_ratio", "1:1");
let resp = await fetch("http://localhost:8000/v1/cap/image/generate_image", {
method: "POST", headers: HDRS, body: fd,
});
const { uuid, task_id } = await resp.json();
console.log("任务标识:", uuid ?? task_id);
// 2. 轮询直到完成(uuid 路径:status === 2)
let result;
while (true) {
const url = uuid
? `http://localhost:8000/v1/cap/image/history/${uuid}`
: `http://localhost:8000/v1/cap/tasks/${task_id}`;
resp = await fetch(url, { headers: HDRS });
result = await resp.json();
const st = result.status;
console.log("status:", st);
if (st === 2 || st === "completed") break;
await new Promise((r) => setTimeout(r, 5000));
}
// 3. 取结果图 URL 并下载
const imageUrl = result.image_url ?? result.result_urls?.[0];
console.log("结果图:", imageUrl);
resp = await fetch(imageUrl);
await writeFile("result.jpg", Buffer.from(await resp.arrayBuffer()));
工作流 3:上传本地图片 → 图生图(文件接口 + 生图组合)
顺序依赖:① 先把本地图片上传到文件接口(purpose=relay),拿到 file_id;② 拼出公开访问 URL http://<服务器>/api/file/public/{file_id}(purpose=relay 的文件可通过该公开端点被上游拉取);③ 把这个 URL 作为 image 参数发起图生图。若直接用 multipart @本地文件 一步上传也可以(见图片章节),本工作流适用于「先存档再引用」或需要复用同一张图的场景。文件默认保留 2 天,过期后 URL 失效。
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID>; KEY=<sk-tg-你的密钥> # 1. 上传本地图片(purpose=relay,返回 file_id) curl http://localhost:8000/api/file/ \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \ -F "file=@photo.jpg" -F "purpose=relay" # 2. 拼出公开 URL(purpose=relay 的文件无需鉴权即可访问) # http://localhost:8000/api/file/public/<file_id> # 3. 用该 URL 发起图生图 curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer $KEY" \ -F "prompt=turn this into a watercolor painting" \ -F "model=nano-banana-2-lite" \ -F "image=http://localhost:8000/api/file/public/<file_id>"
import requests
SAT = "<tg-sat-系统访问令牌>"
UID = "<你的用户ID>"
KEY = "<sk-tg-你的密钥>"
ADMIN = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
# 1. 上传本地图片(purpose=relay,返回 file_id)
with open("photo.jpg", "rb") as f:
r = requests.post("http://localhost:8000/api/file/", headers=ADMIN,
files={"file": ("photo.jpg", f, "image/jpeg")},
data={"purpose": "relay"}, timeout=120)
file_id = r.json()["data"]["file_id"]
print("file_id:", file_id)
# 2. 拼出公开 URL(purpose=relay 的文件无需鉴权即可被上游拉取)
public_url = f"http://localhost:8000/api/file/public/{file_id}"
# 3. 用该 URL 发起图生图
r = requests.post("http://localhost:8000/v1/cap/image/generate_image",
headers={"Authorization": f"Bearer {KEY}"},
data={"prompt": "turn this into a watercolor painting",
"model": "nano-banana-2-lite",
"image": public_url},
timeout=120)
print(r.json()) # {"uuid": "..."} → 按工作流 2 轮询取结果
// JDK 11+(上传 multipart 细节见文件接口章节 Java 示例)
var http = java.net.http.HttpClient.newHttpClient();
// 1. POST /api/file/ 上传本地图片(purpose=relay),取 data.file_id
// 2. 公开 URL = http://localhost:8000/api/file/public/<file_id>
var publicUrl = "http://localhost:8000/api/file/public/<file_id>";
// 3. 用该 URL 发起图生图(form-data,字段 image = publicUrl),
// 响应 {"uuid": "..."} → 按工作流 2 轮询取结果
System.out.println("image=" + publicUrl);
// Node.js 18+
import { readFile } from "node:fs/promises";
const SAT = "<tg-sat-系统访问令牌>";
const UID = "<你的用户ID>";
const KEY = "<sk-tg-你的密钥>";
const ADMIN = { Authorization: `Bearer ${SAT}`, "New-Api-User": UID };
// 1. 上传本地图片(purpose=relay,返回 file_id)
const fd = new FormData();
fd.append("file", new Blob([await readFile("photo.jpg")]), "photo.jpg");
fd.append("purpose", "relay");
let resp = await fetch("http://localhost:8000/api/file/", {
method: "POST", headers: ADMIN, body: fd,
});
const file_id = (await resp.json()).data.file_id;
console.log("file_id:", file_id);
// 2. 拼出公开 URL(purpose=relay 的文件无需鉴权即可被上游拉取)
const publicUrl = `http://localhost:8000/api/file/public/${file_id}`;
// 3. 用该 URL 发起图生图
const gen = new FormData();
gen.append("prompt", "turn this into a watercolor painting");
gen.append("model", "nano-banana-2-lite");
gen.append("image", publicUrl);
resp = await fetch("http://localhost:8000/v1/cap/image/generate_image", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}` },
body: gen,
});
console.log(await resp.json()); // {"uuid": "..."} → 按工作流 2 轮询取结果
NewAPI 兼容管理 API(tg-sat-)
平台管理面兼容 NewAPI(原 one-api)接口风格,NewAPI 生态客户端与脚本可直接对接。
认证同上(tg-sat- 令牌 + New-Api-User 头,或登录会话 Cookie);
所有响应为统一信封 {"success": true, "message": "", "data": ...},
分页列表的 data 为 {"items": [...], "total": N, "page": 1, "page_size": 20}。
统一约定:下文示例中 SAT 为系统访问令牌(tg-sat-...)、UID 为你的用户 ID。使用 Bearer 令牌时必须同时携带 New-Api-User 头且与令牌归属用户一致,否则返回 401;浏览器 Cookie 会话则无需该头。标「管理员」的接口需要管理员权限,否则 403。
1. 系统状态(公开)
GET/api/status — 查询站点版本、系统名与本次启动时间。无需认证,常用于接入前的连通性探测(健康检查)。等价于 NewAPI 的同名端点。
返回 data 字段:version(后端版本号)、start_time(服务启动 Unix 秒级时间戳)、system_name(站点名,如「可信词元」)。
{
"success": true, "message": "",
"data": {"version": "1.0.0", "start_time": 1789722965, "system_name": "可信词元"}
}
curl http://localhost:8000/api/status
import requests
r = requests.get("http://localhost:8000/api/status", timeout=30)
print(r.json()["data"]) # {"version": "1.0.0", "start_time": ..., "system_name": ...}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/status")).GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const r = await fetch("http://localhost:8000/api/status");
console.log((await r.json()).data);
2. 登录(建立会话 + 获取访问令牌)
POST/api/user/login — 用户名密码登录,一次请求完成两件事:① 设置 tg_session 会话 Cookie(HttpOnly,7 天滑动有效,供浏览器调用 /api/*);② 返回系统访问令牌 data.access_token(tg-sat- 前缀,首次登录自动生成),纯 API 客户端可直接用它 + New-Api-User 头访问管理接口。响应还带 data.user 完整用户对象,前端免二次请求。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | string | 是 | 用户名 |
password | string | 是 | 密码 |
返回 data 字段:access_token(系统访问令牌,即下文示例中的 SAT)、user(用户对象:字段同「3. 查询当前用户」)。失败时 HTTP 401(密码错误)或 403(账号停用)。
{
"success": true, "message": "",
"data": {
"access_token": "tg-sat-xxxxxxxxxxxxxxxx",
"user": {"id": 1, "username": "alice", "nickname": "Alice", "balance": 1000.0, "...": "..."}
}
}
curl -X POST http://localhost:8000/api/user/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"你的密码"}'
import requests
r = requests.post("http://localhost:8000/api/user/login",
json={"username": "alice", "password": "你的密码"}, timeout=30)
data = r.json()["data"]
sat = data["access_token"] # tg-sat-...,后续管理接口的 Bearer 令牌
uid = data["user"]["id"] # 后续 New-Api-User 头的值
print(sat, uid)
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"username\":\"alice\",\"password\":\"你的密码\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/login"))
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "alice", password: "你的密码" }),
});
const { data } = await resp.json();
const sat = data.access_token; // tg-sat-...
const uid = data.user.id; // New-Api-User 头的值
3. 查询当前用户(含余额)
GET/api/user/self — 查询当前认证用户的完整账户信息,是核对余额、确认令牌归属最常用的端点。可用 Cookie 会话或 tg-sat- + New-Api-User 调用。
返回 data 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id / username / nickname | int / string | 用户 ID(New-Api-User 头的值)、用户名、昵称 |
email / email_verified / wechat_bound | string / bool | 绑定邮箱、是否已验证、是否绑定微信 |
balance | float | 积分余额(每次成功调用模型按实际用量扣减) |
rate_multiplier | float | 个人计费系数(已废弃:不参与计费,仅为兼容保留) |
group_id / group_name / group_rate | int / string / float | 所属用户分组与分组倍率(未分组时为 null,倍率按 1.0) |
is_admin / has_admin_access | bool | 是否超级管理员 / 是否拥有后台权限 |
access_token | string | 当前系统访问令牌明文(可随时回看) |
{
"success": true, "message": "",
"data": {"id": 1, "username": "alice", "nickname": "Alice",
"balance": 1000.0,
"group_id": 2, "group_name": "vip", "group_rate": 0.8, "...": "..."}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID> curl http://localhost:8000/api/user/self \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.get("http://localhost:8000/api/user/self",
headers={"Authorization": f"Bearer {SAT}",
"New-Api-User": UID}, timeout=30)
data = r.json()["data"]
print(data["balance"], data["group_rate"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/self"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/self", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
},
});
console.log((await resp.json()).data);
4. 修改昵称 / 密码
PUT/api/user/self — 修改当前用户资料。改昵称时昵称全局唯一(重复返回 409);改密码必须携带原密码 original_password(错误返回 401),改密成功后会向绑定邮箱发送通知。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
nickname | string | 否 | 新昵称(1–64 字符,全局唯一) |
original_password | string | 改密时必填 | 原密码 |
password | string | 否 | 新密码(6–128 字符) |
返回 data:修改后的完整用户对象(同「3. 查询当前用户」)。
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID>
curl -X PUT http://localhost:8000/api/user/self \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"nickname":"新昵称","original_password":"旧密码","password":"新密码"}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.put("http://localhost:8000/api/user/self",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"nickname": "新昵称"}, timeout=30)
print(r.json())
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"nickname\":\"新昵称\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/self"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/self", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ nickname: "新昵称" }),
});
console.log(await resp.json());
5. 我的可用模型
GET/api/user/models — 查询当前账号的模型池(可用模型名数组,NewAPI 形状),已按分组/授权范围过滤。可用于校验 sk-tg- Key 调用某模型前是否在可用范围内。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
detail | int (query) | 否 | 0(默认)返回模型名字符串数组;1 返回富对象数组(含 brands / manufacturers / is_open / available,供控制台配置用) |
{
"success": true, "message": "",
"data": ["deepseek-v4-flash", "gpt-4o-mini", "glm-5.2"]
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID> curl http://localhost:8000/api/user/models \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.get("http://localhost:8000/api/user/models",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"]) # ["deepseek-v4-flash", ...]
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/models"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/models", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
},
});
console.log((await resp.json()).data);
6. 重置系统访问令牌
GET/api/user/token — 生成新的系统访问令牌(tg-sat-),旧令牌立即失效。令牌泄露或定期轮换时使用。浏览器登录态调用即可(GET 操作但语义为重置)。
返回 data 字段:access_token(新令牌明文,请妥善保存,可在 /api/user/self 随时回看)。
{
"success": true, "message": "",
"data": {"access_token": "tg-sat-xxxxxxxxxxxxxxxx"}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID> curl http://localhost:8000/api/user/token \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.get("http://localhost:8000/api/user/token",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"]["access_token"]) # 新令牌(旧的已失效)
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/token"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/token", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
},
});
console.log((await resp.json()).data.access_token); // 新令牌
7. 令牌列表(我的 API Key)
GET/api/token/ — 分页查询当前用户的令牌列表。此处的「令牌」即模型调用密钥(ApiKey,sk-tg- 前缀)的 NewAPI 视图:可用于程序化盘点自己有哪些 Key、各自的剩余额度与累计消耗,而不必登录控制台。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p | int (query) | 否 | 页码,默认 1 |
page_size | int (query) | 否 | 每页条数,默认 20,最大 100 |
返回 data.items 字段(NewAPI 令牌形状):id(令牌 ID)、name(名称)、key(密钥明文,sk-tg- 前缀)、status(1 启用 / 2 停用)、created_time / accessed_time(创建 / 最近使用 Unix 时间戳,无记录为 0)、expired_time(恒 -1,我方令牌不设过期时间)、remain_quota(剩余额度积分,不限额时为 0)、unlimited_quota(是否不限额度)、used_quota(按调用日志聚合的累计消耗积分)、model_limits_enabled(是否启用模型白名单)、model_limits(白名单模型名,逗号分隔字符串)。
{
"success": true, "message": "",
"data": {
"items": [{
"id": 3, "user_id": 1, "name": "prod-key", "key": "sk-tg-9f8e7d6c...",
"status": 1, "created_time": 1789723000, "accessed_time": 1789800123,
"expired_time": -1, "remain_quota": 500, "unlimited_quota": false,
"used_quota": 128, "model_limits_enabled": true,
"model_limits": "gpt-4o-mini,glm-5.2"
}],
"total": 1, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID> curl "http://localhost:8000/api/token/?p=1&page_size=10" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.get("http://localhost:8000/api/token/",
params={"p": 1, "page_size": 10},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for t in r.json()["data"]["items"]:
print(t["id"], t["name"], t["status"], t["remain_quota"], t["used_quota"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/?p=1&page_size=10"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/token/?p=1&page_size=10", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
},
});
for (const t of (await resp.json()).data.items) {
console.log(t.id, t.name, t.status, t.remain_quota, t.used_quota);
}
8. 创建令牌(生成 sk-tg- 密钥)
POST/api/token/ — 创建一个新令牌,底层落库为模型调用密钥(ApiKey)。返回的 data.key 即完整的 sk-tg-... 密钥,可直接用于 /v1/* 模型调用——这是「先建 Key、再调用、再对账」组合流程的第一步(见组合调用示例 · 工作流 1)。可同时设置额度上限与模型白名单。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string (body) | 否 | 令牌名称,默认 default |
remain_quota | number (body) | 否 | 额度上限(积分)。设置后该 Key 累计扣费达到上限即不可再调用 |
unlimited_quota | bool (body) | 否 | true 不限额度(默认);false 且给了 remain_quota 时启用额度上限 |
model_limits_enabled | bool (body) | 否 | 是否启用模型白名单 |
model_limits | string (body) | 否 | 白名单模型名,逗号分隔(如 "gpt-4o-mini,glm-5.2");模型必须在你的可用模型池内,否则 400 |
expired_time | number (body) | 否 | 兼容字段:我方令牌不设过期,该值被忽略(返回恒 -1) |
返回 data 字段:同「7. 令牌列表」的单个令牌对象;重点取 key(sk-tg- 明文)与 id(后续更新 / 删除用)。
{
"success": true, "message": "",
"data": {
"id": 4, "user_id": 1, "name": "ci-pipeline", "key": "sk-tg-1a2b3c4d...",
"status": 1, "created_time": 1789810000, "accessed_time": 0,
"expired_time": -1, "remain_quota": 500, "unlimited_quota": false,
"used_quota": 0, "model_limits_enabled": true,
"model_limits": "gpt-4o-mini"
}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID>
curl -X POST http://localhost:8000/api/token/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"name":"ci-pipeline","remain_quota":500,"unlimited_quota":false,"model_limits_enabled":true,"model_limits":"gpt-4o-mini"}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.post("http://localhost:8000/api/token/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"name": "ci-pipeline", "remain_quota": 500,
"unlimited_quota": False,
"model_limits_enabled": True,
"model_limits": "gpt-4o-mini"},
timeout=30)
key = r.json()["data"]["key"] # sk-tg-...,可直接用于 /v1/* 调用
print(key)
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"name\":\"ci-pipeline\",\"remain_quota\":500,"
+ "\"unlimited_quota\":false,\"model_limits_enabled\":true,"
+ "\"model_limits\":\"gpt-4o-mini\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/token/", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "ci-pipeline",
remain_quota: 500,
unlimited_quota: false,
model_limits_enabled: true,
model_limits: "gpt-4o-mini",
}),
});
const key = (await resp.json()).data.key; // sk-tg-...
console.log(key);
9. 更新令牌 / 批量启停
PUT/api/token/ — 两种用法:单个更新(body 含 id,可改名称、额度与模型白名单)与批量启停(body 为 {"ids": [...], "enable": true/false})。额度语义:给 unlimited_quota: true(或不给 remain_quota)即清除上限,恢复不限额;remain_quota 为全量重设(非增量),如想把 500 剩 100 的 Key 提到 800,应直接传 800。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | int (body) | 是* | 单个更新时的令牌 ID(*批量启停模式改用 ids) |
name | string (body) | 否 | 新名称(空串忽略) |
remain_quota | number (body) | 否 | 新的额度上限(全量重设) |
unlimited_quota | bool (body) | 否 | true 清除额度上限 |
model_limits_enabled + model_limits | bool / string (body) | 否 | 重设模型白名单;enabled=false 时清除白名单 |
ids | int[] (body) | 是* | 批量启停模式:令牌 ID 数组,配合 enable |
enable | bool (body) | 否 | true 启用(status=1)/ false 停用(status=2) |
返回 data 字段:单个更新返回更新后的令牌对象(形状同小节 7);批量启停返回 {"ok": true, "modified": N}(N 为实际改动的条数,仅本人令牌会被改动)。
{
"success": true, "message": "",
"data": {"ok": true, "modified": 3}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID>
# 单个更新:把 4 号令牌额度重设为 800
curl -X PUT http://localhost:8000/api/token/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":4,"remain_quota":800}'
# 批量停用 4、5、6 号令牌
curl -X PUT http://localhost:8000/api/token/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"ids":[4,5,6],"enable":false}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
# 单个更新
r1 = requests.put("http://localhost:8000/api/token/", headers=H,
json={"id": 4, "remain_quota": 800}, timeout=30)
print(r1.json()["data"]["remain_quota"]) # 800
# 批量停用
r2 = requests.put("http://localhost:8000/api/token/", headers=H,
json={"ids": [4, 5, 6], "enable": False}, timeout=30)
print(r2.json()["data"]) # {"ok": true, "modified": 3}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":4,\"remain_quota\":800}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
// 单个更新
let resp = await fetch("http://localhost:8000/api/token/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 4, remain_quota: 800 }),
});
console.log((await resp.json()).data.remain_quota); // 800
// 批量停用
resp = await fetch("http://localhost:8000/api/token/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ ids: [4, 5, 6], enable: false }),
});
console.log((await resp.json()).data); // { ok: true, modified: 3 }
10. 查询 / 删除 / 搜索令牌
同一资源的其余四个操作:
- GET/api/token/{id} — 按 ID 查询单个令牌(形状同小节 7 的 item;非本人令牌返回 404)
- DELETE/api/token/{id} — 物理删除令牌(调用日志仅存 ID 引用,历史记录不受影响);返回
{"ok": true} - POST/api/token/batch — 批量删除,body 为
{"ids": [1, 2, 3]};不存在的 ID 静默跳过,返回{"ok": true, "deleted": N}(N 为实际删除数) - GET/api/token/search?keyword=xx — 按名称关键字搜索(包含匹配),返回裸数组(无分页信封),最多 100 条
{
"success": true, "message": "",
"data": {"ok": true, "deleted": 2}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID>
# 查询单个
curl http://localhost:8000/api/token/4 -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
# 删除单个
curl -X DELETE http://localhost:8000/api/token/4 -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
# 批量删除
curl -X POST http://localhost:8000/api/token/batch \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" -d '{"ids":[5,6]}'
# 按名称搜索
curl "http://localhost:8000/api/token/search?keyword=ci" \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
base = "http://localhost:8000/api/token"
# 查询单个
print(requests.get(f"{base}/4", headers=H, timeout=30).json()["data"]["name"])
# 批量删除
r = requests.post(f"{base}/batch", headers=H,
json={"ids": [5, 6]}, timeout=30)
print(r.json()["data"]) # {"ok": true, "deleted": 2}
# 按名称搜索
r = requests.get(f"{base}/search", headers=H,
params={"keyword": "ci"}, timeout=30)
print([t["name"] for t in r.json()["data"]])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
// 查询单个
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/4"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
// 批量删除
var body = "{\"ids\":[5,6]}";
var req2 = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/batch"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req2,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
};
const base = "http://localhost:8000/api/token";
// 查询单个
let resp = await fetch(`${base}/4`, { headers: H });
console.log((await resp.json()).data.name);
// 批量删除
resp = await fetch(`${base}/batch`, {
method: "POST",
headers: { ...H, "Content-Type": "application/json" },
body: JSON.stringify({ ids: [5, 6] }),
});
console.log((await resp.json()).data); // { ok: true, deleted: 2 }
// 按名称搜索
resp = await fetch(`${base}/search?keyword=ci`, { headers: H });
console.log((await resp.json()).data.map((t) => t.name));
11. 我的调用日志
GET/api/log/self — 分页查询自己的模型调用日志(含计费明细),是「调用后对账」的核心端点(见组合调用示例 · 工作流 1)。每一行对应一次 /v1/* 请求,包含该次的实际扣费、倍率与延迟。配套 GET/api/log/self/search?keyword=xx 按模型名 / 路径 / request_id / 错误信息做关键字搜索(返回裸数组,最多 100 条)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
category | string (query) | 否 | 按类别过滤:llm / image / video / speech |
p | int (query) | 否 | 页码,默认 1 |
page_size | int (query) | 否 | 每页条数,默认 20,最大 100 |
返回 data.items 字段:id、request_id(本次调用的请求 ID,与响应头一致,可作对账主键)、category / model / path(类别 / 模型 / 路径)、http_status、stream(是否流式)、prompt_tokens / completion_tokens(输入 / 输出 token 数)、credits_charged(本次实际扣费积分)、rate_multiplier / sale_coefficient(计费倍率 / 售价系数)、upstream_cost / upstream_cost_cny / exchange_rate(上游成本美元 / 人民币 / 汇率)、latency_ms / ttft_ms(总延迟 / 首字延迟)、error(错误信息,成功为 null)、created_at。
{
"success": true, "message": "",
"data": {
"items": [{
"id": 9012, "user_id": 1, "request_id": "req-7f2c...", "category": "llm",
"model": "gpt-4o-mini", "path": "/v1/chat/completions",
"http_status": 200, "stream": false,
"prompt_tokens": 128, "completion_tokens": 256,
"upstream_cost": 0.000192, "upstream_cost_cny": 0.00138, "exchange_rate": 7.2,
"rate_multiplier": 1.0, "sale_coefficient": 1.5,
"credits_charged": 0.33, "latency_ms": 842, "ttft_ms": 311,
"error": null, "created_at": "2026-09-18T10:24:31"
}],
"total": 57, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID> curl "http://localhost:8000/api/log/self?category=llm&p=1&page_size=10" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # 关键字搜索(按模型名 / request_id / 错误信息) curl "http://localhost:8000/api/log/self/search?keyword=gpt-4o" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.get("http://localhost:8000/api/log/self",
params={"category": "llm", "p": 1, "page_size": 10},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for log in r.json()["data"]["items"]:
print(log["created_at"], log["model"], log["credits_charged"],
log["latency_ms"], log["request_id"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/log/self?category=llm&p=1&page_size=10"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch(
"http://localhost:8000/api/log/self?category=llm&p=1&page_size=10",
{
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
},
},
);
for (const log of (await resp.json()).data.items) {
console.log(log.created_at, log.model, log.credits_charged, log.request_id);
}
12. 我的用量统计
GET/api/log/self/stat — 查询自己的汇总用量,三个数字即可覆盖常见监控场景:累计扣费、最近 1 分钟请求速率、最近 1 分钟 token 速率。适合做轻量巡检(如每分钟采一次写入监控面板,无需拉全量日志)。
返回 data 字段:quota(历史累计扣费积分,float)、rpm(Requests Per Minute,近 1 分钟请求数)、tpm(Tokens Per Minute,近 1 分钟输入+输出 token 总数)。
{
"success": true, "message": "",
"data": {"quota": 128.5, "rpm": 3, "tpm": 2140}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID> curl http://localhost:8000/api/log/self/stat \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.get("http://localhost:8000/api/log/self/stat",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"]) # {"quota": 128.5, "rpm": 3, "tpm": 2140}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/log/self/stat"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/log/self/stat", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
},
});
console.log((await resp.json()).data); // { quota: 128.5, rpm: 3, tpm: 2140 }
13. 我的看板数据
GET/api/data/self — 个人看板三要素:当前余额、累计消费、可用模型数。打开控制台首页看到的数字就是它;程序化场景适合做「启动时先检查余额是否充足」的前置判断。
返回 data 字段:model_count(当前可用模型数量)、quota(账户余额,积分,float)、used_quota(历史累计消费积分,float)。
{
"success": true, "message": "",
"data": {"model_count": 46, "quota": 1000.0, "used_quota": 128.5}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID> curl http://localhost:8000/api/data/self \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.get("http://localhost:8000/api/data/self",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
data = r.json()["data"]
if data["quota"] < 10: # 余额不足预警
print("余额不足:", data["quota"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/data/self"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/data/self", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
},
});
const data = (await resp.json()).data;
if (data.quota < 10) console.log("余额不足:", data.quota);
14. 全站调用日志(管理员)
GET/api/log/ — 管理员查询全站调用日志,支持按用户、类别、供应商、HTTP 状态与日期范围过滤。配套 GET/api/log/search?keyword=xx(关键字搜索,裸数组最多 100 条)与 GET/api/log/stat(全站汇总:quota 累计扣费 / rpm / tpm 近 1 分钟速率)。需要 admin:calls:view 权限。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id | int (query) | 否 | 按用户过滤 |
category | string (query) | 否 | llm / image / video / speech |
provider_id | int (query) | 否 | 按上游供应商过滤 |
http_status / http_status_group | int / string (query) | 否 | 按状态码 / 状态组(如 4xx、5xx)过滤 |
start_date / end_date | date (query) | 否 | 日期范围,格式 YYYY-MM-DD |
page / page_size | int (query) | 否 | 分页,默认 1 / 20(page_size 最大 100) |
返回 data.items 字段:同「11. 我的调用日志」(CallLogOut 形状,含全站视角的 user_id)。
{
"success": true, "message": "",
"data": {
"items": [{
"id": 9013, "user_id": 42, "request_id": "req-a31f...", "category": "image",
"model": "seedream-4.0", "path": "/v1/cap/image/generate_image",
"http_status": 200, "stream": false, "prompt_tokens": 0, "completion_tokens": 0,
"upstream_cost": 0.03, "upstream_cost_cny": 0.216, "exchange_rate": 7.2,
"rate_multiplier": 1.0, "sale_coefficient": 1.4,
"credits_charged": 4.2, "latency_ms": 5230, "ttft_ms": null,
"error": null, "created_at": "2026-09-18T10:31:07"
}],
"total": 1532, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> # 查今天起、5xx 错误的全站日志 curl "http://localhost:8000/api/log/?http_status_group=5xx&start_date=2026-09-18&page=1" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # 全站汇总 curl http://localhost:8000/api/log/stat \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/log/",
params={"user_id": 42, "category": "llm",
"start_date": "2026-09-18", "page": 1},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
data = r.json()["data"]
print("total:", data["total"])
for log in data["items"]:
print(log["created_at"], log["user_id"], log["model"], log["credits_charged"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/log/?user_id=42&category=llm&page=1"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch(
"http://localhost:8000/api/log/?user_id=42&category=llm&page=1",
{
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
},
);
const data = (await resp.json()).data;
console.log("total:", data.total);
for (const log of data.items) {
console.log(log.created_at, log.user_id, log.model, log.credits_charged);
}
15. 全站看板 / 用户分组(管理员)
GET/api/data/ — 管理员看板数据(需要 admin:stats:view 权限):注册用户数、总调用量、累计扣费 / 授予积分、今日调用数与活跃供应商数。GET/api/group/ — 用户分组列表(NewAPI 形状:symbol = 分组名、ratio = 分组倍率),需 admin:groups:view 或 admin:users:edit 权限。
/api/data/ 返回 data 字段:total_users(用户数)、total_calls(总调用次数)、total_credits_consumed / total_credits_granted(累计消费 / 授予积分)、calls_today(今日调用数)、active_providers(活跃供应商数)。
{
"success": true, "message": "",
"data": {"total_users": 128, "total_calls": 45678, "total_credits_consumed": 9123.4,
"total_credits_granted": 50000.0, "calls_today": 356, "active_providers": 12}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl http://localhost:8000/api/data/ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # 用户分组列表(symbol=分组名,ratio=倍率) curl http://localhost:8000/api/group/ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
stats = requests.get("http://localhost:8000/api/data/", headers=H,
timeout=30).json()["data"]
print(stats["calls_today"], "calls today,", stats["active_providers"], "providers")
groups = requests.get("http://localhost:8000/api/group/", headers=H,
timeout=30).json()["data"]
for g in groups:
print(g["id"], g["symbol"], "x", g["ratio"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/data/"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
};
const stats = (await (await fetch("http://localhost:8000/api/data/", { headers: H })).json()).data;
console.log(stats.calls_today, "calls today,", stats.active_providers, "providers");
const groups = (await (await fetch("http://localhost:8000/api/group/", { headers: H })).json()).data;
for (const g of groups) console.log(g.id, g.symbol, "x", g.ratio);
16. 配置项读写(管理员)
GET/api/option/ — 读取全站配置项(键值对数组,需要 admin:stats:view 或 admin:config:edit 权限;敏感项如 SMTP 密码不返回)。PUT/api/option/ — 批量更新配置(需要 admin:config:edit 权限):body 兼容单对象 {"key": "...", "value": "..."} 与数组 [{"key": "...", "value": "..."}] 两种形式,返回更新后的键值对(含 updated_at)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | string (body) | 是 | 配置键,如 usd_to_cny(美元汇率)、rate_limit_per_key(每 Key 秒级限流,0 = 不限) |
value | string (body) | 是 | 配置值(一律为字符串形式,数字也转字符串) |
{
"success": true, "message": "",
"data": [
{"key": "usd_to_cny", "value": "7.2"},
{"key": "rate_limit_per_key", "value": "200"}
]
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID>
# 读取全部配置
curl http://localhost:8000/api/option/ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
# 更新汇率(单对象形式)
curl -X PUT http://localhost:8000/api/option/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"key":"usd_to_cny","value":"7.1"}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
opts = requests.get("http://localhost:8000/api/option/", headers=H,
timeout=30).json()["data"]
print({o["key"]: o["value"] for o in opts})
r = requests.put("http://localhost:8000/api/option/", headers=H,
json=[{"key": "usd_to_cny", "value": "7.1"}], # 数组批量形式
timeout=30)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"key\":\"usd_to_cny\",\"value\":\"7.1\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/option/"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
"Content-Type": "application/json",
};
// 更新配置
let resp = await fetch("http://localhost:8000/api/option/", {
method: "PUT",
headers: H,
body: JSON.stringify({ key: "usd_to_cny", value: "7.1" }),
});
console.log((await resp.json()).data);
// 读取全部
resp = await fetch("http://localhost:8000/api/option/", { headers: H });
const opts = (await resp.json()).data;
console.log(Object.fromEntries(opts.map((o) => [o.key, o.value])));
17. 渠道列表与详情(管理员)
GET/api/channel/ — 分页查询渠道列表。「渠道」在本平台等价于上游供应商(一个 NewAPI 渠道 = 一个 OpenAI 兼容上游 + 一组密钥)。GET/api/channel/{id} — 单个渠道详情(含明文密钥);GET/api/channel/search?keyword=xx — 按名称 / base_url / 模型名搜索(裸数组最多 100 条)。列表需要 admin:providers:view 权限。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p / page_size | int (query) | 否 | 分页,默认 1 / 20(page_size 最大 100) |
status | int (query) | 否 | 1 启用 / 2 禁用 |
name | string (query) | 否 | 按渠道名称包含过滤 |
返回 data.items 字段(NewAPI Channel 形状):id、type(恒 1,统一为 OpenAI 兼容类型)、status(1 启用 / 2 禁用)、name、weight / priority(调度权重)、base_url(上游地址)、key(密钥,多行合并:首行 = 供应商主密钥,其余行 = 子密钥)、models(该渠道的活跃模型名,逗号分隔)、group(恒 default)、balance / balance_updated_time(上游余额与抓取时间)、created_time;used_quota / model_mapping 等为兼容占位(用量分析走原生接口)。
{
"success": true, "message": "",
"data": {
"items": [{
"id": 1, "type": 1, "status": 1, "name": "openai-relay", "weight": 100,
"created_time": 1789600000, "test_time": 0, "response_time": 0,
"base_url": "https://api.upstream.example/v1",
"key": "sk-up-main-xxx\nsk-up-sub-yyy",
"models": "gpt-4o,gpt-4o-mini", "group": "default",
"used_quota": 0, "model_mapping": "", "priority": 100,
"auto_ban": 1, "other_info": "", "balance": 96.5,
"balance_updated_time": 1789790000, "tag": "", "setting": "",
"param_override": ""
}],
"total": 12, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl "http://localhost:8000/api/channel/?p=1&page_size=10" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # 单个渠道详情 curl http://localhost:8000/api/channel/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/channel/",
params={"p": 1, "page_size": 10, "status": 1},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for ch in r.json()["data"]["items"]:
print(ch["id"], ch["name"], ch["base_url"],
ch["models"].split(","), "balance:", ch["balance"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/?p=1&page_size=10"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/?p=1&page_size=10", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
for (const ch of (await resp.json()).data.items) {
console.log(ch.id, ch.name, ch.models.split(","), "balance:", ch.balance);
}
18. 创建渠道(管理员)
POST/api/channel/ — 创建渠道(供应商),需要 admin:providers:manage 权限。支持 NewAPI 三种模式:single(默认,单渠道单密钥)、batch(key 多行时每行各建一个同名供应商)、multi_to_single(key 多行合并为一个供应商:首行主密钥,其余行入子密钥池)。models 会同步创建目录条目与基础路由(模型随即进入调度池)。另可用 POST/api/channel/fetch_models(body:{"base_url": "...", "key": "..."})先探测上游模型列表,不落库。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string (body) | 是 | 渠道名称 |
base_url | string (body) | 是 | 上游地址(OpenAI 兼容) |
key | string (body) | 是 | 上游密钥;多行(\n 分隔)时按 mode 解释 |
mode | string (body) | 否 | single(默认)/ batch / multi_to_single |
models | string / string[] (body) | 否 | 模型名列表,逗号分隔字符串或数组 |
status | int (body) | 否 | 1 启用(默认)/ 2 禁用 |
priority / weight | int (body) | 否 | 调度权重,默认 0 |
category | string (body) | 否 | 目录条目的默认类别,默认 llm |
返回 data 字段:单个渠道对象(形状同小节 17);batch 模式返回数组。
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID>
curl -X POST http://localhost:8000/api/channel/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"name":"my-openai","base_url":"https://api.upstream.example/v1","key":"sk-up-xxx","models":"gpt-4o,gpt-4o-mini","priority":100}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.post("http://localhost:8000/api/channel/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"name": "my-openai",
"base_url": "https://api.upstream.example/v1",
"key": "sk-up-xxx",
"models": "gpt-4o,gpt-4o-mini",
"priority": 100},
timeout=30)
print(r.json()["data"]["id"]) # 新渠道 ID
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"name\":\"my-openai\",\"base_url\":\"https://api.upstream.example/v1\","
+ "\"key\":\"sk-up-xxx\",\"models\":\"gpt-4o,gpt-4o-mini\",\"priority\":100}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "my-openai",
base_url: "https://api.upstream.example/v1",
key: "sk-up-xxx",
models: "gpt-4o,gpt-4o-mini",
priority: 100,
}),
});
console.log((await resp.json()).data.id); // 新渠道 ID
19. 更新渠道 / 子密钥管理(管理员)
PUT/api/channel/ — 更新渠道(body 必含 id,需要 admin:providers:manage 权限)。key 多行 = 全量重写密钥(首行替换主密钥,其余行整体替换子密钥池);models = 全量同步目录,不在列表中的旧条目将被停用。model_mapping / tag / setting 等我方无对应概念的字段接受但忽略。配套 POST/api/channel/multi_key/manage — 管理单个子密钥:body 为 {"channel_id": 1, "key_id": 3, "action": "enable|disable|delete"}。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | int (body) | 是 | 渠道 ID |
name / base_url | string (body) | 否 | 新名称 / 新地址(空串忽略) |
key | string (body) | 否 | 多行密钥全量重写(首行主密钥) |
models | string / string[] (body) | 否 | 全量同步模型目录(列表外停用) |
status | int (body) | 否 | 1 启用 / 2 禁用 |
priority / weight | int (body) | 否 | 调度权重 |
返回 data 字段:更新后的渠道对象(形状同小节 17);子密钥管理返回 {"ok": true, "action": "...", "key_id": N}。
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID>
# 改名 + 重写密钥(一行主密钥)+ 全量同步模型
curl -X PUT http://localhost:8000/api/channel/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":1,"name":"openai-relay-v2","key":"sk-up-newkey","models":"gpt-4o,gpt-4o-mini,deepseek-v4-flash"}'
# 停用 1 号渠道的 3 号子密钥
curl -X POST http://localhost:8000/api/channel/multi_key/manage \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"channel_id":1,"key_id":3,"action":"disable"}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.put("http://localhost:8000/api/channel/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"id": 1, "status": 2}, # 禁用渠道
timeout=30)
print(r.json()["data"]["status"]) # 2
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":1,\"status\":2}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 1, status: 2 }), // 禁用渠道
});
console.log((await resp.json()).data.status); // 2
20. 渠道测试 / 余额刷新 / 删除(管理员)
渠道运维三件套:
- GET/api/channel/test/{id} — 测试单个渠道:探测该供应商首个活跃路由(一次真实上游请求,同步返回);无路由时回退为上游
/v1/models连通性探测。返回{"ok", "channel_id", "available", "error", "time"(毫秒), "mode"(route_probe / upstream_probe)}。不带 ID 的GET /api/channel/test触发全量后台健康检测。 - GET/api/channel/update_balance — 刷新全部供应商的上游余额(同步执行);
GET /api/channel/update_balance/{id}— 刷新单个。余额低于告警线时自动触发通知。 - DELETE/api/channel/{id} — 删除渠道(级联清理该供应商的路由 / 目录条目 / 子密钥),返回
{"ok": true}。测试需要admin:models:manage,余额 / 删除需要admin:providers:view/admin:providers:manage。
{
"success": true, "message": "",
"data": {"ok": true, "channel_id": 1, "available": true,
"error": null, "time": 643, "mode": "route_probe"}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> # 测试 1 号渠道 curl http://localhost:8000/api/channel/test/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # 刷新 1 号渠道余额 curl http://localhost:8000/api/channel/update_balance/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # 删除 1 号渠道 curl -X DELETE http://localhost:8000/api/channel/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
# 测试
r = requests.get("http://localhost:8000/api/channel/test/1", headers=H, timeout=60)
d = r.json()["data"]
print("available:", d["available"], "time:", d["time"], "ms")
# 刷新余额
r = requests.get("http://localhost:8000/api/channel/update_balance/1",
headers=H, timeout=60)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/test/1"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
};
// 测试
let resp = await fetch("http://localhost:8000/api/channel/test/1", { headers: H });
let d = (await resp.json()).data;
console.log("available:", d.available, "time:", d.time, "ms");
// 删除
resp = await fetch("http://localhost:8000/api/channel/1", { method: "DELETE", headers: H });
console.log((await resp.json()).data); // { ok: true }
21. 模型元数据列表与详情(管理员)
GET/api/models/ — 分页查询模型目录(元数据管理视角,需要 admin:models:view 权限)。注意与无尾斜杠的 GET/api/models 区分:后者返回当前登录用户的可用模型名数组(与 /api/user/models 同源)。配套:GET/api/models/search?keyword=xx(搜索,裸数组)、GET/api/models/missing(缺成本报价的模型清单——这些模型无法精确计费,放量前需补价)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p / page_size | int (query) | 否 | 分页,默认 1 / 20(page_size 最大 100) |
keyword | string (query) | 否 | 按模型名 / 对外名 / 说明搜索 |
返回 data.items 字段:id(目录条目 ID)、model_name(引入模型名)、description(模型说明)、vendor_id(恒 1,统一品牌「可信词元」)、status(1 启用 / 2 禁用)、sort_order;我方扩展字段:provider_id(所属供应商)、public_name(对外显示名)、category(类别数组)、is_public(是否公开)、context_length(上下文长度)。
{
"success": true, "message": "",
"data": {
"items": [{
"id": 12, "model_name": "gpt-4o-mini", "description": "OpenAI 快速对话模型",
"vendor_id": 1, "status": 1, "sort_order": 12, "provider_id": 1,
"public_name": "gpt-4o-mini", "category": ["llm"],
"is_public": true, "context_length": 128000
}],
"total": 46, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> # 元数据列表(注意尾斜杠) curl "http://localhost:8000/api/models/?p=1&page_size=10" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # 单个模型(按目录条目 id) curl http://localhost:8000/api/models/12 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # 缺价模型清单 curl http://localhost:8000/api/models/missing \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/models/",
params={"keyword": "gpt", "page_size": 10},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for m in r.json()["data"]["items"]:
print(m["id"], m["model_name"], m["status"], m["category"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/models/?p=1&page_size=10"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/models/?p=1&page_size=10", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
for (const m of (await resp.json()).data.items) {
console.log(m.id, m.model_name, m.status, m.category);
}
22. 更新模型元数据(管理员)
PUT/api/models/ — 更新目录条目的元数据(body 含 id,需要 admin:models:manage 权限)。字段映射:model_name → 引入模型名、description → 模型说明、status(1/2)→ 启停。传 "status_only": true 时只更新状态,其余字段忽略——只做启停操作的客户端建议始终带上它,防止误清空说明文字。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | int (body) | 是 | 目录条目 ID |
status | int (body) | 否 | 1 启用 / 2 禁用 |
model_name | string (body) | 否 | 新模型名(status_only 时忽略) |
description | string (body) | 否 | 新说明(status_only 时忽略) |
status_only | bool (body) | 否 | true 仅更新状态(推荐用于纯启停) |
返回 data 字段:{"ok": true, "id": N, "model_name": "...", "status": "active|disabled"}。
{
"success": true, "message": "",
"data": {"ok": true, "id": 12, "model_name": "gpt-4o-mini", "status": "active"}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID>
# 仅停用 12 号模型
curl -X PUT http://localhost:8000/api/models/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":12,"status":2,"status_only":true}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.put("http://localhost:8000/api/models/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"id": 12, "status": 2, "status_only": True},
timeout=30)
print(r.json()["data"]) # {"ok": true, "id": 12, ...}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":12,\"status\":2,\"status_only\":true}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/models/"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/models/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 12, status: 2, status_only: true }),
});
console.log((await resp.json()).data); // { ok: true, id: 12, ... }
23. 价格目录与供应商(公开)
GET/api/pricing — 面向下游软件的价格目录:各对外模型的售价(积分计),可带 ?category=llm,image 过滤(多类别取交集)。GET/api/vendors — 供应商列表:本平台对用户侧统一展示为单一品牌「可信词元」,恒返回 [{"id": 1, "name": "可信词元", ...}],不接受创建 / 修改 / 删除。两个接口均无需管理员权限。
/api/pricing 返回 data 字段(数组):id(对外模型名)、display_name(显示名)、category(类别)、input_price / output_price(输入 / 输出单价)、currency(恒 credits)、unit(计价单位)、available(当前是否可用)。
{
"success": true, "message": "",
"data": [
{"id": "gpt-4o-mini", "display_name": "gpt-4o-mini", "category": "llm",
"input_price": 0.0012, "output_price": 0.0048,
"currency": "credits", "unit": "per 1k tokens", "available": true}
]
}
curl "http://localhost:8000/api/pricing?category=llm" curl http://localhost:8000/api/vendors
import requests
prices = requests.get("http://localhost:8000/api/pricing",
params={"category": "llm"}, timeout=30).json()["data"]
for p in prices:
print(p["id"], "in:", p["input_price"], "out:", p["output_price"])
vendors = requests.get("http://localhost:8000/api/vendors", timeout=30).json()["data"]
print(vendors) # [{"id": 1, "name": "可信词元", ...}]
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/pricing?category=llm")).GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const prices = (await (await fetch("http://localhost:8000/api/pricing?category=llm")).json()).data;
for (const p of prices) console.log(p.id, "in:", p.input_price, "out:", p.output_price);
const vendors = (await (await fetch("http://localhost:8000/api/vendors")).json()).data;
console.log(vendors); // [{ id: 1, name: "可信词元", ... }]
24. 扩展域(NewAPI 风格命名)
除上述对齐端点外,原生管理能力也按 NewAPI 风格路径提供(鉴权与信封约定一致,权限要求与原生管理后台相同)。常用扩展域:
/api/role/、/api/permission/self— 角色与权限管理/api/transaction/— 积分流水的管理视角(管理员用my_auth("user:finance:read")鉴权)/api/model_route/、/api/model_proxy/— 模型路由与代理配置/api/key/— Key 级配置(如模型使用时段)/api/exchange_rate/、/api/bill_reconcile/— 汇率与账单对账
完整清单见《运维操作手册》。扩展域接口形状遵循同一信封约定,此处不再逐一展开。
用户认证补充(公开 / 登录态)
25. 注册(需邮箱验证码)
POST/api/user/register — 公开注册端点。与 NewAPI 的差异:email 与 email_code 均为必填(防垃圾注册),请先调用「28. 发送邮箱验证码」获取验证码。注册成功返回 {"ok": true}(不签发会话),随后调用 /api/user/login 登录。用户名已被注册返回 409;绑定代理商的专属皮肤域名禁止自助注册(403)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | string | 是 | 用户名(3–64 字符) |
password | string | 是 | 密码(6–128 字符) |
email | string | 是 | 邮箱(注册即视为已验证) |
email_code | string | 是 | 邮箱验证码(4–16 字符,来自 /api/verification) |
nickname | string | 否 | 昵称,缺省同用户名 |
返回 data 字段:{"ok": true}。失败时 HTTP 400(邮箱格式 / 验证码无效)、409(用户名占用)、403(专属站点禁注册)。
{
"success": true, "message": "",
"data": {"ok": true}
}
curl -X POST http://localhost:8000/api/user/register \
-H "Content-Type: application/json" \
-d '{"username":"newuser","password":"password123","email":"newuser@example.com","email_code":"123456"}'
import requests
r = requests.post("http://localhost:8000/api/user/register",
json={"username": "newuser", "password": "password123",
"email": "newuser@example.com", "email_code": "123456"},
timeout=30)
print(r.json()) # {"success": true, "message": "", "data": {"ok": true}}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"username\":\"newuser\",\"password\":\"password123\","
+ "\"email\":\"newuser@example.com\",\"email_code\":\"123456\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/register"))
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "newuser", password: "password123",
email: "newuser@example.com", email_code: "123456" }),
});
console.log(await resp.json());
26. 退出登录(会话)
GET/api/user/logout — 退出浏览器会话:删除服务端 session 并清除 tg_session Cookie。仅对 Cookie 会话有意义;tg-sat- 系统访问令牌是无状态的,不受本接口影响(重置令牌请用「5. 重置系统访问令牌」)。
返回 data 字段:{"ok": true}。
{
"success": true, "message": "",
"data": {"ok": true}
}
curl http://localhost:8000/api/user/logout \ -b "tg_session=<会话Cookie值>"
import requests
s = requests.Session() # 已登录的会话对象
print(s.get("http://localhost:8000/api/user/logout", timeout=30).json())
// JDK 11+(携带登录后获得的 tg_session Cookie)
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/logout"))
.header("Cookie", "tg_session=<会话Cookie值>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/logout", {
headers: { Cookie: "tg_session=<会话Cookie值>" },
});
console.log(await resp.json());
27. 重置密码(登录态,发新密码到邮箱)
POST/api/user/reset — 已登录用户重置自己的密码:生成随机新密码并发送到绑定邮箱(旧密码立即失效,邮件发送失败则整体回滚)。未绑定邮箱返回 400。可用 Cookie 会话或 tg-sat- + New-Api-User 调用。
返回 data 字段:ok(恒 true)、email(新密码发送到的邮箱)。
{
"success": true, "message": "",
"data": {"ok": true, "email": "alice@example.com"}
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID> curl -X POST http://localhost:8000/api/user/reset \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.post("http://localhost:8000/api/user/reset",
headers={"Authorization": f"Bearer {SAT}",
"New-Api-User": UID}, timeout=30)
print(r.json()["data"]) # {"ok": true, "email": "..."}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/reset"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>")
.POST(java.net.http.HttpRequest.BodyPublishers.noBody()).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/reset", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
},
});
console.log((await resp.json()).data);
28. 发送邮箱验证码(公开)
GET/api/verification — 向指定邮箱发送注册验证码(供 /api/user/register 的 email_code 使用)。邮箱格式不正确返回 400;邮件服务未配置返回 503,发送失败返回 502。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
email | string (query) | 是 | 目标邮箱 |
返回 data 字段:ok(恒 true)、ttl(验证码有效期,秒)。
{
"success": true, "message": "",
"data": {"ok": true, "ttl": 600}
}
curl "http://localhost:8000/api/verification?email=newuser@example.com"
import requests
r = requests.get("http://localhost:8000/api/verification",
params={"email": "newuser@example.com"}, timeout=30)
print(r.json()["data"]) # {"ok": true, "ttl": 600}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/verification?email=newuser@example.com"))
.GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/verification?email=newuser@example.com");
console.log((await resp.json()).data);
29. 找回密码(公开,按用户名 + 邮箱)
GET/api/reset_password — 忘记密码时自助找回:校验用户名与绑定邮箱一致后,生成随机新密码发送到该邮箱(旧密码立即失效,邮件发送失败则整体回滚)。两参数均必填;不匹配统一返回 400(避免账号枚举)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | string (query) | 是 | 用户名 |
email | string (query) | 是 | 该用户绑定的邮箱 |
返回 data 字段:ok(恒 true)、email(新密码发送到的邮箱)。
{
"success": true, "message": "",
"data": {"ok": true, "email": "alice@example.com"}
}
curl "http://localhost:8000/api/reset_password?username=alice&email=alice@example.com"
import requests
r = requests.get("http://localhost:8000/api/reset_password",
params={"username": "alice", "email": "alice@example.com"},
timeout=30)
print(r.json()["data"]) # {"ok": true, "email": "alice@example.com"}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/reset_password?username=alice&email=alice@example.com"))
.GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/reset_password?username=alice&email=alice@example.com");
console.log((await resp.json()).data);
用户管理补充(管理员)
30. 用户列表(管理员)
GET/api/user/ — 分页查询全站用户(需要 admin:users:view 权限)。注意路径带尾斜杠,与「33. 用户详情」的 /api/user/{id} 区分。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p | int (query) | 否 | 页码,默认 1 |
page_size | int (query) | 否 | 每页条数,默认 20,最大 100 |
keyword | string (query) | 否 | 按用户名 / 昵称模糊匹配 |
返回 data 字段:分页信封 {"items": [...], "total": N, "page": 1, "page_size": 20};items 为管理员视角用户对象(字段同「33. 用户详情」)。
{
"success": true, "message": "",
"data": {
"items": [{"id": 2, "username": "bob", "nickname": "Bob",
"email": "bob@example.com", "balance": 100.0,
"status": "active", "is_admin": false, "...": "..."}],
"total": 57, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl "http://localhost:8000/api/user/?p=1&page_size=20&keyword=alice" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
r = requests.get("http://localhost:8000/api/user/",
params={"p": 1, "page_size": 20, "keyword": "alice"},
headers=H, timeout=30)
data = r.json()["data"]
print(data["total"], [u["username"] for u in data["items"]])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/?p=1&page_size=20"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
};
const resp = await fetch("http://localhost:8000/api/user/?p=1&page_size=20", { headers: H });
const d = (await resp.json()).data;
console.log(d.total, d.items.map(u => u.username));
31. 创建用户(管理员)
POST/api/user/ — 单个创建用户(需要 admin:users:batch_create 权限)。内部复用批量创建逻辑:遵循托管积分规则、自动生成默认 API Key、不发欢迎邮件。若配置了创建赠送且由管理员承担,charged_from / charged_total 反映从调用者余额划转的情况。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | string | 是 | 用户名(3–64 字符) |
password | string | 是 | 密码(6–128 字符) |
email | string | 否 | 邮箱,默认空 |
nickname | string | 否 | 昵称,默认空(同用户名) |
credits | float | 否 | 初始积分(≥ 0,默认 0) |
返回 data 字段:ok(是否全部成功)、created / failed / total(成功 / 失败 / 总数)、charged_from(积分划转来源)、charged_total(划转总额)。
{
"success": true, "message": "",
"data": {"ok": true, "created": 1, "failed": 0, "total": 1,
"charged_from": null, "charged_total": 0}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID>
curl -X POST http://localhost:8000/api/user/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"username":"bob","password":"secret123","email":"bob@example.com","credits":100}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.post("http://localhost:8000/api/user/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"username": "bob", "password": "secret123",
"email": "bob@example.com", "credits": 100},
timeout=30)
print(r.json()["data"]) # {"ok": true, "created": 1, ...}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"username\":\"bob\",\"password\":\"secret123\","
+ "\"email\":\"bob@example.com\",\"credits\":100}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ username: "bob", password: "secret123",
email: "bob@example.com", credits: 100 }),
});
console.log((await resp.json()).data);
32. 编辑用户(管理员)
PUT/api/user/ — 编辑指定用户(需要 admin:users:edit 权限)。body 必须包含 id,其余字段只传需要修改的;与原生后台编辑共用同一核心逻辑与权限校验。返回更新后的管理员用户对象。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | int | 是 | 目标用户 ID |
group_id | int | 否 | 所属分组 ID |
status | string | 否 | active / disabled |
is_admin | bool | 否 | 是否超级管理员 |
nickname / email | string | 否 | 昵称 / 邮箱 |
allowed_models / denied_models | string[] | 否 | 模型白名单 / 黑名单 |
role_ids | int[] | 否 | 角色 ID 列表 |
file_retention_days / log_retention_days | int | 否 | 文件 / 日志保留天数 |
is_agent / agent_site_name / agent_logo | bool / string | 否 | 代理商身份与专属站点配置 |
{
"success": true, "message": "",
"data": {"id": 2, "username": "bob", "nickname": "Bobby",
"status": "active", "...": "..."}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID>
curl -X PUT http://localhost:8000/api/user/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":2,"group_id":1}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.put("http://localhost:8000/api/user/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"id": 2, "group_id": 1},
timeout=30)
print(r.json()["data"]["group_id"]) # 1
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":2,\"group_id\":1}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 2, group_id: 1 }),
});
console.log((await resp.json()).data);
33. 用户详情(管理员)
GET/api/user/{id} — 查询单个用户详情(需要 admin:users:view 权限),含分组、角色与代理商信息。用户不存在返回 404。
返回 data 字段:管理员视角用户对象,常用字段:id、username、nickname、email、balance、rate_multiplier、group_id / group_name、status、is_admin、is_agent、created_at 等。
{
"success": true, "message": "",
"data": {"id": 2, "username": "bob", "nickname": "Bob",
"email": "bob@example.com", "balance": 100.0,
"group_id": 1, "group_name": "default",
"status": "active", "is_admin": false, "is_agent": false, "...": "..."}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl http://localhost:8000/api/user/2 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/user/2",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/2"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/2", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
console.log((await resp.json()).data);
34. 用户搜索(管理员)
GET/api/user/search — 按关键字搜索用户(需要 admin:users:view 权限),匹配用户名 / 昵称 / 邮箱。返回裸数组(不带分页信封),最多 100 条,按用户 ID 倒序;不带 keyword 时返回最新的 100 个用户。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
keyword | string (query) | 否 | 匹配用户名 / 昵称 / 邮箱 |
{
"success": true, "message": "",
"data": [
{"id": 2, "username": "bob", "nickname": "Bob",
"email": "bob@example.com", "balance": 100.0, "...": "..."}
]
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl "http://localhost:8000/api/user/search?keyword=bob" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/user/search",
params={"keyword": "bob"},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for u in r.json()["data"]:
print(u["id"], u["username"], u["email"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/search?keyword=bob"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/search?keyword=bob", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
for (const u of (await resp.json()).data) console.log(u.id, u.username, u.email);
35. 用户状态操作(管理员)
POST/api/user/manage — 对用户执行快捷操作(需要 admin:users:edit 权限)。不提供 delete:停用请用 disable。不支持的 action 返回 400,用户不存在返回 404。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | int | 是 | 目标用户 ID |
action | string | 是 | disable(停用)/ enable(启用)/ promote(设为管理员)/ demote(取消管理员) |
返回 data 字段:{"ok": true, "id": N, "action": "..."}。
{
"success": true, "message": "",
"data": {"ok": true, "id": 2, "action": "disable"}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID>
curl -X POST http://localhost:8000/api/user/manage \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":2,"action":"disable"}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.post("http://localhost:8000/api/user/manage",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"id": 2, "action": "disable"}, timeout=30)
print(r.json()["data"]) # {"ok": true, "id": 2, "action": "disable"}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":2,\"action\":\"disable\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/manage"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/manage", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 2, action: "disable" }),
});
console.log((await resp.json()).data);
36. 充值记录(管理员)
GET/api/user/topup — 分页查询充值记录(type=recharge 的积分流水,需要 admin:tx:view 权限),可按用户过滤。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p / page_size | int (query) | 否 | 分页,默认 1 / 20(page_size 最大 100) |
user_id | int (query) | 否 | 只看指定用户的充值 |
返回 data.items 字段:id(流水 ID)、user_id、amount(充值金额)、balance_after(到账后余额)、remark(备注)、operator_id(经办管理员)、created_at。
{
"success": true, "message": "",
"data": {
"items": [{"id": 301, "user_id": 2, "amount": 100.0,
"balance_after": 100.0, "remark": "对公转账",
"operator_id": 1, "created_at": "2026-09-18T10:00:00"}],
"total": 12, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl "http://localhost:8000/api/user/topup?p=1&page_size=20&user_id=2" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/user/topup",
params={"p": 1, "page_size": 20, "user_id": 2},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for t in r.json()["data"]["items"]:
print(t["id"], t["user_id"], t["amount"], t["remark"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/topup?p=1&page_size=20&user_id=2"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/topup?p=1&page_size=20&user_id=2", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
for (const t of (await resp.json()).data.items) console.log(t.id, t.amount, t.remark);
37. 确认充值到账(管理员)
POST/api/user/topup/complete — 人工确认到账并给用户加积分(需要 admin:users:grant 权限),JSON 版授予接口;需要凭证附件时改用 /api/user/{id}/grant。remark 为财务审计必填。用户不存在返回 404。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id | int | 是 | 目标用户 ID |
amount | float | 是 | 充值金额(> 0) |
remark | string | 是 | 操作说明(财务审计必填,不能为空) |
返回 data 字段:ok、user_id、amount、balance(到账后余额)。
{
"success": true, "message": "",
"data": {"ok": true, "user_id": 2, "amount": 100.0, "balance": 200.0}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID>
curl -X POST http://localhost:8000/api/user/topup/complete \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"user_id":2,"amount":100,"remark":"对公转账到账"}'
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.post("http://localhost:8000/api/user/topup/complete",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"user_id": 2, "amount": 100, "remark": "对公转账到账"},
timeout=30)
print(r.json()["data"]["balance"]) # 到账后余额
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"user_id\":2,\"amount\":100,\"remark\":\"对公转账到账\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/topup/complete"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/topup/complete", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ user_id: 2, amount: 100, remark: "对公转账到账" }),
});
console.log((await resp.json()).data.balance);
38. 我的可用分组(登录态)
GET/api/user/self/groups — 查询当前用户可见的启用中分组列表(登录态即可,无需管理员权限;可用 Cookie 会话或 tg-sat- + New-Api-User)。返回裸数组。
返回 data 字段(数组):id、name(分组标识)、display_name(显示名)、rate(分组倍率)、is_current(是否当前用户所属分组)。
{
"success": true, "message": "",
"data": [
{"id": 1, "name": "default", "display_name": "默认分组", "rate": 1.0, "is_current": true},
{"id": 2, "name": "vip", "display_name": "VIP", "rate": 0.8, "is_current": false}
]
}
SAT=<tg-sat-系统访问令牌>; UID=<你的用户ID> curl http://localhost:8000/api/user/self/groups \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<你的用户ID>"
r = requests.get("http://localhost:8000/api/user/self/groups",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for g in r.json()["data"]:
print(g["name"], g["rate"], g["is_current"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/self/groups"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<你的用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/self/groups", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<你的用户ID>",
},
});
for (const g of (await resp.json()).data) console.log(g.name, g.rate, g.is_current);
渠道补充(管理员)
39. 全部渠道模型名(管理员)
GET/api/channel/models — 返回所有渠道已知模型名(需要 admin:providers:view 权限),含已停用条目,去重后按名称排序。返回裸数组,常用于渠道配置页的模型下拉候选。
{
"success": true, "message": "",
"data": ["gpt-4o", "gpt-4o-mini", "claude-sonnet-4-5", "..."]
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl http://localhost:8000/api/channel/models \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/channel/models",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"]) # ["gpt-4o", "gpt-4o-mini", ...]
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/models"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/models", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
console.log((await resp.json()).data);
40. 启用中模型名(管理员)
GET/api/channel/models_enabled — 返回当前启用中(供应商与目录条目均 active)的模型名(需要 admin:providers:view 权限),去重排序。返回裸数组。
{
"success": true, "message": "",
"data": ["gpt-4o-mini", "claude-sonnet-4-5", "..."]
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl http://localhost:8000/api/channel/models_enabled \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/channel/models_enabled",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/models_enabled"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/models_enabled", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
console.log((await resp.json()).data);
41. 同步上游模型(管理员)
GET/api/channel/fetch_models/{id} — 从指定渠道(供应商)的上游实时拉取模型列表并录入本地目录(等价原生「同步模型」,需要 admin:providers:manage 权限)。返回裸数组:同步完成后该供应商当前全部活跃模型名。渠道不存在返回 404,上游拉取失败返回 502。另有 POST/api/channel/fetch_models(body 传 base_url + key)用于建渠道前的探测,不落库。
{
"success": true, "message": "",
"data": ["gpt-4o", "gpt-4o-mini", "gpt-5", "..."]
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl http://localhost:8000/api/channel/fetch_models/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/channel/fetch_models/1",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=60) # 上游拉取可能较慢
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/fetch_models/1"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/fetch_models/1", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
console.log((await resp.json()).data);
42. 查看渠道明文密钥(管理员)
POST/api/channel/{id}/key — 查看渠道的明文密钥(需要 admin:providers:view 权限,沿用密钥明文查看权限码)。返回 data 为多行合并的明文密钥串:首行为主密钥,其余每行一个子密钥(无子密钥时只有一行)。渠道不存在返回 404。
{
"success": true, "message": "",
"data": "sk-main-xxxxxxxx\nsk-sub-yyyyyyyy"
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl -X POST http://localhost:8000/api/channel/1/key \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.post("http://localhost:8000/api/channel/1/key",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"].splitlines()) # 每行一个密钥
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/1/key"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>")
.POST(java.net.http.HttpRequest.BodyPublishers.noBody()).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/1/key", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
console.log(((await resp.json()).data || "").split("\n"));
其他补充(Key 用量 / 日志 / 模型 / 供应商)
43. Key 用量查询(仅 sk-tg-)
GET/api/usage/token/ — 凭模型调用密钥(sk-tg-)自身鉴权查询该 Key 的累计用量。这是 /api/* 管理面中唯一接受 sk-tg- 鉴权的例外端点(不接受 tg-sat- / 登录会话,用其他凭证调用返回 403)。路径带不带尾斜杠均可。
返回 data 字段:count(该 Key 的调用次数)、quota(该 Key 累计扣费积分)、key_name(Key 名称)、key_status(Key 状态)。
{
"success": true, "message": "",
"data": {"count": 128, "quota": 35.6, "key_name": "default", "key_status": "active"}
}
KEY=<sk-tg-模型调用密钥> curl http://localhost:8000/api/usage/token/ \ -H "Authorization: Bearer $KEY"
import requests
KEY = "<sk-tg-模型调用密钥>"
r = requests.get("http://localhost:8000/api/usage/token/",
headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
d = r.json()["data"]
print(d["count"], d["quota"], d["key_name"], d["key_status"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/usage/token/"))
.header("Authorization", "Bearer <sk-tg-模型调用密钥>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/usage/token/", {
headers: { Authorization: "Bearer <sk-tg-模型调用密钥>" },
});
console.log((await resp.json()).data);
44. Key 调用日志(仅 sk-tg-)
GET/api/log/token/ — 凭模型调用密钥(sk-tg-)自身鉴权分页查询该 Key 自己的调用日志(不接受 tg-sat- / 登录会话,用其他凭证调用返回 403)。登录用户查个人日志请用 /api/log/self。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p / page_size | int (query) | 否 | 分页,默认 1 / 20(page_size 最大 100) |
返回 data.items 字段:id、user_id、request_id、category、model、path、http_status、stream、prompt_tokens / completion_tokens、upstream_cost(上游成本)、credits_charged(实际扣费)、latency_ms、error、created_at 等。
{
"success": true, "message": "",
"data": {
"items": [{"id": 9001, "user_id": 1, "request_id": "chatcmpl-abc",
"category": "llm", "model": "gpt-4o-mini",
"path": "/v1/chat/completions", "http_status": 200, "stream": false,
"prompt_tokens": 120, "completion_tokens": 45,
"upstream_cost": 0.00021, "credits_charged": 0.35,
"latency_ms": 820, "error": null,
"created_at": "2026-09-18T10:00:00"}],
"total": 128, "page": 1, "page_size": 20
}
}
KEY=<sk-tg-模型调用密钥> curl "http://localhost:8000/api/log/token/?p=1&page_size=20" \ -H "Authorization: Bearer $KEY"
import requests
KEY = "<sk-tg-模型调用密钥>"
r = requests.get("http://localhost:8000/api/log/token/",
params={"p": 1, "page_size": 20},
headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
data = r.json()["data"]
for log in data["items"]:
print(log["created_at"], log["model"], log["credits_charged"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/log/token/?p=1&page_size=20"))
.header("Authorization", "Bearer <sk-tg-模型调用密钥>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/log/token/?p=1&page_size=20", {
headers: { Authorization: "Bearer <sk-tg-模型调用密钥>" },
});
for (const log of (await resp.json()).data.items) {
console.log(log.created_at, log.model, log.credits_charged);
}
45. 单个模型元数据(管理员)
GET/api/models/{id} — 按目录条目 ID 查询单个模型的元数据(需要 admin:models:view 权限),与「21. 模型元数据列表」的 items 形状一致。条目不存在返回 404。
{
"success": true, "message": "",
"data": {"id": 12, "model_name": "gpt-4o-mini", "description": "OpenAI 快速对话模型",
"vendor_id": 1, "status": 1, "sort_order": 12, "provider_id": 1,
"public_name": "gpt-4o-mini", "category": ["llm"],
"is_public": true, "context_length": 128000}
}
SAT=<tg-sat-系统访问令牌>; UID=<管理员用户ID> curl http://localhost:8000/api/models/12 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-系统访问令牌>"; UID = "<管理员用户ID>"
r = requests.get("http://localhost:8000/api/models/12",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/models/12"))
.header("Authorization", "Bearer <tg-sat-系统访问令牌>")
.header("New-Api-User", "<管理员用户ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/models/12", {
headers: {
Authorization: "Bearer <tg-sat-系统访问令牌>",
"New-Api-User": "<管理员用户ID>",
},
});
console.log((await resp.json()).data);
46. 供应商搜索与详情(公开)
GET/api/vendors/search 与 GET/api/vendors/{id} — 「23. 价格目录与供应商」中 /api/vendors 的配套端点,均无需认证。平台为单供应商体系:搜索仅在 keyword 命中「可信词元」时返回该供应商(否则空数组);详情仅 id=1 存在,其余返回 404。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
keyword | string (query) | 否 | 搜索关键字(仅 /search) |
id | int (path) | 是 | 供应商 ID,仅 1 有效(仅 /{id}) |
返回 data 字段:id(恒 1)、name(「可信词元」)、description(「平台统一供应商」)、icon、status(恒 enabled)。
{
"success": true, "message": "",
"data": {"id": 1, "name": "可信词元", "description": "平台统一供应商",
"icon": "", "status": "enabled"}
}
curl "http://localhost:8000/api/vendors/search?keyword=可信词元" curl http://localhost:8000/api/vendors/1
import requests
print(requests.get("http://localhost:8000/api/vendors/search",
params={"keyword": "可信词元"}, timeout=30).json()["data"])
print(requests.get("http://localhost:8000/api/vendors/1", timeout=30).json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/vendors/1")).GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const search = await (await fetch("http://localhost:8000/api/vendors/search?keyword=可信词元")).json();
console.log(search.data);
const one = await (await fetch("http://localhost:8000/api/vendors/1")).json();
console.log(one.data);
用户自助 API(tg-usr- 用户令牌)
面向终端用户的自助接口:积分流水对账,以及配置自己 API Key 的模型使用时段。
tg-usr- 用户令牌与模型调用 Key(sk-tg-)、登录会话完全隔离,
把 tg-usr- 令牌交给第三方系统对账也不会泄露模型调用能力。
如何获取:登录控制台 →「我的令牌」→ 新建令牌并勾选权限范围(可随时修改/吊销):
user:finance:read— 财务查询(积分流水)user:keys:config— Key 配置(模型使用时段)
调用方式:Authorization: Bearer tg-usr-...(无需 New-Api-User 头;登录会话 Cookie 亦可直接调用)。
1. 我的积分流水
GET/api/transaction/self — 分页查询自己的积分流水(充值 / 消费 / 授予 / 调整),每笔含变动后余额与计费上下文。这是第三方对账系统最常用的端点:拿 type=consume 的流水与自己的业务日志按 request_id 逐笔核对即可完成对账(见组合调用示例 · 工作流 1)。需要 user:finance:read 权限。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string (query) | 否 | 流水类型:recharge(充值)/ consume(消费)/ grant(管理员授予)/ adjust(调整) |
page | int (query) | 否 | 页码,默认 1 |
page_size | int (query) | 否 | 每页条数,默认 20,最大 100 |
返回 data.items 字段(TransactionOut):id(流水号)、type(类型)、amount(变动积分:消费为负、充值 / 授予为正)、balance_after(变动后余额)、multiplier(该笔计费倍率)、upstream_cost / upstream_cost_cny / exchange_rate(上游成本美元 / 人民币与汇率,消费类有值)、sale_coefficient(售价系数)、request_id(关联调用的请求 ID,充值类为 null)、remark(备注)、created_at(时间)。
{
"success": true, "message": "",
"data": {
"items": [{
"id": 5678, "user_id": 1, "type": "consume", "amount": -0.33,
"balance_after": 999.67, "multiplier": 1.0,
"upstream_cost": 0.000192, "upstream_cost_cny": 0.00138,
"exchange_rate": 7.2, "sale_coefficient": 1.5,
"request_id": "req-7f2c...", "remark": null,
"created_at": "2026-09-18T10:24:31"
}, {
"id": 5001, "user_id": 1, "type": "recharge", "amount": 1000.0,
"balance_after": 1000.0, "multiplier": null,
"upstream_cost": null, "upstream_cost_cny": null,
"exchange_rate": null, "sale_coefficient": null,
"request_id": null, "remark": "在线充值",
"created_at": "2026-09-01T09:00:00"
}],
"total": 233, "page": 1, "page_size": 20
}
}
TOKEN=<tg-usr-用户令牌> # 全部流水 curl "http://localhost:8000/api/transaction/self?page=1" \ -H "Authorization: Bearer $TOKEN" # 仅消费流水(对账用) curl "http://localhost:8000/api/transaction/self?type=consume&page=1" \ -H "Authorization: Bearer $TOKEN"
import requests
TOKEN = "<tg-usr-用户令牌>"
r = requests.get("http://localhost:8000/api/transaction/self",
params={"type": "consume", "page": 1},
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30)
for t in r.json()["data"]["items"]:
print(t["created_at"], t["amount"], t["balance_after"], t["request_id"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/transaction/self?type=consume&page=1"))
.header("Authorization", "Bearer <tg-usr-用户令牌>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch(
"http://localhost:8000/api/transaction/self?type=consume&page=1",
{
headers: { Authorization: "Bearer <tg-usr-用户令牌>" },
},
);
for (const t of (await resp.json()).data.items) {
console.log(t.created_at, t.amount, t.balance_after, t.request_id);
}
2. Key 模型使用时段
约束某个 API Key 只能在指定时间调用某个模型(北京时间;只影响该 Key 的调用时间,不影响额度)。三个端点(鉴权:tg-usr- 令牌需 user:keys:config 权限,或登录会话):
- GET/api/key/{key_id}/model_window — 该 Key 已配置的全部时段规则
- PUT/api/key/{key_id}/model_window — 设置某模型的使用时段,请求体
{"model": "模型名", "windows": [...]}(覆盖式更新) - DELETE/api/key/{key_id}/model_window?model=模型名 — 删除该模型的时段(恢复不限)
时段规则语法(windows 数组,任一命中即可用,空数组 = 全天可用):
{"start":"09:00","end":"18:00"}— 每天时段(支持跨午夜,如 22:00-06:00){"weekdays":[0,1,2,3,4],"start":"09:00:00","end":"18:00:00"}— 按星期(0=周一…6=周日){"date":"2026-10-01","action":"closed"}— 指定日期全天关闭{"date":"2026-10-09","start":"09:00","end":"12:00"}— 指定日期仅此时段开放
{
"success": true, "message": "",
"data": [
{"model": "deepseek-v4-flash",
"windows": [{"start": "09:00", "end": "18:00"}]},
{"model": "gpt-4o-mini",
"windows": [{"weekdays": [0,1,2,3,4], "start": "09:00:00", "end": "18:00:00"},
{"date": "2026-10-01", "action": "closed"}]}
]
}
TOKEN=<tg-usr-用户令牌>
# 查看已配置的时段
curl http://localhost:8000/api/key/123/model_window \
-H "Authorization: Bearer $TOKEN"
# 设置:工作日 09:00-18:00 才能用 deepseek-v4-flash
curl -X PUT http://localhost:8000/api/key/123/model_window \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"model":"deepseek-v4-flash","windows":[{"weekdays":[0,1,2,3,4],"start":"09:00","end":"18:00"}]}'
# 删除时段(恢复不限)
curl -X DELETE "http://localhost:8000/api/key/123/model_window?model=deepseek-v4-flash" \
-H "Authorization: Bearer $TOKEN"
import requests
TOKEN = "<tg-usr-用户令牌>"
H = {"Authorization": f"Bearer {TOKEN}"}
# 查看已配置的时段
r = requests.get("http://localhost:8000/api/key/123/model_window",
headers=H, timeout=30)
print(r.json()["data"])
# 设置时段
r = requests.put("http://localhost:8000/api/key/123/model_window",
headers=H,
json={"model": "deepseek-v4-flash",
"windows": [{"start": "09:00", "end": "18:00"}]},
timeout=30)
print(r.json()["success"]) # True
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"model\":\"deepseek-v4-flash\","
+ "\"windows\":[{\"start\":\"09:00\",\"end\":\"18:00\"}]}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/key/123/model_window"))
.header("Authorization", "Bearer <tg-usr-用户令牌>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-usr-用户令牌>",
"Content-Type": "application/json",
};
// 查看已配置的时段
let resp = await fetch("http://localhost:8000/api/key/123/model_window", {
headers: H,
});
console.log((await resp.json()).data);
// 设置时段
resp = await fetch("http://localhost:8000/api/key/123/model_window", {
method: "PUT",
headers: H,
body: JSON.stringify({
model: "deepseek-v4-flash",
windows: [{ start: "09:00", end: "18:00" }],
}),
});
console.log((await resp.json()).success); // true
3. tg-usr- 令牌的适用范围
tg-usr- 用户令牌是最小权限令牌:它只被上文两组端点接受,其余接口一律不认。把令牌交给第三方系统时,对方能做的只有这两件事。
| 端点 | 所需 scope | 说明 |
|---|---|---|
| GET/api/transaction/self | user:finance:read | 我的积分流水(上文条目 1) |
| GET/PUT/DELETE/api/key/{key_id}/model_window | user:keys:config | Key 的模型使用时段(上文条目 2) |
不接受 tg-usr- 的常见场景(需登录会话 Cookie 或 tg-sat- + New-Api-User 头):
- 令牌管理:
/api/my/tokens、/api/my/token-scopes(创建 / 修改 / 吊销用户令牌本身) - Key 管理:
/api/key的列表 / 创建 / 修改 / 删除等 - 其余
/api/*管理面端点(见上文 NewAPI 兼容管理 API 章节)
同样地,模型调用密钥(sk-tg-)与运维令牌(tg-ops-)调用本组接口一律返回 401——三套令牌完全隔离。
Quick Start
TokenGet is fully compatible with the OpenAI API. Point your base_url to this gateway and use a TokenGet API key.
- 1. Sign up (username/password or WeChat), then create an API key (
sk-tg-...) in the console. - 2. Base URL:
http://<your-server>/v1 - 3. List models by category: GET/v1/models?category=llm. A model can belong to multiple categories (e.g.
["llm","image"]); use?category=llm&category=imageto get the intersection.
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello"}]}'
import requests
# Python users can also use the official openai SDK directly:
# client = OpenAI(base_url="http://localhost:8000/v1", api_key="sk-tg-your-key")
r = requests.post(
"http://localhost:8000/v1/chat/completions",
headers={"Authorization": "Bearer sk-tg-your-key"},
json={
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello"}],
},
timeout=120,
)
data = r.json()
print(data["choices"][0]["message"]["content"])
print(data["usage"]) # prompt_tokens / completion_tokens / total_tokens
// JDK 11+ (java.net.http), no third-party dependencies
var http = java.net.http.HttpClient.newHttpClient();
var body = """
{"model":"gpt-4o-mini",
"messages":[{"role":"user","content":"Hello"}]}
""".strip();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/chat/completions"))
.header("Authorization", "Bearer sk-tg-your-key")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
// Node.js 18+ built-in fetch, no dependencies
const resp = await fetch("http://localhost:8000/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer sk-tg-your-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Hello" }],
}),
});
const data = await resp.json();
console.log(data.choices[0].message.content);
console.log(data.usage); // prompt_tokens / completion_tokens / total_tokens
Authentication
All /v1/* endpoints accept your API key in the header:
Authorization: Bearer sk-tg-your-key
Web management APIs (/api/*) use the sign-in session cookie (tg_session); programmatic calls may use a tg-sat- access token with the New-Api-User header (NewAPI-compatible).
Note: sk-tg- model keys are only accepted on the /v1/* gateway (plus the two self-query endpoints /api/usage/token and /api/log/token); they cannot be used on other /api/* management endpoints.
LLM
POST/v1/chat/completions — streaming (stream:true) and non-streaming.
{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello"}],
"stream": true
}
Also available: POST/v1/completions and POST/v1/embeddings.
Parameters & Limits
model(required): fetch the available list via GET/v1/models?category=llm; the platform routes to the highest-priority active upstream automaticallymessages(required): OpenAI-compatible array;rolesupportssystem/user/assistant/tool;contentsupports plain text or a multimodal array[{"type":"text","text":"..."},{"type":"image_url","image_url":{"url":"..."}}](vision support depends on the model)stream: boolean, defaultfalse; enables SSE streamingtemperature: default1, range 0–2top_p: default1, range 0–1max_tokens/max_completion_tokens: max generated tokens (context limits vary by model)frequency_penalty/presence_penalty: default0, range -2–2stop: stop sequence(s), string or array of stringstools/tool_choice: function calling (passed through when the model supports it)response_format: JSON Schema / JSON mode (passed through when supported)seed: deterministic seed (when the upstream supports it)- Single-message size and total context length are constrained by the chosen model; request body size is limited by nginx (1000MB)
Model List Fields
Each model object returned by GET/v1/models includes, besides id / display_name / category / price_display / sale_price_display:
description: model description, including input parameters, applicable scenarios, model notes, and version differencescapabilities: capability object, e.g.{"vision": true}(legacy field)supports_image: supports image inputsupports_audio: supports audio inputsupports_video: supports video input
Pass multimodal inputs as image_url / audio_url / video_url inside the messages array. If the requested model does not support the corresponding input type, the platform automatically switches to the cheapest available model in the same category that does, and returns the actual model name in the X-Switched-Model response header.
Responses API
POST/v1/responses — native support for the OpenAI Responses API, for clients using the new openai SDK (client.responses.create) or Codex CLI. Requests are converted to chat/completions at the gateway; billing is identical to chat/completions.
curl http://localhost:8000/v1/responses \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{"model":"glm-5.2","input":"Introduce yourself in one sentence"}'
import requests
# Python users can also use the official openai SDK directly:
# resp = client.responses.create(model="glm-5.2", input="Introduce yourself in one sentence")
r = requests.post(
"http://localhost:8000/v1/responses",
headers={"Authorization": "Bearer sk-tg-your-key"},
json={"model": "glm-5.2", "input": "Introduce yourself in one sentence"},
timeout=120,
)
data = r.json()
print(data["output"][0]["content"][0]["text"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"model\":\"glm-5.2\",\"input\":\"Introduce yourself in one sentence\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/responses"))
.header("Authorization", "Bearer sk-tg-your-key")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
// Node.js 18+
const resp = await fetch("http://localhost:8000/v1/responses", {
method: "POST",
headers: {
"Authorization": "Bearer sk-tg-your-key",
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "glm-5.2", input: "Introduce yourself in one sentence" }),
});
const data = await resp.json();
console.log(data.output[0].content[0].text);
Parameters
model(required): same model list as chat/completionsinput(required): a string, or an array of message/tool objects (message/function_call/function_call_output); message content supports plain text or a multimodal array[{"type":"input_text",...},{"type":"input_image","image_url":...}]instructions: equivalent to a system message, prepended automaticallymax_output_tokens→ mapped tomax_tokenstools/tool_choice:function-type tools supported (format converted automatically)temperature/top_p/parallel_tool_calls: passed throughreasoning.effort→ best-effort mapping toreasoning_effort(may error if the upstream rejects it)stream: enables SSE streaming with the Responses API event sequencestore/metadata/include/truncation: ignored
Streaming Event Sequence
With stream:true, the following SSE events are emitted in order (each carries an increasing sequence_number):
response.created— response created (status: in_progress)response.output_item.added— new output item (message / function_call / reasoning)response.content_part.added— message content part started (output_text)response.output_text.delta— text deltaresponse.reasoning_summary_text.delta— reasoning delta for thinking models (if any)response.function_call_arguments.delta— tool-call arguments delta (if any)response.output_text.done/response.content_part.done/response.output_item.done— progressive closingresponse.completed— final event with full output andusage(input_tokens/output_tokens)
Limitations
- Stateless forwarding:
previous_response_id/conversation(include full history ininput) andbackgroundtasks are not supported - Built-in tools (
web_search/file_search/computer_use/code_interpreter/mcp) andinput_fileare not supported call_idreplay across tool-call turns is lossy; for deep agent scenarios prefer/v1/chat/completions
Completions & Embeddings
curl http://localhost:8000/v1/completions \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","prompt":"Introduce TokenGet in one sentence","max_tokens":64}'
curl http://localhost:8000/v1/embeddings \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{"model":"text-embedding-3-small","input":"Hello world"}'
Image Generation
The unified image endpoint is POST/v1/cap/image/{path}. Just specify the model; the gateway routes to the correct upstream automatically (SnapGen native image endpoints, or OpenRouter multi-modal LLM chat.completions).
- POST/v1/cap/image/generate_image — text-to-image / image-to-image / image editing (form-data)
- GET/v1/cap/image/history/{uuid} — query a single result (done when
status == 2) - GET/v1/cap/image/histories — history list
- GET/v1/cap/tasks/{task_id} — query async task status and result (use when the call returns a
task_id)
Model Options
Connected image models include native image models (nano-banana-pro, nano-banana-2, nano-banana-2-lite, grok-image, gpt-image-2) and multi-modal LLM image models (grok, gemini, inkling, etc.). For the full, real-time list, call GET/v1/models?category=image; supports multi-category intersection queries such as ?category=image&category=llm. Models not in this list cannot be used through this capability route.
Return forms are normalized by the gateway: models that already provide an async task identifier return {"uuid": "..."} and require polling /v1/cap/image/history/{uuid}; models whose upstream returns synchronously are automatically wrapped into an async task, returning {"task_id": "..."} that you query with GET /v1/cap/tasks/{task_id}. Downstream callers can simply branch on whichever identifier is returned.
Text-to-Image
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-your-key" \ -F "prompt=a cute cat on a windowsill" \ -F "model=nano-banana-2-lite" \ -F "aspect_ratio=1:1"
import requests
# Submit a text-to-image task via form-data;
# the response is {"uuid": "..."} — poll the history endpoint with it
r = requests.post(
"http://localhost:8000/v1/cap/image/generate_image",
headers={"Authorization": "Bearer sk-tg-your-key"},
data={"prompt": "a cute cat on a windowsill",
"model": "nano-banana-2-lite",
"aspect_ratio": "1:1"},
timeout=120,
)
print(r.json()) # {"uuid": "9f8e7d6c-xxxx-..."}
// JDK 11+: multipart/form-data requires a hand-built boundary
var http = java.net.http.HttpClient.newHttpClient();
var boundary = "----TokenGet" + System.currentTimeMillis();
var body = java.net.http.HttpRequest.BodyPublishers.ofString(
"--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n"
+ "a cute cat on a windowsill\r\n"
+ "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"model\"\r\n\r\n"
+ "nano-banana-2-lite\r\n"
+ "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"aspect_ratio\"\r\n\r\n"
+ "1:1\r\n"
+ "--" + boundary + "--\r\n");
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/cap/image/generate_image"))
.header("Authorization", "Bearer sk-tg-your-key")
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(body).build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // {"uuid": "..."}
// Node.js 18+: build the multipart request with FormData
const fd = new FormData();
fd.append("prompt", "a cute cat on a windowsill");
fd.append("model", "nano-banana-2-lite");
fd.append("aspect_ratio", "1:1");
const resp = await fetch("http://localhost:8000/v1/cap/image/generate_image", {
method: "POST",
headers: { Authorization: "Bearer sk-tg-your-key" },
body: fd,
});
console.log(await resp.json()); // {"uuid": "..."}
Image-to-Image / Image Editing
Add the image parameter with a reference image URL for image-to-image or style transfer:
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-your-key" \ -F "prompt=turn this into a watercolor painting" \ -F "model=nano-banana-2-lite" \ -F "image=https://example.com/photo.jpg" \ -F "aspect_ratio=1:1"
Upload Local Images Directly (multipart/form-data)
Reference images can also be uploaded as local files in the same request — use @/path/to/file as the image field value. The platform imposes no size limit of its own (only the gateway request-body limit applies):
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-your-key" \ -F "prompt=turn this photo into a watercolor painting" \ -F "model=nano-banana-2-lite" \ -F "image=@/Users/me/photo.jpg"
URLs and local files can be mixed; repeat the image field for multiple references.
Multi-Image Reference
form-data allows repeated fields with the same name — pass multiple image fields for multi-image reference (support depends on the model):
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-your-key" \ -F "prompt=put the person from image 1 into the scene of image 2, keep the style of image 3" \ -F "model=nano-banana-2-lite" \ -F "image=https://example.com/person.jpg" \ -F "image=https://example.com/scene.jpg" \ -F "image=https://example.com/style.jpg"
Parameters
prompt(required): description / edit instructionmodel(required): generation model; current option isnano-banana-2-lite, full real-time list via GET/v1/models?category=imageaspect_ratio: e.g.1:1/16:9/9:16image: reference image — public URL or@/local/fileupload, repeatable (count limited by the selected model)image_width/image_height: output size (optional)quality:hdfor higher quality (optional)seed: pin the random seed (optional)
Polling the Result
curl http://localhost:8000/v1/cap/image/history/9f8e7d6c-xxxx \ -H "Authorization: Bearer sk-tg-your-key"
import time
import requests
# status == 2 means done — take the result image URL from the response;
# poll every 5–10 seconds
uuid = "9f8e7d6c-xxxx"
while True:
r = requests.get(
f"http://localhost:8000/v1/cap/image/history/{uuid}",
headers={"Authorization": "Bearer sk-tg-your-key"},
timeout=30,
)
data = r.json()
print(data.get("status"), data.get("progress", ""))
if data.get("status") == 2: # done
print(data) # the result image URL is in the response
break
if data.get("status") == 3: # failed
raise RuntimeError(data)
time.sleep(5)
// JDK 11+: poll every 5 seconds; status == 2 means done
var http = java.net.http.HttpClient.newHttpClient();
var uuid = "9f8e7d6c-xxxx";
while (true) {
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/cap/image/history/" + uuid))
.header("Authorization", "Bearer sk-tg-your-key")
.GET().build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
if (resp.body().contains("\"status\":2") || resp.body().contains("\"status\": 2")) {
break; // done — the result image URL is in the response
}
Thread.sleep(5000);
}
// Node.js 18+: poll every 5 seconds; status === 2 means done
const uuid = "9f8e7d6c-xxxx";
while (true) {
const resp = await fetch(
`http://localhost:8000/v1/cap/image/history/${uuid}`,
{ headers: { Authorization: "Bearer sk-tg-your-key" } },
);
const data = await resp.json();
console.log(data.status, data.progress ?? "");
if (data.status === 2) { console.log(data); break; } // done
if (data.status === 3) throw new Error(JSON.stringify(data)); // failed
await new Promise((r) => setTimeout(r, 5000));
}
status == 2 means done — take the result image URL from the response. Poll every 5–10 seconds.
Async Task Wrapping
The gateway automatically wraps synchronous upstream image responses into async tasks at the capability-route layer. Downstream callers keep using the same endpoint and parameters; when the upstream would otherwise return immediately, the endpoint returns a task identifier {"task_id": "..."}:
curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer sk-tg-your-key" \ -F "prompt=a cute cat" \ -F "model=your-image-model"
Use GET/v1/cap/tasks/{task_id} to query the result:
curl http://localhost:8000/v1/cap/tasks/task-xxx \ -H "Authorization: Bearer sk-tg-your-key"
Example response:
{
"task_id": "task-...",
"status": "completed",
"category": "image",
"result_urls": ["https://..."],
"upstream_response": { ... },
"error": null,
"created_at": "...",
"completed_at": "..."
}
When status is completed, image URLs are in the result_urls array. If the upstream call fails, status becomes failed and the error field contains the reason. This wrapping is handled automatically by TokenGet; the downstream interface path and calling convention remain unchanged.
OpenAI-Compatible Image Generation
In addition to capability routes, the gateway exposes the OpenAI-compatible endpoint POST/v1/images/generations. Send JSON parameters and the gateway routes to the correct upstream automatically; when the upstream uses multipart/form-data, the gateway transparently converts the request body in the background.
curl http://localhost:8000/v1/images/generations \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-1",
"prompt": "a cute cat on a windowsill",
"aspect_ratio": "1:1"
}'
You can also call it directly with multipart/form-data, using the same fields as the upstream native API:
curl http://localhost:8000/v1/images/generations \ -H "Authorization: Bearer sk-tg-your-key" \ -F "model=grok-imagine-2.0" \ -F "prompt=a futuristic city at night" \ -F "aspect_ratio=16:9" \ -F "image=@/Users/me/ref.jpg"
Available models are returned in real time by GET/v1/models?category=image, such as gpt-image-1 / gpt-image-1.5 / grok-imagine-2.0 / nana-banana / nana-banana-pro, etc. The gateway normalizes the response form: if it returns {"uuid":"..."}, poll /v1/cap/image/history/{uuid}; if it returns {"task_id":"..."}, call GET /v1/cap/tasks/{task_id} for the result.
Video Generation
The unified video endpoint is POST/v1/cap/video/{path}. Just specify the model; the gateway routes to the correct upstream automatically (SnapGen / MiniMax native video endpoints, or OpenRouter multi-modal LLM chat.completions).
- POST/v1/cap/video/v2/video_generation — submit a video task
- GET/v1/cap/video/v2/query/video_generation/{task_id} — query task status
Model Options
Connected video models include:
- SnapGen native video:
flux-3/veo-3 - MiniMax native video:
MiniMax-H3(full features, 768P/2K, 4–15s) /MiniMax-H3-Max(faster, 480P/768P, 5–15s) - OpenRouter multi-modal LLMs: grok, gemini, inkling, etc.
For the full, real-time list, call GET/v1/models?category=video; supports multi-category intersection queries such as ?category=video&category=llm. Models not in this list cannot be used through this capability route.
Return formats differ by upstream: native video models return a task ID and require polling; OpenRouter multi-modal LLMs return a chat.completion with the video URL in the assistant message.
1. Text-to-Video
content contains text only; ratio is required (cannot be adaptive). Camera-movement commands like [push in] are supported in the prompt:
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [{"type": "text", "text": "An orange cat stretches on the windowsill, sunlight pours in, [push in]"}],
"resolution": "768P",
"duration": 6,
"ratio": "16:9"
}'
import requests
# Submit the task — it returns a task_id, then poll for the result
r = requests.post(
"http://localhost:8000/v1/cap/video/v2/video_generation",
headers={"Authorization": "Bearer sk-tg-your-key"},
json={
"model": "MiniMax-H3",
"content": [{"type": "text", "text": "An orange cat stretches on the windowsill, sunlight pours in, [push in]"}],
"resolution": "768P",
"duration": 6,
"ratio": "16:9",
},
timeout=120,
)
print(r.json()) # {"task_id": "424010985738629", ...}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = """
{"model":"MiniMax-H3",
"content":[{"type":"text","text":"An orange cat stretches on the windowsill, sunlight pours in, [push in]"}],
"resolution":"768P","duration":6,"ratio":"16:9"}
""".strip().replaceAll("\n\\s*", "");
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/cap/video/v2/video_generation"))
.header("Authorization", "Bearer sk-tg-your-key")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // {"task_id": "..."}
// Node.js 18+
const resp = await fetch(
"http://localhost:8000/v1/cap/video/v2/video_generation",
{
method: "POST",
headers: {
Authorization: "Bearer sk-tg-your-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "MiniMax-H3",
content: [{ type: "text", text: "An orange cat stretches on the windowsill, sunlight pours in, [push in]" }],
resolution: "768P",
duration: 6,
ratio: "16:9",
}),
},
);
console.log(await resp.json()); // {"task_id": "..."}
2. Image-to-Video (First Frame)
The image becomes the opening frame; ratio follows the image (always adaptive, can be omitted):
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "An orange cat stretches on the windowsill, [push in]"},
{"type": "image_url", "image_url": {"url": "https://example.com/cat.jpg"}, "role": "first_frame"}
],
"resolution": "768P",
"duration": 6
}'
3. First + Last Frame Video
Specify both the start and end frames (first_frame + last_frame); the model generates the in-between transition:
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "The cat leaps gracefully from the windowsill onto the sofa"},
{"type": "image_url", "image_url": {"url": "https://example.com/start.jpg"}, "role": "first_frame"},
{"type": "image_url", "image_url": {"url": "https://example.com/end.jpg"}, "role": "last_frame"}
],
"resolution": "768P",
"duration": 6
}'
4. Multi-Reference Video (image + audio + video inputs)
Multi-modal reference generation: reference images set subject/style (up to 9), reference videos set motion (up to 3), reference audios set voice/sound (up to 3) — combine as needed:
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "An astronaut walks on the moon under a vast starry sky"},
{"type": "image_url", "image_url": {"url": "https://example.com/astronaut.jpg"}, "role": "reference_image"},
{"type": "image_url", "image_url": {"url": "https://example.com/style.jpg"}, "role": "reference_image"},
{"type": "video_url", "video_url": {"url": "https://example.com/walk.mp4"}, "role": "reference_video"},
{"type": "audio_url", "audio_url": {"url": "https://example.com/space.mp3"}, "role": "reference_audio"}
],
"resolution": "2K",
"duration": 8,
"ratio": "16:9"
}'
5. Video Editing
Pass the video to edit as reference_video and describe the changes in text (restyle, swap background, change motion, add effects). Optionally add reference_image for a new look and reference_audio for the soundtrack:
curl http://localhost:8000/v1/cap/video/v2/video_generation \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"content": [
{"type": "text", "text": "Turn the footage into cyberpunk style, neon lights, rainy night"},
{"type": "video_url", "video_url": {"url": "https://example.com/source.mp4"}, "role": "reference_video"}
],
"resolution": "768P",
"duration": 6
}'
6. Upload Local Files Directly (multipart/form-data)
Besides JSON, the video endpoint also accepts multipart/form-data: form fields and files go in the same request; use @/local/path for file values and the platform stores them and hands public URLs to the upstream. No platform-imposed file size limit (only the gateway request-body limit applies). Form fields map to JSON content roles as follows:
promptortext→ text itemfirst_frame/last_frame→ first / last frame imageimage(orreference_image) → reference image, repeatablevideo(orreference_video) → reference video, repeatableaudio(orreference_audio) → reference audio, repeatable- All other fields (
model/resolution/duration/ratio…) become top-level parameters as-is
Image-to-video with a local first-frame image:
curl http://localhost:8000/v1/cap/video/v2/video_generation \ -H "Authorization: Bearer sk-tg-your-key" \ -F "model=MiniMax-H3" \ -F "prompt=An orange cat stretches on the windowsill, [push in]" \ -F "first_frame=@/Users/me/cat.jpg" \ -F "resolution=768P" \ -F "duration=6"
Multi-reference video with mixed local files and URLs:
curl http://localhost:8000/v1/cap/video/v2/video_generation \ -H "Authorization: Bearer sk-tg-your-key" \ -F "model=MiniMax-H3" \ -F "prompt=An astronaut walks on the moon under a vast starry sky" \ -F "image=@/Users/me/astronaut.jpg" \ -F "image=https://example.com/style.jpg" \ -F "video=@/Users/me/walk.mp4" \ -F "audio=@/Users/me/space.mp3" \ -F "resolution=2K" \ -F "duration=8" \ -F "ratio=16:9"
7. Polling the Task
curl http://localhost:8000/v1/cap/video/v2/query/video_generation/424010985738629 \ -H "Authorization: Bearer sk-tg-your-key"
import time
import requests
# Poll every ~10 seconds; when task.status == "succeeded",
# task.content.url is the downloadable video URL
task_id = "424010985738629"
while True:
r = requests.get(
f"http://localhost:8000/v1/cap/video/v2/query/video_generation/{task_id}",
headers={"Authorization": "Bearer sk-tg-your-key"},
timeout=30,
)
task = r.json()["task"]
print(task["status"], task.get("progress", ""))
if task["status"] == "succeeded":
print(task["content"]["url"]) # the video download URL
break
time.sleep(10)
// JDK 11+: poll every 10 seconds; when task.status == "succeeded",
// the final video is at task.content.url
var http = java.net.http.HttpClient.newHttpClient();
var taskId = "424010985738629";
while (true) {
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create(
"http://localhost:8000/v1/cap/video/v2/query/video_generation/" + taskId))
.header("Authorization", "Bearer sk-tg-your-key")
.GET().build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
if (resp.body().contains("\"status\":\"succeed\"")
|| resp.body().contains("\"status\": \"succeed\"")) {
break; // parse task.content.url for the video URL
}
Thread.sleep(10000);
}
// Node.js 18+: poll every 10 seconds
const taskId = "424010985738629";
while (true) {
const resp = await fetch(
`http://localhost:8000/v1/cap/video/v2/query/video_generation/${taskId}`,
{ headers: { Authorization: "Bearer sk-tg-your-key" } },
);
const { task } = await resp.json();
console.log(task.status, task.progress ?? "");
if (task.status === "succeed" || task.status === "succeeded") {
console.log(task.content.url); // the video download URL
break;
}
await new Promise((r) => setTimeout(r, 10000));
}
Poll every ~10 seconds. When task.status becomes succeeded, task.content.url is the downloadable video.
Parameter & Limit Cheat Sheet
model(required): video generation model; see "Model Options" above for current choices, full real-time list via GET/v1/models?category=videocontent[].type:text/image_url/video_url/audio_urlcontent[].role:first_frame/last_frame/reference_image/reference_video/reference_audioresolution:480P/768P/2K(model dependent);duration: secondsratio: required for text-to-video (21:9/16:9/4:3/1:1/3:4/9:16); follows the image for image-to-video; optional for multi-reference (default adaptive)- Reference limits are configured per model and enforced by the platform (exceeding them returns 400) — e.g. MiniMax-H3: ≤ 9 images, ≤ 3 videos (each 2–15s, total ≤ 15s), ≤ 3 audios; local uploads are bounded only by the gateway request-body limit (prefer multipart upload or public URLs for large files, not Base64)
- Image-to-video and multi-reference are mutually exclusive: once any
reference_*appears,first_frame/last_framecannot be used - Media requirements: images JPG/PNG/WEBP ≤ 30MB (256–5760px, aspect 0.4–2.5); videos MP4/MOV (H.264/H.265) ≤ 50MB; audio WAV/MP3 ≤ 15MB
callback_url: optional webhook notified on task status changes
OpenAI-Compatible Video Generation
The gateway also exposes the OpenAI-compatible endpoint POST/v1/videos. Send JSON parameters and the gateway routes to the correct upstream automatically; when the upstream uses multipart/form-data (e.g. SnapGen), the gateway transparently converts the request body in the background.
curl http://localhost:8000/v1/videos \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2-0-mini",
"prompt": "an astronaut walking on the moon, cinematic lighting",
"ratio": "16:9",
"duration": 6
}'
You can also call it directly with multipart/form-data:
curl http://localhost:8000/v1/videos \ -H "Authorization: Bearer sk-tg-your-key" \ -F "model=grok-imagine-video" \ -F "prompt=a robot dancing in neon lights" \ -F "ratio=9:16" \ -F "duration=5" \ -F "first_frame=@/Users/me/start.jpg"
Available models are returned in real time by GET/v1/models?category=video, such as seedance-2-0-mini / seedance-2.0-fast / grok-imagine-video, etc. The response follows the upstream native format (SnapGen returns a task ID; poll the corresponding history endpoint for the final video URL).
GoAnyAI Supplier & Channels
GoAnyAI is integrated as an independent upstream supplier. Each GoAnyAI model is split into multiple public model names according to its marketplace channel, using the format {model-name}--{channel-name}. You can also use the base model name to route to the default (lowest-price) channel.
- Base model names:
claude-fable-5/gpt-image-2/grok-imagine-video, etc. - Specific channels:
claude-fable-5--AWS-Claude-分组/gpt-image-2--特价--image-1k, etc. - Prices are fetched from the GoAnyAI marketplace, stored as CNY cost, and displayed to downstream clients after applying the platform sale coefficient.
Call it like any other model, for example with a specific image channel:
curl http://localhost:8000/v1/images/generations \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2--特价--image-1k",
"prompt": "a cute cat on a windowsill"
}'
Or with a specific chat channel:
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-4-5--kiro-Claude",
"messages": [{"role": "user", "content": "Hello"}]
}'
Admins can find goanyai in the provider list and click "Sync GoAnyAI" to pull the latest models, channels, and prices.
Speech Synthesis
Speech is exposed through the unified capability route POST/v1/cap/speech/{upstream-path}. Requests are routed automatically by model (MiniMax series) or model_id (ElevenLabs series) prefix. Responses are passed through directly as binary audio or JSON-wrapped audio.
1. MiniMax TTS
POST/v1/cap/speech/v1/t2a_v2 — returns JSON; audio is hex-encoded in data.audio, character usage is in extra_info.usage_characters.
curl http://localhost:8000/v1/cap/speech/v1/t2a_v2 \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-d '{
"model": "speech-2.8-hd",
"text": "Hello, welcome to TokenGet.",
"voice_setting": {
"voice_id": "female-shaonv",
"speed": 1,
"vol": 1,
"pitch": 0,
"emotion": "happy"
},
"audio_setting": {
"sample_rate": 32000,
"bitrate": 128000,
"format": "mp3",
"channel": 1
}
}'
import requests
r = requests.post(
"http://localhost:8000/v1/cap/speech/v1/t2a_v2",
headers={"Authorization": "Bearer sk-tg-your-key"},
json={
"model": "speech-2.8-hd",
"text": "Hello, welcome to TokenGet.",
"voice_setting": {
"voice_id": "female-shaonv",
"speed": 1,
"vol": 1,
"pitch": 0,
"emotion": "happy",
},
"audio_setting": {
"sample_rate": 32000,
"bitrate": 128000,
"format": "mp3",
"channel": 1,
},
},
timeout=120,
)
data = r.json()
audio_hex = data["data"]["audio"] # audio content (hex encoded)
usage = data["extra_info"]["usage_characters"] # billed character count
with open("speech.mp3", "wb") as f:
f.write(bytes.fromhex(audio_hex)) # decode and save as mp3
// JDK 11+: data.audio is hex encoded — decode it and write to a file
var http = java.net.http.HttpClient.newHttpClient();
var body = """
{"model":"speech-2.8-hd","text":"Hello, welcome to TokenGet.",
"voice_setting":{"voice_id":"female-shaonv","speed":1,"vol":1,"pitch":0,"emotion":"happy"},
"audio_setting":{"sample_rate":32000,"bitrate":128000,"format":"mp3","channel":1}}
""".strip().replaceAll("\n\\s*", "");
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/v1/cap/speech/v1/t2a_v2"))
.header("Authorization", "Bearer sk-tg-your-key")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // parse data.audio (hex), then write an mp3 file
// Node.js 18+
import { writeFile } from "node:fs/promises";
const resp = await fetch("http://localhost:8000/v1/cap/speech/v1/t2a_v2", {
method: "POST",
headers: {
Authorization: "Bearer sk-tg-your-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "speech-2.8-hd",
text: "Hello, welcome to TokenGet.",
voice_setting: {
voice_id: "female-shaonv",
speed: 1,
vol: 1,
pitch: 0,
emotion: "happy",
},
audio_setting: { sample_rate: 32000, bitrate: 128000, format: "mp3", channel: 1 },
}),
});
const data = await resp.json();
await writeFile("speech.mp3", Buffer.from(data.data.audio, "hex"));
MiniMax Model Options
speech-2.8-hd/speech-2.8-turbospeech-2.6-hd/speech-2.6-turbospeech-02-hd/speech-02-turbospeech-01-hd/speech-01-turbo
MiniMax Parameters
model(required): choose from the list abovetext(required): text to synthesize; max length depends on the model (commonly ≤ 10,000 chars)voice_setting(required):voice_id: voice ID, e.g.female-shaonv,male-qnqingse(refer to MiniMax official docs for the latest list)speed: default1, recommended 0.5–2.0vol: default1, recommended 0–10pitch: default0, recommended -12–12emotion: e.g.happy/sad/neutral(optional, when the model supports it)
audio_setting(required):sample_rate:16000/24000/32000/44100/48000bitrate: e.g.64000/128000/192000format:mp3/wav/pcm/oggchannel:1(mono) or2(stereo)
- Billing: credits = request text char count × 1.5; failed requests are free
2. ElevenLabs TTS
POST/v1/cap/speech/v1/text-to-speech/{voice_id} — returns audio/mpeg binary directly.
curl http://localhost:8000/v1/cap/speech/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb \
-H "Authorization: Bearer sk-tg-your-key" \
-H "Content-Type: application/json" \
-o speech.mp3 \
-d '{
"model_id": "eleven_multilingual_v2",
"text": "Hello, welcome to TokenGet.",
"voice_settings": {
"stability": 0.5,
"similarity_boost": 0.75,
"style": 0.0,
"use_speaker_boost": true
}
}'
ElevenLabs Model Options
eleven_v3(highest quality, multilingual)eleven_multilingual_v2/eleven_multilingual_v1eleven_flash_v2_5/eleven_flash_v2(low latency)eleven_turbo_v2_5/eleven_turbo_v2eleven_monolingual_v1
ElevenLabs Parameters
model_id(required): choose from the list abovetext(required): text to synthesizevoice_id(path parameter): target voice ID, e.g.JBFqnCBsd6RMkjVDRZzb; retrieve available voices from the upstream/v1/voicesendpointvoice_settings(optional):stability: 0–1similarity_boost: 0–1style: 0–1 (supported by some models)use_speaker_boost: boolean
pronunciation_dictionary_locators/seed/previous_text/next_text: optional advanced parameters (passed through when supported)- Billing: credits = request text char count × 1.5; failed requests are free
3. Other ElevenLabs Capabilities
Accessed directly through /v1/cap/speech/v1/..., parameters match the official API:
- POST/v1/cap/speech/v1/text-to-voice/create-previews — generate preview voices from a description (
textmust be ≥ 100 chars) - POST/v1/cap/speech/v1/text-to-voice/create-voice-from-preview — save a preview as a permanent voice
- POST/v1/cap/speech/v1/sound-generation — sound-effect generation
- POST/v1/cap/speech/v1/voice_clone / POST/v1/cap/speech/v1/files/upload / POST/v1/cap/speech/v1/voices/add — voice-cloning related endpoints (follow the upstream API)
General Notes
- Only text-to-speech (TTS) is supported; speech-to-text (STT) and music generation are not yet connected
- Model options are returned live by GET/v1/models?category=speech; unlisted model/model_id values will fail to route
- Request body size is limited by nginx (1000MB)
Files API
Files are kept for 2 days by default (adjustable per user by admins). These endpoints use the sign-in session cookie; programmatic calls may use a tg-sat- access token.
- POST/api/file/ — multipart upload, returns
file_id - GET/api/file/ — list files
- GET/api/file/{file_id} — download
- DELETE/api/file/{file_id} — delete
TOKEN=<tg-sat-access-token>; UID=<your user ID> # Upload (returns file_id) curl http://localhost:8000/api/file/ -H "Authorization: Bearer $TOKEN" -H "New-Api-User: $UID" -F "file=@photo.jpg" # List curl http://localhost:8000/api/file/ -H "Authorization: Bearer $TOKEN" -H "New-Api-User: $UID" # Download curl http://localhost:8000/api/file/<file_id> -H "Authorization: Bearer $TOKEN" -H "New-Api-User: $UID" -o photo.jpg # Delete curl -X DELETE http://localhost:8000/api/file/<file_id> -H "Authorization: Bearer $TOKEN" -H "New-Api-User: $UID"
import requests
BASE = "http://localhost:8000/api/file"
# Bearer-token calls to /api/* must also carry the New-Api-User (user ID) header
HDRS = {"Authorization": "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>"}
# Upload: the multipart field name must be "file"; returns file_id (kept 2 days by default)
with open("photo.jpg", "rb") as f:
r = requests.post(BASE + "/", headers=HDRS,
files={"file": ("photo.jpg", f, "image/jpeg")},
data={"purpose": "general"}, timeout=120)
file_id = r.json()["data"]["file_id"]
# List
r = requests.get(BASE + "/", headers=HDRS, params={"page": 1, "page_size": 20})
print(r.json()["data"]["items"])
# Download
r = requests.get(f"{BASE}/{file_id}", headers=HDRS, timeout=120)
with open("photo_copy.jpg", "wb") as f:
f.write(r.content)
# Delete
r = requests.delete(f"{BASE}/{file_id}", headers=HDRS, timeout=30)
print(r.json()) # {"success": true, "data": {"ok": true}}
// JDK 11+: upload requires a hand-built multipart/form-data body
var http = java.net.http.HttpClient.newHttpClient();
var token = "<tg-sat-access-token>";
var uid = "<your user ID>";
var boundary = "----TokenGet" + System.currentTimeMillis();
var fileBytes = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("photo.jpg"));
var body = java.net.http.HttpRequest.BodyPublishers.ofByteArrays(java.util.List.of(
("--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; "
+ "filename=\"photo.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n").getBytes(),
fileBytes,
("\r\n--" + boundary + "--\r\n").getBytes()));
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/file/"))
.header("Authorization", "Bearer " + token)
.header("New-Api-User", uid)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(body).build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // data.file_id is the ID used for download/delete
// Node.js 18+: upload + download
import { readFile, writeFile } from "node:fs/promises";
const BASE = "http://localhost:8000/api/file";
// Bearer-token calls to /api/* must also carry the New-Api-User (user ID) header
const HDRS = {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
};
// Upload
const fd = new FormData();
fd.append("file", new Blob([await readFile("photo.jpg")]), "photo.jpg");
fd.append("purpose", "general");
let resp = await fetch(BASE + "/", { method: "POST", headers: HDRS, body: fd });
const { data: { file_id } } = await resp.json();
// List
resp = await fetch(BASE + "/", { headers: HDRS });
console.log((await resp.json()).data.items);
// Download
resp = await fetch(`${BASE}/${file_id}`, { headers: HDRS });
await writeFile("photo_copy.jpg", Buffer.from(await resp.arrayBuffer()));
// Delete
resp = await fetch(`${BASE}/${file_id}`, { method: "DELETE", headers: HDRS });
console.log(await resp.json()); // {"success": true, "data": {"ok": true}}
Parameters
file(upload, required): multipart file field; no format restriction, size limited only by nginx (1000MB)purpose(optional): file-purpose tag, e.g.general/fine-tune/relay; defaults togeneralpage/page_size(list, optional): pagination, default page 1, 20 per page, max 100- Files are kept for 2 days by default; admins can adjust
file_retention_daysvia PUT/api/user/ (body with id)
Billing
- Pay-as-you-go: each successful call deducts credits based on actual usage; details are available in the console (Credit Transactions / API Call Logs).
- Failed requests (HTTP ≥ 400) are free.
- Requests are rejected with HTTP 402 when your balance is ≤ 0; top up to resume immediately.
- New accounts receive free trial credits on sign-up.
Records
- GET/api/user/self — current balance (with account info)
- GET/api/transaction/self?type=consume&page=1 — credit transactions
- GET/api/log/self?category=llm&p=1 — API call logs (kept 2 days by default)
SAT=<tg-sat-access-token>; UID=<your user ID>; UTOKEN=<tg-usr- user token> curl http://localhost:8000/api/user/self -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" curl "http://localhost:8000/api/transaction/self?type=consume&page=1" -H "Authorization: Bearer $UTOKEN" curl "http://localhost:8000/api/log/self?category=llm&p=1" -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
Combined Call Workflows
The endpoints above are documented separately, but real integrations usually call them in a fixed order: the return value of one step (a token, a task_id, a file_id, …) feeds the next. Below are the four most common combined workflows; each step is numbered 1/2/3 in the comments, and each sample is ready to run as-is.
Workflow 1: Create a Key → Call the LLM → Verify the Billing (Caller Onboarding)
Ordering: ① First create an API key with your system access token (tg-sat-); the data.key field of the response is the model-calling key (sk-tg-). ② Call /v1/chat/completions with this new key. ③ Only after a successful call is the billing record generated — then query your credit transactions (tg-usr- user token, requires user:finance:read) and your balance to reconcile. If you query transactions before making the call, this deduction will not be there yet.
SAT=<tg-sat-access-token>; UID=<your user ID>; UTOKEN=<tg-usr- user token>
# 1. Create an API-key token (data.key in the response is the sk-tg- key)
curl -X POST http://localhost:8000/api/token/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"name":"my-key","remain_quota":500000}'
# 2. Call the LLM with the returned sk-tg- key
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-tg-key-from-previous-step" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello"}]}'
# 3. Query credit transactions to verify this deduction (type=consume)
curl "http://localhost:8000/api/transaction/self?type=consume&page=1" \
-H "Authorization: Bearer $UTOKEN"
# 4. Confirm the balance (data.balance is the post-deduction value)
curl http://localhost:8000/api/user/self \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"
UID = "<your user ID>"
UTOKEN = "<tg-usr- user token>"
ADMIN = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
# 1. Create an API-key token; data.key in the response is the sk-tg- model key
r = requests.post("http://localhost:8000/api/token/", headers=ADMIN,
json={"name": "my-key", "remain_quota": 500000}, timeout=30)
api_key = r.json()["data"]["key"] # sk-tg-...
print("new key:", api_key)
# 2. Call the LLM with the new key
r = requests.post("http://localhost:8000/v1/chat/completions",
headers={"Authorization": f"Bearer {api_key}"},
json={"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello"}]},
timeout=120)
print(r.json()["choices"][0]["message"]["content"])
# 3. Query credit transactions to verify the deduction (the record exists now)
r = requests.get("http://localhost:8000/api/transaction/self",
headers={"Authorization": f"Bearer {UTOKEN}"},
params={"type": "consume", "page": 1, "page_size": 5}, timeout=30)
for tx in r.json()["data"]["items"]:
print(tx["id"], tx["amount"], tx["balance_after"], tx["created_at"])
# 4. Confirm the balance
r = requests.get("http://localhost:8000/api/user/self", headers=ADMIN, timeout=30)
print("balance:", r.json()["data"]["balance"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var sat = "<tg-sat-access-token>";
var uid = "<your user ID>";
// 1. Create an API-key token; parse data.key to get the sk-tg- key
var body = "{\"name\":\"my-key\",\"remain_quota\":500000}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/"))
.header("Authorization", "Bearer " + sat)
.header("New-Api-User", uid)
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
var resp = http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body()); // take data.key
// 2. Call the LLM with the returned sk-tg- key
// header Authorization: Bearer <sk-tg-...>, POST /v1/chat/completions
// 3. Query credit transactions to verify the deduction (tg-usr- token)
// GET /api/transaction/self?type=consume&page=1
// 4. Query the balance (tg-sat- + New-Api-User)
// GET /api/user/self — parse data.balance
// Node.js 18+
const SAT = "<tg-sat-access-token>";
const UID = "<your user ID>";
const UTOKEN = "<tg-usr- user token>";
const ADMIN = { Authorization: `Bearer ${SAT}`, "New-Api-User": UID };
// 1. Create an API-key token; data.key in the response is the sk-tg- model key
let resp = await fetch("http://localhost:8000/api/token/", {
method: "POST",
headers: { ...ADMIN, "Content-Type": "application/json" },
body: JSON.stringify({ name: "my-key", remain_quota: 500000 }),
});
const api_key = (await resp.json()).data.key; // sk-tg-...
console.log("new key:", api_key);
// 2. Call the LLM with the new key
resp = await fetch("http://localhost:8000/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${api_key}`, "Content-Type": "application/json" },
body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: "Hello" }] }),
});
console.log((await resp.json()).choices[0].message.content);
// 3. Query credit transactions to verify the deduction (the record exists now)
resp = await fetch("http://localhost:8000/api/transaction/self?type=consume&page=1", {
headers: { Authorization: `Bearer ${UTOKEN}` },
});
for (const tx of (await resp.json()).data.items)
console.log(tx.id, tx.amount, tx.balance_after, tx.created_at);
// 4. Confirm the balance
resp = await fetch("http://localhost:8000/api/user/self", { headers: ADMIN });
console.log("balance:", (await resp.json()).data.balance);
Workflow 2: Async Image Generation — Submit → Poll → Download
Ordering: ① Submit the image task and take the uuid (native image models) or task_id (gateway-wrapped task) from the response. ② Poll the matching query endpoint until status == 2 (uuid path) or status == "completed" (task_id path). ③ Only after the task finishes does the result image URL exist — download it then. Poll every 5–10 seconds; the task will not fail because you are not polling.
KEY=<sk-tg-your-key>
# 1. Submit a text-to-image task and record the returned uuid
curl http://localhost:8000/v1/cap/image/generate_image \
-H "Authorization: Bearer $KEY" \
-F "prompt=a cute cat on a windowsill" \
-F "model=nano-banana-2-lite" \
-F "aspect_ratio=1:1"
# Returns {"uuid":"9f8e7d6c-xxxx"}; if it returns {"task_id":"task-xxx"}, then in
# step 2 query GET /v1/cap/tasks/{task_id} instead — done state is "completed",
# result URLs are in result_urls
# 2. Poll the task (every 5–10 seconds; status == 2 means done)
curl http://localhost:8000/v1/cap/image/history/9f8e7d6c-xxxx \
-H "Authorization: Bearer $KEY"
# 3. Download the result image (URL comes from the step-2 response)
curl -o result.jpg "<result-image-url-from-step-2>"
import time
import requests
KEY = "<sk-tg-your-key>"
HDRS = {"Authorization": f"Bearer {KEY}"}
# 1. Submit a text-to-image task and take the uuid
r = requests.post("http://localhost:8000/v1/cap/image/generate_image",
headers=HDRS,
data={"prompt": "a cute cat on a windowsill",
"model": "nano-banana-2-lite",
"aspect_ratio": "1:1"},
timeout=120)
data = r.json()
uuid = data.get("uuid")
task_id = data.get("task_id") # one of the two: gateway-wrapped tasks use /v1/cap/tasks
print("task id:", uuid or task_id)
# 2. Poll until done (uuid path: status == 2)
while True:
if uuid:
r = requests.get(
f"http://localhost:8000/v1/cap/image/history/{uuid}",
headers=HDRS, timeout=30)
st, result = r.json().get("status"), r.json()
done = st == 2
else:
r = requests.get(
f"http://localhost:8000/v1/cap/tasks/{task_id}",
headers=HDRS, timeout=30)
result = r.json()
st, done = result.get("status"), result.get("status") == "completed"
print("status:", st)
if done:
break
time.sleep(5)
# 3. Take the result image URL and download it
url = result.get("image_url") or (result.get("result_urls") or [""])[0]
print("result image:", url)
img = requests.get(url, timeout=120)
with open("result.jpg", "wb") as f:
f.write(img.content)
// JDK 11+: submit → poll → download (core skeleton)
var http = java.net.http.HttpClient.newHttpClient();
var key = "<sk-tg-your-key>";
// 1. Submit the task (form-data omitted — see the Java sample in the Image
// Generation chapter) and take the uuid from the response
// 2. Poll GET /v1/cap/image/history/{uuid} every 5 seconds;
// a response containing "status": 2 means done; for the task_id path query
// GET /v1/cap/tasks/{task_id} instead — "status": "completed", results
// are in result_urls
// 3. Download with the result URL:
var dl = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("<result-image-url>")).GET().build();
var bytes = http.send(dl, java.net.http.HttpResponse.BodyHandlers.ofByteArray());
java.nio.file.Files.write(java.nio.file.Path.of("result.jpg"), bytes.body());
// Node.js 18+
import { writeFile } from "node:fs/promises";
const KEY = "<sk-tg-your-key>";
const HDRS = { Authorization: `Bearer ${KEY}` };
// 1. Submit a text-to-image task and take the uuid
const fd = new FormData();
fd.append("prompt", "a cute cat on a windowsill");
fd.append("model", "nano-banana-2-lite");
fd.append("aspect_ratio", "1:1");
let resp = await fetch("http://localhost:8000/v1/cap/image/generate_image", {
method: "POST", headers: HDRS, body: fd,
});
const { uuid, task_id } = await resp.json();
console.log("task id:", uuid ?? task_id);
// 2. Poll until done (uuid path: status === 2)
let result;
while (true) {
const url = uuid
? `http://localhost:8000/v1/cap/image/history/${uuid}`
: `http://localhost:8000/v1/cap/tasks/${task_id}`;
resp = await fetch(url, { headers: HDRS });
result = await resp.json();
const st = result.status;
console.log("status:", st);
if (st === 2 || st === "completed") break;
await new Promise((r) => setTimeout(r, 5000));
}
// 3. Take the result image URL and download it
const imageUrl = result.image_url ?? result.result_urls?.[0];
console.log("result image:", imageUrl);
resp = await fetch(imageUrl);
await writeFile("result.jpg", Buffer.from(await resp.arrayBuffer()));
Workflow 3: Upload a Local Image → Image-to-Image (Files API + Image Generation)
Ordering: ① First upload the local image to the Files API (purpose=relay) and take the file_id. ② Build the public URL http://<server>/api/file/public/{file_id} (files with purpose=relay are fetchable by upstreams through this public endpoint). ③ Pass this URL as the image parameter of an image-to-image call. Uploading the local file directly with multipart @file in one step also works (see the Image Generation chapter); this workflow suits the "archive first, reference later" pattern or reusing the same image. Files are kept for 2 days by default; the URL expires after that.
SAT=<tg-sat-access-token>; UID=<your user ID>; KEY=<sk-tg-your-key> # 1. Upload a local image (purpose=relay; returns file_id) curl http://localhost:8000/api/file/ \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \ -F "file=@photo.jpg" -F "purpose=relay" # 2. Build the public URL (purpose=relay files need no auth to access) # http://localhost:8000/api/file/public/<file_id> # 3. Start an image-to-image call with that URL curl http://localhost:8000/v1/cap/image/generate_image \ -H "Authorization: Bearer $KEY" \ -F "prompt=turn this into a watercolor painting" \ -F "model=nano-banana-2-lite" \ -F "image=http://localhost:8000/api/file/public/<file_id>"
import requests
SAT = "<tg-sat-access-token>"
UID = "<your user ID>"
KEY = "<sk-tg-your-key>"
ADMIN = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
# 1. Upload a local image (purpose=relay; returns file_id)
with open("photo.jpg", "rb") as f:
r = requests.post("http://localhost:8000/api/file/", headers=ADMIN,
files={"file": ("photo.jpg", f, "image/jpeg")},
data={"purpose": "relay"}, timeout=120)
file_id = r.json()["data"]["file_id"]
print("file_id:", file_id)
# 2. Build the public URL (purpose=relay files can be fetched by upstreams without auth)
public_url = f"http://localhost:8000/api/file/public/{file_id}"
# 3. Start an image-to-image call with that URL
r = requests.post("http://localhost:8000/v1/cap/image/generate_image",
headers={"Authorization": f"Bearer {KEY}"},
data={"prompt": "turn this into a watercolor painting",
"model": "nano-banana-2-lite",
"image": public_url},
timeout=120)
print(r.json()) # {"uuid": "..."} → poll as in Workflow 2
// JDK 11+ (multipart upload details: see the Java sample in the Files API chapter)
var http = java.net.http.HttpClient.newHttpClient();
// 1. POST /api/file/ to upload the local image (purpose=relay); take data.file_id
// 2. Public URL = http://localhost:8000/api/file/public/<file_id>
var publicUrl = "http://localhost:8000/api/file/public/<file_id>";
// 3. Start an image-to-image call with that URL (form-data, field image = publicUrl);
// the response is {"uuid": "..."} → poll as in Workflow 2
System.out.println("image=" + publicUrl);
// Node.js 18+
import { readFile } from "node:fs/promises";
const SAT = "<tg-sat-access-token>";
const UID = "<your user ID>";
const KEY = "<sk-tg-your-key>";
const ADMIN = { Authorization: `Bearer ${SAT}`, "New-Api-User": UID };
// 1. Upload a local image (purpose=relay; returns file_id)
const fd = new FormData();
fd.append("file", new Blob([await readFile("photo.jpg")]), "photo.jpg");
fd.append("purpose", "relay");
let resp = await fetch("http://localhost:8000/api/file/", {
method: "POST", headers: ADMIN, body: fd,
});
const file_id = (await resp.json()).data.file_id;
console.log("file_id:", file_id);
// 2. Build the public URL (purpose=relay files can be fetched by upstreams without auth)
const publicUrl = `http://localhost:8000/api/file/public/${file_id}`;
// 3. Start an image-to-image call with that URL
const gen = new FormData();
gen.append("prompt", "turn this into a watercolor painting");
gen.append("model", "nano-banana-2-lite");
gen.append("image", publicUrl);
resp = await fetch("http://localhost:8000/v1/cap/image/generate_image", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}` },
body: gen,
});
console.log(await resp.json()); // {"uuid": "..."} → poll as in Workflow 2
NewAPI-Compatible Management API (tg-sat-)
The management plane follows the NewAPI (formerly one-api) API style, so NewAPI-ecosystem clients and scripts can integrate directly.
Authentication as above (tg-sat- token + New-Api-User header, or the session cookie);
every response uses the envelope {"success": true, "message": "", "data": ...},
and paginated lists return data as {"items": [...], "total": N, "page": 1, "page_size": 20}.
Conventions: in the examples below, SAT is your system access token (tg-sat-...) and UID is your user ID. When calling with a Bearer token you must also send the New-Api-User header and it must match the token's owner, otherwise the API returns 401; browser cookie sessions do not need this header. Endpoints marked "admin" require administrator permissions (403 otherwise).
1. System Status (Public)
GET/api/status — returns the site version, system name and the current service start time. No authentication required; commonly used as a connectivity probe (health check) before integration. Equivalent to the NewAPI endpoint of the same name.
Response data fields: version (backend version), start_time (Unix timestamp in seconds when the service started), system_name (site name, e.g. "TokenGet").
{
"success": true, "message": "",
"data": {"version": "1.0.0", "start_time": 1789722965, "system_name": "TokenGet"}
}
curl http://localhost:8000/api/status
import requests
r = requests.get("http://localhost:8000/api/status", timeout=30)
print(r.json()["data"]) # {"version": "1.0.0", "start_time": ..., "system_name": ...}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/status")).GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const r = await fetch("http://localhost:8000/api/status");
console.log((await r.json()).data);
2. Login (Session + Access Token)
POST/api/user/login — username/password login; one request does two things: ① it sets the tg_session session cookie (HttpOnly, 7-day sliding validity, for browser calls to /api/*); ② it returns the system access token data.access_token (tg-sat- prefix, auto-generated on first login), which pure API clients can use together with the New-Api-User header to access the management endpoints. The response also carries the full user object in data.user, so the frontend needs no second request.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | string | Yes | Username |
password | string | Yes | Password |
Response data fields: access_token (the system access token, i.e. SAT in the examples below), user (the user object; fields as in "3. Current User"). On failure: HTTP 401 (wrong password) or 403 (account disabled).
{
"success": true, "message": "",
"data": {
"access_token": "tg-sat-xxxxxxxxxxxxxxxx",
"user": {"id": 1, "username": "alice", "nickname": "Alice", "balance": 1000.0, "...": "..."}
}
}
curl -X POST http://localhost:8000/api/user/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"your-password"}'
import requests
r = requests.post("http://localhost:8000/api/user/login",
json={"username": "alice", "password": "your-password"}, timeout=30)
data = r.json()["data"]
sat = data["access_token"] # tg-sat-..., the Bearer token for the management APIs
uid = data["user"]["id"] # the value of the New-Api-User header
print(sat, uid)
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"username\":\"alice\",\"password\":\"your-password\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/login"))
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "alice", password: "your-password" }),
});
const { data } = await resp.json();
const sat = data.access_token; // tg-sat-...
const uid = data.user.id; // the value of the New-Api-User header
3. Current User (with Balance)
GET/api/user/self — returns the full account profile of the currently authenticated user; the most-used endpoint for checking your balance and confirming token ownership. Callable with a cookie session or with tg-sat- + New-Api-User.
Response data fields:
| Field | Type | Description |
|---|---|---|
id / username / nickname | int / string | User ID (the value of the New-Api-User header), username, nickname |
email / email_verified / wechat_bound | string / bool | Bound email, whether verified, whether WeChat is bound |
balance | float | Credit balance (each successful model call deducts credits by actual usage) |
rate_multiplier | float | Personal billing multiplier (deprecated: no longer used in billing, kept only for compatibility) |
group_id / group_name / group_rate | int / string / float | The user's group and its rate (null when ungrouped; rate then 1.0) |
is_admin / has_admin_access | bool | Whether super admin / whether the account has admin access |
access_token | string | The current system access token in plain text (can be viewed again anytime) |
{
"success": true, "message": "",
"data": {"id": 1, "username": "alice", "nickname": "Alice",
"balance": 1000.0,
"group_id": 2, "group_name": "vip", "group_rate": 0.8, "...": "..."}
}
SAT=<tg-sat-access-token>; UID=<your user ID> curl http://localhost:8000/api/user/self \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.get("http://localhost:8000/api/user/self",
headers={"Authorization": f"Bearer {SAT}",
"New-Api-User": UID}, timeout=30)
data = r.json()["data"]
print(data["balance"], data["group_rate"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/self"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/self", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
},
});
console.log((await resp.json()).data);
4. Update Nickname / Password
PUT/api/user/self — updates the current user's profile. Nicknames are globally unique (duplicates return 409); changing the password requires the original password in original_password (a wrong one returns 401); a successful password change sends a notification to the bound email.
| Parameter | Type | Required | Description |
|---|---|---|---|
nickname | string | No | New nickname (1-64 chars, globally unique) |
original_password | string | Required when changing the password | The original password |
password | string | No | New password (6-128 chars) |
Response data: the updated full user object (same shape as "3. Current User").
SAT=<tg-sat-access-token>; UID=<your user ID>
curl -X PUT http://localhost:8000/api/user/self \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"nickname":"new-nickname","original_password":"old-password","password":"new-password"}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.put("http://localhost:8000/api/user/self",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"nickname": "new-nickname"}, timeout=30)
print(r.json())
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"nickname\":\"new-nickname\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/self"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/self", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ nickname: "new-nickname" }),
});
console.log(await resp.json());
5. My Available Models
GET/api/user/models — returns the current account's model pool (an array of available model names, NewAPI shape), already filtered by group/authorization scope. Use it to check whether a model is within your callable range before calling it with an sk-tg- key.
| Parameter | Type | Required | Description |
|---|---|---|---|
detail | int (query) | No | 0 (default) returns an array of model-name strings; 1 returns an array of rich objects (with brands / manufacturers / is_open / available, used by the Console) |
{
"success": true, "message": "",
"data": ["deepseek-v4-flash", "gpt-4o-mini", "glm-5.2"]
}
SAT=<tg-sat-access-token>; UID=<your user ID> curl http://localhost:8000/api/user/models \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.get("http://localhost:8000/api/user/models",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"]) # ["deepseek-v4-flash", ...]
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/models"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/models", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
},
});
console.log((await resp.json()).data);
6. Reset the System Access Token
GET/api/user/token — generates a new system access token (tg-sat-); the old token is invalidated immediately. Use it when a token leaks or for periodic rotation. Works with a browser login session (a GET in form, but semantically a reset).
Response data fields: access_token (the new token in plain text; store it safely — it can be viewed again anytime via /api/user/self).
{
"success": true, "message": "",
"data": {"access_token": "tg-sat-xxxxxxxxxxxxxxxx"}
}
SAT=<tg-sat-access-token>; UID=<your user ID> curl http://localhost:8000/api/user/token \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.get("http://localhost:8000/api/user/token",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"]["access_token"]) # the new token (the old one is now invalid)
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/token"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/token", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
},
});
console.log((await resp.json()).data.access_token); // the new token
7. Token List (My API Keys)
GET/api/token/ — paginated list of the current user's tokens. A "token" here is the NewAPI view of a model-calling key (ApiKey, sk-tg- prefix): use it to programmatically inventory your keys, their remaining quota and cumulative consumption, without signing in to the Console.
| Parameter | Type | Required | Description |
|---|---|---|---|
p | int (query) | No | Page number, default 1 |
page_size | int (query) | No | Page size, default 20, max 100 |
Response data.items fields (NewAPI token shape): id (token ID), name, key (the key in plain text, sk-tg- prefix), status (1 enabled / 2 disabled), created_time / accessed_time (creation / last-used Unix timestamps, 0 when never used), expired_time (always -1; our tokens never expire), remain_quota (remaining quota in credits, 0 when unlimited), unlimited_quota (whether the quota is unlimited), used_quota (cumulative consumption in credits, aggregated from call logs), model_limits_enabled (whether the model whitelist is enabled), model_limits (whitelisted model names, comma-separated string).
{
"success": true, "message": "",
"data": {
"items": [{
"id": 3, "user_id": 1, "name": "prod-key", "key": "sk-tg-9f8e7d6c...",
"status": 1, "created_time": 1789723000, "accessed_time": 1789800123,
"expired_time": -1, "remain_quota": 500, "unlimited_quota": false,
"used_quota": 128, "model_limits_enabled": true,
"model_limits": "gpt-4o-mini,glm-5.2"
}],
"total": 1, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-access-token>; UID=<your user ID> curl "http://localhost:8000/api/token/?p=1&page_size=10" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.get("http://localhost:8000/api/token/",
params={"p": 1, "page_size": 10},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for t in r.json()["data"]["items"]:
print(t["id"], t["name"], t["status"], t["remain_quota"], t["used_quota"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/?p=1&page_size=10"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/token/?p=1&page_size=10", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
},
});
for (const t of (await resp.json()).data.items) {
console.log(t.id, t.name, t.status, t.remain_quota, t.used_quota);
}
8. Create a Token (Generate an sk-tg- Key)
POST/api/token/ — creates a new token, stored underneath as a model-calling key (ApiKey). The returned data.key is the complete sk-tg-... key and can be used directly for /v1/* model calls — step one of the "create a key → call → reconcile" combined flow (see Workflows · Workflow 1). A quota cap and a model whitelist can be set at the same time.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string (body) | No | Token name, default default |
remain_quota | number (body) | No | Quota cap (credits); once the key's cumulative charges reach the cap, it can no longer call |
unlimited_quota | bool (body) | No | true = unlimited quota (default); false together with remain_quota enables the cap |
model_limits_enabled | bool (body) | No | Whether to enable the model whitelist |
model_limits | string (body) | No | Whitelisted model names, comma-separated (e.g. "gpt-4o-mini,glm-5.2"); the models must be in your available pool, otherwise 400 |
expired_time | number (body) | No | Compatibility field: our tokens never expire, the value is ignored (always returns -1) |
Response data fields: the created token object, same shape as a single item of "7. Token List"; mainly take key (the sk-tg- plain text) and id (for later update/delete).
{
"success": true, "message": "",
"data": {
"id": 4, "user_id": 1, "name": "ci-pipeline", "key": "sk-tg-1a2b3c4d...",
"status": 1, "created_time": 1789810000, "accessed_time": 0,
"expired_time": -1, "remain_quota": 500, "unlimited_quota": false,
"used_quota": 0, "model_limits_enabled": true,
"model_limits": "gpt-4o-mini"
}
}
SAT=<tg-sat-access-token>; UID=<your user ID>
curl -X POST http://localhost:8000/api/token/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"name":"ci-pipeline","remain_quota":500,"unlimited_quota":false,"model_limits_enabled":true,"model_limits":"gpt-4o-mini"}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.post("http://localhost:8000/api/token/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"name": "ci-pipeline", "remain_quota": 500,
"unlimited_quota": False,
"model_limits_enabled": True,
"model_limits": "gpt-4o-mini"},
timeout=30)
key = r.json()["data"]["key"] # sk-tg-..., ready for /v1/* calls
print(key)
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"name\":\"ci-pipeline\",\"remain_quota\":500,"
+ "\"unlimited_quota\":false,\"model_limits_enabled\":true,"
+ "\"model_limits\":\"gpt-4o-mini\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/token/", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "ci-pipeline",
remain_quota: 500,
unlimited_quota: false,
model_limits_enabled: true,
model_limits: "gpt-4o-mini",
}),
});
const key = (await resp.json()).data.key; // sk-tg-...
console.log(key);
9. Update a Token / Batch Enable-Disable
PUT/api/token/ — two modes: single update (body contains id; can change the name, quota and model whitelist) and batch enable-disable (body is {"ids": [...], "enable": true/false}). Quota semantics: passing unlimited_quota: true (or omitting remain_quota) clears the cap and restores unlimited; remain_quota is a full reset (not an increment) — to raise a key from 100 remaining of 500 up to 800, just pass 800.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | int (body) | Yes* | Token ID for single update (*batch mode uses ids instead) |
name | string (body) | No | New name (empty string ignored) |
remain_quota | number (body) | No | New quota cap (full reset) |
unlimited_quota | bool (body) | No | true clears the quota cap |
model_limits_enabled + model_limits | bool / string (body) | No | Reset the model whitelist; enabled=false clears it |
ids | int[] (body) | Yes* | Batch mode: array of token IDs, together with enable |
enable | bool (body) | No | true enable (status=1) / false disable (status=2) |
Response data fields: single update returns the updated token object (same shape as section 7); batch enable-disable returns {"ok": true, "modified": N} (N = number actually changed; only your own tokens are affected).
{
"success": true, "message": "",
"data": {"ok": true, "modified": 3}
}
SAT=<tg-sat-access-token>; UID=<your user ID>
# Single update: reset token 4's quota to 800
curl -X PUT http://localhost:8000/api/token/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":4,"remain_quota":800}'
# Batch disable tokens 4, 5, 6
curl -X PUT http://localhost:8000/api/token/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"ids":[4,5,6],"enable":false}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
# Single update
r1 = requests.put("http://localhost:8000/api/token/", headers=H,
json={"id": 4, "remain_quota": 800}, timeout=30)
print(r1.json()["data"]["remain_quota"]) # 800
# Batch disable
r2 = requests.put("http://localhost:8000/api/token/", headers=H,
json={"ids": [4, 5, 6], "enable": False}, timeout=30)
print(r2.json()["data"]) # {"ok": true, "modified": 3}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":4,\"remain_quota\":800}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
// Single update
let resp = await fetch("http://localhost:8000/api/token/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 4, remain_quota: 800 }),
});
console.log((await resp.json()).data.remain_quota); // 800
// Batch disable
resp = await fetch("http://localhost:8000/api/token/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ ids: [4, 5, 6], enable: false }),
});
console.log((await resp.json()).data); // { ok: true, modified: 3 }
10. Get / Delete / Search Tokens
The remaining four operations on the same resource:
- GET/api/token/{id} — get a single token by ID (same shape as an item of section 7; 404 for tokens you don't own)
- DELETE/api/token/{id} — physically delete the token (call logs only store the ID reference, history is unaffected); returns
{"ok": true} - POST/api/token/batch — batch delete, body is
{"ids": [1, 2, 3]}; non-existent IDs are silently skipped, returns{"ok": true, "deleted": N}(N = number actually deleted) - GET/api/token/search?keyword=xx — search by name keyword (substring match), returns a bare array (no pagination envelope), max 100 entries
{
"success": true, "message": "",
"data": {"ok": true, "deleted": 2}
}
SAT=<tg-sat-access-token>; UID=<your user ID>
# Get one
curl http://localhost:8000/api/token/4 -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
# Delete one
curl -X DELETE http://localhost:8000/api/token/4 -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
# Batch delete
curl -X POST http://localhost:8000/api/token/batch \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" -d '{"ids":[5,6]}'
# Search by name
curl "http://localhost:8000/api/token/search?keyword=ci" \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
base = "http://localhost:8000/api/token"
# Get one
print(requests.get(f"{base}/4", headers=H, timeout=30).json()["data"]["name"])
# Batch delete
r = requests.post(f"{base}/batch", headers=H,
json={"ids": [5, 6]}, timeout=30)
print(r.json()["data"]) # {"ok": true, "deleted": 2}
# Search by name
r = requests.get(f"{base}/search", headers=H,
params={"keyword": "ci"}, timeout=30)
print([t["name"] for t in r.json()["data"]])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
// Get one
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/4"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
// Batch delete
var body = "{\"ids\":[5,6]}";
var req2 = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/token/batch"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req2,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
};
const base = "http://localhost:8000/api/token";
// Get one
let resp = await fetch(`${base}/4`, { headers: H });
console.log((await resp.json()).data.name);
// Batch delete
resp = await fetch(`${base}/batch`, {
method: "POST",
headers: { ...H, "Content-Type": "application/json" },
body: JSON.stringify({ ids: [5, 6] }),
});
console.log((await resp.json()).data); // { ok: true, deleted: 2 }
// Search by name
resp = await fetch(`${base}/search?keyword=ci`, { headers: H });
console.log((await resp.json()).data.map((t) => t.name));
11. My Call Logs
GET/api/log/self — paginated query of your own model call logs (with billing details); the core endpoint for post-call reconciliation (see Workflows · Workflow 1). Each row corresponds to one /v1/* request and contains the actual charge, multipliers and latency of that call. Companion endpoint GET/api/log/self/search?keyword=xx searches by model name / path / request_id / error message (returns a bare array, max 100 entries).
| Parameter | Type | Required | Description |
|---|---|---|---|
category | string (query) | No | Filter by category: llm / image / video / speech |
p | int (query) | No | Page number, default 1 |
page_size | int (query) | No | Page size, default 20, max 100 |
Response data.items fields: id, request_id (the request ID of this call, matching the response header — use it as the reconciliation key), category / model / path (category / model / path), http_status, stream (whether streaming), prompt_tokens / completion_tokens (input / output token counts), credits_charged (credits actually charged for this call), rate_multiplier / sale_coefficient (billing multiplier / sale coefficient), upstream_cost / upstream_cost_cny / exchange_rate (upstream cost in USD / CNY and the exchange rate), latency_ms / ttft_ms (total latency / time to first token), error (error message, null on success), created_at.
{
"success": true, "message": "",
"data": {
"items": [{
"id": 9012, "user_id": 1, "request_id": "req-7f2c...", "category": "llm",
"model": "gpt-4o-mini", "path": "/v1/chat/completions",
"http_status": 200, "stream": false,
"prompt_tokens": 128, "completion_tokens": 256,
"upstream_cost": 0.000192, "upstream_cost_cny": 0.00138, "exchange_rate": 7.2,
"rate_multiplier": 1.0, "sale_coefficient": 1.5,
"credits_charged": 0.33, "latency_ms": 842, "ttft_ms": 311,
"error": null, "created_at": "2026-09-18T10:24:31"
}],
"total": 57, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-access-token>; UID=<your user ID> curl "http://localhost:8000/api/log/self?category=llm&p=1&page_size=10" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # Keyword search (by model name / request_id / error message) curl "http://localhost:8000/api/log/self/search?keyword=gpt-4o" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.get("http://localhost:8000/api/log/self",
params={"category": "llm", "p": 1, "page_size": 10},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for log in r.json()["data"]["items"]:
print(log["created_at"], log["model"], log["credits_charged"],
log["latency_ms"], log["request_id"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/log/self?category=llm&p=1&page_size=10"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch(
"http://localhost:8000/api/log/self?category=llm&p=1&page_size=10",
{
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
},
},
);
for (const log of (await resp.json()).data.items) {
console.log(log.created_at, log.model, log.credits_charged, log.request_id);
}
12. My Usage Stats
GET/api/log/self/stat — your aggregate usage; three numbers cover the common monitoring scenarios: cumulative charges, request rate and token rate over the last minute. Suitable for lightweight patrols (sample once per minute into a monitoring dashboard, no need to pull full logs).
Response data fields: quota (cumulative credits charged, float), rpm (Requests Per Minute over the last 1 min), tpm (input+output Tokens Per Minute over the last 1 min).
{
"success": true, "message": "",
"data": {"quota": 128.5, "rpm": 3, "tpm": 2140}
}
SAT=<tg-sat-access-token>; UID=<your user ID> curl http://localhost:8000/api/log/self/stat \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.get("http://localhost:8000/api/log/self/stat",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"]) # {"quota": 128.5, "rpm": 3, "tpm": 2140}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/log/self/stat"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/log/self/stat", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
},
});
console.log((await resp.json()).data); // { quota: 128.5, rpm: 3, tpm: 2140 }
13. My Dashboard Data
GET/api/data/self — the three essentials of your personal dashboard: current balance, cumulative spend and the number of available models. These are the numbers shown on the Console home page; programmatically it fits the pre-flight check of "verify the balance is sufficient before startup".
Response data fields: model_count (number of models currently available to you), quota (account balance, credits, float), used_quota (cumulative credits spent, float).
{
"success": true, "message": "",
"data": {"model_count": 46, "quota": 1000.0, "used_quota": 128.5}
}
SAT=<tg-sat-access-token>; UID=<your user ID> curl http://localhost:8000/api/data/self \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.get("http://localhost:8000/api/data/self",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
data = r.json()["data"]
if data["quota"] < 10: # low-balance warning
print("Low balance:", data["quota"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/data/self"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/data/self", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
},
});
const data = (await resp.json()).data;
if (data.quota < 10) console.log("Low balance:", data.quota);
14. Site-wide Call Logs (Admin)
GET/api/log/ — admin query of site-wide call logs, filterable by user, category, provider, HTTP status and date range. Companion endpoints: GET/api/log/search?keyword=xx (keyword search, bare array, max 100 entries) and GET/api/log/stat (site-wide summary: quota cumulative charges, rpm / tpm rates over the last minute). Requires the admin:calls:view permission.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | int (query) | No | Filter by user |
category | string (query) | No | llm / image / video / speech |
provider_id | int (query) | No | Filter by upstream provider |
http_status / http_status_group | int / string (query) | No | Filter by status code / status group (e.g. 4xx, 5xx) |
start_date / end_date | date (query) | No | Date range, format YYYY-MM-DD |
page / page_size | int (query) | No | Pagination, default 1 / 20 (page_size max 100) |
Response data.items fields: the same CallLogOut shape as 11. My Call Logs, plus the site-wide perspective user_id.
{
"success": true, "message": "",
"data": {
"items": [{
"id": 9013, "user_id": 42, "request_id": "req-a31f...", "category": "image",
"model": "seedream-4.0", "path": "/v1/cap/image/generate_image",
"http_status": 200, "stream": false, "prompt_tokens": 0, "completion_tokens": 0,
"upstream_cost": 0.03, "upstream_cost_cny": 0.216, "exchange_rate": 7.2,
"rate_multiplier": 1.0, "sale_coefficient": 1.4,
"credits_charged": 4.2, "latency_ms": 5230, "ttft_ms": null,
"error": null, "created_at": "2026-09-18T10:31:07"
}],
"total": 1532, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-access-token>; UID=<admin user ID> # site-wide 5xx errors since today curl "http://localhost:8000/api/log/?http_status_group=5xx&start_date=2026-09-18&page=1" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # site-wide summary curl http://localhost:8000/api/log/stat \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/log/",
params={"user_id": 42, "category": "llm",
"start_date": "2026-09-18", "page": 1},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
data = r.json()["data"]
print("total:", data["total"])
for log in data["items"]:
print(log["created_at"], log["user_id"], log["model"], log["credits_charged"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/log/?user_id=42&category=llm&page=1"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch(
"http://localhost:8000/api/log/?user_id=42&category=llm&page=1",
{
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
},
);
const data = (await resp.json()).data;
console.log("total:", data.total);
for (const log of data.items) {
console.log(log.created_at, log.user_id, log.model, log.credits_charged);
}
15. Site Dashboard / User Groups (Admin)
GET/api/data/ — admin dashboard data (requires admin:stats:view): registered users, total calls, cumulative credits consumed / granted, today's calls and active providers. GET/api/group/ — the user group list (NewAPI shape: symbol = group name, ratio = group rate), requires admin:groups:view or admin:users:edit.
/api/data/ response data fields: total_users (user count), total_calls (total call count), total_credits_consumed / total_credits_granted (cumulative credits consumed / granted), calls_today (calls today), active_providers (number of active providers).
{
"success": true, "message": "",
"data": {"total_users": 128, "total_calls": 45678, "total_credits_consumed": 9123.4,
"total_credits_granted": 50000.0, "calls_today": 356, "active_providers": 12}
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl http://localhost:8000/api/data/ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # user group list (symbol = group name, ratio = rate) curl http://localhost:8000/api/group/ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
stats = requests.get("http://localhost:8000/api/data/", headers=H,
timeout=30).json()["data"]
print(stats["calls_today"], "calls today,", stats["active_providers"], "providers")
groups = requests.get("http://localhost:8000/api/group/", headers=H,
timeout=30).json()["data"]
for g in groups:
print(g["id"], g["symbol"], "x", g["ratio"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/data/"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
};
const stats = (await (await fetch("http://localhost:8000/api/data/", { headers: H })).json()).data;
console.log(stats.calls_today, "calls today,", stats.active_providers, "providers");
const groups = (await (await fetch("http://localhost:8000/api/group/", { headers: H })).json()).data;
for (const g of groups) console.log(g.id, g.symbol, "x", g.ratio);
16. Options Read / Write (Admin)
GET/api/option/ — read site-wide options (an array of key-value pairs; requires admin:stats:view or admin:config:edit; sensitive items such as SMTP passwords are not returned). PUT/api/option/ — batch update options (requires admin:config:edit): the body accepts either a single object {"key": "...", "value": "..."} or an array [{"key": "...", "value": "..."}], and returns the updated key-value pairs (including updated_at).
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string (body) | Yes | Option key, e.g. usd_to_cny (USD/CNY exchange rate), rate_limit_per_key (per-key requests per second, 0 = unlimited) |
value | string (body) | Yes | Option value (always string form; numbers are converted to strings as well) |
{
"success": true, "message": "",
"data": [
{"key": "usd_to_cny", "value": "7.2"},
{"key": "rate_limit_per_key", "value": "200"}
]
}
SAT=<tg-sat-access-token>; UID=<admin user ID>
# read all options
curl http://localhost:8000/api/option/ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
# update the exchange rate (single-object form)
curl -X PUT http://localhost:8000/api/option/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"key":"usd_to_cny","value":"7.1"}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
opts = requests.get("http://localhost:8000/api/option/", headers=H,
timeout=30).json()["data"]
print({o["key"]: o["value"] for o in opts})
r = requests.put("http://localhost:8000/api/option/", headers=H,
json=[{"key": "usd_to_cny", "value": "7.1"}], # array batch form
timeout=30)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"key\":\"usd_to_cny\",\"value\":\"7.1\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/option/"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
"Content-Type": "application/json",
};
// update an option
let resp = await fetch("http://localhost:8000/api/option/", {
method: "PUT",
headers: H,
body: JSON.stringify({ key: "usd_to_cny", value: "7.1" }),
});
console.log((await resp.json()).data);
// read all
resp = await fetch("http://localhost:8000/api/option/", { headers: H });
const opts = (await resp.json()).data;
console.log(Object.fromEntries(opts.map((o) => [o.key, o.value])));
17. Channel List & Details (Admin)
GET/api/channel/ — paginated channel list. A "channel" on this platform is equivalent to an upstream provider (one NewAPI channel = one OpenAI-compatible upstream + a set of keys). GET/api/channel/{id} — single channel details (including plaintext keys); GET/api/channel/search?keyword=xx — search by name / base_url / model name (bare array, max 100 entries). The list requires admin:providers:view.
| Parameter | Type | Required | Description |
|---|---|---|---|
p / page_size | int (query) | No | Pagination, default 1 / 20 (page_size max 100) |
status | int (query) | No | 1 enabled / 2 disabled |
name | string (query) | No | Filter by channel name substring |
Response data.items fields (NewAPI Channel shape): id, type (always 1, unified as the OpenAI-compatible type), status (1 enabled / 2 disabled), name, weight / priority (scheduling weight), base_url (upstream address), key (the keys, multi-line merged: first line = the provider's main key, remaining lines = sub-keys), models (active model names of this channel, comma separated), group (always default), balance / balance_updated_time (upstream balance and fetch time), created_time; used_quota / model_mapping etc. are compatibility placeholders (usage analysis goes through the native endpoints).
{
"success": true, "message": "",
"data": {
"items": [{
"id": 1, "type": 1, "status": 1, "name": "openai-relay", "weight": 100,
"created_time": 1789600000, "test_time": 0, "response_time": 0,
"base_url": "https://api.upstream.example/v1",
"key": "sk-up-main-xxx\nsk-up-sub-yyy",
"models": "gpt-4o,gpt-4o-mini", "group": "default",
"used_quota": 0, "model_mapping": "", "priority": 100,
"auto_ban": 1, "other_info": "", "balance": 96.5,
"balance_updated_time": 1789790000, "tag": "", "setting": "",
"param_override": ""
}],
"total": 12, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl "http://localhost:8000/api/channel/?p=1&page_size=10" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # single channel details curl http://localhost:8000/api/channel/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/channel/",
params={"p": 1, "page_size": 10, "status": 1},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for ch in r.json()["data"]["items"]:
print(ch["id"], ch["name"], ch["base_url"],
ch["models"].split(","), "balance:", ch["balance"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/?p=1&page_size=10"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/?p=1&page_size=10", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
for (const ch of (await resp.json()).data.items) {
console.log(ch.id, ch.name, ch.models.split(","), "balance:", ch.balance);
}
18. Create a Channel (Admin)
POST/api/channel/ — create a channel (provider), requires admin:providers:manage. Supports the three NewAPI modes: single (default, one channel with a single key), batch (when key has multiple lines, create one same-named provider per line), multi_to_single (merge the multi-line key into one provider: first line = main key, remaining lines join the sub-key pool). models also creates catalog entries and base routes in sync, so the models enter the scheduling pool immediately. You can also call POST/api/channel/fetch_models (body: {"base_url": "...", "key": "..."}) to probe the upstream model list first without persisting anything.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string (body) | Yes | Channel name |
base_url | string (body) | Yes | Upstream address (OpenAI compatible) |
key | string (body) | Yes | Upstream key; when multi-line (\n separated) it is interpreted per mode |
mode | string (body) | No | single (default) / batch / multi_to_single |
models | string / string[] (body) | No | Model names, as a comma-separated string or an array |
status | int (body) | No | 1 enabled (default) / 2 disabled |
priority / weight | int (body) | No | Scheduling weight, default 0 |
category | string (body) | No | Default category of the catalog entries, default llm |
Response data fields: a single channel object (same shape as 17. Channel List & Details); batch mode returns an array.
SAT=<tg-sat-access-token>; UID=<admin user ID>
curl -X POST http://localhost:8000/api/channel/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"name":"my-openai","base_url":"https://api.upstream.example/v1","key":"sk-up-xxx","models":"gpt-4o,gpt-4o-mini","priority":100}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.post("http://localhost:8000/api/channel/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"name": "my-openai",
"base_url": "https://api.upstream.example/v1",
"key": "sk-up-xxx",
"models": "gpt-4o,gpt-4o-mini",
"priority": 100},
timeout=30)
print(r.json()["data"]["id"]) # new channel ID
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"name\":\"my-openai\",\"base_url\":\"https://api.upstream.example/v1\","
+ "\"key\":\"sk-up-xxx\",\"models\":\"gpt-4o,gpt-4o-mini\",\"priority\":100}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "my-openai",
base_url: "https://api.upstream.example/v1",
key: "sk-up-xxx",
models: "gpt-4o,gpt-4o-mini",
priority: 100,
}),
});
console.log((await resp.json()).data.id); // new channel ID
19. Update a Channel / Manage Sub-keys (Admin)
PUT/api/channel/ — update a channel (body must include id; requires admin:providers:manage). A multi-line key means a full rewrite of the keys (the first line replaces the main key, the remaining lines replace the whole sub-key pool); models means a full catalog sync — old entries not in the list get disabled. Fields with no counterpart on our side (such as model_mapping / tag / setting) are accepted but ignored. Companion endpoint POST/api/channel/multi_key/manage — manage a single sub-key: body is {"channel_id": 1, "key_id": 3, "action": "enable|disable|delete"}.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | int (body) | Yes | Channel ID |
name / base_url | string (body) | No | New name / new address (empty strings ignored) |
key | string (body) | No | Multi-line keys, fully rewritten (first line = main key) |
models | string / string[] (body) | No | Full catalog sync (entries outside the list get disabled) |
status | int (body) | No | 1 enabled / 2 disabled |
priority / weight | int (body) | No | Scheduling weight |
Response data fields: the updated channel object (same shape as 17. Channel List & Details); sub-key management returns {"ok": true, "action": "...", "key_id": N}.
SAT=<tg-sat-access-token>; UID=<admin user ID>
# rename + rewrite the key (one line = main key) + full model sync
curl -X PUT http://localhost:8000/api/channel/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":1,"name":"openai-relay-v2","key":"sk-up-newkey","models":"gpt-4o,gpt-4o-mini,deepseek-v4-flash"}'
# disable sub-key 3 of channel 1
curl -X POST http://localhost:8000/api/channel/multi_key/manage \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"channel_id":1,"key_id":3,"action":"disable"}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.put("http://localhost:8000/api/channel/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"id": 1, "status": 2}, # disable the channel
timeout=30)
print(r.json()["data"]["status"]) # 2
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":1,\"status\":2}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 1, status: 2 }), // disable the channel
});
console.log((await resp.json()).data.status); // 2
20. Channel Test / Balance Refresh / Delete (Admin)
The channel ops trio:
- GET/api/channel/test/{id} — test a single channel: probes the provider's first active route (one real upstream request, returned synchronously); falls back to an upstream
/v1/modelsconnectivity probe when there is no route. Returns{"ok", "channel_id", "available", "error", "time" (ms), "mode" (route_probe / upstream_probe)}.GET /api/channel/testwithout an ID triggers a full background health check. - GET/api/channel/update_balance — refresh the upstream balance of all providers (synchronous);
GET /api/channel/update_balance/{id}— refresh a single one. Notifications are triggered automatically when a balance falls below the alert threshold. - DELETE/api/channel/{id} — delete the channel (cascade-cleans the provider's routes / catalog entries / sub-keys), returns
{"ok": true}. Testing requiresadmin:models:manage; balance refresh / delete requireadmin:providers:view/admin:providers:manage.
{
"success": true, "message": "",
"data": {"ok": true, "channel_id": 1, "available": true,
"error": null, "time": 643, "mode": "route_probe"}
}
SAT=<tg-sat-access-token>; UID=<admin user ID> # test channel 1 curl http://localhost:8000/api/channel/test/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # refresh channel 1's balance curl http://localhost:8000/api/channel/update_balance/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # delete channel 1 curl -X DELETE http://localhost:8000/api/channel/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
# test
r = requests.get("http://localhost:8000/api/channel/test/1", headers=H, timeout=60)
d = r.json()["data"]
print("available:", d["available"], "time:", d["time"], "ms")
# refresh balance
r = requests.get("http://localhost:8000/api/channel/update_balance/1",
headers=H, timeout=60)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/test/1"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
};
// test
let resp = await fetch("http://localhost:8000/api/channel/test/1", { headers: H });
let d = (await resp.json()).data;
console.log("available:", d.available, "time:", d.time, "ms");
// delete
resp = await fetch("http://localhost:8000/api/channel/1", { method: "DELETE", headers: H });
console.log((await resp.json()).data); // { ok: true }
21. Model Metadata List & Details (Admin)
GET/api/models/ — paginated query of the model catalog (metadata-management perspective; requires admin:models:view). Note the difference from the no-trailing-slash GET/api/models: the latter returns the array of model names available to the current logged-in user (same source as /api/user/models). Companions: GET/api/models/search?keyword=xx (search, bare array) and GET/api/models/missing (list of models missing cost quotes — these cannot be billed accurately; fill in their prices before scaling up).
| Parameter | Type | Required | Description |
|---|---|---|---|
p / page_size | int (query) | No | Pagination, default 1 / 20 (page_size max 100) |
keyword | string (query) | No | Search by model name / public name / description |
Response data.items fields: id (catalog entry ID), model_name (imported model name), description (model description), vendor_id (always 1, the unified single brand "可信词元"), status (1 enabled / 2 disabled), sort_order; our extension fields: provider_id (owning provider), public_name (display name), category (category array), is_public (public or not), context_length (context length).
{
"success": true, "message": "",
"data": {
"items": [{
"id": 12, "model_name": "gpt-4o-mini", "description": "OpenAI fast chat model",
"vendor_id": 1, "status": 1, "sort_order": 12, "provider_id": 1,
"public_name": "gpt-4o-mini", "category": ["llm"],
"is_public": true, "context_length": 128000
}],
"total": 46, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-access-token>; UID=<admin user ID> # metadata list (mind the trailing slash) curl "http://localhost:8000/api/models/?p=1&page_size=10" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # single model (by catalog entry id) curl http://localhost:8000/api/models/12 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" # models missing prices curl http://localhost:8000/api/models/missing \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/models/",
params={"keyword": "gpt", "page_size": 10},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for m in r.json()["data"]["items"]:
print(m["id"], m["model_name"], m["status"], m["category"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/models/?p=1&page_size=10"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/models/?p=1&page_size=10", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
for (const m of (await resp.json()).data.items) {
console.log(m.id, m.model_name, m.status, m.category);
}
22. Update Model Metadata (Admin)
PUT/api/models/ — update a catalog entry's metadata (body includes id; requires admin:models:manage). Field mapping: model_name → imported model name, description → model description, status (1/2) → enable/disable. When "status_only": true is passed, only the status is updated and the other fields are ignored — clients doing a pure enable/disable toggle should always pass it to avoid accidentally clearing the description.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | int (body) | Yes | Catalog entry ID |
status | int (body) | No | 1 enabled / 2 disabled |
model_name | string (body) | No | New model name (ignored with status_only) |
description | string (body) | No | New description (ignored with status_only) |
status_only | bool (body) | No | true = update the status only (recommended for pure enable/disable) |
Response data fields: {"ok": true, "id": N, "model_name": "...", "status": "active|disabled"}.
{
"success": true, "message": "",
"data": {"ok": true, "id": 12, "model_name": "gpt-4o-mini", "status": "active"}
}
SAT=<tg-sat-access-token>; UID=<admin user ID>
# disable model 12 only
curl -X PUT http://localhost:8000/api/models/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":12,"status":2,"status_only":true}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.put("http://localhost:8000/api/models/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"id": 12, "status": 2, "status_only": True},
timeout=30)
print(r.json()["data"]) # {"ok": true, "id": 12, ...}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":12,\"status\":2,\"status_only\":true}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/models/"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/models/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 12, status: 2, status_only: true }),
});
console.log((await resp.json()).data); // { ok: true, id: 12, ... }
23. Pricing Catalog & Vendors (Public)
GET/api/pricing — the price catalog for downstream software: the sale price (in credits) of each public model; supports ?category=llm,image filtering (multiple categories take the intersection). GET/api/vendors — the vendor list: this platform presents a single brand "可信词元" to the user side and always returns [{"id": 1, "name": "可信词元", ...}]; create / update / delete are not accepted. Neither endpoint requires admin permissions.
/api/pricing response data fields (array): id (public model name), display_name (display name), category (category), input_price / output_price (input / output unit price), currency (always credits), unit (billing unit), available (whether currently available).
{
"success": true, "message": "",
"data": [
{"id": "gpt-4o-mini", "display_name": "gpt-4o-mini", "category": "llm",
"input_price": 0.0012, "output_price": 0.0048,
"currency": "credits", "unit": "per 1k tokens", "available": true}
]
}
curl "http://localhost:8000/api/pricing?category=llm" curl http://localhost:8000/api/vendors
import requests
prices = requests.get("http://localhost:8000/api/pricing",
params={"category": "llm"}, timeout=30).json()["data"]
for p in prices:
print(p["id"], "in:", p["input_price"], "out:", p["output_price"])
vendors = requests.get("http://localhost:8000/api/vendors", timeout=30).json()["data"]
print(vendors) # [{"id": 1, "name": "可信词元", ...}]
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/pricing?category=llm")).GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const prices = (await (await fetch("http://localhost:8000/api/pricing?category=llm")).json()).data;
for (const p of prices) console.log(p.id, "in:", p.input_price, "out:", p.output_price);
const vendors = (await (await fetch("http://localhost:8000/api/vendors")).json()).data;
console.log(vendors); // [{ id: 1, name: "可信词元", ... }]
24. Extended Domains
Beyond the aligned endpoints above, the native management capabilities are also available under NewAPI-style paths (same auth and envelope conventions; permission requirements match the native admin console). Common extended domains:
/api/role/,/api/permission/self— role and permission management/api/transaction/— the admin view of credit transactions (admins authenticate viamy_auth("user:finance:read"))/api/model_route/,/api/model_proxy/— model route and proxy configuration/api/key/— per-key configuration (e.g. model time windows)/api/exchange_rate/,/api/bill_reconcile/— exchange rate and bill reconciliation
See the Ops Manual for the full list. Extended-domain endpoints follow the same envelope convention and are not expanded one by one here.
User Auth Additions (public / logged-in)
25. Register (email code required)
POST/api/user/register — public registration endpoint. Difference from NewAPI: email and email_code are both required (anti-spam); call "28. Send email verification code" first. On success returns {"ok": true} (no session issued); then call /api/user/login to sign in. HTTP 409 if the username is taken; agent-branded dedicated domains forbid self-registration (403).
| Parameter | Type | Required | Description |
|---|---|---|---|
username | string | Yes | Username (3–64 chars) |
password | string | Yes | Password (6–128 chars) |
email | string | Yes | Email (treated as verified upon registration) |
email_code | string | Yes | Email verification code (4–16 chars, from /api/verification) |
nickname | string | No | Nickname, defaults to username |
Response data fields: {"ok": true}. Failures: HTTP 400 (bad email format / invalid code), 409 (username taken), 403 (registration forbidden on this site).
{
"success": true, "message": "",
"data": {"ok": true}
}
curl -X POST http://localhost:8000/api/user/register \
-H "Content-Type: application/json" \
-d '{"username":"newuser","password":"password123","email":"newuser@example.com","email_code":"123456"}'
import requests
r = requests.post("http://localhost:8000/api/user/register",
json={"username": "newuser", "password": "password123",
"email": "newuser@example.com", "email_code": "123456"},
timeout=30)
print(r.json()) # {"success": true, "message": "", "data": {"ok": true}}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"username\":\"newuser\",\"password\":\"password123\","
+ "\"email\":\"newuser@example.com\",\"email_code\":\"123456\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/register"))
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "newuser", password: "password123",
email: "newuser@example.com", email_code: "123456" }),
});
console.log(await resp.json());
26. Logout (session)
GET/api/user/logout — ends the browser session: deletes the server-side session and clears the tg_session cookie. Only meaningful for cookie sessions; tg-sat- access tokens are stateless and unaffected (use "5. Reset access token" to rotate one).
Response data fields: {"ok": true}.
{
"success": true, "message": "",
"data": {"ok": true}
}
curl http://localhost:8000/api/user/logout \ -b "tg_session=<session cookie value>"
import requests
s = requests.Session() # already logged-in session
print(s.get("http://localhost:8000/api/user/logout", timeout=30).json())
// JDK 11+ (carry the tg_session cookie obtained at login)
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/logout"))
.header("Cookie", "tg_session=<session cookie value>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/logout", {
headers: { Cookie: "tg_session=<session cookie value>" },
});
console.log(await resp.json());
27. Reset Password (logged-in, new password emailed)
POST/api/user/reset — a logged-in user resets their own password: a random new password is generated and sent to the bound email (the old password is invalidated immediately; the whole operation rolls back if the email fails). HTTP 400 if no email is bound. Works with a cookie session or tg-sat- + New-Api-User.
Response data fields: ok (always true), email (where the new password was sent).
{
"success": true, "message": "",
"data": {"ok": true, "email": "alice@example.com"}
}
SAT=<tg-sat-access-token>; UID=<your user ID> curl -X POST http://localhost:8000/api/user/reset \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.post("http://localhost:8000/api/user/reset",
headers={"Authorization": f"Bearer {SAT}",
"New-Api-User": UID}, timeout=30)
print(r.json()["data"]) # {"ok": true, "email": "..."}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/reset"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>")
.POST(java.net.http.HttpRequest.BodyPublishers.noBody()).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/reset", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
},
});
console.log((await resp.json()).data);
28. Send Email Verification Code (public)
GET/api/verification — sends a registration verification code to the given email (used as email_code for /api/user/register). HTTP 400 for invalid email format; 503 if the mail service is not configured; 502 if sending fails.
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string (query) | Yes | Target email address |
Response data fields: ok (always true), ttl (code validity, seconds).
{
"success": true, "message": "",
"data": {"ok": true, "ttl": 600}
}
curl "http://localhost:8000/api/verification?email=newuser@example.com"
import requests
r = requests.get("http://localhost:8000/api/verification",
params={"email": "newuser@example.com"}, timeout=30)
print(r.json()["data"]) # {"ok": true, "ttl": 600}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/verification?email=newuser@example.com"))
.GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/verification?email=newuser@example.com");
console.log((await resp.json()).data);
29. Password Recovery (public, by username + email)
GET/api/reset_password — self-service password recovery: after verifying that the username matches the bound email, a random new password is sent to that email (the old password is invalidated immediately; the whole operation rolls back if the email fails). Both parameters are required; a mismatch returns a generic HTTP 400 (to avoid account enumeration).
| Parameter | Type | Required | Description |
|---|---|---|---|
username | string (query) | Yes | Username |
email | string (query) | Yes | The email bound to that account |
Response data fields: ok (always true), email (where the new password was sent).
{
"success": true, "message": "",
"data": {"ok": true, "email": "alice@example.com"}
}
curl "http://localhost:8000/api/reset_password?username=alice&email=alice@example.com"
import requests
r = requests.get("http://localhost:8000/api/reset_password",
params={"username": "alice", "email": "alice@example.com"},
timeout=30)
print(r.json()["data"]) # {"ok": true, "email": "alice@example.com"}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/reset_password?username=alice&email=alice@example.com"))
.GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/reset_password?username=alice&email=alice@example.com");
console.log((await resp.json()).data);
User Management Additions (admin)
30. User List (admin)
GET/api/user/ — paginated list of all users (requires admin:users:view). Note the trailing slash, which distinguishes it from "33. User Detail" at /api/user/{id}.
| Parameter | Type | Required | Description |
|---|---|---|---|
p | int (query) | No | Page number, default 1 |
page_size | int (query) | No | Page size, default 20, max 100 |
keyword | string (query) | No | Fuzzy match on username / nickname |
Response data fields: the pagination envelope {"items": [...], "total": N, "page": 1, "page_size": 20}; items are admin-view user objects (same fields as "33. User Detail").
{
"success": true, "message": "",
"data": {
"items": [{"id": 2, "username": "bob", "nickname": "Bob",
"email": "bob@example.com", "balance": 100.0,
"status": "active", "is_admin": false, "...": "..."}],
"total": 57, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl "http://localhost:8000/api/user/?p=1&page_size=20&keyword=alice" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
H = {"Authorization": f"Bearer {SAT}", "New-Api-User": UID}
r = requests.get("http://localhost:8000/api/user/",
params={"p": 1, "page_size": 20, "keyword": "alice"},
headers=H, timeout=30)
data = r.json()["data"]
print(data["total"], [u["username"] for u in data["items"]])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/?p=1&page_size=20"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
};
const resp = await fetch("http://localhost:8000/api/user/?p=1&page_size=20", { headers: H });
const d = (await resp.json()).data;
console.log(d.total, d.items.map(u => u.username));
31. Create User (admin)
POST/api/user/ — create a single user (requires admin:users:batch_create). Internally reuses the batch-creation logic: hosted-credit rules apply, a default API key is generated, and no welcome email is sent. If sign-up grants are configured and charged to the admin, charged_from / charged_total reflect the transfer from the caller's balance.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | string | Yes | Username (3–64 chars) |
password | string | Yes | Password (6–128 chars) |
email | string | No | Email, defaults to empty |
nickname | string | No | Nickname, defaults to empty (falls back to username) |
credits | float | No | Initial credits (≥ 0, default 0) |
Response data fields: ok (all succeeded?), created / failed / total, charged_from (source of the credit transfer), charged_total (total transferred).
{
"success": true, "message": "",
"data": {"ok": true, "created": 1, "failed": 0, "total": 1,
"charged_from": null, "charged_total": 0}
}
SAT=<tg-sat-access-token>; UID=<admin user ID>
curl -X POST http://localhost:8000/api/user/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"username":"bob","password":"secret123","email":"bob@example.com","credits":100}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.post("http://localhost:8000/api/user/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"username": "bob", "password": "secret123",
"email": "bob@example.com", "credits": 100},
timeout=30)
print(r.json()["data"]) # {"ok": true, "created": 1, ...}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"username\":\"bob\",\"password\":\"secret123\","
+ "\"email\":\"bob@example.com\",\"credits\":100}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ username: "bob", password: "secret123",
email: "bob@example.com", credits: 100 }),
});
console.log((await resp.json()).data);
32. Edit User (admin)
PUT/api/user/ — edit a user (requires admin:users:edit). The body must include id; only send the fields you want to change. Shares the same core logic and permission checks as the native admin console. Returns the updated admin-view user object.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | int | Yes | Target user ID |
group_id | int | No | User group ID |
status | string | No | active / disabled |
is_admin | bool | No | Super-admin flag |
nickname / email | string | No | Nickname / email |
allowed_models / denied_models | string[] | No | Model allowlist / denylist |
role_ids | int[] | No | Role ID list |
file_retention_days / log_retention_days | int | No | File / log retention days |
is_agent / agent_site_name / agent_logo | bool / string | No | Agent identity and dedicated-site settings |
{
"success": true, "message": "",
"data": {"id": 2, "username": "bob", "nickname": "Bobby",
"status": "active", "...": "..."}
}
SAT=<tg-sat-access-token>; UID=<admin user ID>
curl -X PUT http://localhost:8000/api/user/ \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":2,"group_id":1}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.put("http://localhost:8000/api/user/",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"id": 2, "group_id": 1},
timeout=30)
print(r.json()["data"]["group_id"]) # 1
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":2,\"group_id\":1}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/", {
method: "PUT",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 2, group_id: 1 }),
});
console.log((await resp.json()).data);
33. User Detail (admin)
GET/api/user/{id} — fetch a single user's details (requires admin:users:view), including group, roles and agent information. HTTP 404 if the user does not exist.
Response data fields: admin-view user object; common fields: id, username, nickname, email, balance, rate_multiplier, group_id / group_name, status, is_admin, is_agent, created_at, etc.
{
"success": true, "message": "",
"data": {"id": 2, "username": "bob", "nickname": "Bob",
"email": "bob@example.com", "balance": 100.0,
"group_id": 1, "group_name": "default",
"status": "active", "is_admin": false, "is_agent": false, "...": "..."}
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl http://localhost:8000/api/user/2 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/user/2",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/2"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/2", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
console.log((await resp.json()).data);
34. User Search (admin)
GET/api/user/search — search users by keyword (requires admin:users:view), matching username / nickname / email. Returns a bare array (no pagination envelope), at most 100 entries in descending user-ID order; without keyword it returns the 100 most recent users.
| Parameter | Type | Required | Description |
|---|---|---|---|
keyword | string (query) | No | Matches username / nickname / email |
{
"success": true, "message": "",
"data": [
{"id": 2, "username": "bob", "nickname": "Bob",
"email": "bob@example.com", "balance": 100.0, "...": "..."}
]
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl "http://localhost:8000/api/user/search?keyword=bob" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/user/search",
params={"keyword": "bob"},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for u in r.json()["data"]:
print(u["id"], u["username"], u["email"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/search?keyword=bob"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/search?keyword=bob", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
for (const u of (await resp.json()).data) console.log(u.id, u.username, u.email);
35. User Status Actions (admin)
POST/api/user/manage — perform a quick action on a user (requires admin:users:edit). No delete: use disable instead. Unsupported actions return 400; a missing user returns 404.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | int | Yes | Target user ID |
action | string | Yes | disable / enable / promote (make admin) / demote (revoke admin) |
Response data fields: {"ok": true, "id": N, "action": "..."}.
{
"success": true, "message": "",
"data": {"ok": true, "id": 2, "action": "disable"}
}
SAT=<tg-sat-access-token>; UID=<admin user ID>
curl -X POST http://localhost:8000/api/user/manage \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"id":2,"action":"disable"}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.post("http://localhost:8000/api/user/manage",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"id": 2, "action": "disable"}, timeout=30)
print(r.json()["data"]) # {"ok": true, "id": 2, "action": "disable"}
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"id\":2,\"action\":\"disable\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/manage"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/manage", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ id: 2, action: "disable" }),
});
console.log((await resp.json()).data);
36. Top-up Records (admin)
GET/api/user/topup — paginated top-up records (credit transactions with type=recharge; requires admin:tx:view), optionally filtered by user.
| Parameter | Type | Required | Description |
|---|---|---|---|
p / page_size | int (query) | No | Pagination, default 1 / 20 (page_size max 100) |
user_id | int (query) | No | Only top-ups of the given user |
Response data.items fields: id (transaction ID), user_id, amount (top-up amount), balance_after (balance after credit), remark, operator_id (the admin who processed it), created_at.
{
"success": true, "message": "",
"data": {
"items": [{"id": 301, "user_id": 2, "amount": 100.0,
"balance_after": 100.0, "remark": "bank transfer",
"operator_id": 1, "created_at": "2026-09-18T10:00:00"}],
"total": 12, "page": 1, "page_size": 20
}
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl "http://localhost:8000/api/user/topup?p=1&page_size=20&user_id=2" \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/user/topup",
params={"p": 1, "page_size": 20, "user_id": 2},
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for t in r.json()["data"]["items"]:
print(t["id"], t["user_id"], t["amount"], t["remark"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/topup?p=1&page_size=20&user_id=2"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/topup?p=1&page_size=20&user_id=2", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
for (const t of (await resp.json()).data.items) console.log(t.id, t.amount, t.remark);
37. Confirm Top-up (admin)
POST/api/user/topup/complete — manually confirm a received payment and credit the user (requires admin:users:grant); the JSON variant of the grant endpoint — use /api/user/{id}/grant when a receipt attachment is needed. remark is mandatory for financial auditing. HTTP 404 if the user does not exist.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | int | Yes | Target user ID |
amount | float | Yes | Top-up amount (> 0) |
remark | string | Yes | Operation note (required for audit, non-empty) |
Response data fields: ok, user_id, amount, balance (balance after credit).
{
"success": true, "message": "",
"data": {"ok": true, "user_id": 2, "amount": 100.0, "balance": 200.0}
}
SAT=<tg-sat-access-token>; UID=<admin user ID>
curl -X POST http://localhost:8000/api/user/topup/complete \
-H "Authorization: Bearer $SAT" -H "New-Api-User: $UID" \
-H "Content-Type: application/json" \
-d '{"user_id":2,"amount":100,"remark":"bank transfer received"}'
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.post("http://localhost:8000/api/user/topup/complete",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
json={"user_id": 2, "amount": 100, "remark": "bank transfer received"},
timeout=30)
print(r.json()["data"]["balance"]) # balance after credit
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"user_id\":2,\"amount\":100,\"remark\":\"bank transfer received\"}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/topup/complete"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>")
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/topup/complete", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
"Content-Type": "application/json",
},
body: JSON.stringify({ user_id: 2, amount: 100, remark: "bank transfer received" }),
});
console.log((await resp.json()).data.balance);
38. My Available Groups (logged-in)
GET/api/user/self/groups — list the active groups visible to the current user (any logged-in user, no admin permission; works with a cookie session or tg-sat- + New-Api-User). Returns a bare array.
Response data fields (array): id, name (group key), display_name, rate (group multiplier), is_current (whether it is the current user's group).
{
"success": true, "message": "",
"data": [
{"id": 1, "name": "default", "display_name": "Default", "rate": 1.0, "is_current": true},
{"id": 2, "name": "vip", "display_name": "VIP", "rate": 0.8, "is_current": false}
]
}
SAT=<tg-sat-access-token>; UID=<your user ID> curl http://localhost:8000/api/user/self/groups \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<your user ID>"
r = requests.get("http://localhost:8000/api/user/self/groups",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
for g in r.json()["data"]:
print(g["name"], g["rate"], g["is_current"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/user/self/groups"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<your user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/user/self/groups", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<your user ID>",
},
});
for (const g of (await resp.json()).data) console.log(g.name, g.rate, g.is_current);
Channel Additions (admin)
39. All Channel Model Names (admin)
GET/api/channel/models — returns every model name known to any channel (requires admin:providers:view), including disabled entries, deduplicated and sorted. Returns a bare array, typically used to populate model dropdowns in channel configuration.
{
"success": true, "message": "",
"data": ["gpt-4o", "gpt-4o-mini", "claude-sonnet-4-5", "..."]
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl http://localhost:8000/api/channel/models \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/channel/models",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"]) # ["gpt-4o", "gpt-4o-mini", ...]
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/models"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/models", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
console.log((await resp.json()).data);
40. Enabled Model Names (admin)
GET/api/channel/models_enabled — returns the model names currently enabled (both provider and catalog entry active; requires admin:providers:view), deduplicated and sorted. Returns a bare array.
{
"success": true, "message": "",
"data": ["gpt-4o-mini", "claude-sonnet-4-5", "..."]
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl http://localhost:8000/api/channel/models_enabled \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/channel/models_enabled",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/models_enabled"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/models_enabled", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
console.log((await resp.json()).data);
41. Sync Upstream Models (admin)
GET/api/channel/fetch_models/{id} — fetch the model list from the given channel's (provider's) upstream in real time and register it into the local catalog (equivalent to the native "sync models"; requires admin:providers:manage). Returns a bare array: all currently active model names of that provider after the sync. HTTP 404 if the channel does not exist; 502 if the upstream fetch fails. There is also POST/api/channel/fetch_models (body: base_url + key) for probing before creating a channel — it does not persist anything.
{
"success": true, "message": "",
"data": ["gpt-4o", "gpt-4o-mini", "gpt-5", "..."]
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl http://localhost:8000/api/channel/fetch_models/1 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/channel/fetch_models/1",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=60) # upstream fetch can be slow
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/fetch_models/1"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/fetch_models/1", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
console.log((await resp.json()).data);
42. View Channel Plaintext Key (admin)
POST/api/channel/{id}/key — view the channel's plaintext key (requires admin:providers:view, reusing the plaintext-key-viewing permission). The data field is a multi-line merged plaintext key string: the first line is the primary key, each remaining line a sub-key (only one line when there are no sub-keys). HTTP 404 if the channel does not exist.
{
"success": true, "message": "",
"data": "sk-main-xxxxxxxx\nsk-sub-yyyyyyyy"
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl -X POST http://localhost:8000/api/channel/1/key \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.post("http://localhost:8000/api/channel/1/key",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"].splitlines()) # one key per line
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/channel/1/key"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>")
.POST(java.net.http.HttpRequest.BodyPublishers.noBody()).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/channel/1/key", {
method: "POST",
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
console.log(((await resp.json()).data || "").split("\n"));
Other Additions (key usage / logs / models / vendors)
43. Key Usage Query (sk-tg- only)
GET/api/usage/token/ — query the cumulative usage of a model key, authenticated by the key (sk-tg-) itself. This is the only exception in the /api/* management plane that accepts sk-tg- auth (tg-sat- / login sessions are rejected with 403). The trailing slash is optional.
Response data fields: count (number of calls made by this key), quota (total credits charged to this key), key_name, key_status.
{
"success": true, "message": "",
"data": {"count": 128, "quota": 35.6, "key_name": "default", "key_status": "active"}
}
KEY=<sk-tg-your-key> curl http://localhost:8000/api/usage/token/ \ -H "Authorization: Bearer $KEY"
import requests
KEY = "<sk-tg-your-key>"
r = requests.get("http://localhost:8000/api/usage/token/",
headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
d = r.json()["data"]
print(d["count"], d["quota"], d["key_name"], d["key_status"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/usage/token/"))
.header("Authorization", "Bearer <sk-tg-your-key>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/usage/token/", {
headers: { Authorization: "Bearer <sk-tg-your-key>" },
});
console.log((await resp.json()).data);
44. Key Call Logs (sk-tg- only)
GET/api/log/token/ — paginated call logs of the key itself, authenticated by the model key (sk-tg-) (tg-sat- / login sessions are rejected with 403). Logged-in users should use /api/log/self instead.
| Parameter | Type | Required | Description |
|---|---|---|---|
p / page_size | int (query) | No | Pagination, default 1 / 20 (page_size max 100) |
Response data.items fields: id, user_id, request_id, category, model, path, http_status, stream, prompt_tokens / completion_tokens, upstream_cost, credits_charged, latency_ms, error, created_at, etc.
{
"success": true, "message": "",
"data": {
"items": [{"id": 9001, "user_id": 1, "request_id": "chatcmpl-abc",
"category": "llm", "model": "gpt-4o-mini",
"path": "/v1/chat/completions", "http_status": 200, "stream": false,
"prompt_tokens": 120, "completion_tokens": 45,
"upstream_cost": 0.00021, "credits_charged": 0.35,
"latency_ms": 820, "error": null,
"created_at": "2026-09-18T10:00:00"}],
"total": 128, "page": 1, "page_size": 20
}
}
KEY=<sk-tg-your-key> curl "http://localhost:8000/api/log/token/?p=1&page_size=20" \ -H "Authorization: Bearer $KEY"
import requests
KEY = "<sk-tg-your-key>"
r = requests.get("http://localhost:8000/api/log/token/",
params={"p": 1, "page_size": 20},
headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
data = r.json()["data"]
for log in data["items"]:
print(log["created_at"], log["model"], log["credits_charged"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/log/token/?p=1&page_size=20"))
.header("Authorization", "Bearer <sk-tg-your-key>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/log/token/?p=1&page_size=20", {
headers: { Authorization: "Bearer <sk-tg-your-key>" },
});
for (const log of (await resp.json()).data.items) {
console.log(log.created_at, log.model, log.credits_charged);
}
45. Single Model Metadata (admin)
GET/api/models/{id} — fetch a single model's metadata by catalog entry ID (requires admin:models:view); same shape as the items of "21. Model Metadata List". HTTP 404 if the entry does not exist.
{
"success": true, "message": "",
"data": {"id": 12, "model_name": "gpt-4o-mini", "description": "OpenAI fast chat model",
"vendor_id": 1, "status": 1, "sort_order": 12, "provider_id": 1,
"public_name": "gpt-4o-mini", "category": ["llm"],
"is_public": true, "context_length": 128000}
}
SAT=<tg-sat-access-token>; UID=<admin user ID> curl http://localhost:8000/api/models/12 \ -H "Authorization: Bearer $SAT" -H "New-Api-User: $UID"
import requests
SAT = "<tg-sat-access-token>"; UID = "<admin user ID>"
r = requests.get("http://localhost:8000/api/models/12",
headers={"Authorization": f"Bearer {SAT}", "New-Api-User": UID},
timeout=30)
print(r.json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/models/12"))
.header("Authorization", "Bearer <tg-sat-access-token>")
.header("New-Api-User", "<admin user ID>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch("http://localhost:8000/api/models/12", {
headers: {
Authorization: "Bearer <tg-sat-access-token>",
"New-Api-User": "<admin user ID>",
},
});
console.log((await resp.json()).data);
46. Vendor Search and Detail (public)
GET/api/vendors/search and GET/api/vendors/{id} — companions to /api/vendors from "23. Pricing & Vendors"; both are public. The platform is a single-vendor system: search returns the vendor only when keyword matches "可信词元" (otherwise an empty array); detail exists only for id=1 — anything else returns 404.
| Parameter | Type | Required | Description |
|---|---|---|---|
keyword | string (query) | No | Search keyword (/search only) |
id | int (path) | Yes | Vendor ID; only 1 is valid (/{id} only) |
Response data fields: id (always 1), name ("可信词元"), description ("平台统一供应商"), icon, status (always enabled).
{
"success": true, "message": "",
"data": {"id": 1, "name": "可信词元", "description": "平台统一供应商",
"icon": "", "status": "enabled"}
}
curl "http://localhost:8000/api/vendors/search?keyword=可信词元" curl http://localhost:8000/api/vendors/1
import requests
print(requests.get("http://localhost:8000/api/vendors/search",
params={"keyword": "可信词元"}, timeout=30).json()["data"])
print(requests.get("http://localhost:8000/api/vendors/1", timeout=30).json()["data"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/vendors/1")).GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const search = await (await fetch("http://localhost:8000/api/vendors/search?keyword=可信词元")).json();
console.log(search.data);
const one = await (await fetch("http://localhost:8000/api/vendors/1")).json();
console.log(one.data);
Self-Service API (tg-usr- user tokens)
Self-service endpoints for end users: transaction reconciliation and per-API-key model time windows.
tg-usr- user tokens are fully isolated from model keys (sk-tg-) and login sessions,
so a token shared with a third-party reconciliation system cannot invoke models.
How to get one: sign in to the Console → "My Tokens" → create a token and pick scopes (editable/revocable anytime):
user:finance:read— finance queries (transactions)user:keys:config— key configuration (per-model time windows)
Send as Authorization: Bearer tg-usr-... (no New-Api-User header needed; a login-session cookie works as well).
1. My Credit Transactions
GET/api/transaction/self — paginated query of your own credit transactions (recharge / consume / grant / adjust); each entry carries the post-change balance and the billing context. This is the most-used endpoint for third-party reconciliation systems: match your type=consume entries against your own business logs entry-by-entry via request_id to complete reconciliation (see Workflows · Workflow 1). Requires user:finance:read.
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string (query) | No | Transaction type: recharge / consume / grant (admin grant) / adjust (adjustment) |
page | int (query) | No | Page number, default 1 |
page_size | int (query) | No | Page size, default 20, max 100 |
Response data.items fields (TransactionOut): id (transaction ID), type (type), amount (credit delta: negative for consume, positive for recharge / grant), balance_after (balance after the change), multiplier (billing multiplier of this entry), upstream_cost / upstream_cost_cny / exchange_rate (upstream cost in USD / CNY and the exchange rate; set on consume entries), sale_coefficient (sale coefficient), request_id (request ID of the related call, null for recharge entries), remark (note), created_at (time).
{
"success": true, "message": "",
"data": {
"items": [{
"id": 5678, "user_id": 1, "type": "consume", "amount": -0.33,
"balance_after": 999.67, "multiplier": 1.0,
"upstream_cost": 0.000192, "upstream_cost_cny": 0.00138,
"exchange_rate": 7.2, "sale_coefficient": 1.5,
"request_id": "req-7f2c...", "remark": null,
"created_at": "2026-09-18T10:24:31"
}, {
"id": 5001, "user_id": 1, "type": "recharge", "amount": 1000.0,
"balance_after": 1000.0, "multiplier": null,
"upstream_cost": null, "upstream_cost_cny": null,
"exchange_rate": null, "sale_coefficient": null,
"request_id": null, "remark": "Online top-up",
"created_at": "2026-09-01T09:00:00"
}],
"total": 233, "page": 1, "page_size": 20
}
}
TOKEN=<tg-usr- user token> # all transactions curl "http://localhost:8000/api/transaction/self?page=1" \ -H "Authorization: Bearer $TOKEN" # consume entries only (for reconciliation) curl "http://localhost:8000/api/transaction/self?type=consume&page=1" \ -H "Authorization: Bearer $TOKEN"
import requests
TOKEN = "<tg-usr- user token>"
r = requests.get("http://localhost:8000/api/transaction/self",
params={"type": "consume", "page": 1},
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30)
for t in r.json()["data"]["items"]:
print(t["created_at"], t["amount"], t["balance_after"], t["request_id"])
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/transaction/self?type=consume&page=1"))
.header("Authorization", "Bearer <tg-usr- user token>").GET().build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const resp = await fetch(
"http://localhost:8000/api/transaction/self?type=consume&page=1",
{
headers: { Authorization: "Bearer <tg-usr- user token>" },
},
);
for (const t of (await resp.json()).data.items) {
console.log(t.created_at, t.amount, t.balance_after, t.request_id);
}
2. Per-Key Model Time Windows
Restrict a specific API key to call a specific model only at certain times (Beijing time; it only affects when that key may call, not its quota). Three endpoints (auth: a tg-usr- token with user:keys:config, or a login session):
- GET/api/key/{key_id}/model_window — all window rules configured on that key
- PUT/api/key/{key_id}/model_window — set the windows of a model, body
{"model": "model-name", "windows": [...]}(overwriting update) - DELETE/api/key/{key_id}/model_window?model=model-name — remove that model's windows (unrestricted again)
Window rule syntax (windows array; the key is usable when ANY rule matches; empty array = always available):
{"start":"09:00","end":"18:00"}— daily window (cross-midnight supported, e.g. 22:00-06:00){"weekdays":[0,1,2,3,4],"start":"09:00:00","end":"18:00:00"}— by weekday (0=Mon … 6=Sun){"date":"2026-10-01","action":"closed"}— closed all day on that date{"date":"2026-10-09","start":"09:00","end":"12:00"}— open only within this window on that date
{
"success": true, "message": "",
"data": [
{"model": "deepseek-v4-flash",
"windows": [{"start": "09:00", "end": "18:00"}]},
{"model": "gpt-4o-mini",
"windows": [{"weekdays": [0,1,2,3,4], "start": "09:00:00", "end": "18:00:00"},
{"date": "2026-10-01", "action": "closed"}]}
]
}
TOKEN=<tg-usr- user token>
# view configured windows
curl http://localhost:8000/api/key/123/model_window \
-H "Authorization: Bearer $TOKEN"
# set: deepseek-v4-flash usable only on weekdays 09:00-18:00
curl -X PUT http://localhost:8000/api/key/123/model_window \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"model":"deepseek-v4-flash","windows":[{"weekdays":[0,1,2,3,4],"start":"09:00","end":"18:00"}]}'
# remove the windows (unrestricted again)
curl -X DELETE "http://localhost:8000/api/key/123/model_window?model=deepseek-v4-flash" \
-H "Authorization: Bearer $TOKEN"
import requests
TOKEN = "<tg-usr- user token>"
H = {"Authorization": f"Bearer {TOKEN}"}
# view configured windows
r = requests.get("http://localhost:8000/api/key/123/model_window",
headers=H, timeout=30)
print(r.json()["data"])
# set windows
r = requests.put("http://localhost:8000/api/key/123/model_window",
headers=H,
json={"model": "deepseek-v4-flash",
"windows": [{"start": "09:00", "end": "18:00"}]},
timeout=30)
print(r.json()["success"]) # True
// JDK 11+
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"model\":\"deepseek-v4-flash\","
+ "\"windows\":[{\"start\":\"09:00\",\"end\":\"18:00\"}]}";
var req = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("http://localhost:8000/api/key/123/model_window"))
.header("Authorization", "Bearer <tg-usr- user token>")
.header("Content-Type", "application/json")
.PUT(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req,
java.net.http.HttpResponse.BodyHandlers.ofString()).body());
const H = {
Authorization: "Bearer <tg-usr- user token>",
"Content-Type": "application/json",
};
// view configured windows
let resp = await fetch("http://localhost:8000/api/key/123/model_window", {
headers: H,
});
console.log((await resp.json()).data);
// set windows
resp = await fetch("http://localhost:8000/api/key/123/model_window", {
method: "PUT",
headers: H,
body: JSON.stringify({
model: "deepseek-v4-flash",
windows: [{ start: "09:00", end: "18:00" }],
}),
});
console.log((await resp.json()).success); // true
3. Scope of tg-usr- Tokens
tg-usr- user tokens are least-privilege tokens: they are accepted only by the two endpoint groups above and rejected everywhere else. When you hand a token to a third-party system, these two operations are the only things it can perform.
| Endpoint | Required scope | Description |
|---|---|---|
| GET/api/transaction/self | user:finance:read | My credit transactions (item 1 above) |
| GET/PUT/DELETE/api/key/{key_id}/model_window | user:keys:config | Per-model time windows of a key (item 2 above) |
Common scenarios that do NOT accept tg-usr- (they require a login-session cookie or tg-sat- + New-Api-User header):
- Token management:
/api/my/tokens,/api/my/token-scopes(creating / editing / revoking user tokens themselves) - Key management: listing / creating / editing / deleting via
/api/key - All other
/api/*management-plane endpoints (see the NewAPI-Compatible Management API section above)
Likewise, model keys (sk-tg-) and ops tokens (tg-ops-) always receive 401 on this group of endpoints — the three token families are fully isolated.