OpenAI API на C: первый запрос через libcurl и JSON

В этой статье

Если программа на C должна отправить запрос в OpenAI API, ей не нужен SDK на другом языке. Достаточно HTTPS-клиента, JSON-библиотеки и небольшого слоя, который собирает ответ в памяти. В этом примере запрос идёт напрямую к Responses API через libcurl, а JSON разбирается библиотекой cJSON. Это тот же прямой HTTP-подход, который описан в официальном quickstart для API, только без SDK.

Сначала важное разделение. ChatGPT — пользовательский продукт с подпиской и интерфейсом, а OpenAI API — HTTP-сервис для программ. Подписка ChatGPT сама по себе не выдаёт приложению право делать API-запросы и не заменяет API-ключ. Для API нужен ключ проекта; правила передачи ключа и заголовка описаны в официальном quickstart. Хранить его следует в переменной окружения, а не в исходнике и тем более не в клиентском приложении, которое получает пользователь.

Если задача на самом деле сводится к пользовательскому доступу к AI-сервису, а не к программному ключу и балансу API, варианты оформления можно посмотреть в каталоге услуг доступа к AI-сервисам Amber Market. Это не пополнение OpenAI API: перед заказом нужно открыть карточку, проверить условия и итоговую сумму, затем оформить выбранный вариант через подтверждение email и доступный способ оплаты. Для кода ниже нужен именно API-ключ.

Что понадобится

Нужны:

  • компилятор с поддержкой C99;
  • libcurl с HTTPS-поддержкой;
  • cJSON;
  • API-ключ в переменной OPENAI_API_KEY;
  • идентификатор доступной модели.

Перед запуском проверьте установленные версии и доступность pkg-config:

curl-config --version
pkg-config --modversion libcurl
pkg-config --modversion libcjson 2>/dev/null || pkg-config --modversion cjson

Название пакета cJSON зависит от дистрибутива. Важно не конкретное число версии в статье, а то, что заголовок и библиотека находятся в системе и собираются одним toolchain. Идентификатор модели также сверяйте с официальным списком моделей перед запуском: доступность модели зависит от проекта и может измениться.

Минимальная программа на C99

Сохраните код в main.c. Значение gpt-4.1-mini — пример; при необходимости замените его в JSON-теле на актуальный идентификатор из документации и доступный вашему проекту.

#include <curl/curl.h>
#include <cjson/cJSON.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct response_buffer {
    char *data;
    size_t size;
};

static size_t write_callback(void *contents, size_t size, size_t count,
                             void *userp)
{
    size_t bytes = size * count;
    struct response_buffer *buffer = userp;
    char *new_data;

    if (bytes > (size_t)-1 - buffer->size - 1) {
        return 0;
    }

    new_data = realloc(buffer->data, buffer->size + bytes + 1);
    if (new_data == NULL) {
        return 0;
    }

    buffer->data = new_data;
    memcpy(buffer->data + buffer->size, contents, bytes);
    buffer->size += bytes;
    buffer->data[buffer->size] = '\0';
    return bytes;
}

static const char *find_response_text(const cJSON *root)
{
    const cJSON *output = cJSON_GetObjectItemCaseSensitive(root, "output");
    const cJSON *item;

    if (!cJSON_IsArray(output)) {
        return NULL;
    }

    cJSON_ArrayForEach(item, output) {
        const cJSON *content = cJSON_GetObjectItemCaseSensitive(item, "content");
        const cJSON *part;

        if (!cJSON_IsArray(content)) {
            continue;
        }

        cJSON_ArrayForEach(part, content) {
            const cJSON *type = cJSON_GetObjectItemCaseSensitive(part, "type");
            const cJSON *text = cJSON_GetObjectItemCaseSensitive(part, "text");

            if (cJSON_IsString(type) && strcmp(type->valuestring, "output_text") == 0 &&
                cJSON_IsString(text)) {
                return text->valuestring;
            }
        }
    }

    return NULL;
}

