LLM-Chat-API 多模型路由,Base URL 填 TaoToken 后核对 token 计费
2026/9/18 13:40:17 网站建设 项目流程

LLM-Chat-API 多模型路由跑通之后,最容易被忽略的不是 route_model,而是模型调用凭据。把 Base URL 填成 https://taotoken.net/api,再去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=llm_chat_api_usage 创建 TaoToken Key,然后用原压测脚本发一轮请求,确认流式输出和 usage 都能拿到,最后对照 token 配额看板核对每个模型的调用是否成功。

四年前写 Flask 的时候,接口层换个数据库连接串,最多改一行 config;现在维护 LLM-Chat-API,路由表把 chitchat 发 gpt-3.5-turbo、长文摘要发 qwen-max、其余走 gpt-4-turbo,StreamingResponse、Redis 记忆、log_llm_usage 都还在原处。真正让这轮转型卡住的,往往不是路由函数写错,而是底层 OpenAI 兼容通道从直连切成 TaoToken 之后,Key 和 Base URL 没对齐,导致看板上只有部分模型有记录。

1. 从四年 Flask 到 LLM-Chat-API:路由层之前的凭据层

1.1 route_model 只管分流,不管用哪把钥匙

四年前做 Flask,我的思维习惯是“业务逻辑在哪,配置就在哪”。转到 LLM-Chat-API 之后,route_model 的职责其实很窄:它只判断用户意图是 chitchat、长文摘要还是默认问答,然后返回对应模型名。它不关心这个模型名后续通过哪个 endpoint 发送、用哪把 Key 计费、响应里的 usage 字段长什么样。很多同学习惯性认为改多模型路由就要重写路由表,结果一上来改 ROUTE_TABLE,反而把原本稳定的分流逻辑打乱了。更稳妥的做法是保留 route_model 原样,把模型调用凭据抽到统一客户端里,所有分支都走同一个 OpenAI 兼容对象。这样切换底层通道时,改动面只有 api_key 和 base_url 两处。

1.2 原文里的 log_llm_usage 为什么会在切换后对不上

原文的 log_llm_usage 记录 prompt_tokens、completion_tokens 和 latency,这三项通常来自 OpenAI 兼容响应里的 usage。问题在于,部分 SDK 和部分模型在流式返回时默认不把 usage 塞进每个 chunk,如果切换 Base URL 后没有同步打开 stream_options,就会出现文本正常输出、看板没有 token 记录的情况。另一种对不上,是把模型 ID 从旧文档直接复制过来,而新通道的模型广场里该 ID 已经改名或下线。route_model 返回的字符串没变,但请求体里的 model 字段已经不再被服务端识别,调用自然失败。所以这一轮验证的重点不是“路由准不准”,而是“凭据通不通、usage 回没回、看板记没记”。

2. 把 OpenAI 兼容客户端指到 https://taotoken.net/api

2.1 在环境变量里放 YOUR_API_KEY

先别改 Flask 或 FastAPI 的路由文件。打开 TaoToken,完成注册后进入控制台创建 API Key,把 Key 放到环境变量里,不要写死在代码仓库。可以用.env文件配合 python-dotenv,也可以直接在启动进程时 export。Key 一律用占位符 YOUR_API_KEY 表示,实际值从官网控制台复制。

export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_API_KEY="YOUR_API_KEY"

注意这里有两个地址不要混:给人点的官网落地页是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=llm_chat_api_usage ,填进 OpenAI 客户端的 Base URL 是 https://taotoken.net/api 。后者末尾不要加 /v1,也不要拼任何查询参数。很多 404 或路径重复,都是因为把落地页地址或带 /v1 的旧习惯带进了代码。

2.2 llm_client.py 的 base_url 不要带 /v1

在原来的 LLM-Chat-API 项目里找一个集中创建客户端的文件,通常叫 llm_client.py、model_client.py 或 services/llm.py。把原来的 OpenAI 客户端替换成下面这段,保留 route_model 和业务调用不变。关键点是base_url参数只写 https://taotoken.net/api ,api_key从环境变量读取。

import os from openai import OpenAI TAOTOKEN_BASE_URL = os.getenv("TAOTOKEN_BASE_URL", "https://taotoken.net/api") TAOTOKEN_API_KEY = os.getenv("TAOTOKEN_API_KEY", "YOUR_API_KEY") client = OpenAI( api_key=TAOTOKEN_API_KEY, base_url=TAOTOKEN_BASE_URL, )

如果你用的是 FastAPI,可以在依赖注入里复用这个 client;如果是 Flask,可以挂在 app.extensions 上,或者写一个工厂函数。无论哪种,都不需要在每个路由里重新 new 一个 OpenAI 对象。TaoToken 在这里只提供 Key 和 Base URL,不参与 StreamingResponse 的封装,也不参与 Redis 记忆的读写,更不参与 route_model 的判断。你的业务代码仍然按原来的方式处理流式生成器、缓存和日志。

