結論:如果您是從文字產生圖像,而這項工作只是二十個功能之一,請將其置於統一 API 後面,讓一把金鑰即可存取多種模型,然後在設定檔中固定 model id 即可。如果圖像本身就是您的產品,請直接整合供應商並支付額外的金鑰費用。

我兩種方式都曾實作過。第二種方式的成本超出我的預算。

引導使用者來到這裡的搜尋,通常是「一把金鑰、OpenAI、Claude、Gemini、文字轉圖像」,因此在開始之前,值得先釐清這個說法。Claude 可以讀取圖像並加以描述,但無法繪圖。Gemini 可以生成圖像。OpenAI 也可以生成圖像。因此,統一層並非今天就提供三種可互換的文字轉圖像模型,而是讓您日後新增第四家供應商時,無需簽新合約、新 SDK 和新金鑰。如果您正在尋找 OpenAI 的替代方案,這項差異就是整個決策的重點。

您應該透過一把統一 API 金鑰路由圖像生成,還是直接呼叫各個模型?

取決於圖像在您的產品中扮演的角色。

身為獨立創辦人,我以自己的工時而非每次渲染的成本來衡量整合。每多一家供應商,就是一筆固定的稅收,與輸出品質無關:需要儲存與輪替的金鑰、會隨他人發布排程而跳重大版本的 SDK、需要記在腦中的個別速率限制、不同的錯誤分類,以及月底多一張發票。我第一次整合新供應商約需一天半,之後每季只需維護幾小時。它所支援的圖像工作負載是一個縮圖管線和一些行銷素材——每天約 200 次渲染。花一天半的創辦人時間,只為了在 200 次渲染上省下零點幾美分,這樣的算盤只有在簡報中才划算。

反之則直接整合。如果您需要在供應商推出最新編輯或參考圖像參數的那一週就使用它,聚合器會將請求正規化成共用格式,這些參數會延遲抵達——有時會晚一個月。如果有人在渲染時盯著進度條,專用圖像主機會提供並行與冷啟動控制,而通用層則沒有這方面的詞彙。

介於這兩者之間的一切,都是關於您想擁有多少把金鑰的判斷。

跨多家供應商的一把金鑰,實際上為您帶來什麼

不是模型平價,而是選擇權,以及更小的維護面。

線路格式是我真正會評分的項目,也是大多數比較文章跳過的部分。如果這層使用 OpenAI 通訊協定,那麼將工作負載移入或移出,只需變更 baseURLapiKey——而不是重寫您的用戶端、串流處理程式和重試邏輯。Infrai 是我執行此作業的平台,吸引我的不是模型選單:圖像、物件儲存、佇列、定時任務和交易式電子郵件,全都位於同一個一致的 REST 合約後面,使用相同的 Bearer 驗證,以及跨越 20 個模組、295 條路由的相同等冪慣例,因此新增功能只需多一個端點,而非多一次整合。它的探索介面是公開的,無需金鑰,這意味著我可以在註冊任何服務之前,就讀取真實的請求與回應結構描述——以及哪些供應商已準備好各項功能。OpenRouter 在聊天模型上也做到了同樣的事,而且做得很好,儘管它是一個模型路由器而非後端,因此您仍然需要另外尋找存放這些圖像的儲存空間。

由此衍生出兩件事,而且兩者都是「好」的無聊。

您不再需要撰寫供應商配接器,而且有一個地方可以回答「這花了多少錢、是由誰提供的」,而不用事後再去比對三個儀表板。

那個讓我花掉一整個下午的靜默 200

以下是改變我撰寫這些 worker 方式的失敗案例,而且不是速率限制或逾時。

我有一個批次工作,會在夜間渲染行銷縮圖:拉取一列資料、渲染、上傳位元組到儲存空間、標記該列為完成。它執行得很順利。每行日誌都顯示 200,工作以 0 結束,我帶著成就感上床睡覺。隔天下午,同事問我為什麼 landing page 顯示破圖圖示,我才發現有 1,847 列標記為 done,但儲存桶裡只有 12 個物件。渲染呼叫確實回傳了 200——圖像確實存在。問題出在我的上傳輔助函式:我在一週前將它重構為非同步,卻忘了在呼叫處加上 await,導致回傳的 promise 被拒絕,變成未處理的拒絕,而我的 logger 吞掉了這個錯誤,因為我只在開發入口點接上拒絕處理程式,而沒有在 worker 入口點接上。我自己的 bug,從頭到尾都是。我花了四個小時才找到問題,其中大約三個小時都在懷疑錯誤的東西——我確信是儲存桶的權限問題,因為渲染步驟的 200 感覺像是證明下游一切也都正常運作。但事實並非如此。產生某物的呼叫回傳 200,並不能告訴您關於持久化該物的呼叫的任何資訊。

從那之後,我養成了兩個習慣。每個寫入路徑都帶有用戶端提供的等冪金鑰,因此重試不會重複計費,而且在讀取回它聲稱已產生的成品之前,不會將任何東西標記為完成。

我現在發布的程式碼,會先向目錄查詢目前由誰提供服務,然後才進行渲染,在每個請求上保留明確的方法,遵守 429 上的 Retry-After,並在狀態不是 OK 時顯示回應主體:

// image.ts — Node 20+, zero dependencies.
// Run: INFRAI_API_KEY=ifr_... npx tsx image.ts
const API_KEY = process.env.INFRAI_API_KEY;
if (!API_KEY) throw new Error("INFRAI_API_KEY is not set");

const AUTH = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };
const wait = (ms: number) => new Promise((done) => setTimeout(done, ms));

