拆解 Portkey Gateway,LLM 网关不是转发器
- Portkey Gateway 仓库数据存在差异:README 标注 250+ LLMs,网站宣传 1600+ LLMs,实际维护 2300+ 模型定价数据
- 核心不是简单转发:策略管道架构包含失败重试、熔断、负载均衡、缓存四层,可按请求内容动态路由到最优模型
- 12.3k Stars、1.2k Forks,MIT 协议,Go 语言实现,默认分支当天有代码提交显示项目活跃度高
- 定价数据是差异化优势:Portkey Models 独立维护 2300+ 模型的公开定价,解决 LLM 价格信息分散问题
7 月 3 日下午,我打开 Portkey Gateway 仓库时,最先看到的是几个有点打架的数字。
GitHub 描述里写着,它是一个面向 1,600+ LLM 的 AI Gateway。README 顶部又写着 250+ LLMs,后面还提到 Portkey Models 维护了 2,300+ 模型的开源 pricing。仓库页面显示约 12.3k stars、1.2k forks,MIT 许可,主语言是 TypeScript。
这些数字都很漂亮,但我不太想把它们当成重点。
因为 LLM Gateway 这个词已经被用得太满了。很多项目都能说自己支持多模型、统一 API、fallback、retry、logging。听起来差不多,直到你真的把请求从入口追到 provider。
Portkey Gateway 的核心问题不是「怎么把 OpenAI 请求转发给更多模型」。
它真正回答的是另一个问题。
当一个团队把模型调用放进产品、Agent、脚本、工作流和内部平台之后,一次请求到底应该被谁约束。
谁决定它能不能发出去。
谁决定它应该走哪个 provider。
谁决定失败之后是重试、换模型,还是直接拒绝。
谁记录这次调用命中了哪些 guardrail,花了几次 retry,最终去了哪个 option。
从这个角度看,Portkey Gateway 不是一条转发管道,而是一条策略管线。
从入口看,它先做的是收口
Portkey Gateway 的入口在 src/index.ts。
这里用了 Hono,路由覆盖了 chat completions、responses、embeddings、images、audio、files、batches、finetune、messages、realtime 这些 OpenAI 风格接口。/v1/chat/completions 这类请求进来以后,会先经过 requestValidator,再进入具体 handler。
这个文件里还有两个信号。
一个是 hooks middleware 被挂在全局请求路径上。另一个是 memoryCache 只在 conf.cache === true 时启用,Redis cache 也只在 Node runtime 且配置了 REDIS_CONNECTION_STRING 时打开。
这说明它没有把 gateway 当成纯 HTTP proxy 来写。
它先把入口统一成 OpenAI 兼容形状,再在同一条执行流里插入校验、hook、cache 和 provider adaptation。
本地启动入口在 src/start-server.ts。默认端口是 8787,--headless 可以关闭静态 UI。/public/ 控制台不是随便暴露的,如果 conf.json 没有配置 admin_token,它会直接返回提示,让你去看安全相关讨论。
这个细节很小,但它把 Portkey 的姿态讲清楚了。
它不是 demo server。
它默认假设 gateway 会站在模型密钥和真实流量前面,所以连本地 console 都要考虑访问边界。
真正的骨架在 tryPost
要理解 Portkey Gateway,不能只看 README 的 feature 列表,得看 src/handlers/handlerUtils.ts。
tryPost 是这条管线的核心。
一次请求进来后,它会创建 RequestContext、HooksService、ProviderContext、LogsService 和 ResponseService。这几个对象各管一段生命周期。
接着它先跑 beforeRequestHookHandler。如果 hook 判定 deny,请求不会打到 provider,而是返回 446,并把 hook 结果写进响应。要是 hook 修改了请求体,后面的 provider transform 会吃到修改后的 body。
然后 RequestContext.transformToProviderRequestAndSave() 会把统一请求翻译成 provider 请求。再往后才轮到 cache 查询、pre request validator、真实 fetch、响应映射、after request hook 和日志。
这条顺序很关键。
Portkey 不是先转发,失败后再补日志。
它先把一次模型调用变成一段可执行策略,只有策略允许之后,才会进入 provider 世界。
这一点和很多轻量级 proxy 不一样。轻量级 proxy 的主路径往往是 request in,provider out。Portkey 的主路径更像 policy in,provider out,audit back。
Guardrail 其实是 hook 的一种写法
Portkey 最有意思的地方,是它没有把 guardrail 写成硬编码分支。
src/handlers/handlerUtils.ts 里会把 input_guardrails、output_guardrails、default_input_guardrails、default_output_guardrails 转成 hook object。RequestContext 里也能看到,beforeRequestHooks 会合并显式 hook 和默认 input guardrails,afterRequestHooks 会合并显式 hook 和默认 output guardrails。
真正执行 hook 的地方在 src/middlewares/hooks/index.ts。
HooksManager 会创建一个 HookSpan,把 request json、request text、provider、metadata、streaming 状态和 request type 放进上下文。执行时,它通过 check.id 拆出 source 和 function,再调用 plugins[source][fn]。
插件注册表在 plugins/index.ts。
这里已经接入了 default、portkey、qualifire、patronus、pangea、promptfoo、azure、javelin、f5-guardrails、crowdstrike-aidr 等一批插件。default 插件里有 regexMatch、jsonSchema、contains、webhook、modelWhitelist、jwt、requiredMetadataKeys、allowedRequestTypes 这些基础检查。
这套设计的好处很直接。
请求是否包含敏感信息,响应是否违反安全策略,metadata 是否缺字段,模型是否在白名单里,都可以在同一个 hook 机制里表达。
你不需要把每个安全规则都写进 gateway 主流程。
你只需要把规则变成 check,让它在 before request 或 after request 阶段执行。
但这里也有边界。
plugins/README.md 写得很诚实,插件系统虽然被设计成可扩展,但当前重点还是 guardrails。也就是说,如果你期待它立刻变成任意 middleware marketplace,需要再看清楚接口和构建方式。它现在更像一套以 guardrail 为中心的策略扩展系统。
路由不是选 provider,而是递归执行策略
Portkey 的 routing 逻辑也不只是 if provider equals openai。
同一个 src/handlers/handlerUtils.ts 里,tryTargetsRecursively 会处理 fallback、loadbalance、single、conditional 四种 strategy。它会把上层继承来的 retry、cache、guardrails、hooks、custom host、request timeout 合并到当前 target,再决定下一跳。
fallback 策略会按 targets 顺序尝试。loadbalance 策略会读取 weight,计算总权重后随机挑选目标。conditional 策略会把 metadata、params 和 URL pathname 交给 ConditionalRouter,由条件规则解析出目标。
这个递归结构挺重要。
它允许你把策略嵌套起来。
外层可以按条件选择不同 target,内层再做 fallback 或 loadbalance。每一层还可以继承或覆盖 guardrail、retry 和 cache。
这就把 LLM routing 从「选一个 provider」提升成「执行一棵策略树」。
src/types/requestBody.ts 里的 StrategyModes 只列出了 loadbalance、fallback、single、conditional。这个枚举也提醒我们,Portkey 的开源 gateway 当前主要围绕这四类策略展开,不要把更复杂的实验策略想象成已经全部落地。
还有一个很具体的限制在 src/services/conditionalRouter.ts。
它支持 $eq、$ne、$gt、$gte、$lt、$lte、$in、$nin、$regex、$and、$or。但取上下文值时,只按点号拆两段,实际访问的是 value[parts[0]]?.[parts[1]]。
这意味着条件路由适合简单字段,比如 metadata 里的某个 key,或者 request body 的一层字段。你如果想用很深的嵌套 JSON 路径来做策略条件,就不能只凭直觉认为它会工作。
这是源码里很有价值的诚实边界。
一个 gateway 最危险的地方,不是功能少,而是策略表达看起来很强,实际匹配却和预期不一致。
Provider 适配被压进配置表
Portkey 支持 provider 的方式,也很配置化。
src/providers/index.ts 汇总了 openai、anthropic、azure-openai、bedrock、cohere、google、vertex-ai、mistral-ai、deepseek、groq、openrouter、ollama、dashscope、x-ai、replicate、z-ai 等 75 个 provider 目录。
真正的请求改写在 src/services/transformToProviderRequest.ts。
transformUsingProviderConfig 会遍历 provider config。每个参数都可以声明目标字段名、默认值、required、min、max 和 transform 函数。统一请求里的参数进入这里后,会被写入 provider 需要的嵌套字段。
如果 provider 有自己的 getConfig,Portkey 会按当前 params 和 provider options 动态拿配置。如果 endpoint 不支持,它会抛 GatewayError,明确告诉你某个 function 不被这个 provider 支持。
ProviderContext 再负责拿 headers、base URL、endpoint path、proxy path 和 request handlers。比如 Anthropic 的 proxy path 里还专门处理了 /v1/v1/ 这种路径重复。
这些代码说明,Portkey 没有幻想 provider 是同质的。
它承认每家模型服务都不一样,然后把差异压进 provider config、request handler 和 response handler。
这也是 AI Gateway 工程里最脏、最难、也最不容易被 README 讲清楚的部分。
支持 provider 的数量当然重要,但更重要的是,新增 provider 时差异被放在哪里。
如果差异散落在主流程里,gateway 会越来越不可维护。
Portkey 的选择是让主流程维持生命周期顺序,把 provider 差异放到配置和 provider context 里。
Retry 和 cache 不是装饰
Gateway 的可靠性不只来自 fallback。
src/handlers/retryHandler.ts 用 async-retry 做请求重试,并且支持 request timeout。超时会返回 408。它只对配置里的 status code 重试,遇到 429 时还可以跟随 provider 的 retry header。
但它没有无限信任 provider。
如果 retry-after 大于 MAX_RETRY_LIMIT_MS,或者超过剩余 retry 窗口,它会跳过 provider retry header。并且 async-retry 配置里 randomize 是 false,这意味着重试节奏是确定的,不带 jitter。
这带来一个取舍。
确定性有利于可预测和测试,但在大规模并发流量里,缺少 jitter 可能会让一批请求在同一时间再次打向 provider。生产环境如果非常依赖 retry,需要结合上游限流、队列或外部流量整形一起看。
cache 也类似。
CacheService 会排除 uploadFile、listFiles、batch、finetune、imageEdit 等不适合缓存的 endpoint。命中缓存时,它还会把 before request hook 的结果写入 response,并根据 hook 是否失败把状态设置成 246 或 200。
这不是单纯为了省钱。
它让缓存命中也保留策略执行痕迹。
对一个 gateway 来说,这个细节很重要。否则你会遇到一个奇怪的问题,真实请求经过了 guardrail,缓存响应却像从旁门出来的一样,审计链断掉了。
它和 LiteLLM 的分野
前面我刚拆过 LiteLLM。
如果说 LiteLLM 的强项是把 provider translation、Python SDK、proxy、预算、team、virtual key 这些能力揉成一个很完整的 LLM 控制面,那么 Portkey Gateway 的气质更偏向边缘运行时里的策略执行器。
它用 TypeScript 和 Hono 写,package 里同时提供 workerd 开发、Workers 部署、Node 开发和 Node 启动脚本。README 强调 122kb 和小于 1ms latency,这些表述都指向一个目标。
这层 gateway 要轻,要能跑在接近流量入口的位置。
它不试图在开源仓库里把所有企业后台都塞进去。pre request validator、cache backend、plugin credentials、local console、hosted enterprise features 被留在不同边界上。
README 里有些能力后面带了星号,代表 hosted 或 enterprise 版本。比如 semantic caching、provider optimization、prompt templates 等能力,不应该默认理解为开源本地 gateway 全部开箱即得。
这不是缺点。
这是产品边界。
开源 gateway 负责执行请求路径上的关键策略,Portkey 的商业平台负责更完整的控制台、分析、治理和团队协作。
你评估它的时候,不能只问开源仓库有没有某个 SaaS 功能。更应该问,你要的是一条可自托管的请求策略管线,还是一个全套 LLMOps 平台。
诚实边界
Portkey Gateway 值得关注,但它不是银弹。
README 的模型数量口径在 250+、1,600+、2,300+ 之间切换。这里更合理的读法是,Portkey 生态覆盖面在扩大,但具体到某个 provider、某个 endpoint、某个模型能力,仍然要回到 provider config 和实际测试。
release 状态也要看清楚。
GitHub 页面仍把 2026 年 1 月 12 日的 v1.15.2 标为 latest release。README 顶部同时说 Gateway 2.0 处于 pre-release,并引导到 2.0.0 分支。
这说明项目还在推进,但稳定 release、主分支、2.0 预发布之间有时间差。
如果你准备把它放进生产链路,不要只看 star。至少要做三件事。
把你的真实模型请求跑一遍,包括 streaming、tool calls、图片、embedding 和错误响应。
把你的 guardrail 和 conditional routing 规则写成测试,尤其是深层 metadata 和嵌套 JSON 条件。
把 retry、fallback、cache、provider rate limit 放到压测里一起看。
Gateway 最怕的不是请求失败。
最怕的是策略以为自己生效了,实际没有生效。
我会怎么使用它
如果我是一个小团队,刚开始只是在几个应用里调用 OpenAI 和 Anthropic,我不会急着上 Portkey Gateway。
官方 SDK 加一点封装就够了。
但一旦模型调用变成平台级能力,情况就变了。
只要你开始面对多 provider key、多团队预算、多模型 fallback、用户级 metadata、prompt 安全、输出审核、审计日志和本地部署诉求,gateway 就不是多余的一层。
它是你把模型调用从「代码里的函数调用」提升到「组织里的基础设施」时,必须出现的那层。
Portkey Gateway 的价值就在这里。
它用一个很轻的 TypeScript runtime,把请求入口、策略树、hook/guardrail、provider transform、retry/cache 和响应映射串起来。
你可以不同意它的每一个实现选择,比如 conditional routing 的路径能力、retry 的 jitter 策略、插件系统当前更偏 guardrail。
但这套代码至少给了一个清晰答案。
LLM Gateway 的第一性原理,不是支持更多模型。
是让每一次模型调用,都经过一条可解释、可组合、可审计的策略管线。
评论互动