使用 Cloudflare Workers 搭建轻量级 LLM API 网关

2025-03-04

在这个 LLM 服务商们每天都在进行军备竞赛的时代,对我们这种面向 LLM 编程的程序员来说,最常见的一个痛点莫过于管理一大堆 API Endpoint 和 Secret Key 了,再加上:

  • 不同的服务商 Host 了相同或不同的模型。
  • 在不同的服务商里都撒了币。
  • 不同服务商的 SLA 可能相去甚远。
  • 要在不同的软件里重复配置这些相同的内容。

这一系列问题的解决方案自然是一个统一的 LLM API Gateway,原理上看也并不复杂,只消维护 Model 和 API Provider 之间的映射规则,然后按需转发即可。

{"model": "claude"}
    ⬇️
LLM API Gateway
    ⬇️
{"model": "claude-3-7-sonnet-latest"}
    ⬇️            ⬇️
Anthropic    AWS Bedrock

实际上市面上也早已有成熟的开源方案,例如 songquanpeng/one-api。但对于我来说,部署这么一套管理系统显得有些太重了,作为个人使用,似乎不太需要额外的租户管理和账单系统,加之如果想要进行远端部署,产生额外的服务器、域名等维护成本也有些令人抗拒,所以“造轮子”似乎又成了最终的选择。

比起年轻气盛时动不动就想“万丈高楼平地起”地从零开始造轮子,成年人造轮子的哲学则是“应拼尽拼”,能用现成的预制零件快速拼出来的轮子也是好轮子——于是开始整理造这么一个私人 LLM API Gateway 的基本要求:

  • 易于维护,部署成本要尽可能的低,最好能用现成的 SaaS。
  • 开箱即用,使用和配置方式简单,即暴露一个统一的 API 接口,可以自由配置模型的转发映射。
  • 安全,有基本的鉴权以防止滥用。
  • 通用,适配主流的 LLM API Provider 格式。

首先,作为一个几乎无状态的 API Gateway,最核心的逻辑就是转发 HTTP 请求,所以立马出现在选品单上的就是 Cloudflare Workers,其提供的 Severless 应用部署非常适合写这种 Proxy,再加上配套的 Cloudflare Workers KV,转发配置需要持久化存储的需求也被满足了。于是我基于官方的 Rust SDK worker-rs 实现了 one-united,仅需简单的配置,就可以把一个轻量级的 LLM API Gateway 部署到 Cloudflare Workers 上。

部署方式

因为官方提供了非常齐全的配套,整个部署过程需要做的准备仅需提前安装上较新版本的 Rustnpm 即可。

首先把项目拉到本地,然后开始编辑我们的 wrangler.toml

git clone https://github.com/one-united/one-united.git
cp wrangler.example.toml wrangler.toml

其实这里要做的就是创建一个 KV namespace,一条命令就能搞定:

npx wrangler kv:namespace create config

运行成功后把输出中提供的 kv_namespaces 部分粘贴到 wrangler.toml 文件中即可,格式类似于:

[[kv_namespaces]]
binding = "config"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

然后就是用最后一条命令完成最终部署:

npx wrangler deploy

然后你的服务就跑在 https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev 上可供访问了!

配置文件

刚部署好的 one-united 自然是没有配置任何模型转发的,项目里提供了一个规则模板,其格式还是比较直观的,我以此为例展示一个我自己的使用场景,来看看具体如何配置自己的模型转发。

我最常用 LLM 的一个场景就是翻译,例如 Bob 的 AI 翻译服务。

可以看到我定义了一个名为 translator 的模型,其转发逻辑的配置如下:

rules:
  - model: translator
    providers:
      - identifier: oh-my-gpt
        models:
          - gpt-4o-mini
      - identifier: openrouter
        models:
          - openai/gpt-4o-mini
      - identifier: dailyio
        models:
          - gpt-4o-mini
          - meta-llama/Llama-3.3-70B-Instruct-Turbo

