Текст и потоковые ответы
Вызывайте текстовые модели в официальном формате OpenAI, Anthropic или Google Gemini и правильно обрабатывайте диалоги, инструменты, потоки и оплату.
Выберите доступную текстовую модель
Получите список моделей из /api/v1/models?modality=text&includeSchema=1 с аутентификацией. Отправляйте запрос, только если и enabled, и available равны true. Инструменты, мультимодальные сообщения, поля рассуждений и структурированный вывод должны при этом поддерживаться inputSchema выбранной модели. Совместимость протокола не означает, что любая модель умеет всё.
Один и тот же ID модели работает с любым из протоколов ниже, независимо от того, кто разработал модель: можно вызвать модель другой компании в формате Anthropic или модель не от Google в формате Gemini.
Поддерживаемые протоколы
Все четыре текстовых протокола используют один API-ключ и один каталог моделей, а также общие лимиты частоты запросов, проверку параметров и тарификацию. Различаются только форматы запросов и ответов. Выберите тот протокол, с которым уже работает ваш код или SDK.
| Протокол | Endpoint | Заголовок для ключа |
|---|---|---|
| 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 модели передаётся в пути URL; слеши в ID оставляйте как есть, кодировать их не нужно.
Передавайте ключ только в заголовке запроса. x-api-key и x-goog-api-key действуют только на /v1 и /v1beta, а нативный /api/v1 принимает только Authorization: Bearer. Параметр ?key= в строке запроса URL не принимается никогда: попав в 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 |
SDK Anthropic и Google сами добавляют /v1 или /v1beta, поэтому их базовый URL указывается без версии в пути, а базовый URL для OpenAI SDK должен включать /v1. Все три примера на Python ниже берут из переменных окружения SPICY_API_KEY и SPICY_MODEL — ID модели, выбранной в актуальном каталоге.
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 для дополнительных заголовков (в SDK OpenAI и Anthropic это 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, то 400 вернут и stop в Chat, и stop_sequences в Messages, и generationConfig.stopSequences в Gemini. Сейчас ни одна текстовая модель не объявляет seed в inputSchema, поэтому seed в Chat и generationConfig.seed в Gemini тоже вернут 400.
Несколько исключений:
- Поля, которые имеют смысл только на исходной платформе протокола, игнорируются и на генерацию не влияют:
user,metadata,storeв Chat;user,metadata,store,includeв Responses;metadataв Messages. - 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. Промпт можно заменить; поля запроса должны соответствовать текущей схеме.
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 модели указывается в URL, поэтому в теле запроса поле model не передаётся. Заключите URL в двойные кавычки и запишите переменную как ${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 по завершённым SSE-событиям, а не разбирайте JSON по TCP-фрагментам. Аргументы инструментов могут приходить несколькими частями: соберите полные tool_calls по index и только потом проверяйте и выполняйте их. Сохраняйте сообщение assistant и возвращайте результат с соответствующим tool_call_id. Responses требует всю историю в input и не поддерживает previous_response_id; результаты вызова функций передаются как function_call_output с соответствующим call_id. Messages использует блоки text, tool_use и tool_result в content, и формат сообщений Chat к нему напрямую не применим.
В Gemini contents состоит из элементов, в которых чередуются роли user и model. Когда модели нужно вызвать инструмент, она возвращает часть functionCall. Выполнив инструмент, оформите результат как часть functionResponse и верните его в следующем элементе с ролью user, указав тот же name, что и в вызове.
Завершение, ошибки и разрыв соединения
| Протокол | Нормальное завершение | Данные о расходе |
|---|---|---|
| Chat Completions | data: [DONE] | usage в последнем чанке |
| Responses | Событие response.completed | response.usage в этом событии |
| Messages | message_stop после message_delta | usage в message_delta |
| 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-коды статуса во всех четырёх протоколах одинаковы. Поле status в ошибках Gemini содержит названия статусов 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
}Другие материалы
Расчёт цены и совместимость · Оплата · Интеграция для продакшена · Диагностика и восстановление

