“OpenAI-compatible”不是一個服務有或沒有的開關。它是對一組行為的宣告,而這些行為分佈在三個獨立的層面:協議面,請求與響應的結構以及認證必須對齊;功能面,streaming、tool calling、結構化輸出和輸入模態各自有各自的結論;行為面,錯誤語義、限流和按模型區分的引數支援,決定一個整合能否經受生產環境的考驗。Compatible 也不等於 equivalent:兩個端點都可以配得上這個標籤,卻在你的應用所依賴的細節上並不相同。
本文逐層給出驗證方法,然後把所有內容壓縮成一份八項清單,你可以在一個下午內對任何 OpenAI-compatible 端點跑完,包括 https://www.string.ink/v1,如果那正是你的目標。
第 1 層:OpenAI-compatible 的協議面
OpenAI-compatible API 的最小公共面很小:一個以 /v1 結尾的 base URL、Authorization 頭裡的 Bearer token 認證、用於對話的 POST /v1/chat/completions,以及用於發現的 GET /v1/models。如果你的客戶端能說這四件事,它就能和這個端點開啟對話,而這正是大多數“compatible”宣告真正指的層面。
值得內化的一點是:Chat Completions 和 Responses API 是兩種不同的協議形態,而不是同一件事的兩個名字。Chat Completions 形態傳送一個 messages 陣列,並用 choices 陣列作答,多數第三方端點把它作為公共面來實現。Responses 形態使用另一套請求與事件詞彙。OpenAI 的遷移指南指出兩者在 function calling 形態上不同,結構化輸出在 Responses 形態裡用的是 text.format 而不是 response_format,同時也明確寫道:“While Chat Completions remains supported, Responses is recommended for all new projects.” 兩種形態都是當前形態;它們只是在線上協議層面不能互換。
對整合工作來說,這產生一條規則:讓客戶端傳送的形態與端點提供的形態相匹配。這個選擇往往在客戶端側。例如 Codex CLI 在其 provider 配置裡通過 wire_api 設定把它顯式化。所以評估一個端點時,先寫下你的客戶端說的是哪種形態,然後再驗證端點是否按那種形態作答。String AI 的平臺文件直接描述了它的相容面:base URL 固定為 https://www.string.ink/v1,認證遵循 OpenAI 的 Bearer 約定,介面列表覆蓋 chat completions、文本與視覺輸入、影像生成與編輯,以及模型列表。
第 2 層:功能面,逐項來看
協議一致只是入場券。差異是在功能上累積的,每一項都要單獨測試,而不是一概假設。
- 模型列表。呼叫 GET /v1/models,確認響應帶有 object "list" 和一個 data 陣列。你也在這裡複製要用的 model ID;文件裡的示例(例如 gpt-5.6-luna 和 gpt-6-astra)只是方向指引,即時列表才是事實來源。文件建議先用更輕的 gpt-5.6-luna 驗證鏈路,再切換到其他模型。
- 最小 chat completion。傳送一條 user 訊息,確認響應在 Chat Completions 形態該放的位置包含 assistant 的文本。這端到端驗證了請求路徑,僅此而已。
- Streaming。開啟 stream,讀 Server-Sent Events 序列:每個 chunk 到達時 object 為 "chat.completion.chunk",增量文本出現在 choices[].delta.content 下,最後通常有一個統計 chunk 帶 usage,流以 [DONE] 標記結束。絕不要因為非 streaming 能用就假設 streaming 也能用;事件序列是另一份實現。
- Tool calling。傳入 tools 和 tool_choice,確認模型以預期形態返回一個呼叫,然後把工具結果發回並拿到後續回答,完成整個往返。只發一次、從不迴環的呼叫會掩蓋最常見的 tool calling 缺陷。
- 結構化輸出。請求一個特定的 JSON 形態,並多跑幾次。問題不是文本能不能解析一次;問題是形態在多次執行和不同輸入下是否穩定。
- 視覺輸入。在訊息裡放一張圖,確認模型真的處理了它。文本和視覺輸入在這個端點上是一起走的,但這並不能說明它背後的每個模型都行;要按模型逐個驗證。
- 影像生成與編輯。這些是獨立的端點,不是 chat 功能。如果你的方案涉及生成或編輯影像,就單獨測這些路由;chat 端點做不了這件事。
每一條都是一個勾選框,在你親眼看到行為之前,框保持未勾選。每項兩分鐘,換來一份真正屬於你的相容性說明。
第 3 層:行為面
行為層在文件示例裡看不見,在生產裡卻是決定性的。把它當成一組以症狀為線索的探測來讀。
錯誤形態。401、403、404、model-not-found 和 429 的含義各不相同,客戶端常常需要區分它們才能正確反應。故意觸發每一種:錯誤的 key、畸形的路徑、錯誤的 model ID,以及足夠的量來看是否出現限流響應。記錄每一項的狀態碼和響應體形態。如果你的客戶端的重試邏輯依賴錯誤型別,這份記錄就是它執行的依據。
限流與併發語義。限流是實現行為,不是協議。弄清楚端點如何表示限流,以及它期望你的客戶端接下來做什麼,然後據此設定併發和退避。結果因廠商而異,也可能因 key 而異;把你自己的測量當作規範。
引數支援隨模型而異。temperature 類控制、與 reasoning 相關的引數,以及模態選項,在一個端點服務的每個模型上並不統一。旗艦模型接受的引數,可能在同一 base URL 的其他模型上被拒絕。絕不要假設引數是整個端點通用的;按模型設門檻,並針對你實際呼叫的每個模型驗證。
這一層的主線是:當宣告與觀察衝突時,觀察優先。寫下你看到了什麼、在哪個模型上、在哪個請求下,因為下一個除錯這個整合的人正需要這些上下文。
API 相容性清單
用這八項去跑你正在評估的端點。每一項都產出可記錄的產物,而不是一種感覺。
- 用 GET /v1/models 列出模型,並把當前的 model ID 複製進測試配置。如果這是你第一次呼叫,五步快速上手詳細覆蓋了這一步和下一步。
- 傳送一個最小 chat completion,只帶一條 user 訊息,並儲存原始響應。
- 開啟 stream 重跑一次,從第一個 chunk 走到終止標記。
- 執行一次完整的 tool calling 往返:呼叫、結果、後續回答。
- 在請求裡放一張圖,確認模型讀到了它。
- 請求嚴格的結構化輸出,並確認形態在多次執行中保持成立。
- 故意注入三種錯誤(錯誤的 key、錯誤的路徑、錯誤的 model ID),記錄每一種返回什麼。
- 記一條基線:每次檢查的原始響應加時間戳,再加上觀察到的響應時間,方便你以後重跑清單並對比。方法比這一次的數字更重要;留下產物,而不是印象。
任何端點或客戶端升級之後,以及切換 model ID 之後,都重跑整份清單。
compatible 被誤讀的三種方式
1. “它返回了 200,所以它是相容的。”一個最小 chat 請求返回 200,只證明了協議層,別的什麼都沒證明。streaming、tool calling、結構化輸出和各類模態各自獨立失敗,各自需要單獨通過。
2. “這個模型接受了這個引數,所以端點支援它。”引數支援是按模型而不是按端點。在你自己的檢查說出別的結論之前,把每個引數都當成模型特有的。
3. “示例用的是一種形態,我的客戶端用的是另一種。”文件和示例同時存在 Chat Completions 和 Responses 兩種形態。把 text.format 片段複製進 Chat Completions 客戶端,或者反過來,產生的錯誤看起來像不相容,其實是形態錯配。讓示例與你客戶端真正使用的協議對齊。
當相容還不夠時
一份誠實的相容性說明比標籤暗示的範圍更窄,只要所有人都知道這個寬度就沒問題。你記錄的結論是某一個端點實現加某一個客戶端版本的屬性;任何一方升級都可能改變它,所以上面的清單是為了反覆重跑而設計的,而不是用來信任的。當某項需求落在已驗證子集之外時,有意識地決定怎麼辦:改造客戶端、在同一端點上換一個模型,或者圍繞這個限制重新設計。
讓即時引用保持即時。任何平臺的能力都在向前走,平臺文件裡的介面列表是當前的地圖;而你的清單是你確認地形的方式。還有一個可以從運維借來的節奏:model ID 會變老,退役終會到來,所以也要規劃在模型變化後重跑這套流程。棄用應對手冊直接覆蓋了這個生命週期。
在功能列表之上是線上形態本身:Chat Completions 與 Responses 在端點、輸出和狀態處理上都不同,API 形態指南展示瞭如何測試一個端點真正實現的是哪一種。
相關閱讀
- 用一個 key 執行 Codex CLI、Claude Code 和 Cursor:把一個已驗證的端點應用到多個工具,而不是一個客戶端。
- 開發者文章:平臺的整合說明與配置指南。