不难发现对于翻译服务,我使用的都是主流模型中 Token 价格较低的模型,如此一来在使用较为频繁的翻译场景下,可以在保证质量的前提下尽可能节省 Token。providers 的配置字段都比较直接,在此不表,按需添加和配置自己的提供商即可。

目前 one-united 还没有图形化界面,所以更新配置需要用 curl 直接把 config.json 扔给接口:

curl -X POST https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/config \
-H "Content-Type: application/yaml" \
--data-binary @config.yaml

一些提升体验的功能

在简单转发的基础上,我也根据平时使用的一些经验和习惯加了一些必要的功能和优化。

设置 API KEY

虽然说整个流程属于私人部署,但也不免存在接口泄漏的可能,避免被他人滥用导致 Token 额度被迅速消耗完,可以给自己的 Gateway 也设置上 API Key。这里有两个操作办法, 一个是直接通过命令行设置 ONE_API_KEY

npx wrangler secret put ONE_API_KEY

也可以到 Cloudflare 的 Workers 后台界面添加:

此后所有的 curl 请求都可以带上 -H "Authorization: Bearer $ONE_API_KEY" 进行鉴权了,同样,在类似 Chat Bot 的 API 配置中,也需要填上 $ONE_API_KEY 方可正常调用。

负载均衡

当同一个模型名配置了多个不同的 (Provider, Model) 映射时,为了保证尽可能好的延迟表现,每次会通过负载均衡机制在不同映射间进行切换,并记录每次请求的耗时,最终尽可能地选择转发至延迟较低的提供商。

目前这个策略还比较简陋,我还在考虑是否要添加诸如基于权重或者 Token 成本的负载均衡策略。

常用的接口

one-united 一共提供了以下几个接口:

WorkerRouter::new()
    .get_async("/config", get_config)
    .post_async("/config", save_config)
    .get_async("/stats", get_stats)
    .get_async("/v1/models", get_models)
    .post_async("/v1/chat/completions", route_chat_completions)
    .run(req, env)
    .await

其中 /v1/models/v1/chat/completions 都是 OpenAI 兼容的常用接口,后者不用说,就是最常用的 LLM 使用接入口。前者则是 List Models 接口,对于一些提供了自动获取模型信息功能的软件来说,可以方便的通过这个接口一键添加所有当前可用的模型信息:

前面一个小节提到过 one-united 存在负载均衡机制,通过 /stats 这个接口可以看到当前所请求 Workers 实例内的延迟统计信息,方便判断不同服务商的延迟表现如何:

❯ curl -s -H "Authorization: Bearer $ONE_API_KEY" https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/stats | jq
{
  "created_at": "2025-03-06T04:22:13.862Z",
  "hash": "362c5ee09afe8b5c82f132161496c00072ce850e3a39d204315bf823e8311de8",
  "latency": [
    {
      "identifier": "cf-openrouter",
      "model": "anthropic/claude-3.7-sonnet",
      "ms": 1473
    }
  ],
  "lifetime": "3m 42s 685ms"
}

OpenRouter 统计适配

如果你搭配 OpenRouter 使用的话,可以看到在 OpenRouter 的 Activity 界面上是可以识别到 one-united 转发来的请求标识的,方便掌握具体的用量。

这个也是根据 OpenRouter 官方的文档加的请求识别头来实现的:

// "HTTP-Referer" and "X-Title" will be used by service like OpenRouter to identify the request.
headers.set("HTTP-Referer", "https://github.com/JmPotato/one-united")?;
headers.set("X-Title", "one-united")?;

Bypass Rule

如果你想 Bypass 掉规则,直接请求对应 Provider 的某个模型,可以使用 model@@provider_identifier 这个语法,例如直接请求配置中来自 OpenRouter 的 GPT-4o mini:openai/gpt-4o-mini@@openrouter,请求就会直接发给 OpenRouter,而不会经过负载均衡中转,指哪打哪。