Расширение xllm: вызовы LLM из скриптов
Один вызов, один API, три диалекта провайдеров. xllm берёт на себя сериализацию запросов, разбор SSE-потока и повторы; сеть и TLS даёт xrt хоста — в скрипте остаётся только бизнес-логика.
Обзор
build.bat xllm.34.1 Позиция и границы
xllm (vendored из extlibs xrt, базовый слой v3.0.0) забирает всю грязную работу «разговора с LLM-сервисами»:
- Три диалекта: Chat Completions (OpenAI-совместимый / GLM), OpenAI Responses, Anthropic Messages — один API запросов, проводной формат переключается перечислением
eProvider - SSE-стриминг: фреймизация, пересборка, инкрементальные колбэки; нестримовый режим — частный случай без колбэков
- Повторы и лимиты: повторяемые ошибки уходят в автоповтор с нарастающей паузой, Retry-After уважается; структура диагностики сообщает число попыток
- Отмена: встроена в систему отмены xrt (xcancel), плюс deadline на запрос
Границы столь же явные: она не исполняет инструменты (tool call возвращается вам — исполнение ваше дело) и не помнит историю диалога (каждый запрос лишён состояния; история и сжатие — забота верхних слоёв; xllm-session/xllm-memory из xrt в этот вариант не входят). Сеть, TLS, JSON и отмена — всё на встроенном xrt хоста; скрипт не линкует ни одного дополнительного рантайма.
34.2 Клиент и профили моделей
Первый шаг — всегда профиль модели: несекретный контракт возможностей — окно контекста, потолки входа/выхода, биты возможностей (вызовы инструментов, стриминг, ввод изображений…). Встроено семейство GLM, указатель заимствуется, профиль неизменяем:
xllm_error tErr;
const xllm_model_profile* pProfile;
xllm_client_config tConf;
xllm_client* pClient;
xllmErrorInit(&tErr);
pProfile = xllmModelProfileBuiltin("glm-5.2"); /* заимствуется, без копии */
if ( pProfile == NULL ) { /* имя с ошибкой: NULL */ ... }
xllmClientConfigInit(&tConf); /* все значения по умолчанию */
tConf.sBaseUrl = "https://open.bigmodel.cn/api/paas/v4";
tConf.sApiKey = getenv("XLLM_KEY"); /* заимствованный указатель! продакшн: Custom из xs.json */
tConf.sModel = "glm-5.2";
tConf.eProvider = XLLM_PROVIDER_GLM;
tConf.pModelProfile = pProfile; /* с профилем: проверка + снимок */
pClient = xllmClientCreate(&tConf, &tErr); /* движком владеет библиотека */
Три момента: все строки заимствуются (библиотека их не копирует; время жизни должно перекрывать клиент); движок можно разделять (заполните pNetEngine двигателем хоста/приложения — несколько клиентов его разделят; NULL — библиотека владеет своим); профиль окупается втройне — согласованность конфигурации проверяется при создании, возможности проверяются перед каждым запросом (визуальный запрос текстовой модели будет отвергнут), а xllmModelProfileSupports(pProfile, XLLM_CAP_*) питает бизнес-ветвление.
34.3 Запросы и вызовы
Запрос — стековое значение; конструирование не касается сети. Сообщения четырёх ролей — system / user / assistant / tool; текст — одна API-функция, мультимодальность — через part:
xllm_request tReq;
char* sJson;
xllmRequestInit(&tReq);
xllmRequestSetModel(&tReq, "glm-5.2");
xllmRequestAddTextMessage(&tReq, XLLM_ROLE_SYSTEM, "Ты строгий помощник.");
xllmRequestAddTextMessage(&tReq, XLLM_ROLE_USER, "Расскажи про XServer одним предложением.");
printf("токены: %llu\n",
(unsigned long long)xllmEstimateTextTokens("Расскажи про XServer одним предложением."));
sJson = xllmClientBuildRequestJson(pClient, &tReq, &tErr);
/* {"model":"glm-5.2","messages":[{"role":"system",...},...]} */
xllmFree(sJson); /* всё, что выделила библиотека — xllmFree */
xllmRequestUnit(&tReq);
Два пути вызова: одношаговый xllmClientComplete (внутри повторы и пересборка потока; возвращает полный ответ); асинхронный xllmClientStart → заимствованный xfuture* → xllmCallWait / xllmCallCancel. Вызовы инструментов: на стороне запроса инструменты объявляются xllmRequestAddTool; на стороне ответа tool call попадает в бизнес-код, результат возвращается через xllmRequestAddToolResult — исполнение всегда остаётся у вас.
Типовое разделение в xs: колбэк HTTP-маршрута принимает запрос → Complete работает в фоновом потоке (дисциплина границ из гл. 41) → результат записывается назад. Блокирующий Complete нельзя запускать в колбэке рабочего потока движка.
34.4 Диалекты, стриминг и повторы
| Провайдер | Диалект | Типичный baseUrl |
|---|---|---|
XLLM_PROVIDER_GLM | Chat Completions (поддиалект GLM) | open.bigmodel.cn/api/paas/v4 |
XLLM_PROVIDER_OPENAI_COMPAT | Chat Completions (совместимые конечные точки) | любой совместимый шлюз |
XLLM_PROVIDER_OPENAI_RESPONSES | OpenAI Responses | api.openai.com/v5 |
XLLM_PROVIDER_ANTHROPIC | Anthropic Messages | api.anthropic.com/v1 |
Стриминг: и xllmClientComplete/Start принимают xllm_stream_callbacks (приращения текста, рассуждений, вызовов инструментов, finish); NULL — нестримовый режим. Повторы управляются uMaxAttempts / uRetryBaseDelayMs / uRetryMaxDelayMs; при лимитах уважается Retry-After сервера. xllm_diagnostics в результате (число попыток, итоговая повторяемость) вместе с xllmErrorRetryable(&tErr) решает, ждать ли и пробовать снова на стороне бизнеса. Отмена двухканальная: на запрос — xllmRequestSetDeadline / SetCancel (xcancel), на вызов — xllmCallCancel.
34.5 Контракт
| Объект | Владение | Освобождение |
|---|---|---|
xllm_client_config / xllm_request / xllm_error / xllm_model_profile (свой) | стековые значения вызывающего | Unit / нет (умирают на выходе из области) |
xllm_client | выделяет библиотека | xllmClientDestroy |
xllm_call | выделяет библиотека | xllmCallDestroy (нужен и после Wait) |
xllm_response / результаты BuildRequestJson | выделяет библиотека | xllmResponseDestroy / xllmFree |
| строки в config/request, профили, движки | заимствуются | управляет вызывающий; должны жить дольше клиента |
Потоки: клиент можно использовать из нескольких потоков (внутри пул соединений и блокировки), но один xllm_call не конкурентен; future — заимствованный, действителен до Wait/Destroy. Изоляция вариантов — по закону sqlite/xtp2: в варианте default нет <xllm.h> (ошибка «нет заголовка» на компиляции), скрипты проверяют #ifdef XS_USE_XLLM, вложенный TCC видит то же самое.
Частые ошибки
- Симптом
- Ключ API захардкожен в main.c и утекает с каждым коммитом и пакетом.
- Причина
sApiKey— заимствованный указатель, соблазн вписать его напрямую слишком велик. Правильная поза: ключ в переменной окружения или в ручке Custom из xs.json (никогда в git), читается в ServiceInit и заполняет config — секрет существует только в памяти процесса.
- Симптом
- RequestProc вызывает Complete напрямую; медленная модель держит worker десятки секунд — производительность сервиса рушится.
- Причина
- Complete — блокирующий вызов (с паузами повторов); запускать его в рабочем потоке движка — значит оккупировать протокольный драйвер. Правильная поза: колбэк возвращает управление сразу, вызов уезжает в фоновый поток (граница из гл. 41) или асинхронизируется через Start/Future, результат возвращается каналом приложения.
Упражнения
Без сети прогнать пробник 34.2/34.3: профиль → клиент → запрос → BuildRequestJson.
Ключ из переменной окружения, один настоящий вызов через Complete, печать текста ответа, причины завершения и расхода; повторить путь ошибки с плохим ключом.
xllmErrorRetryable == ложь; ключ не появляется в исходниках.Обернуть POST /ai/ask: вопрос из тела идёт в запрос, ответ возвращается JSON; ключ — в ручке Custom из xs.json; вызов в фоновом потоке.
Итоги
| Пункт | Вывод |
|---|---|
| Позиция | библиотека вызова: сериализация/диалекты/стриминг/повторы; инструменты не исполняет, историю не ведёт |
| Профили | несекретный контракт возможностей: окно/потолки/биты, проверка при создании |
| Вызовы | Complete одним шагом; Start/Future асинхронно + двухканальная отмена |
| Владение | выделила библиотека — Destroy/xllmFree; строки конфигурации заимствуются |
| Дисциплина | ключи через окружение/Custom; блокирующие вызовы вне worker движка |
- xrt · future — модель future/cancel за асинхронными вызовами