中间件应仅拦截 /api/chat、/api/image/generate 等真实调用 AI 服务的路由,排除 /health、/auth/login 等非计费路径及 OPTIONS 预检请求;优先通过路由名判断而非 URL 路径;计费统计宜置于 API 网关后、业务服务前;需为不同服务商实现统一 BillableResponse 接口以适配 token 提取逻辑。
只对真正调用 AI 接口的路由做计费统计,比如 /api/chat、/api/image/generate 这类明确由前端发起、后端转发给第三方 AI 服务的请求。别把健康检查 /health、登录 /auth/login 或静态资源也包进去——这些不产生费用,统计反而污染数据。
常见错误是直接在全局中间件里无差别记录所有请求,结果发现账单对不上,因为日志里混进了大量非计费调用。
$request->route()->getName() 或 ThinkPHP 的 Route::current()->getName() 判断路由名更可靠,比靠 URL 路径匹配容错性高不同服务商返回结构差异大:openai 在响应头带 x-ratelimit-remaining 和 x-ratelimit-reset,但计费核心得看响应体里的 usage;anthropic 返回 content 字段不带 token 数,得靠 usage.input_tokens 和 output_tokens;qwen(通义千问)则把 usage.total_tokens 放在顶层。
不能写死解析逻辑,否则换一个服务商就得改中间件代码。
立即学习“PHP免费学习笔记(深入)”;
BillableResponse,要求各服务商适配器实现 getInputTokens()、getOutputTokens()、</li><li>中间件里不做 JSON 解析,只把原始响应体传给对应适配器——避免中间件承担格式转换职责</li><li>若响应失败(HTTP 4xx/5xx),仍需记录失败事件:有些服务商按“调用次数”收费(如早期 Azure OpenAI 某些 SKU),不是只算成功请求</li></ul><H3>计费数据写入时为什么不能直接 INSERT</H3><p>高并发下多个请求几乎同时完成,直接往数据库 <code>INSERT计费记录,容易因主键冲突或唯一索引报错(比如同一秒内多个请求用了相同 trace_id)。更糟的是,如果中间件里还顺手更新用户余额表,事务锁表会拖慢整个 API 链路。
计费是事后审计行为,不是业务强依赖流程。
LPUSH 把原始计费事件(含 timestamp、model、tokens、cost、request_id)暂存为队列,格式用 JSON 字符串artisan schedule:run 或 Swoole Worker 定时消费队列,批量写入 MySQL 或 ClickHousesleep() 或重试逻辑——超时会传染到前端,必须保证中间件执行时间 最有效的办法是拿真实流量跑 A/B 对照:一边走老逻辑(SDK 内部手动埋点),一边走新中间件,对比两套数据的 total_tokens 总和与请求量。偏差超过 0.5% 就得查。
容易被忽略的是异步调用场景——比如用户上传文件后,后端用 dispatch(new AiImageJob()) 异步生成图,这种请求不在 HTTP 生命周期内,中间件根本捕获不到。
handle() 开头显式调用 BillingRecorder::recordAsync(...),传入 job_id 替代 request_idbilling.enabled,上线初期先设为 false,用日志输出模拟计费数据,确认字段齐全再开写入source 字段(值为 middleware 或 job),方便后续归因