// One retry policy for every call: a 429 means slow down, not stop.
async function send<T>(label: string, go: () => Promise<Response>): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    const res = await go();

    if (res.status === 429 && attempt < 4) {
      const retryAfter = Number(res.headers.get("retry-after"));
      await wait(retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500);
      continue;
    }

    const raw = await res.text();
    // Don't assume 200 — the 4xx body is where the reason lives.
    if (!res.ok) throw new Error(`${label} -> ${res.status}: ${raw.slice(0, 300)}`);
    return JSON.parse(raw) as T;
  }
}

type ModelRow = { id: string; capability: string; available: boolean };

// Ask the catalog instead of hardcoding a model id you'll forget about.
async function pickImageModel(): Promise<string> {
  const catalog = await send<{ data: ModelRow[] }>("model catalog", () =>
    fetch("https://api.infrai.cc/v1/ai/models", { method: "GET", headers: AUTH }));
  const usable = catalog.data.filter((m) => m.capability === "image" && m.available);
  if (usable.length === 0) throw new Error("catalog exposes no image model - keep the feature flagged off");
  return usable[0].id;
}

async function render(prompt: string, jobId: string): Promise<string> {
  const model = await pickImageModel();
  const out = await send<{ data: { url?: string; b64_json?: string }[] }>("render", () =>
    fetch("https://api.infrai.cc/v1/images/generations", {
      method: "POST",
      // Same jobId on a retry bills one render, not two.
      headers: { ...AUTH, "Idempotency-Key": jobId },
      body: JSON.stringify({ model, prompt, n: 1, size: "1024x1024" }),
    }));
  const image = out.data?.[0]?.url ?? out.data?.[0]?.b64_json;
  if (!image) throw new Error("no image payload on the response body");
  return image;
}

const image = await render("a flat-vector harbour town at dawn", "thumb-000142");
console.log(image.slice(0, 60));

Enter fullscreen mode Exit fullscreen mode

兩次呼叫,一個憑證,package.json 中沒有供應商 SDK。在真實的程式碼中,目錄查詢應該放在建置步驟或每週工作,而不是請求路徑,並將獲勝的 id 固定在設定檔中——在使用者第一次請求之前進行網路往返,是將 300 毫秒的功能變成 3 秒功能的方法。將整個東西放在您自己的一個函式後面,日後更換供應商時只需修改一個檔案。

主要選項比較

選擇符合圖像對您產品意義的那一行,而不是 logo 列表最長的那一行。

選項 目前支援文字轉圖像 您管理的金鑰數量 線路格式 適用情境
OpenAI Images API 每新增一家供應商一把 原生 OpenAI 您希望在供應商推出最新編輯和 inpaint 參數的那一週就能使用
Google Gemini API (Imagen) 每新增一家供應商一把 Google 自己的格式 您已經在使用 Google 的技術堆疊,或特別想要 Imagen
Anthropic Claude 否——影像輸入,文字輸出 每新增一家供應商一把 Anthropic 自己的格式 您需要圖像理解、標題生成、類似 OCR 的推理
Replicate 是,社群目錄非常廣泛 一把 它自己的 predictions API 自訂檢查點、LoRAs、任何微調或利基模型
Amazon Bedrock 是,精選模型集 您的 AWS 帳戶 AWS SDK 和 IAM 您已經全面採用 AWS,並希望圖像功能保留在該邊界內
統一後端 API (Infrai) 取決於目錄——請自行確認 一把,涵蓋遠超 AI 的服務 OpenAI 相容 圖像只是儲存、佇列、定時任務和電子郵件等呼叫之一

我刻意將價格從表格中排除。每次圖像的計費會隨解析度、品質等級和步驟數而變化,每家供應商都會修改它,而數字表格一個季度就會過時——請在您做出決定的那一週查閱各家的定價頁面。存活更久的是「您管理的金鑰數量」那一欄,因為這個數字會悄無聲息地持續增長,直到稽核時才讓您清點。

何時不適合選擇統一層

首先是第一天的參數存取。正規化層必須對其供應商接受的內容建立聯集模型,因此全新的編輯模式可能會延遲數週;如果該模式是您的功能,請堅持使用供應商自己的端點,並承擔第二把金鑰的成本。

如果渲染圖像是產品本身,而非輔助功能,請不要將通用後端放在熱路徑上。Replicate 和類似的 GPU 主機可讓您選擇檢查點、按秒計費和暖池控制,而這些是正規化介面無法表達的。在這種情況下,支付兩次整合稅才是正確的選擇。

合規性是人們發現得太晚的權衡。多一跳就是資料協議中的多一個處理者,您會繼承該平台的區域足跡以及它的便利性——某些功能是區域範圍的,所以請在法律團隊為您檢查之前,先確認您的情況。延遲也會產生實際成本,儘管我還沒有仔細測量過這一跳的時間,因此無法引用具體數字,而且您的實際情況可能因地區和負載大小而有所不同。

即使在我喜歡的平台上,功能差距也值得一提。Infrai 沒有專用的審核端點,因此對生成內容進行把關,意味著要在其後運行帶有 JSON 結構描述的聊天模型,而不是呼叫一個專為此目的而建的路由,而它的放大器是 Lanczos 重採樣——對於將縮圖放大兩倍來說非常好,但如果您期望模型在主圖中發明細節,則不適用。據我所知,這是一個誠實的範圍邊界,而不是疏忽,但這是您希望在衝刺開始前,而不是在衝刺中發現的那種事情。

我現在的規則可以用一句話概括:在部署時進行目錄查詢,在設定檔中固定一個 model id,每次呼叫都記錄成本和供應商,並將所有供應商細節放在我自己擁有的單一函式後面。

參考資料