用云函数代理 OpenAI / Anthropic 等海外 LLM API
一句话定义:在 CloudBase Web 云函数(HTTP 触发) 里写一个 OpenAI 兼容协议的薄代理,让前端拿不到 真实 API key,同时把 SSE 流式响应原样透传给浏览器。
预计耗时:30 分钟 | 难度:进阶
适用场景
- 已有一个 CloudBase 环境,想给前端/小程序/Run 服务加 LLM 能力,但服务器在境外、key 又不能放前端
- 想统一切换底层模型(OpenAI / Anthropic / DeepSeek / 通义 / 混元)而不影响前端代码——前端只对一个内部 endpoint 说话
- 需要把 OpenAI 协议作为事实标准接入层,方便后续接 Vercel AI SDK 等上层框架
不适用:
- 只调用国内模型(混元、通义)的,直接用 SDK 即可,不需要绕一层代理
- 想做 prompt 编排、retrieval 等业务逻辑,那是上层 chatbot 该做的事,本篇只管"透传 + 鉴权"
环境要求
| 依赖 | 版本 |
|---|---|
| Node.js(云函数运行时) | ≥ 18(自带 fetch,老版本需要 node-fetch) |
@cloudbase/cli | latest |
| 云函数类型 | Web 云函数(HTTP 触发) —— 普通事件函数无法做 SSE 流式 |
| 公网出口 | 云函数默认无固定公网出口;调用海外 API 需要在控制台「网络 → 公网访问」启用 |
需要准备:
- 一个海外 LLM 的 API key(OpenAI、Anthropic、DeepSeek 任 一;DeepSeek/通义/混元天然走 OpenAI 兼容协议,最简单)
- 一个用于校验调用方的 token(最朴素就是一个 32 字节的随机串,下文叫
PROXY_ACCESS_TOKEN)
第一步:初始化 Web 云函数项目
mkdir llm-proxy && cd llm-proxy
npm init -y
npm install --save express
package.json 里 main 改成 index.js,并确保有 start 脚本:
{
"name": "llm-proxy",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
Web 云函数本质是一个监听 9000 端口的 HTTP 服务,CloudBase 平台启动时会执行 npm start。这里用 Express 是因为它对响应头/流式 res.write 的控制最直接。
第二步:写代理代码(OpenAI 兼容协议 + SSE 透传)
index.js:
const express = require('express');
const app = express();
app.use(express.json({ limit: '10mb' }));
// 上游配置全部走环境变量,代码里不留任何 key
const UPSTREAM_BASE_URL = process.env.UPSTREAM_BASE_URL || 'https://api.openai.com/v1';
const UPSTREAM_API_KEY = process.env.UPSTREAM_API_KEY;
const PROXY_ACCESS_TOKEN = process.env.PROXY_ACCESS_TOKEN;
if (!UPSTREAM_API_KEY || !PROXY_ACCESS_TOKEN) {
console.error('Missing UPSTREAM_API_KEY or PROXY_ACCESS_TOKEN env');
process.exit(1);
}
// 简单鉴权:校验 Authorization: Bearer <PROXY_ACCESS_TOKEN>
function requireAuth(req, res, next) {
const auth = req.headers.authorization || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
if (token !== PROXY_ACCESS_TOKEN) {
return res.status(401).json({ error: 'unauthorized' });
}
next();
}
// /v1/chat/completions 透传到上游
app.post('/v1/chat/completions', requireAuth, async (req, res) => {
const isStream = req.body && req.body.stream === true;
let upstream;
try {
upstream = await fetch(`${UPSTREAM_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${UPSTREAM_API_KEY}`,
},
body: JSON.stringify(req.body),
});
} catch (err) {
console.error('upstream fetch failed', err);
return res.status(502).json({ error: 'upstream_unreachable', message: err.message });
}
// 上游返回非 2xx,把状态码和 body 原样透传(便于前端排错)
if (!upstream.ok) {
const text = await upstream.text();
return res
.status(upstream.status)
.type(upstream.headers.get('content-type') || 'application/json')
.send(text);
}
if (isStream) {
// SSE 流式:把上游字节流原样写回客户端
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders?.();
const reader = upstream.body.getReader();
req.on('close', () => {
reader.cancel().catch(() => {});
});
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
}
} catch (err) {
console.error('stream pipe error', err);
} finally {
res.end();
}
return;
}
// 非流式:整体读完返回 JSON
const json = await upstream.json();
res.json(json);
});
app.get('/health', (_req, res) => res.json({ ok: true }));
const PORT = process.env.PORT || 9000;
app.listen(PORT, () => {
console.log(`llm-proxy listening on ${PORT}`);
});
几个容易踩的点:
- 必须用 Web 云函数(HTTP 触发),事件型云函数没法做长连接 SSE
res.flushHeaders()在 Express 4 里存在但是可选;调用一次能确保浏览器尽早收到响应头开始解析 SSE- 上游
upstream.body是 Web Stream(ReadableStream),用getReader()拉取;如果你用 axios 而不是fetch,得换写法,axios 默认会把整个响应缓冲住 req.on('close')监听客户端主动断开(用户关页面),要记得reader.cancel(),否则上游连接会一直挂着到超时
第三步:部署到 CloudBase 并配环境变量
部署:
tcb login
tcb fn deploy llm-proxy --httpFn -e your-env-id
部署完到控制台「云函数 → llm-proxy」做三件事:
- 环境变量 里加:
UPSTREAM_BASE_URL:以 OpenAI 为例填https://api.openai.com/v1;DeepSeek 填https://api.deepseek.com/v1;Anthropic 走 OpenAI 协议 时填https://api.anthropic.com/v1(注意 Anthropic 原生 API 不完全兼容,多数情况推荐用支持 OpenAI 协议的网关或 SDK 的@anthropic-ai/sdk)UPSTREAM_API_KEY:你的真实 key(不要写到package.json或代码里)PROXY_ACCESS_TOKEN:随机生成 32 字节字符串,例如node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
- 网络配置,开启「公网访问」(云函数默认能访问公网,但部分环境/地域配置不同,以控制台实际选项为准)
- 触发方式,确认「HTTP 访问服务」已启用,记下访问 URL,类似
https://your-env.service.tcloudbase.com/llm-proxy
第四步:本地验证
非流式请求:
curl -X POST 'https://your-env.service.tcloudbase.com/llm-proxy/v1/chat/completions' \
-H 'Authorization: Bearer YOUR_PROXY_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-4o-mini",
"messages": [{ "role": "user", "content": "用一句话介绍 CloudBase" }]
}'
预期:返回标准 OpenAI 格式的 JSON,含 choices[0].message.content。
流式请求:
curl -N -X POST 'https://your-env.service.tcloudbase.com/llm-proxy/v1/chat/completions' \
-H 'Authorization: Bearer YOUR_PROXY_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-4o-mini",
"stream": true,
"messages": [{ "role": "user", "content": "数到 5" }]
}'
预期:每隔几十毫秒打印一行 data: {"id":...,"choices":[{"delta":{"content":"..."}}]},最后一行是 data: [DONE]。curl -N 关掉缓冲,否则你会看到一坨数据一次性吐出来——那不是函数错了,是 curl 在攒。
鉴权层选型对照
文中用的是最朴素的「共享 token」,足够覆盖小流量场景。生产环境按需要升级:
| 方案 | 适用 | 缺点 |
|---|---|---|
| 共享 token(本文) | 内部应用、白名单前端 | token 一旦泄露要全员换;前端代码里如果硬编码了 token,等于裸奔 |
走 CloudBase 用户认证,前端带 access_token,云函数里用 @cloudbase/node-sdk 校验 | 真有"用户"概念的应用 | 需要前端先登录;云函数里要解析 ID token |
| API Gateway + 签名 | 对外开放 API、需要按调用方计量 | 复杂度高,本文 scope 之外 |
常见错误
| 错误信息 | 原因 | 修复 |
|---|---|---|
getaddrinfo EAI_AGAIN api.openai.com | 云函数没有公网出口或地域限制 | 控制台开启公网访问;或者在网络配置里挂出 口 NAT |
| 流式请求一直没数据,10 秒后才一次性出现 | 中间某层在缓冲(CDN、代理、curl 没加 -N) | 客户端用 -N;代码里加 res.flushHeaders();确认上游确实开了 stream: true |
401 unauthorized | 前端忘带 Authorization: Bearer 或 token 写错 | 比对前后端两边的 PROXY_ACCESS_TOKEN;注意控制台改了环境变量后函数需要重新部署或重启实例 |
502 upstream_unreachable | 上游 API 域名解析失败 / TLS 握手失败 | 确认 UPSTREAM_BASE_URL 末尾不要带 /,且不要写成 https://api.openai.com(少了 /v1) |
| 函数运行 30 秒后断流 | 函数最大执行时长默认 30 秒 | 控制台 → 函数配置 → 超时时间,按需调到 60-900 秒;过长的对话仍建议拆成多次调用 |
Anthropic 原生 API 报 unknown field "messages.role.user" | Anthropic 原生协议不是 OpenAI 协议 | 要么换支持 OpenAI 协议的 Anthropic 兼容网关;要么把代理代码替换成 Anthropic SDK 调用,路径相应改 /v1/messages |
错误码可在云函数日志里看到完整堆栈,控制台「日志」面板支持按 requestId 过滤。
相关文档
- SSE 协议支持 — Web 云函数的 SSE 流式响应原理
- HTTP 云函数 — Web 云函数(HTTP 触发)的快速上手
- 函数环境变量 — 配置注入和读取
- 函数超时时间 — 长连接相关配置项
- 调用云函数 — 云函数调用方式与 HTTP 访问服务总览
下一步
把这层代理跑起来后,建议接着做:
add-vercel-ai-sdk-streaming-chatbot— 用 Vercel AI SDK 做前端 chatbot,把 baseURL 指向本篇的代理 URLadd-rag-with-pgvector-cloudbase— 在代理之上加检索增强,让模型基于你自己的文档作答secure-secrets-in-cloud-function— 把UPSTREAM_API_KEY等敏感值的本地开发/CI/生产分层管好,避免一不小心进 git