2.3 ROUTE_TABLE 里的模型 ID 从模型广场抄

route_model 里的映射可以先保持原样,但模型 ID 必须重新核对。原文把 chitchat 映射到 gpt-3.5-turbo,长文摘要映射到 qwen-max,其余走 gpt-4-turbo,这是旧通道的写法。切到 TaoToken 之后,模型是否可用、ID 是否一致,以模型广场当时列表为准。打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=llm_chat_api_usage 的模型广场,把当前可用的模型 ID 复制到你的 ROUTE_TABLE 或配置中心。不要凭记忆手写,也不要把带日期后缀的猜测 ID 当成正式配置。

ROUTE_TABLE = { "chitchat": "gpt-3.5-turbo", "long_summary": "qwen-max", "default": "gpt-4-turbo", } def route_model(intent: str) -> str: if intent == "chitchat": return ROUTE_TABLE["chitchat"] if intent == "long_summary": return ROUTE_TABLE["long_summary"] return ROUTE_TABLE["default"]

上面这些模型名只是保留原文的分流结构,真正上线前请以官网模型广场的实时列表为准。如果某个 ID 不在列表里,调用会在服务端被拒绝,而 route_model 本身不会报错,因为它的返回值只是一个字符串。这也是为什么验证用量时要按模型分别发请求,而不是只发一条默认请求。

3. 用原压测脚本打穿 chitchat、长文摘要和默认模型

3.1 非流式先验 usage

先把压测脚本里的 Base URL 和 Key 改成同一套 TaoToken 凭据,但第一轮建议用非流式请求,因为非流式响应里的 usage 字段最直观。构造三条消息:一条短的日常聊天,一条长文摘要,一条默认技术问答,分别调用 route_model 返回的模型名。观察响应对象里是否有usage.prompt_tokensusage.completion_tokens和总 token 数。如果这一步就报 401,先查 Key 是否复制完整、环境变量是否带上了引号或空格。如果报模型不存在,回到模型广场重新复制 ID。

import time from openai import OpenAI client = OpenAI( api_key="YOUR_API_KEY", base_url="https://taotoken.net/api", ) def run_non_stream(model: str, prompt: str): start = time.time() resp = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], ) latency = time.time() - start usage = resp.usage print(model, { "prompt_tokens": usage.prompt_tokens, "completion_tokens": usage.completion_tokens, "latency": round(latency, 3), "text": resp.choices[0].message.content[:40], })

非流式跑通只能说明 Key 和 Base URL 没问题,还不能证明你的 StreamingResponse 也没问题。LLM-Chat-API 如果对外提供流式接口,业务代码里通常会把 OpenAI 返回的 chunk 再包一层生成器。这层包装不会因为 Base URL 改变而失效,但它会掩盖 usage 的缺失。所以下一轮必须回到流式模式,专门看最后一个 chunk。

3.2 流式补上 stream_options

流式请求要拿到 usage,需要在请求体里加stream_options={"include_usage": True}。这个参数不是所有旧版 SDK 都支持,建议先把 openai 包升到较新的版本。然后按下面对方式写一个最小验证脚本,一边拼文本,一边记录最后一个 chunk 的 usage。注意流式返回时 usage 可能只在最后一个 chunk 出现,前面的 chunk 的 usage 为 None,这是正常现象。

def run_stream(model: str, prompt: str): stream = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], stream=True, stream_options={"include_usage": True}, ) parts = [] usage = None for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: parts.append(chunk.choices[0].delta.content) if getattr(chunk, "usage", None): usage = chunk.usage prompt_tokens = usage.prompt_tokens if usage else None completion_tokens = usage.completion_tokens if usage else None print(model, "".join(parts)[:40], prompt_tokens, completion_tokens)

把 chitchat、长文摘要和默认模型各跑一遍。如果文本能流出来但 usage 始终是 None,先确认stream_options有没有拼错,再确认当前模型是否支持返回 usage。有些兼容通道对 usage 的返回时机和直连略有差异,但字段名通常还是 prompt_tokens 和 completion_tokens。如果差异很大,先用非流式把计费链路跑通,再考虑是否要在业务层做兜底统计。

3.3 把 prompt_tokens / completion_tokens / latency 接回 log_llm_usage

原文的 log_llm_usage 是看板的数据来源,切换 Base URL 后这个函数不需要重写,只需要确认传参没有被流式包装截断。下面是一个保底写法:非流式直接用响应里的 usage,流式在生成器结束后把累计的 usage 传进去。如果原项目用 Redis 或数据库记录,保持原来的写入方式,不要把 TaoToken 的地址写进存储层。

