文字模型與串流
用 OpenAI、Anthropic 或 Google Gemini 的官方格式呼叫文字模型,處理好對話、工具、串流與費用。
先選可呼叫的文字模型
從已認證 /api/v1/models?modality=text&includeSchema=1 讀取模型。只有 enabled 與 available 均為 true 才提交;工具、多模態訊息、推理欄位和結構化輸出必須同時被該模型的 inputSchema 支援。協定相容不意味著所有模型都具備所有能力。
同一個模型 ID 可以用下面任何一種協定呼叫,和模型出自哪家無關:用 Anthropic 格式呼叫其他廠商的模型、用 Gemini 格式呼叫非 Google 的模型都可以。
支援的協定
四種文字協定共用同一把 API Key、同一份模型目錄,以及同一套速率限制、參數驗證與計費,差別只在請求與回應的格式。選你現有程式碼或 SDK 已經在用的那一種即可。
| 協定 | 入口 | 放金鑰的標頭 |
|---|---|---|
| OpenAI Chat Completions | POST /v1/chat/completions | Authorization: Bearer |
| OpenAI Responses | POST /v1/responses | Authorization: Bearer |
| Anthropic Messages | POST /v1/messages | x-api-key 或 Authorization: Bearer |
| Google Gemini | POST /v1beta/models/{model}:generateContent | x-goog-api-key 或 Authorization: Bearer |
| Google Gemini 串流 | POST /v1beta/models/{model}:streamGenerateContent | x-goog-api-key 或 Authorization: Bearer |
| 原生串流 | POST /api/v1/jobs/stream | Authorization: Bearer |
/v1 與 /v1beta 的回應和錯誤遵循各自協定,不帶原生的 {code,msg,data,request_id} 信封,不要用只會讀取 body.data 的用戶端解析它們。Gemini 格式把模型 ID 放在網址路徑裡,ID 中的斜線原樣保留,不需要編碼。
金鑰只放在請求標頭。x-api-key 與 x-goog-api-key 只在 /v1 與 /v1beta 生效,原生 /api/v1 只認 Authorization: Bearer。網址查詢參數裡的 ?key= 一律不接受:金鑰一旦進入網址,就會留在瀏覽器歷史、代理伺服器與存取日誌裡。官方 SDK 都用請求標頭傳金鑰,不受影響。
用官方 SDK 串接
只換 Base URL 和金鑰,其餘照各 SDK 的正常用法。
| SDK | Base URL |
|---|---|
OpenAI(openai) | https://api.spicyapi.ai/v1 |
Anthropic(anthropic) | https://api.spicyapi.ai |
Google GenAI(google-genai) | https://api.spicyapi.ai |
Anthropic 與 Google 的 SDK 會自己補上 /v1 或 /v1beta,因此 Base URL 不帶版本路徑;OpenAI SDK 的 Base URL 則要帶 /v1。下面三段 Python 範例都從環境變數讀取 SPICY_API_KEY,以及從即時目錄選出的 SPICY_MODEL。
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai/v1")
reply = client.chat.completions.create(
model=os.environ["SPICY_MODEL"],
messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
)
print(reply.choices[0].message.content)import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ["SPICY_API_KEY"], base_url="https://api.spicyapi.ai")
message = client.messages.create(
model=os.environ["SPICY_MODEL"],
max_tokens=256,
messages=[{"role": "user", "content": "Explain a rainbow in one sentence."}],
)
print(message.content[0].text)import os
from google import genai
from google.genai import types
client = genai.Client(
api_key=os.environ["SPICY_API_KEY"],
http_options=types.HttpOptions(base_url="https://api.spicyapi.ai"),
)
response = client.models.generate_content(
model=os.environ["SPICY_MODEL"],
contents="Explain a rainbow in one sentence.",
)
print(response.text)要避免網路重試造成重複扣費,為每次使用者動作產生一個 Idempotency-Key 並持久儲存,用 SDK 的自訂標頭選項帶上(OpenAI 與 Anthropic 的 SDK 是 extra_headers)。
各協定的參數怎麼對應
協定層的欄位會先換成平台統一的參數名,再按所選模型的 inputSchema 驗證。模型不支援的參數回 400,不會被靜默忽略後照常計費。下表只說明對應關係,「—」表示該協定沒有對應欄位;某個模型收不收某個參數、取值範圍多少,以它的 inputSchema 為準。
| 統一參數 | Chat Completions | Responses | Messages | Gemini |
|---|---|---|---|---|
messages | messages | instructions、input | system、messages | systemInstruction、contents |
max_tokens | max_tokens、max_completion_tokens | max_output_tokens | max_tokens | generationConfig.maxOutputTokens |
temperature | temperature | temperature | temperature | generationConfig.temperature |
top_p | top_p | top_p | top_p | generationConfig.topP |
tools | tools | tools(function 類型) | tools(自訂工具) | tools[].functionDeclarations |
tool_choice | tool_choice | tool_choice | tool_choice | toolConfig.functionCallingConfig |
response_format | response_format | text.format | — | generationConfig.responseMimeType 加 responseSchema 或 responseJsonSchema |
reasoning_effort | reasoning_effort | reasoning.effort | — | generationConfig.thinkingConfig |
表外的欄位同樣換成統一參數後交給模型驗證,inputSchema 裡沒有的回 400。例如模型不收 stop 時,Chat 的 stop、Messages 的 stop_sequences、Gemini 的 generationConfig.stopSequences 都會回 400。目前沒有任何一個文字模型的 inputSchema 列出 seed,所以 Chat 的 seed 與 Gemini 的 generationConfig.seed 同樣回 400。
幾個例外:
- 只對協定原本所屬平台有意義的欄位會被忽略,不影響生成:Chat 的
user、metadata、store,Responses 的user、metadata、store、include,Messages 的metadata。 - Responses 不支援
previous_response_id,需在input裡帶上完整歷史。 - Gemini 的
safetySettings會被接受但不生效;cachedContent,以及googleSearch、codeExecution這類在伺服器端執行的工具不支援,回 400。圖片可用inlineData(base64)或fileData.fileUri(https 網址)傳入,模型是否收圖以 inputSchema 為準。
傳送 Chat 串流
先在自己的伺服器端設定 SPICY_API_KEY、從即時目錄取得的 SPICY_MODEL,以及為本次業務動作持久儲存的 SPICY_IDEMPOTENCY_KEY。範例需要 jq;提示詞只是範例,可替換為自己的業務輸入。模型參數仍須先通過目前 Schema 驗證。
jq -n --arg model "$SPICY_MODEL" '{
model: $model,
messages: [{role: "user", content: "Explain a rainbow in one sentence."}],
stream: true
}' > chat-request.json
curl --fail-with-body --no-buffer --max-time 120 \
https://api.spicyapi.ai/v1/chat/completions \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data-binary @chat-request.json傳送 Gemini 請求
環境變數與上一節相同。模型 ID 寫在網址裡,請求本文不帶 model;網址用雙引號包起來,並寫成 ${SPICY_MODEL},免得 shell 把後面的冒號當成變數修飾符。
jq -n '{
systemInstruction: {parts: [{text: "Answer in one sentence."}]},
contents: [{role: "user", parts: [{text: "Explain a rainbow."}]}],
generationConfig: {temperature: 0.7, maxOutputTokens: 256}
}' > gemini-request.json
curl --fail-with-body --no-buffer --max-time 120 \
"https://api.spicyapi.ai/v1beta/models/${SPICY_MODEL}:streamGenerateContent?alt=sse" \
-H "x-goog-api-key: $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
--data-binary @gemini-request.json不需要串流時把 :streamGenerateContent?alt=sse 換成 :generateContent,從 candidates[0].content.parts[].text 讀取回答,用量在 usageMetadata。串流請求不帶 alt=sse 時,回應是一個逐步寫出的 JSON 陣列;官方 SDK 會自己帶上 alt=sse。
對話與工具呼叫
Chat 的非串流回應讀取 choices[0].message;串流則逐塊讀取 choices[].delta,不是逐 TCP 分片解析 JSON。工具參數可能分多塊回傳,按 index 組裝完整 tool_calls 後再驗證和執行,保留 assistant 訊息並用對應 tool_call_id 回填結果。Responses 在 input 中傳送完整歷史,不支援 previous_response_id;函式呼叫結果用 function_call_output 和對應 call_id。Messages 使用 content 中的 text、tool_use、tool_result 塊,不能直接套用 Chat 的訊息格式。
Gemini 的 contents 由 user 與 model 兩種角色交替組成。模型要呼叫工具時回傳 functionCall 部件;執行後把結果做成 functionResponse 部件,放在下一則 user 內容裡回填,name 與呼叫時一致。
結束、錯誤與斷線
| 協定 | 正常結束 | 用量 |
|---|---|---|
| Chat Completions | data: [DONE] | 最後一個 chunk 的 usage |
| Responses | response.completed 事件 | 事件內的 response.usage |
| Messages | message_delta 之後的 message_stop | message_delta 的 usage |
| Gemini | 帶 finishReason 的最後一個事件,沒有 [DONE] | usageMetadata |
串流開始前先檢查 HTTP 狀態和 Content-Type,錯誤是各協定自己的 JSON:
| 協定 | 錯誤本文 |
|---|---|
| Chat Completions、Responses | {"error": {"message", "type", "param", "code"}} |
| Messages | {"type": "error", "error": {"type", "message"}} |
| Gemini | {"error": {"code", "message", "status"}} |
HTTP 狀態碼在四種協定間一致。Gemini 的 status 使用 Google 的狀態名:400 是 INVALID_ARGUMENT,401 是 UNAUTHENTICATED,402 是 FAILED_PRECONDITION,403 是 PERMISSION_DENIED,404 是 NOT_FOUND,429 是 RESOURCE_EXHAUSTED,503 是 UNAVAILABLE。程式判斷請看狀態碼與錯誤類型,不要比對 message 文字。
串流開始後出錯時,Chat 與 Gemini 會送出一個帶 error 物件的事件,Responses 送出 error 事件,Messages 送出 event: error;用戶端必須處理這些錯誤事件、逾時和提前斷開。代理層停用回應緩衝。關閉瀏覽器或 AbortSignal 只停止接收,不等於取消或承諾退款。伺服器端有可信的用量時,用戶端主動斷線仍按實際已產生的用量結算;缺少用量時不能把連線關閉當成免費完成。
報價與帳單
要讓使用者先確認精確報價和上限,可改用原生 jobs/quote → jobs/stream,使用同一 model/input 和 quoteId、expectedCost。/v1 與 /v1beta 不接收這組原生報價憑證,按受理時價格執行。最終費用以任務記錄和帳單為準,文字串流的結束標記不是結算憑證;reasoning_tokens 已包含在 completion_tokens,不能重複相加。
其他請求形狀
以下範例只說明協定信封。把 MODEL_ID_FROM_CATALOG 替換為支援這些欄位的即時模型;不要把預留位置原樣提交。
{
"model": "MODEL_ID_FROM_CATALOG",
"input": "Explain a rainbow in one sentence.",
"stream": false
}{
"model": "MODEL_ID_FROM_CATALOG",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Explain a rainbow in one sentence."}],
"stream": false
}
