Один ключ к DeepSeek, GPT, Claude и Gemini — оплата в рублях, доступ без VPN.

Получить ключ

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 не даёт скрипту висеть бесконечно.

php
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 — причина остановки.

Если curl_exec() вернул false, проверять HTTP-код бессмысленно — транспорт не дошёл до сервера. Поэтому curl_errno() проверяется первым.

Класс-обёртка DeepSeekClient

Когда запросов становится больше одного, удобнее вынести транспорт в класс: адрес API и ключ задаются один раз, а вызывающий код видит только текст ответа и понятные исключения.

php
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 'Не удалось получить ответ.';
    }
}

Пользовательские исключения удобны тем, что их легко отличить от ошибок самого PHP: код HTTP и тело ответа сохраняются в свойствах $status и $body и попадают в лог целиком.

Нужен API-ключ сегодня

Регистрация по email или Telegram занимает пару минут, оплата идёт в рублях, VPN не нужен. Один ключ работает и с DeepSeek, и с другими моделями.

Получить ключ

Потоковая выдача

В потоковом режиме сервер отвечает в формате SSE: каждая строка начинается с data: , внутри — JSON-чанк, а завершает поток строка data: [DONE]. В PHP удобнее не ждать ответ целиком, а обрабатывать его по мере поступления через CURLOPT_WRITEFUNCTION.

php
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» — это требование формата. Модель не обязана вернуть именно вашу схему, поэтому проверяйте и разбор, и наличие нужных полей.

php
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 ₽

Двойной json_decode — это норма: первый разбирает конверт HTTP-ответа, второй — JSON, сгенерированный моделью.

Обработка ошибок

Со стороны PHP полезно различать два класса сбоев: транспортные (нет соединения, таймаут — curl_errno() не равен нулю) и прикладные, которые приходят с HTTP-кодом.

HTTP-коды API DeepSeek и что делать
КодЗначениеДействие
400Invalid FormatИсправить тело запроса по тексту сообщения
401Authentication FailsПроверить ключ: повторять бессмысленно
402Insufficient BalanceПополнить баланс
422Invalid ParametersПроверить значения параметров
429Rate Limit ReachedСнизить темп и повторить с задержкой
500Server ErrorПовторить после короткой паузы
503Server OverloadedПовторить после короткой паузы
php
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-совместимый шлюз: меняются только адрес и ключ. У агрегаторов свои идентификаторы моделей — точное имя нужно смотреть в каталоге сервиса.

php
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, и с другими моделями.

Получить ключ

Читайте также

Один ключ вместо пяти аккаунтов

DeepSeek, GPT, Claude и Gemini через один OpenAI-совместимый адрес: api.aitunnel.ru/v1. Регистрация занимает пару минут, оплата в рублях, доступ без VPN.

  • Один ключ на все модели
  • Оплата в рублях и счёт для юрлиц
  • Совместимо с OpenAI SDK