踩坑3天,我终于搞懂为什么OpenRouter数据会"偷偷坑"你的多云路由方案

0 阅读3分钟

背景:OpenRouter数据突变,我的路由权重全乱了

上周四晚上,我的模型调用监控告警突然响——Claude Sonnet 4.6的调用量跳了,DeepSeek相关接口响应有点慢,整体Token消耗比预期高了30%。

去查了一下,原来是GPT-6发布后,OpenRouter平台整体流量结构发生了变化,Claude Sonnet 4.6冲上了调用量榜首,带动了我路由策略里一个没想到的副作用:权重配置是按历史流量比例写死的,平台调用格局一变,我的路由就不准了。

就是这个小失误,让我重新审视了一遍自己的多模型路由方案,踩了三个坑,记录下来。


技术方案:动态权重路由 + 调用量感知

技术原理科普:为什么需要多模型路由?

现在主流AI模型各有擅长场景——GPT-6胜在超长上下文(200万Token)和多模态,Claude Sonnet 4.6胜在代码和合规类长文档,DeepSeek V3(V4即将发布)胜在中文和超低成本(约GPT-6的1/70)。

硬写一个模型的系统,就像只带一把锤子出门,什么都往钉子上想。

我的路由方案:

import httpx
import asyncio
from dataclasses import dataclass
​
@dataclass
class ModelConfig:
    name: str
    base_url: str
    api_key: str
    cost_per_1k_tokens: float
    max_context: int
    priority: intclass DynamicRouter:
    def __init__(self):
        self.models = {
            "claude-sonnet-4.6": ModelConfig(
                name="claude-sonnet-4.6",
                base_url="https://api.anthropic.com/v1",
                api_key="your_key",
                cost_per_1k_tokens=0.003,
                max_context=1000000,
                priority=1
            ),
            "deepseek-v3": ModelConfig(
                name="deepseek-v3",
                base_url="https://api.deepseek.com/v1",
                api_key="your_key",
                cost_per_1k_tokens=0.0002,
                max_context=128000,
                priority=2
            ),
        }
        self.failure_count = {}
    
    def select_model(self, prompt_length: int, task_type: str) -> str:
        """动态选择:超长文本→GPT-6,中文任务→DeepSeek,其余→Claude"""
        if prompt_length > 100000:
            return "gpt-6"
        if task_type in ["chinese_doc", "cost_sensitive"]:
            return "deepseek-v3"
        return "claude-sonnet-4.6"
    
    async def call_with_fallback(self, model: str, prompt: str) -> dict:
        try:
            return await self._call(model, prompt)
        except Exception as e:
            self.failure_count[model] = self.failure_count.get(model, 0) + 1
            fallback = self._get_fallback(model)
            print(f"[WARN] {model} 失败({e}),切换到 {fallback}")
            return await self._call(fallback, prompt)
    
    def _get_fallback(self, failed: str) -> str:
        fallback_map = {
            "claude-sonnet-4.6": "deepseek-v3",
            "deepseek-v3": "claude-sonnet-4.6",
            "gpt-6": "claude-sonnet-4.6"
        }
        return fallback_map.get(failed, "claude-sonnet-4.6")

踩坑记录

坑1:OpenRouter 返回的模型名称和原生API不一致

我在 OpenRouter 统一接口里调用 DeepSeek,模型名是 deepseek/deepseek-chat,但直连 DeepSeek API 是 deepseek-chat。两边不统一,切换逻辑里的 model_name 映射没处理好,导致fallback静默失败——它跑完了,但实际调用的是上一次缓存的模型。

排查方式

# 打印每次请求的实际model参数
curl https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_KEY" \
  -d '{"model": "deepseek/deepseek-chat", "messages": [{"role":"user","content":"test"}]}' \
  | python -c "import sys,json; r=json.load(sys.stdin); print(r.get('model','unknown'))"

坑2:Claude Sonnet 4.6的usage字段偶发缺失

在高并发场景下(并发>20),Claude的response里 usage.output_tokens 偶尔返回 null,导致我的成本计算直接报 TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

修复

# 安全获取token用量
def safe_get_usage(response: dict) -> int:
    usage = response.get("usage") or {}
    return usage.get("output_tokens") or usage.get("completion_tokens") or 0

环境准备

我自己的多云账号(AWS、阿里云、DeepSeek、Anthropic)是通过 Ztopcloud 聚合管理的,企业级结算账期比较灵活,省了很多逐个平台充值的麻烦,V4出来之后应该也会第一时间上架。

如果你的团队也在做多模型路由,建议先把账号管理这层做稳,再搞路由策略,否则某个账号欠费了整个链路就断了。


小结

中美AI调用量这周反转,对普通用户来说是个新闻,对做基础设施的人来说是个提醒:不要把路由逻辑和某一家平台或某一周的调用量格局绑死。 动态路由、健壮的fallback、安全的字段解析,这三件事做好了,外部格局怎么变都不怕。

DeepSeek V4本周极有可能发布,建议提前准备好灰度切换脚本,别等着出了问题再追。