有幾位讀者在看了我關於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
