OpenRouter 中转也救不了:X-Forwarded-For 透传让境外模型在国内依然 403
项目需要一个 LLM 备选通道:主力模型偶尔不稳定时自动切换,不能让用户感知。选了 OpenRouter——一个聚合了几十家模型的统一接口,看起来很完美:账号在美国,服务器也在美国,从国内请求过去应该没问题吧?
结果部署上去第一次冒烟测试就挂了。
现象
从国内云服务器(阿里云北京 region)通过 OpenRouter 调任何一个美系厂商模型,稳定收到:
HTTP 403
{"error":{"message":"This model is not available in your region.","code":403}}
试了 google/gemini-2.5-flash-lite、openai/gpt-4o、anthropic/claude-*,全部 403。
第一反应:是不是 OpenRouter 账户配置问题?打开 Privacy 设置,把 Allowed Providers 逐一确认——配置没问题。重新调,还是 403。
根因
OpenRouter 有一个平台级的抗滥用机制:把客户端的真实 IP 通过 X-Forwarded-For 请求头原样透传给上游模型 API。
请求链路实际是这样的:
国内服务器(中国 IP)
→ OpenRouter 美国机房(OpenRouter 自己接受这个请求)
→ 上游 API(OpenAI / Google / Anthropic),Header 里带着:
X-Forwarded-For: <你的中国大陆公网 IP>
→ 上游基于 X-Forwarded-For 做 region 检查 → 拒绝 → 403
OpenRouter 自己虽然在美国,但它不替你隐藏来源 IP。上游厂商拿到的不是 OpenRouter 的 IP,而是你服务器的 IP。
经过 OpenRouter 的请求,上游(OpenAI / Google / Anthropic)看到的 X-Forwarded-For 仍然是你的国内服务器 IP。等价于直连 api.openai.com——同样 403。这是 OpenRouter 平台故意设计的抗滥用机制,没有开关可以关。
正解
从国内部署的服务直接放弃调美系模型,改用中国厂商系模型——它们通过 OpenRouter 路由时上游不做 region 限制:
| OpenRouter 路径 | 是否可用 |
|---|---|
| qwen/qwen3.6-flash(阿里通义) | ✅ |
| moonshotai/kimi-k2.6(Moonshot) | ✅ |
| z-ai/glm-5(智谱 GLM) | ✅ |
| google/gemini-* | ❌ |
| openai/gpt-* | ❌ |
| anthropic/claude-* | ❌ |
实测 qwen/qwen3.6-flash 立刻 200,把上面的 openai/gpt-4o 换回去,还是 403——这步反验很重要,可以排除 key 或账户问题,锁定根因就是 region 拦截。
验证命令:
# 正验:应该 200
curl https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer sk-or-v1-..." \
-H "Content-Type: application/json" \
-d '{"model":"qwen/qwen3.6-flash","messages":[{"role":"user","content":"ping"}],"max_tokens":8}'
# 反验:应该 403
curl https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer sk-or-v1-..." \
-H "Content-Type: application/json" \
-d '{"model":"openai/gpt-4o","messages":[{"role":"user","content":"ping"}],"max_tokens":8}'如果业务上非美系不可
如果必须用 GPT / Claude,唯一可行方案是在服务器端架一层真正的代理出口,让 OpenRouter 上游看到的 X-Forwarded-For 是美国 IP:
- 开一台美国 ECS,跑 shadowsocks-rust 服务端
- 国内部署机跑 sslocal,同时监听 SOCKS 和 HTTP 两个 local 端口:
{
"locals": [
{ "protocol": "socks", "local_address": "127.0.0.1", "local_port": 51080 },
{ "protocol": "http", "local_address": "127.0.0.1", "local_port": 51081 }
]
}- Node.js 服务端 fetch 时走 HTTP CONNECT 代理(undici
ProxyAgent):
import { ProxyAgent } from 'undici'
const dispatcher = new ProxyAgent('http://127.0.0.1:51081')
fetch(url, { dispatcher } as any) // dispatcher 不在 lib.dom 类型里,需要 as any注意:undici 没有内置 SOCKS dispatcher,必须走 HTTP 端口(51081),不能直接用 SOCKS(51080)。
国内服务器 → OpenRouter → 中国厂商模型(Qwen / Kimi / GLM)可以直接通,不需要任何代理。非要调美系就必须在服务端搭真实的境外出口,让上游拿到的 X-Forwarded-For 是境外 IP,OpenRouter 本身的中转解决不了这个问题。
一句话外卖
OpenRouter 是模型路由聚合层,不是 IP 匿名化层——它的 X-Forwarded-For 透传是设计,不是 bug,国内部署选模型时直接跳过所有美系厂商。