def log_llm_usage(model: str, prompt_tokens: int, completion_tokens: int, latency: float): record = { "model": model, "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "latency": latency, } # 这里保留你原来的写入逻辑:Redis、SQLite、日志或看板 print("usage_record", record)

验证时把 log_llm_usage 的打印打开,对照压测脚本里的模型名、token 数和延迟。如果文本输出正常但 log_llm_usage 没打印,说明生成器没有被消费到底,或者 usage 提取逻辑有问题。如果打印了但 token 数为 0,优先检查是否把include_usage加在了错误的位置。TaoToken 只负责把请求送到模型并返回 usage,它不会替你补写日志,所以这一步的业务埋点仍然要自己核对。

4. token 配额看板对不上时,先查这五个点

4.1 401 与 Key 空格

最常见的 401 不是 Key 失效,而是复制时带上了换行或空格。从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=llm_chat_api_usage 创建 Key 后,先粘贴到文本编辑器里看一眼首尾,再放进环境变量。如果是 Docker 或 systemd 启动,确认环境变量确实传进了进程,而不是只在当前 shell 生效。另外,OpenAI 兼容客户端通常会自动加 Bearer 前缀,你只需要填原始 Key,不要手动拼Bearer

4.2 usage 为空的两种常见原因

第一种是流式请求没加stream_options={"include_usage": True},第二种是 SDK 版本过旧,即使加了参数也不会解析。解决顺序是先升级 openai 包,再用非流式请求确认响应里有 usage,最后回到流式请求。如果业务层自己实现了 SSE,还要确认最后一个 chunk 没有被业务代码过滤掉。很多 LLM-Chat-API 的流式包装只转发delta.content,却把包含 usage 的空 choices chunk 丢了,看板自然没有记录。

4.3 看板延迟与模型 ID 写错

控制台的用量统计通常不是实时毫秒级,跑完压测后等几十秒到一两分钟再刷新。如果长时间没有记录,检查 route_model 返回的模型 ID 是否和模型广场一致。比如 chitchat 分支仍返回旧 ID,请求会被服务端拒绝,看板只会显示失败调用或者干脆不显示。把三个分支的模型名逐个打印出来,和模型广场的列表对一遍,比盲目重试更有效。

4.4 别让 StreamingResponse 和 Redis 背锅

Base URL 改变不会影响 FastAPI 的 StreamingResponse,也不会影响 Flask 的流式生成器。Redis 记忆的读写发生在你的业务层,和模型通道无关。如果切换后流式输出正常、usage 也拿到了,但 Redis 里的会话记录没更新,应该去查业务代码里的缓存键和过期时间,而不是改 Base URL。TaoToken 不介入这些环节,所以排障时要先把网络通道问题和业务状态问题分开。

5. 跑通一次多模型请求后,去控制台对账

5.1 在模型对话里用同一把 Key 复现

压测脚本跑通之后,先别急着把这套配置推到所有环境。打开 TaoToken 模型对话,用同一把 YOUR_API_KEY 和同一个模型 ID 发一条测试消息。模型对话页面能帮你排除代码里的 Base URL 拼写问题,也能快速对比流式输出是否正常。如果这里能通、代码里不通,大概率是环境变量没生效或者客户端初始化时用了旧地址。

5.2 控制台 API Keys 与用量页

然后进入 控制台 API Keys,确认刚才压测用的 Key 还在、没有过期,并查看对应的用量记录。重点核对三件事:调用时间是否对得上、模型名是否和 route_model 返回值一致、token 消耗是否接近压测脚本打印的数值。如果看板里有调用但 token 数偏低,先检查是否命中了缓存或是否有一部分请求走了非流式分支没被记录。

5.3 长期跑之前看 Coding Plan

如果你的 LLM-Chat-API 要长期给团队或线上业务用,建议在 Coding Plan 里看当前套餐是否覆盖多模型并发。多模型路由的特点是 chitchat 请求量大但单次 token 少,长文摘要请求量小但 token 消耗高,默认问答介于两者之间。把这三种流量拆开看,比只看总调用次数更能判断额度是否够用。后面如果要用 Claude Code 之类的工具生成对照代码,环境变量对照可以看 Claude Code 接入文档,但记住工具只负责生成和解释,执行 SQL 或诊断命令仍然要在本地终端自己跑。

配完这轮之后,最踏实的感觉不是路由函数写得多优雅,而是看板上每个模型都有一笔清楚的 token 记录。chitchat 走短请求、长文摘要走大 token、默认问答走另一条模型,三条线在同一个 Base URL 下各记各的账,这才算把四年 Flask 的手感和 LLM-Chat-API 的计费真正接上了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询