Гайды
POST /chat/completions с stream: true возвращает Content-Type: text/event-stream. Части имеют object=chat.completion.chunk, собственные id/model и choices.delta. Несовместимый маршрут отклоняется до генерации; поддержка определяется опубликованной операцией модели.
Событие — строка data: JSON и пустая строка. Текст приходит в delta.content; tool_calls.arguments может состоять из нескольких частей. Поток завершается data: [DONE]. Собирайте части по choices.index и tool_calls.index, не исполняйте незавершённые аргументы.
POST /images с stream: true передаёт события image_generation.partial_image и image_generation.completed с data[], затем image_generation.settled с итоговым статусом и расчётом. Промежуточные кадры зависят от модели; полный JSON поставщика даёт одно событие completed. Файлы выдаются через защищённые адреса Formula AI либо запрошенный response_format=b64_json.
Для аудио передайте modalities=["text","audio"], audio.voice и audio.format="pcm16", stream=true. delta.audio.data содержит независимый base64-фрагмент: сначала декодируйте каждый фрагмент, затем объединяйте байты. delta.audio.transcript содержит части расшифровки. PCM16 — знаковые 16-битные значения little-endian, моно, 24000 Гц; контейнер WAV/MP3 автоматически не добавляется. После completed запросите GET /requests/{id}: choices.message.audio содержит приватный url, format, sample_rate, channels и transcript. Голоса указаны в схеме запроса.
stream_options: {"include_usage": true} включает клиентский usage. Последнее JSON-событие содержит ai_service.status и billing. DONE означает закрытие соединения: проверяйте status и события error. Стоимость поставщика, его модель, ключ и адрес не выдаются.
Ошибка до начала ответа сохраняет обычный HTTP/JSON-формат. После первой части HTTP-статус уже не меняется: ошибка передаётся в data.error, затем итоговый расчёт и DONE. После начала выдачи новая попытка у другого ключа или поставщика не создаётся.
Idempotency-Key сохраняет одну операцию. Повтор завершённого потока воспроизводит сохранённые части без нового расхода. Пока первоначальная операция выполняется, повтор возвращает HTTP 202 с собственным статусом; используйте GET /requests/{id}. Повтор после очистки содержимого даёт 410.
При отключении клиента сервис продолжает получать результат и учитывает фактический расход. Обрыв внешнего потока оставляет частичный ответ и pending-сверку стоимости; отправленная генерация не повторяется при восстановлении воркера. Поддержка потока зависит от опубликованного профиля модели.
ID передаётся сразу: заголовок X-Generation-ID и первый data-объект с собственным id и ai_service.status_url, для текста choices=[]; для изображений type=image_generation.accepted. Это подтверждение принятия, а не текст модели. Комментарии SSE : keep-alive поддерживают соединение во время ожидания и игнорируются сборщиком data-событий. При отключении клиента обработка и сохранение результата продолжаются. GET /requests/{id} возвращает полный собранный result после completed. Повтор с тем же ключом воспроизводит сохранённые части и следует за ещё выполняемой операцией без новой генерации. Prefer: respond-async вместе со stream=true возвращает JSON 202; поток поставщика сохраняется в фоне.
cURL · POST /chat/completions
curl "https://formula-ai.ru/api/v1/chat/completions" \
-H "Authorization: Bearer $AI_SERVICE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: example-stream-1" \
--data-binary "{
\"model\": \"$AI_SERVICE_MODEL\",
\"messages\": [
{
\"role\": \"user\",
\"content\": \"Reply OK\"
}
],
\"stream\": true,
\"stream_options\": {
\"include_usage\": true
}
}" \
--no-buffer
cURL · POST /images
curl "https://formula-ai.ru/api/v1/images" \
-H "Authorization: Bearer $AI_SERVICE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: example-image_stream-1" \
--data-binary "{
\"model\": \"$AI_SERVICE_MODEL\",
\"prompt\": \"A red circle on a white background\",
\"stream\": true
}" \
--no-buffer
cURL · POST /chat/completions
curl "https://formula-ai.ru/api/v1/chat/completions" \
-H "Authorization: Bearer $AI_SERVICE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: example-audio_stream-1" \
--data-binary "{
\"model\": \"$AI_SERVICE_MODEL\",
\"messages\": [
{
\"role\": \"user\",
\"content\": \"Reply OK\"
}
],
\"modalities\": [
\"text\",
\"audio\"
],
\"audio\": {
\"voice\": \"alloy\",
\"format\": \"pcm16\"
},
\"stream\": true,
\"stream_options\": {
\"include_usage\": true
}
}" \
--no-buffer
PHP · POST /chat/completions · SSE
<?php
$buffer = '';
$done = false;
$completed = false;
$audioBytes = '';
$handle = curl_init('https://formula-ai.ru/api/v1/chat/completions');
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('AI_SERVICE_API_KEY'), 'Idempotency-Key: php-stream-1'],
CURLOPT_POSTFIELDS => json_encode(['model' => getenv('AI_SERVICE_MODEL'), 'messages' => [['role' => 'user', 'content' => 'Reply OK']], 'stream' => true, 'stream_options' => ['include_usage' => true]], JSON_THROW_ON_ERROR),
CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$buffer, &$done, &$completed, &$audioBytes): int {
$buffer .= $bytes;
while (($position = strpos($buffer, "\n\n")) !== false) {
$event = substr($buffer, 0, $position);
$buffer = substr($buffer, $position + 2);
if (!str_starts_with($event, 'data: ')) {
continue;
}
$data = substr($event, 6);
if ($data === '[DONE]') {
$done = true;
continue;
}
$chunk = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
foreach ($chunk['choices'] ?? [] as $choice) {
echo $choice['delta']['content'] ?? '';
if (isset($choice['delta']['audio']['data'])) {
$part = base64_decode($choice['delta']['audio']['data'], true);
if ($part === false) {
throw new RuntimeException('Invalid audio base64');
}
$audioBytes .= $part;
}
echo $choice['delta']['audio']['transcript'] ?? '';
}
if (isset($chunk['type'], $chunk['data'])) {
echo json_encode($chunk, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n";
}
if (isset($chunk['ai_service'])) {
$completed = $chunk['ai_service']['status'] === 'completed';
}
if (isset($chunk['error']) || isset($chunk['ai_service'])) {
fwrite(STDERR, json_encode($chunk, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n");
}
}
return strlen($bytes);
},
]);
if (getenv('AI_SERVICE_CA')) {
curl_setopt($handle, CURLOPT_CAINFO, getenv('AI_SERVICE_CA'));
}
$success = curl_exec($handle);
$status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
$error = curl_error($handle);
curl_close($handle);
if (!$success || $status !== 200 || !$done || !$completed) {
throw new RuntimeException('HTTP ' . $status . ': ' . $error . ' ' . $buffer);
}
if ($audioBytes !== '') {
file_put_contents(getenv('AI_SERVICE_AUDIO_OUTPUT') ?: 'speech.pcm', $audioBytes);
}
PHP · POST /images · SSE
<?php
$buffer = '';
$done = false;
$completed = false;
$audioBytes = '';
$handle = curl_init('https://formula-ai.ru/api/v1/images');
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('AI_SERVICE_API_KEY'), 'Idempotency-Key: php-image_stream-1'],
CURLOPT_POSTFIELDS => json_encode(['model' => getenv('AI_SERVICE_MODEL'), 'prompt' => 'A red circle on a white background', 'stream' => true], JSON_THROW_ON_ERROR),
CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$buffer, &$done, &$completed, &$audioBytes): int {
$buffer .= $bytes;
while (($position = strpos($buffer, "\n\n")) !== false) {
$event = substr($buffer, 0, $position);
$buffer = substr($buffer, $position + 2);
if (!str_starts_with($event, 'data: ')) {
continue;
}
$data = substr($event, 6);
if ($data === '[DONE]') {
$done = true;
continue;
}
$chunk = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
foreach ($chunk['choices'] ?? [] as $choice) {
echo $choice['delta']['content'] ?? '';
if (isset($choice['delta']['audio']['data'])) {
$part = base64_decode($choice['delta']['audio']['data'], true);
if ($part === false) {
throw new RuntimeException('Invalid audio base64');
}
$audioBytes .= $part;
}
echo $choice['delta']['audio']['transcript'] ?? '';
}
if (isset($chunk['type'], $chunk['data'])) {
echo json_encode($chunk, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n";
}
if (isset($chunk['ai_service'])) {
$completed = $chunk['ai_service']['status'] === 'completed';
}
if (isset($chunk['error']) || isset($chunk['ai_service'])) {
fwrite(STDERR, json_encode($chunk, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n");
}
}
return strlen($bytes);
},
]);
if (getenv('AI_SERVICE_CA')) {
curl_setopt($handle, CURLOPT_CAINFO, getenv('AI_SERVICE_CA'));
}
$success = curl_exec($handle);
$status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
$error = curl_error($handle);
curl_close($handle);
if (!$success || $status !== 200 || !$done || !$completed) {
throw new RuntimeException('HTTP ' . $status . ': ' . $error . ' ' . $buffer);
}
if ($audioBytes !== '') {
file_put_contents(getenv('AI_SERVICE_AUDIO_OUTPUT') ?: 'speech.pcm', $audioBytes);
}
PHP · POST /chat/completions · SSE
<?php
$buffer = '';
$done = false;
$completed = false;
$audioBytes = '';
$handle = curl_init('https://formula-ai.ru/api/v1/chat/completions');
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('AI_SERVICE_API_KEY'), 'Idempotency-Key: php-audio_stream-1'],
CURLOPT_POSTFIELDS => json_encode(['model' => getenv('AI_SERVICE_MODEL'), 'messages' => [['role' => 'user', 'content' => 'Reply OK']], 'modalities' => ['text', 'audio'], 'audio' => ['voice' => 'alloy', 'format' => 'pcm16'], 'stream' => true, 'stream_options' => ['include_usage' => true]], JSON_THROW_ON_ERROR),
CURLOPT_WRITEFUNCTION => function ($handle, string $bytes) use (&$buffer, &$done, &$completed, &$audioBytes): int {
$buffer .= $bytes;
while (($position = strpos($buffer, "\n\n")) !== false) {
$event = substr($buffer, 0, $position);
$buffer = substr($buffer, $position + 2);
if (!str_starts_with($event, 'data: ')) {
continue;
}
$data = substr($event, 6);
if ($data === '[DONE]') {
$done = true;
continue;
}
$chunk = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
foreach ($chunk['choices'] ?? [] as $choice) {
echo $choice['delta']['content'] ?? '';
if (isset($choice['delta']['audio']['data'])) {
$part = base64_decode($choice['delta']['audio']['data'], true);
if ($part === false) {
throw new RuntimeException('Invalid audio base64');
}
$audioBytes .= $part;
}
echo $choice['delta']['audio']['transcript'] ?? '';
}
if (isset($chunk['type'], $chunk['data'])) {
echo json_encode($chunk, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n";
}
if (isset($chunk['ai_service'])) {
$completed = $chunk['ai_service']['status'] === 'completed';
}
if (isset($chunk['error']) || isset($chunk['ai_service'])) {
fwrite(STDERR, json_encode($chunk, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n");
}
}
return strlen($bytes);
},
]);
if (getenv('AI_SERVICE_CA')) {
curl_setopt($handle, CURLOPT_CAINFO, getenv('AI_SERVICE_CA'));
}
$success = curl_exec($handle);
$status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
$error = curl_error($handle);
curl_close($handle);
if (!$success || $status !== 200 || !$done || !$completed) {
throw new RuntimeException('HTTP ' . $status . ': ' . $error . ' ' . $buffer);
}
if ($audioBytes !== '') {
file_put_contents(getenv('AI_SERVICE_AUDIO_OUTPUT') ?: 'speech.pcm', $audioBytes);
}
Примеры cURL используют Bash. Задайте AI_SERVICE_API_KEY и AI_SERVICE_MODEL; модель выберите из /models. В PHP для опроса/файла задайте AI_SERVICE_REQUEST_ID/AI_SERVICE_MEDIA_ID, для правки — AI_SERVICE_SOURCE_URL. Для локального сертификата используйте доверенный CA через --cacert, для PHP — AI_SERVICE_CA. Проверку TLS сохраняйте включённой.