边缘代理与 AI 网关:一次 LLM 请求怎么被截获并转发
30 秒导读: Helicone 的可观测性能看到你每一次 LLM 调用,前提是这些调用先流过它。这一章讲的就是「流过」的前半程——一条请求打到 Helicone 的边缘 Worker,如何被接住、认出是哪个组织、选好去哪个提供商,然后转发出去。打出去之后怎么把日志留下来,是下一章的事。
本章属于 Helicone 系列的第 01 章,只讲「请求进来 → 打给 provider」这半程,它是整套可观测性的数据入口。想先看全景请回到 index.md。
1. 这是什么(零基础也能懂)
一句话定义: Helicone 的「边缘代理」是一个跑在 Cloudflare Workers 上的中间人——你本来直接调 api.openai.com,现在把地址换成 Helicone 的域名,请求先到 Helicone,它转发给 OpenAI,顺手把这次调用记下来。
它解决什么问题。 假设你有个 App 在调 GPT-4,你想知道:花了多少钱、慢不慢、谁在用、出没出错。直接调 OpenAI 你什么都看不到。最省事的办法:改一个 base URL,让流量绕道 Helicone。
用起来就是这么轻:
# 示意,非源码:接入 Helicone 只改一行 base_url
from openai import OpenAI
client = OpenAI(
base_url="https://oai.helicone.ai/v1", # 原本是 https://api.openai.com/v1
default_headers={
"Helicone-Auth": "Bearer sk-helicone-xxx", # 告诉 Helicone 你是谁
},
)
# 之后照常调用,请求会先经过 Helicone 再到 OpenAI
resp = client.chat.completions.create(model="gpt-4", messages=[...])
一句话直觉: 把 Helicone 想成你和大模型之间的一道收费站。车(请求)必须过站,过站时拍个照、记个账(可观测性),然后照常放行到目的地(真实 provider)。这一章讲的就是「进站到出站」这一段路。
两种接法,先建立心智模型。 Helicone 的边缘其实提供了两类差别很大的入口:
| 接法 | 你给的地址 | 谁决定去哪个 provider | 典型场景 |
|---|---|---|---|
| 经典代理(proxy) | 按 provider 分的子域名,如 oai.helicone.ai | 域名/请求头就锁定了(你自带 key) | 只想加一层观测,少改代码 |
| AI 网关(AI Gateway) | 统一入口 ai-gateway.helicone.ai | Helicone 按模型名选路、可回退多家 | 想要多 provider 路由、按量计费、故障转移 |
两者前半程的差别就是本章的主线;但你会看到一个漂亮的收束:它们最后都汇到同一个转发函数。
2. 顶层全景(半程怎么转)
先看这半程的骨架。怎么读这张图:从上到下是一条请求的时间顺序,左边是"经典代理"路、右边是"AI 网关"路,两条路在底部汇合。
请求打到 *.helicone.ai
│
▼
┌──────────────────────────────────┐
│ ① Worker 入口 (index.ts fetch) │
│ 看子域名 → 定 WORKER_TYPE │ modifyEnvBasedOnPath
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ ② 封装请求 (RequestWrapper) │
│ 拆鉴权头、认出组织、备好 body │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ ③ 选路由 (buildRouter/WORKER_MAP) │
└───────────────┬──────────────────┘
经典代理 │ AI 网关
┌──────────────┴──────────────┐
▼ ▼
④a proxyForwarder ④b SimpleAIGateway.handle
(定 api_base、缓存、 (拆模型串、建 attempts、
限流、安全审查) 逐个尝试 + 回退)
│ │
│ 每个 attempt 也回落到
│ gatewayForwarder → proxyForwarder
└──────────────┬───────────────┘
▼
┌──────────────────────────────────┐
│ ⑤ callProvider:真正 fetch 出去 │
│ 拼目标 URL、剥 helicone-* 头 │
└──────────────────────────────────┘
│
▼
真实 Provider
(捕获日志 → 见第 02 章)
各部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
| Worker 入口 | 唯一的 fetch 处理器;按 host 判定这台 Worker 现在扮演哪种网关 | worker/src/index.ts:391 fetch |
modifyEnvBasedOnPath | 看子域名把 WORKER_TYPE(和转发目标)写进 env | worker/src/index.ts:51 |
RequestWrapper | 把原始 Request 焊成可反复读写的对象;拆鉴权、定位组织、管 body | worker/src/lib/RequestWrapper.ts:67 |
buildRouter / WORKER_MAP | 用 WORKER_TYPE 查出对应的路由工厂 | worker/src/routers/routerFactory.ts:124、:26 |
proxyForwarder | 经典代理链路:定目标、缓存、限流、安全审查,再转发 | worker/src/lib/HeliconeProxyRequest/ProxyForwarder.ts:45 |
SimpleAIGateway | AI 网关编排器:拆模型、建尝试列表、逐个试并回退 | worker/src/lib/ai-gateway/SimpleAIGateway.ts:55 |
callProvider | 两条路的终点:拼出真实 URL、剥掉内部头,fetch 打给 provider | worker/src/lib/clients/ProviderClient.ts:103 |
主线走一遍(高层): 请求到 fetch → RequestWrapper.create 封装并鉴权 → modifyEnvBasedOnPath 认出网关类型 → buildRouter 选对路由 → 经典代理走 proxyForwarder、AI 网关走 SimpleAIGateway.handle(内部每次尝试又落回 proxyForwarder)→ 最终 都进 callProvider 打出去。下面逐段拆开。
3. 入口:一个 Worker 如何"分身"成多种网关
要解决的小问题: Helicone 想用同一份 Worker 代码同时服务 oai.helicone.ai(OpenAI 代理)、anthropic.helicone.ai、ai-gateway.helicone.ai、gateway.helicone.ai 等一堆入口。怎么让一份代码知道"我这次是哪种网关"?
思路: 看请求的子域名。整个 fetch 只做四件事,顺序很重要。
真实入口非常薄:
// worker/src/index.ts:391 默认导出的 fetch 处理器(节选)
const requestWrapper = await RequestWrapper.create(request, env); // ② 先封装+鉴权
env = await modifyEnvBasedOnPath(env, requestWrapper.data); // ① 按 host 定 WORKER_TYPE
if (env.WORKER_DEFINED_REDIRECT_URL) {
return Response.redirect(env.WORKER_DEFINED_REDIRECT_URL, 301); // 裸访问首页 → 重定向
}
const router = buildRouter(env.WORKER_TYPE, request.url.includes("browser")); // ③ 选路由
return router.handle(request, requestWrapper.data, env, ctx).catch(handleError);
"看子域名定类型"的逻辑 在 modifyEnvBasedOnPath(worker/src/index.ts:51)。它把 host 按 . 拆开,看第一段 hostParts[0]:
| 子域名首段(示例) | 判定的 WORKER_TYPE | 含义 |
|---|---|---|
oai | OPENAI_PROXY | OpenAI 经典代理 |
anthropic | ANTHROPIC_PROXY | Anthropic 经典代理 |
ai-gateway | AI_GATEWAY_API | 统一 AI 网关(本章重点) |
gateway | GATEWAY_API | 通用直通网关(自带 target) |
api | HELICONE_API | Helicone 自家 API |
generate | GENERATE_API | 生成类 API |
对应源码是一长串 if / else if:
// worker/src/index.ts:111 按子域名首段分流(节选)
if (hostParts[0].includes("ai-gateway")) {
return { ...env, WORKER_TYPE: "AI_GATEWAY_API" };
} else if (hostParts[0].includes("gateway")) {
return { ...env, WORKER_TYPE: "GATEWAY_API" };
} else if (hostParts[0].includes("oai")) {
return { ...env, WORKER_TYPE: "OPENAI_PROXY" };
} else if (hostParts[0].includes("anthropic")) {
return { ...env, WORKER_TYPE: "ANTHROPIC_PROXY" };
}
// ... 还有一大票厂商子域名走 GATEWAY_API + 写死 GATEWAY_TARGET
三个值得记住的细节:
- 本地开发能"钉死"类型。 如果
env.WORKER_TYPE已经有值(比如wrangler dev --var WORKER_TYPE:OPENAI_PROXY),modifyEnvBasedOnPath直接原样返回,不再看域名(worker/src/index.ts:102-104)。 - 一大批第三方厂商子域名(
groq、mistral、deepseek、bedrock等)统一归为GATEWAY_API,并顺手把转发目标写进GATEWAY_TARGET(如hostParts[0]==="groq"→https://api.groq.com,worker/src/index.ts:219)。这是"通用直通",不同于本章后面讲的智能 AI Gateway。 - EU 数据驻留在这里切换。
request.isEU()为真时,整份 env 被替换成 EU 版的数据库/队列/S3 配置(worker/src/index.ts:81-101),保证欧洲请求不落美区。
从类型到路由: buildRouter 拿 WORKER_TYPE 去查 WORKER_MAP 这张表,取出对应的路由工厂:
// worker/src/routers/routerFactory.ts:26 类型 → 路由工厂 的映射表
const WORKER_MAP = {
ANTHROPIC_PROXY: getAnthropicProxyRouter,
OPENAI_PROXY: getOpenAIProxyRouter,
HELICONE_API: getAPIRouter,
GATEWAY_API: getGatewayAPIRouter,
AI_GATEWAY_API: getAIGatewayRouter, // ← AI Gateway 入口
CUSTOMER_GATEWAY: (router) => { /* 客户自建网关,按 host 查 target */ },
// ...
};
到这里,请求已经被交给了正确的路由。接下来分两条路讲:先看 RequestWrapper 这个所有路都要用的公共对象,再分别看两条转发路。