DeepSeek API на PHP
API DeepSeek совместим с форматом OpenAI, а значит для работы достаточно HTTP-клиента — расширения curl, которое есть почти на любом хостинге. Ниже — рабочие примеры для PHP 8.1+ без Composer: от запроса в шесть строк до класса-обёртки со стримингом.
Зачем PHP-разработчику DeepSeek API
В большинстве PHP-проектов модель нужна не сама по себе, а как часть уже существующего интерфейса: административной панели, формы, обработчика заявок. Типичные сценарии:
- WordPress — генерация и перевод черновиков, классификация обращений, краткое резюме длинных материалов, подсказки в редакторе блоков.
- Bitrix и другие CMS — автоматическое заполнение карточек товаров, разбор писем и заявок, ответы в чате на сайте.
- Самописные админки и внутренние CRM — извлечение полей из неструктурированного текста в JSON, поиск по базе знаний, сводки по звонкам.
Практические детали: запросы к API удобно держать в отдельном классе, ключ — в переменной окружения, а тяжёлые операции (обработка большого документа, стенограмма) — в очереди, потому что thinking-режим по умолчанию включён и ответ может генерироваться заметно дольше, чем допускает лимит времени PHP-FPM.
PHP 8.1 или новее, расширения curl и json, ключ DEEPSEEK_API_KEY, адрес https://api.deepseek.com и метод POST /chat/completions. Авторизация — заголовок Authorization: Bearer ${DEEPSEEK_API_KEY}. Актуальные модели для примеров: deepseek-flash и deepseek-v4-pro.
Минимальный запрос на чистом cURL
Основной приём — отправить JSON и разобрать ответ. Ключ берётся из окружения, чтобы не хранить его в репозитории; JSON_UNESCAPED_UNICODE сохраняет кириллицу читаемой, а CURLOPT_TIMEOUT не даёт скрипту висеть бесконечно.
declare(strict_types=1);
/**
* Отправляет один запрос в DeepSeek API и возвращает текст ответа.
*
* @throws RuntimeException при ошибке транспорта, HTTP-коде вне 2xx
* или неожиданной структуре ответа.
*/
function deepseek_chat(string $prompt): string
{
$apiKey = getenv('DEEPSEEK_API_KEY');
if ($apiKey === false || $apiKey === '') {
throw new RuntimeException('Не задан DEEPSEEK_API_KEY');
}
$payload = [
'model' => 'deepseek-flash',
'messages' => [
['role' => 'system', 'content' => 'Отвечай по-русски, кратко и по делу.'],
['role' => 'user', 'content' => $prompt],
],
// Режим рассуждений включён по умолчанию. Для простых задач
// его выгодно отключить: быстрее и дешевле.
'thinking' => ['type' => 'disabled'],
];
$ch = curl_init('https://api.deepseek.com/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true, // вернуть ответ строкой, а не вывести в поток
CURLOPT_TIMEOUT => 60, // общий лимит на запрос, секунды
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$raw = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($errno !== 0) {
throw new RuntimeException("cURL: {$error} (код {$errno})");
}
if ($code < 200 || $code >= 300) {
throw new RuntimeException("HTTP {$code}: {$raw}");
}
$data = json_decode((string) $raw, true);
if (!is_array($data) || !isset($data['choices'][0]['message']['content'])) {
throw new RuntimeException('Неожиданный ответ API: ' . (string) $raw);
}
return (string) $data['choices'][0]['message']['content'];
}
echo deepseek_chat('Что такое контекстное кэширование в API?'), PHP_EOL;
Ответ устроен так же, как у OpenAI: choices[0].message.content — текст, choices[0].message.reasoning_content — цепочка рассуждений (поле есть, когда thinking включён), usage — расход токенов, finish_reason — причина остановки.
Класс-обёртка DeepSeekClient
Когда запросов становится больше одного, удобнее вынести транспорт в класс: адрес API и ключ задаются один раз, а вызывающий код видит только текст ответа и понятные исключения.
declare(strict_types=1);
/** Ошибка обращения к API DeepSeek. */
class DeepSeekException extends RuntimeException
{
public function __construct(
string $message,
public readonly int $status = 0,
public readonly string $body = '',
) {
parent::__construct($message, $status);
}
}
final class DeepSeekClient
{
public function __construct(
private readonly string $apiKey,
private readonly string $baseUrl = 'https://api.deepseek.com',
private readonly int $timeout = 60,
) {
if ($this->apiKey === '') {
throw new DeepSeekException('Пустой API-ключ');
}
}
/**
* @param list<array{role: string, content: string}> $messages
* @param array<string, mixed> $extra
*/
public function chat(array $messages, array $extra = []): string
{
$payload = $extra + [
'model' => 'deepseek-flash',
'messages' => $messages,
'stream' => false,
];
$ch = $this->init($payload);
$raw = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($errno !== 0) {
throw new DeepSeekException("Транспортная ошибка: {$error}", 0, '');
}
if ($code < 200 || $code >= 300) {
throw new DeepSeekException("HTTP {$code}", $code, (string) $raw);
}
$data = json_decode((string) $raw, true);
$content = is_array($data) ? ($data['choices'][0]['message']['content'] ?? null) : null;
if (!is_string($content)) {
throw new DeepSeekException('В ответе нет поля choices[0].message.content', $code, (string) $raw);
}
return $content;
}
/** @param array<string, mixed> $payload */
private function init(array $payload): CurlHandle
{
$ch = curl_init($this->baseUrl . '/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey,
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
return $ch;
}
}
// Использование
$client = new DeepSeekClient((string) getenv('DEEPSEEK_API_KEY'));
try {
echo $client->chat([['role' => 'user', 'content' => 'Назови три плюса статической типизации.']]);
} catch (DeepSeekException $e) {
error_log($e->getMessage());
if ($e->status === 429) {
echo 'Слишком много запросов, попробуйте позже.';
} else {
echo 'Не удалось получить ответ.';
}
}
Нужен API-ключ сегодня
Регистрация по email или Telegram занимает пару минут, оплата идёт в рублях, VPN не нужен. Один ключ работает и с DeepSeek, и с другими моделями.
Потоковая выдача
В потоковом режиме сервер отвечает в формате SSE: каждая строка начинается с data: , внутри — JSON-чанк, а завершает поток строка data: [DONE]. В PHP удобнее не ждать ответ целиком, а обрабатывать его по мере поступления через CURLOPT_WRITEFUNCTION.
declare(strict_types=1);
/**
* Печатает ответ по мере генерации.
* include_usage добавляет последний чанк с расходом токенов;
* параметр работает только при stream: true, иначе API вернёт 400.
*/
function deepseek_stream(string $prompt): void
{
$apiKey = (string) getenv('DEEPSEEK_API_KEY');
$buffer = '';
$usage = null;
$payload = [
'model' => 'deepseek-flash',
'messages' => [['role' => 'user', 'content' => $prompt]],
'stream' => true,
'stream_options' => ['include_usage' => true],
];
$ch = curl_init('https://api.deepseek.com/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: text/event-stream',
'Authorization: Bearer ' . $apiKey,
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
// Вызывается на каждый кусок тела ответа
CURLOPT_WRITEFUNCTION => function ($ch, string $chunk) use (&$buffer, &$usage): int {
$buffer .= $chunk;
// Строка SSE может прийти по частям, поэтому режем только по "\n"
while (($pos = strpos($buffer, "\n")) !== false) {
$line = trim(substr($buffer, 0, $pos));
$buffer = substr($buffer, $pos + 1);
if ($line === '' || !str_starts_with($line, 'data:')) {
continue; // пустые строки и keep-alive комментарии
}
$data = trim(substr($line, 5));
if ($data === '[DONE]') {
continue;
}
$json = json_decode($data, true);
if (!is_array($json)) {
continue;
}
$delta = $json['choices'][0]['delta']['content'] ?? null;
if (is_string($delta) && $delta !== '') {
echo $delta;
flush();
}
if (isset($json['usage'])) {
$usage = $json['usage']; // приходит последним чанком
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($errno !== 0) {
throw new RuntimeException("cURL: {$error}");
}
// При ошибке сервер отвечает JSON, а не потоком SSE: callback его
// пропустит, поэтому код ответа нужно проверить отдельно.
if ($code < 200 || $code >= 300) {
throw new RuntimeException("HTTP {$code}: " . trim($buffer));
}
echo PHP_EOL;
if (is_array($usage)) {
echo 'Входных токенов: ', $usage['prompt_tokens'] ?? '—', PHP_EOL;
}
}
Чтобы текст появлялся в браузере по мере генерации, а не после закрытия соединения, в начале скрипта полезно вызвать ob_implicit_flush(true) и убедиться, что output_buffering в настройках PHP выключен. Также учтите, что на длинных паузах сервер присылает SSE-комментарии вида : keep-alive — строки без префикса data: нужно просто пропускать.
Ответ строго в JSON
Когда результат уходит не человеку, а в базу или в другую систему, включается режим json_object. В промпте при этом должно присутствовать слово «json» — это требование формата. Модель не обязана вернуть именно вашу схему, поэтому проверяйте и разбор, и наличие нужных полей.
declare(strict_types=1);
$payload = [
'model' => 'deepseek-flash',
'messages' => [
['role' => 'system', 'content' => 'Отвечай только валидным JSON, без пояснений.'],
['role' => 'user', 'content' => 'Извлеки данные и верни JSON с полями invoice, amount, currency, '
. 'due_date: «Оплатите счёт № 4471 на 12 500 ₽ до 30 сентября 2026 года.»'],
],
'response_format' => ['type' => 'json_object'],
'stream' => false,
];
$ch = curl_init('https://api.deepseek.com/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . getenv('DEEPSEEK_API_KEY'),
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$raw = (string) curl_exec($ch);
curl_close($ch);
$data = json_decode($raw, true) ?? [];
$text = $data['choices'][0]['message']['content'] ?? '';
$invoice = json_decode($text, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Модель вернула не JSON: ' . json_last_error_msg());
}
if (!isset($invoice['amount'], $invoice['currency'])) {
throw new RuntimeException('В ответе нет обязательных полей');
}
echo $invoice['amount'], ' ', $invoice['currency'], PHP_EOL; // 12500 ₽
Обработка ошибок
Со стороны PHP полезно различать два класса сбоев: транспортные (нет соединения, таймаут — curl_errno() не равен нулю) и прикладные, которые приходят с HTTP-кодом.
| Код | Значение | Действие |
|---|---|---|
400 | Invalid Format | Исправить тело запроса по тексту сообщения |
401 | Authentication Fails | Проверить ключ: повторять бессмысленно |
402 | Insufficient Balance | Пополнить баланс |
422 | Invalid Parameters | Проверить значения параметров |
429 | Rate Limit Reached | Снизить темп и повторить с задержкой |
500 | Server Error | Повторить после короткой паузы |
503 | Server Overloaded | Повторить после короткой паузы |
declare(strict_types=1);
/**
* @param callable(): string $call
*/
function deepseek_with_retry(callable $call, int $attempts = 5): string
{
$delay = 1.0;
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
try {
return $call();
} catch (DeepSeekException $e) {
$retryable = $e->status === 429 || $e->status >= 500;
if (!$retryable || $attempt === $attempts) {
throw $e;
}
error_log(sprintf('Попытка %d не удалась (HTTP %d), пауза %.1f с', $attempt, $e->status, $delay));
usleep((int) ($delay * 1_000_000));
$delay *= 2;
}
}
throw new DeepSeekException('Исчерпаны попытки');
}
Заголовка Retry-After в документации DeepSeek нет, поэтому паузу выбирает клиент: начните с секунды и удваивайте её до пяти попыток. Если к API обращаются несколько процессов сразу, к задержке стоит добавлять небольшой случайный разброс, чтобы повторные запросы не приходили синхронно.
Через агрегатор
Тот же код работает через OpenAI-совместимый шлюз: меняются только адрес и ключ. У агрегаторов свои идентификаторы моделей — точное имя нужно смотреть в каталоге сервиса.
declare(strict_types=1);
// Ключ вида sk-aitunnel-... создаётся в личном кабинете агрегатора
$apiKey = (string) getenv('AITUNNEL_API_KEY');
$baseUrl = 'https://api.aitunnel.ru/v1/'; // вместо https://api.deepseek.com
$payload = [
'model' => 'deepseek-v4.1-flash', // идентификатор из каталога сервиса
'messages' => [['role' => 'user', 'content' => 'Привет!']],
];
$ch = curl_init($baseUrl . 'chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$raw = (string) curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($code !== 200) {
throw new RuntimeException("AITUNNEL HTTP {$code}: {$raw}");
}
$data = json_decode($raw, true);
echo $data['choices'][0]['message']['content'] ?? '', PHP_EOL;
Обратите внимание на имя модели: в официальном API DeepSeek это deepseek-flash, а на AITUNNEL та же модель называется иначе — например, deepseek-v4.1-flash. Остальной код, включая стриминг и разбор ошибок, менять не нужно.
Подробный разбор подключения через AITUNNEL →
Нужен API-ключ сегодня
Регистрация по email или Telegram занимает пару минут, оплата идёт в рублях, VPN не нужен. Один ключ работает и с DeepSeek, и с другими моделями.
Читайте также
- Тот же запрос через cURL Проверить ключ и увидеть «сырой» ответ API до того, как писать код на PHP.
- Примеры на Python Тот же набор сценариев на официальном OpenAI SDK — удобно для сверки поведения.
- Ошибки и лимиты Все коды ответов API, лимиты параллельных запросов и рекомендации по повторам.
- Стриминг и токены Формат SSE, поля usage и типичные ошибки при потоковой выдаче.