有几位读者在看了我关于GeminiOmni首个版本的API密钥如何在17分钟内被爬取的文章后,希望我分享完整的代理架构。本文正是为此而写。
如果你正在构建任何需要调用付费AI API的应用程序——无论是Gemini、OpenAI、Anthropic、Replicate,还是任何按用量计费的密钥服务——下面这个模式就是让你免于在某个周二早晨被账单警报惊出一身冷汗的关键。它并不复杂,但至关重要。
核心模式图解
无论你使用哪家AI供应商,核心架构都是一致的:
┌─────────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ 浏览器 │ │ 你的API路由 │ │ 供应商 │
│ (不可信) │ ───→ │ (服务端, 持有密钥) │ ───→ │ (Gemini / │
│ │ │ │ │ OpenAI...) │
│ - 提示词 │ │ - 身份认证 │ │ │
│ - 上传文件 │ │ - 速率限制 │ └──────────────┘
│ │ │ - 配额检查 │
│ │ │ - 附加API密钥 │
│ │ │ - 调用供应商 │
│ │ │ - 返回结果 │
│ │ ←─── │ │
└─────────────────┘ └──────────────────────┘关键在于:浏览器只看到输入提示词和输出结果。它完全不知道调用了哪个模型、API密钥是什么、上游供应商长什么样、或者这次调用花了多少钱。这才是浏览器应该拥有的信息量。
严格意义上的"正确"做法
要让这个架构真正保护你,必须满足四个条件。任何一条缺失,防护就会崩塌。
1. API密钥只能存在于服务端
用Next.js的话来说:环境变量绝对不能以NEXT_PUBLIC_开头。在运行时层面:只有运行在路由处理程序(app/api/.../route.ts)、服务端组件或服务端action中的代码才能接触密钥。
// app/api/ai/generate/route.ts — 仅服务端
const GEMINI_KEY = process.env.GEMINI_API_KEY; // ✅ 仅服务端
if (!GEMINI_KEY) throw new Error('GEMINI_API_KEY 未设置');在启动时检查GEMINI_API_KEY是否存在,缺失则让应用崩溃,这个检查的价值堪比黄金。最糟糕的失败模式是应用在缺少密钥时静默运行,并回退到某个默认值,最终掏空你的积蓄。
2. 浏览器绝不能看到原始供应商响应
当供应商调用返回后,你必须精心挑选返回给浏览器的内容。浏览器不需要知道:
- 具体调用了哪个模型(Veo 3.1 Fast vs Veo 3.1 Standard)
- 供应商的请求ID(对技术支持有用,但对客户端无用)
- 供应商返回的任何速率限制头信息
- 调用的token数量或成本
为什么这很重要:信息泄露会叠加。如果浏览器能看到"model: gemini-2.5-flash, tokens: 4096",爬虫就能将你的前端与供应商的定价页面关联起来,反向推导出你的单位经济模型。虽不致命,但也绝非好事。去掉它。
// 只返回UI所需的数据
return Response.json({
imageUrl: result.generated_images[0].url,
watermarked: !user.isPro,
// 不要返回:模型名称、使用的token数、原始供应商响应
});3. 身份认证、速率限制和配额检查必须在供应商调用之前执行
代理不仅仅是数据通道,它还是你执行规则的地方:
// 在API路由中,按顺序执行:
1. 对请求进行身份认证(会话cookie、JWT等)。
匿名请求允许使用免费功能,但需按IP追踪。
2. 按用户ID(或匿名用户的IP)进行速率限制。
GeminiOmni: 匿名用户每分钟10次,Pro用户60次,Team用户200次。
3. 检查用户对特定操作的配额。
免费用户每月只能生成5次视频——必须在昂贵的API调用之前
查询数据库,而不是之后。
4. 现在——终于可以调用供应商了。
5. 成功后增加使用计数器。顺序很重要。如果你先调用供应商,然后再检查配额,那么无论用户是否有权限调用,你都已经为推理支付了费用。每次配额检查都必须提前完成。
4. 代理必须拥有自己的可观测性
记录代理发出的每一次供应商调用,包含:时间戳、用户ID、路由、模型、输入大小、输出大小、延迟、成功/失败状态以及大致美元成本。这是你的账单收据和调试界面。
await logUsage({
userId,
route: '/api/ai/generate',
model: 'gemini-2.5-flash-image',
inputBytes: prompt.length + imageBytes,
outputBytes: result.length,
latencyMs: Date.now() - start,
costUsd: estimateCost(model, inputBytes, outputBytes),
status: 'success',
});当某个周二早晨的账单警报响起,显示过夜支出翻倍时,你需要用这张表进行SQL查询,找出是哪个用户、哪个路由、哪个模型导致的。没有它,你只能盲目猜测。
生产环境中遇到的边界情况
运行代理一个月后,发现了一些不那么直观的问题。
流式响应需要流式代理。 Gemini的streamGenerateContent端点会返回部分生成结果的流。如果你在代理中简单地调用response.json(),你会缓冲整个流,从而失去延迟优势。解决方案是将上游流通过ReadableStream管道传输到你的代理中,这意味着你的API路由返回new Response(stream)而不是Response.json(...)。值得这样做——视频生成的感知延迟会从"等待90秒"降至"立即看到有东西在动"。
大文件上传需要直接上传到存储。 如果用户上传一个50MB的视频用于图生视频生成,你不希望这50MB通过你的Next.js API路由传输。模式是:客户端通过预签名URL直接将文件上传到Cloud Storage / R2,然后只将URL发送给你的代理。代理将该URL传递给Gemini的"媒体URI"参数,而字节数据永远不会触及你的服务器。这将上传密集型路径的出口流量成本降低约95%。
上游的错误信息需要翻译。 Gemini会返回诸如"PROMPT_BLOCKED"或"QUOTA_EXCEEDED"或"INVALID_ARGUMENT"等错误。浏览器不需要看到这些字符串——它们令人困惑,并且会泄露供应商的细节信息。将每个错误翻译成用户友好的消息:"您的提示词被安全过滤器拦截"、"您本月的免费生成额度已用完"、"该图片似乎已损坏"。一个三行代码的查找表即可解决。
成本估算基于每次调用,而非每个token。 我曾试图通过统计token数来估算每次调用的美元成本。然后我注意到Veo是按输出的秒数计费,Imagen是按图像张数计费,而Nano Banana 2则根据分辨率有四个价格等级。代理的成本函数是一个switch语句,而不是一个乘法运算。没有简便的方法绕过这一点——每个供应商的计费方式不同,你必须为每个供应商单独编码。
在GeminiOmni代码库中的实际体现
/tools/text-to-video、/tools/image-to-video、/tools/nano-banana-edit、/tools/pdf-chat以及/chat的实时聊天中的所有模型调用,都经过app/api/ai/generate/route.ts和app/api/ai/query/route.ts这两个端点。两个端点加起来大约400行代码。上述四个检查在每次供应商调用离开我的服务器之前都会被强制执行。
我在用户界面中展示了模型名称,但不在API响应中展示。每个生成片段下方的徽章显示"Veo 3.1 Fast"——这来自渲染时的服务端查找,而不是API响应。这种事听起来有点傻,直到你意识到API返回的任何字符串都可能在一张截图之间成为攻击目标。
诚实的总结
代理模式并不令人兴奋。里面没有什么巧妙的算法。它只是在公共互联网上运行付费AI API时,避免因未付费用户的调用而亏损所需的最低要求。
但正是这个"最低要求"。我认识的每一位在没有它的情况下推出产品的独立开发者,都以同样的方式学到了教训:一个尴尬时刻的账单警报,一次惊慌失措的密钥撤销,以及一个漫长的夜晚重写数据流。第一天就做好它,比事后补救要便宜得多。
继续阅读
—— Lena
