Kimi K3 白嫖,Cloudflare Workers 立大功

Modal 的免费额度很慷慨:注册就送每月 $30 算力,而且它的 Endpoints 功能一条命令就能把开源大模型架成推理服务,闲时自动缩到零。拿它白嫖一个 Kimi K3,条件近乎完美。

但真接进日常工具,总觉得哪里都差半口气:官方确实提供了 OpenAI 兼容接口,能直连,可用起来别别扭扭。与其凑合,不如自己造一个顺手的。于是决定用 Cloudflare Workers 做一层转发,把 Modal 端点包成一个干干净净的 API。

流水账记录一下。照例,这篇由 Fable 5 代笔,哈哈。

一条命令,Kimi K3 就有了

Modal Endpoints 支持 Qwen、Kimi、DeepSeek、GLM、GPT-OSS 等一大票开源模型,部署真的只要一条命令:

1
modal endpoint create --model moonshotai/Kimi-K3

Modal 会自动解析模型、挑选推理引擎和 GPU 配方,然后给你一个专属端点,URL 长这样:

1
https://<workspace>--ep-kimi-k3-server.us-west.modal.direct

重点是计费方式:只按容器实际运行的算力收费,负载上来自动扩容,没人用就缩到零。对“每月 $30 额度、坚决白嫖”的个人用户来说,scale-to-zero 就是最重要的功能,没有之一。

官方直连:两个 token 拼一个 key

端点默认带鉴权,先创建一对 proxy token:

1
modal workspace proxy-tokens create

会得到一个 token ID(wk- 开头)和一个 secret(ws- 开头,只显示这一次,赶紧存好)。官方的用法很有意思:把两个 token 中间加一个点拼起来,当成一个普通的 API key 用:

1
Authorization: Bearer wk-<id>.ws-<secret>

而端点本身就提供 OpenAI Chat Completions 接口,路径在 /v1 下。所以理论上任何 OpenAI 兼容客户端都能直连:

1
2
3
4
5
6
7
8
9
10
11
from openai import OpenAI

client = OpenAI(
base_url="https://<workspace>--ep-kimi-k3-server.us-west.modal.direct/v1",
api_key="wk-<id>.ws-<secret>",
)

client.chat.completions.create(
model="moonshotai/Kimi-K3",
messages=[{"role": "user", "content": "你好呀"}],
)

官网还提供了一个统一入口 https://inference.us-west.modal.direct/v1,同一个拼接 key 也能用。

到这一步其实已经“能用”了,但真往日常工具里塞,就发现几个别扭的地方:

  • 模型名诡异。尤其走统一入口时,模型名不是干净的 kimi-k3,而是一长串带端点信息的名字。不少客户端会拿模型名做展示甚至能力判断,要么难看,要么直接出岔子,搞不好还得再垫一层名字转换。
  • 真凭证满天飞wk-/ws- 这对 token 是 workspace 级的门票,每个工具里都贴一份,哪天想换 token,得挨个客户端改一遍。
  • 单账号额度有限。$30 烧完或者端点被打挂,就只能干瞪眼。

我的方案:让 Cloudflare Worker 看大门

既然横竖要垫一层,干脆把这一层做成自己的“前台”:一个不到一百行的 Cloudflare Worker,对外只暴露我自己发的 API key,对内替我保管 Modal 凭证。

1
2
3
4
5
6
7
各种客户端
↓ Bearer 我自己发的 key
Cloudflare Worker(kimi-proxy)
↓ Modal-Key / Modal-Secret
Modal 主账号(用到底) → 备用账号(主账号出问题才接手)

Kimi K3 端点

这里有个恰到好处的细节:Modal 除了 Authorization: Bearer 的拼接写法,还支持把 token 拆成两个独立的请求头传过去:

1
2
Modal-Key: wk-...
Modal-Secret: ws-...

文档里说,这条通道就是为“Authorization 头要留给别的 token”的场景准备的。正好:Authorization 让出来放我自己的 key,Modal 凭证走专用头,两层鉴权互不打架。

主备账号和故障切换

Worker 里配了两个 Modal 账号(也就是两份每月 $30),但不做负载均衡:所有请求固定先走主账号,只有主账号自身出问题——凭证失效(401)、额度耗尽(402/403)、限流(429)、服务端错误(5xx)——才切到备用账号重试。至于 400 这类请求本身的错误,换个账号也一样失败,不折腾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function shouldFailOver(response) {
return [401, 402, 403, 429].includes(response.status) || response.status >= 500;
}

const [primary, backup] = accounts;
const retryRequest = request.clone();

const firstResponse = await fetch(createUpstreamRequest(request, primary));
if (!shouldFailOver(firstResponse)) {
return firstResponse;
}

await firstResponse.body?.cancel();
return fetch(createUpstreamRequest(retryRequest, backup));

一开始也想过随机分流,让两个账号平摊额度,后来想明白对这种场景是负优化,关键在缓存:

  • 前缀缓存要集中。聊天和 agent 类请求会反复携带同一段长前缀——系统提示词、越滚越长的对话历史,推理引擎会缓存这部分计算结果,命中了就不用重算。随机分流等于把请求拆到两个互不相通的端点上,缓存命中率直接腰斩,而每一次未命中,都是实打实按 GPU 时间计费的重新预填充,又慢又贵。
  • scale-to-zero 要成全。流量集中在主账号,备用账号的端点就能安安稳稳缩在零上,一分钱不烧;随机分流则是两边容器轮流被唤醒,闲置消耗直接翻倍。

所以现在的分工是:主账号用到底,备用账号纯待机,主账号限流或者抽风时才顶上,客户端全程无感。哪天主账号的 $30 真烧干了,多半也是以这几类账号级错误的面目出现,请求会自动落到备用账号头上——用到底,烧干自动换。

这里有一个小坑:请求的 body 是流,只能被读一次。第一次转发就把它消费掉了,等想故障切换时已经没有 body 可发了。所以要在首发之前 request.clone() 留个底,重试时用副本。另外失败响应的流记得 body?.cancel() 释放掉,不然它会一直占着连接。

部署

代码全部加起来九十行左右,部署就是标准 wrangler 流程,凭证全部塞进 secret:

1
2
3
4
5
6
wrangler deploy
wrangler secret put CLIENT_API_KEY # 自己生成一个长随机串
wrangler secret put MODAL_KEY
wrangler secret put MODAL_SECRET
wrangler secret put MODAL_KEY_2
wrangler secret put MODAL_SECRET_2

之后所有工具里的配置就统一成了:

1
2
3
base_url: https://kimi-proxy.<你的子域>.workers.dev/v1
api_key: 自己发的那个 key
model: moonshotai/Kimi-K3

上游换账号、换 token,甚至哪天把 Kimi K3 换成别的模型,客户端配置一个字都不用改。

算账环节

  • Modal:两个账号,每月各 $30 免费额度,端点闲时缩到零,零消耗
  • Cloudflare Workers:免费计划目前每天 10 万次请求,对个人使用是天文数字
  • 合计:¥0