テキストとストリーミング
OpenAI、Anthropic、Google Gemini の公式フォーマットでテキストモデルを呼び出し、会話、ツール、ストリーム、料金を正しく扱います。
利用可能なテキストモデルを選ぶ
認証付きの /api/v1/models?modality=text&includeSchema=1 からモデルを取得し、enabled と available がどちらも true のモデルにだけ送信します。ツール、マルチモーダルメッセージ、推論フィールド、構造化出力は、そのモデルの inputSchema も対応している場合に限り使えます。プロトコルに互換性があっても、すべてのモデルがすべての機能を備えているわけではありません。
同じモデル ID は、下記のどのプロトコルでも呼び出せます。モデルの開発元は問いません。Anthropic 形式で他社のモデルを呼び出すことも、Gemini 形式で Google 以外のモデルを呼び出すこともできます。
対応プロトコル
4 つのテキストプロトコルは、同じ API キー、同じモデルカタログ、同じレート制限・パラメータ検証・課金を共有しています。違うのはリクエストと応答の形式だけです。既存のコードや 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 の応答とエラーはそれぞれのプロトコルの形式に従い、ネイティブ API の {code,msg,data,request_id} 外枠は付きません。body.data だけを読むクライアントで解析しないでください。Gemini 形式ではモデル ID を URL のパスに含めます。ID 内のスラッシュはそのままでよく、エンコードは不要です。
キーはリクエストヘッダーにだけ入れます。x-api-key と x-goog-api-key が有効なのは /v1 と /v1beta だけで、ネイティブの /api/v1 は Authorization: Bearer しか受け付けません。URL のクエリパラメータ ?key= は一切受け付けません。キーが URL に入ると、ブラウザーの履歴やプロキシサーバー、アクセスログに残ってしまうからです。公式 SDK はどれもリクエストヘッダーでキーを送るため、影響はありません。
公式 SDK で接続する
変えるのはベース URL とキーだけです。それ以外は各 SDK の通常の使い方どおりで構いません。
| SDK | ベース 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 を自動で付け足すため、ベース URL にバージョンのパスは含めません。OpenAI SDK のベース URL には /v1 が必要です。以下の 3 つの 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 を 1 つ生成して永続的に保存し、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 の URL)で渡せます。モデルが画像を受け付けるかどうかは 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.jsonGemini のリクエストを送る
環境変数は前の節と同じです。モデル ID は URL に書くので、リクエストボディには model を含めません。URL はダブルクォートで囲み、${SPICY_MODEL} の形で書いてください。そうしないと、シェルが直後のコロンを変数の修飾子として解釈してしまいます。
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 を付けないと、応答は少しずつ書き出される 1 つの JSON 配列になります。公式 SDK は alt=sse を自動で付けます。
会話とツール呼び出し
Chat の非ストリーミング応答では choices[0].message を読みます。ストリーミングでは choices[].delta をチャンクごとに読みます。TCP の分割単位で JSON を解析しないでください。ツールの引数は複数のチャンクに分かれて届くことがあるため、index ごとに組み立てて tool_calls が完成してから検証・実行します。assistant メッセージは残したまま、対応する tool_call_id を付けて結果を返します。Responses は input に全履歴を含めて送り、previous_response_id には対応していません。関数呼び出しの結果は、対応する call_id を付けた function_call_output で返します。Messages は content 内の text、tool_use、tool_result ブロックを使うため、Chat のメッセージ形式をそのまま当てはめることはできません。
Gemini の contents は、user と model の 2 つのロールが交互に並んで構成されます。モデルはツールを呼び出すとき functionCall パートを返します。実行後は結果を functionResponse パートにまとめ、次の user コンテンツに入れて返します。name は呼び出し時と同じ値にしてください。
終了・エラー・切断
| プロトコル | 正常終了 | 使用量 |
|---|---|---|
| Chat Completions | data: [DONE] | 最後のチャンクの 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 ステータスコードは 4 つのプロトコルで共通です。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 オブジェクトを含むイベントを 1 つ送り、Responses は error イベントを、Messages は event: error を送ります。クライアントはこれらのエラーイベントに加え、タイムアウトや途中での切断も処理する必要があります。プロキシでは応答のバッファリングを無効にします。ブラウザーを閉じたり AbortSignal で中断したりしても受信が止まるだけで、キャンセルや返金の約束にはなりません。サーバー側に信頼できる使用量がある場合、クライアントから切断しても、実際に発生した使用量で精算されます。使用量が得られない場合も、接続が閉じたことを無料での完了として扱うことはできません。
見積もりと精算
正確な見積額と上限をユーザーに先に確認してもらうには、ネイティブの jobs/quote → jobs/stream を使い、同じ model/input に quoteId と expectedCost を付けます。/v1 と /v1beta はこのネイティブの見積もり情報を受け付けず、受理時の料金で実行します。最終費用はタスクの記録と請求履歴で確認してください。テキストの終了マーカーは精算の根拠になりません。reasoning_tokens は completion_tokens に含まれるため、二重に加算しないでください。
ほかのリクエスト形式
以下はプロトコルの外枠だけを示す例です。MODEL_ID_FROM_CATALOG は、これらのフィールドに対応する最新カタログのモデル ID に置き換えてください。プレースホルダーのまま送信しないでください。
{
"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
}