int main(void)
{
    const char *api_key = getenv("OPENAI_API_KEY");
    const char *request_json =
        "{\"model\":\"gpt-4.1-mini\","
        "\"input\":\"Reply with exactly: hello from C\"}";
    char authorization[4096];
    CURL *curl = NULL;
    CURLcode curl_result;
    long http_code = 0;
    struct curl_slist *headers = NULL;
    struct response_buffer response = {0};
    cJSON *root = NULL;
    const char *text = NULL;

    if (api_key == NULL || api_key[0] == '\0') {
        fprintf(stderr, "OPENAI_API_KEY is not set\n");
        return EXIT_FAILURE;
    }

    int header_length = snprintf(authorization, sizeof(authorization),
                                 "Authorization: Bearer %s", api_key);
    if (header_length < 0 || (size_t)header_length >= sizeof(authorization)) {
        fprintf(stderr, "API key is too long for the authorization header\n");
        return EXIT_FAILURE;
    }

    if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) {
        fprintf(stderr, "curl_global_init failed\n");
        return EXIT_FAILURE;
    }

    curl = curl_easy_init();
    if (curl == NULL) {
        fprintf(stderr, "curl_easy_init failed\n");
        curl_global_cleanup();
        return EXIT_FAILURE;
    }

    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = curl_slist_append(headers, authorization);
    if (headers == NULL) {
        fprintf(stderr, "could not allocate HTTP headers\n");
        curl_easy_cleanup(curl);
        curl_global_cleanup();
        return EXIT_FAILURE;
    }

    curl_easy_setopt(curl, CURLOPT_URL, "https://api.openai.com/v1/responses");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_json);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);

    curl_result = curl_easy_perform(curl);
    if (curl_result != CURLE_OK) {
        fprintf(stderr, "HTTPS request failed: %s\n",
                curl_easy_strerror(curl_result));
        goto cleanup;
    }

    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
    printf("HTTP status: %ld\n", http_code);

    if (response.data == NULL) {
        fprintf(stderr, "server returned an empty body\n");
        goto cleanup;
    }

    root = cJSON_Parse(response.data);
    if (root == NULL) {
        fprintf(stderr, "invalid JSON response near: %s\n",
                cJSON_GetErrorPtr() != NULL ? cJSON_GetErrorPtr() : "unknown");
        goto cleanup;
    }

    if (http_code < 200 || http_code >= 300) {
        const cJSON *error = cJSON_GetObjectItemCaseSensitive(root, "error");
        const cJSON *message = error != NULL
            ? cJSON_GetObjectItemCaseSensitive(error, "message")
            : NULL;

        if (cJSON_IsString(message)) {
            fprintf(stderr, "API error: %s\n", message->valuestring);
        } else {
            fprintf(stderr, "API returned HTTP %ld\n", http_code);
        }
        goto cleanup;
    }

    text = find_response_text(root);
    if (text == NULL) {
        fprintf(stderr, "valid JSON did not contain output_text\n");
        goto cleanup;
    }

    printf("Response: %s\n", text);

cleanup:
    cJSON_Delete(root);
    free(response.data);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    curl_global_cleanup();
    return (curl_result == CURLE_OK && http_code >= 200 && http_code < 300 &&
            text != NULL) ? EXIT_SUCCESS : EXIT_FAILURE;
}

В коде есть одна намеренно скучная, но полезная граница. curl_easy_perform сообщает о проблеме транспорта: DNS, TLS, таймауте или разрыве соединения. HTTP-код проверяется отдельно, потому что успешно установленное HTTPS-соединение ещё не означает успешный API-запрос. Например, неверная модель или ключ дадут JSON с ошибкой и кодом 4xx.

Функция callback вызывается частями, поэтому нельзя рассчитывать на один готовый буфер; именно такую настройку callback и других параметров описывает документация libcurl. Она расширяет динамический массив, добавляет завершающий нулевой байт и возвращает число принятых байт. После запроса память освобождается через free, JSON-дерево — через cJSON_Delete, как требует README cJSON, а easy handle и список заголовков — средствами libcurl.

Сборка и запуск

На Linux сначала установите dev-пакеты libcurl и cJSON средствами своего дистрибутива. Названия могут отличаться; нужны заголовки и библиотеки, а не только команда curl. Затем соберите программу:

cc -std=c99 -Wall -Wextra -O2 main.c \
  $(pkg-config --cflags --libs libcurl libcjson) \
  -o openai_c

Если ваш пакет cJSON зарегистрировал модуль под именем cjson, замените последний аргумент:

cc -std=c99 -Wall -Wextra -O2 main.c \
  $(pkg-config --cflags --libs libcurl cjson) \
  -o openai_c

На macOS тот же подход работает после установки libcurl и cJSON через используемый менеджер пакетов. Если pkg-config не видит формулу libcurl, передайте компилятору пути, которые показывает curl-config --cflags и curl-config --libs, а для cJSON — include- и library-path из установки cJSON.

Ключ задайте только в текущем сеансе оболочки:

export OPENAI_API_KEY='ваш_ключ_проекта'
./openai_c

Ожидаемый успешный результат выглядит примерно так:

HTTP status: 200
Response: hello from C

Точный текст зависит от модели, но код ответа должен быть в диапазоне 200–299, а в JSON должен находиться элемент output_text внутри output.

Если запрос не проходит

Проверяйте проблему по слоям — иначе легко чинить JSON, когда на самом деле не настроен TLS.

  1. OPENAI_API_KEY is not set означает, что процесс C не унаследовал переменную окружения. Проверьте её наличие в том же терминале и не выводите значение ключа в лог.
  2. Ошибка HTTPS request failed относится к libcurl: сеть, DNS, сертификаты, прокси или таймаут. Это ещё не ответ API.
  3. HTTP 401 обычно означает проблему с ключом или заголовком Authorization. Ключ должен передаваться как Bearer <token>.
  4. HTTP 400 с сообщением о модели требует проверить model, доступность модели для проекта и формат тела запроса.
  5. HTTP 429 указывает на ограничение частоты или квоты. Повторять запрос вслепую не стоит: для операций с побочным эффектом повторная попытка требует отдельной стратегии идемпотентности.
  6. Ошибка разбора JSON означает, что серверный ответ не удалось представить как JSON, либо буфер был повреждён. Поэтому программа сначала проверяет результат cJSON_Parse, а не обращается к полям наугад.

Для сравнения общей схемы первого API-запроса можно посмотреть вариант на Python, а для быстрой проверки endpoint без программы — отдельную проверку через curl. Но в C ответственность остаётся на вашем коде: он сам владеет буфером ответа, проверяет транспорт и HTTP, разбирает JSON и освобождает ресурсы.

Источники

Есть следующая задача?Ещё по теме «Работа с API» →