Рецепт: добавляем адаптер LLM
Как подключить нового провайдера модели. Эталонные реализации: packages/llm/llm-deepseek (прямой HTTP, SSE разбирает eventsource-parser) и packages/llm/llm-pi-ai (обёртка над LLM-библиотекой). Сначала прочитайте описание StreamChunk в packages/llm/llm/src/types.ts — там записаны соглашения протокола, по которым сверялись оба адаптера.
Форма
class MyAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> { … }
}
export const name = 'llm-myprovider'
export const inject = ['llm']
export const Config: z<Config> = z.object({ apiKey: z.string(), … })
export function apply(ctx: Context, config: Config) {
ctx.llm.registerAdapter(['my-provider'], new MyAdapter(…))
}
Регистрация построена на эффектах (безопасна для HMR); один адаптер на маршрут провайдера — дубликаты бросают исключение, а регистрация на несколько маршрутов проходит либо целиком, либо никак. options.provider выбирает адаптер, а options.model — идентификатор модели у провайдера, поэтому адаптер с динамическим каталогом обслуживает новые модели без перенастройки жизненного цикла. Секреты живут по правилам cordis: Config на schemastery с запасными значениями из окружения, которые приходят из cordis.yml через !!js process.env.MY_KEY. Никогда не читайте в коде произвольные файлы с ключами.
Обязательства протокола (контракт, сверенный на двух реализациях)
usageотдавайте ДОfinish; послеfinish— НИЧЕГО. Надёжный способ: буферизовать finish/usage до маркера конца стрима у провайдера, а потом сбросить (так обрабатываются провайдеры, которые присылают хвостовые чанки только с usage).argumentsвызова инструмента — СЫРЫЕ JSON-строки на всём пути; фрагменты стримьте какargumentsDelta. Если провайдер отдаёт уже разобранные объекты, сериализуйте их обратно в строку наblock-end.indexблоков выделяйте в порядке первого появления в стриме; для каждой дельты того же блока используйте тот же индекс.- У ошибок ровно два разрешённых пути: БРОСИТЬ исключение из
stream()(сбои транспорта и протокола — используйтеLlmErrorсо стабильным кодом) или завершить стримfinish {kind: 'error' | 'aborted'}(сбои провайдера внутри потока). Потребители обрабатывают оба; выбирайте по классу сбоя и документируйте выбор. - Уважайте
options.signal(передавайте его в fetch / ваш SDK). - Поле
GenerateOptions, которое провайдер не может учесть (например, списокstopу провайдера без стоп-последовательностей): бросайтеLlmError(..., 'UNSUPPORTED_OPTION'), а не отбрасывайте молча. - Если провайдеру для последующих вызовов нужны идентификаторы ответов, подписи или другие нативные метаданные, отдавайте минимальную проекцию в JSON без потерь как
finish.replayState. Проверяйте её при восстановлении истории.LlmRuntimeпередаёт её только тогда, когда исторический маршрут провайдера и целевой маршрут провайдера прямо сейчас принадлежат одному и тому же экземпляру адаптера; ваш адаптер решает, законно ли восстановление на той же модели, между моделями или между провайдерами. Никогда не выводите нативное воспроизведение из одних лишь имён провайдера/модели, если состояния нет.
Специфичные для провайдера переключатели режима размышлений остаются в Config адаптера. Точные метаданные модели идут через один нейтральный к провайдеру шов возможностей: реализуйте resolveModel() с идентичностью провайдера/модели и необязательными полями context и reasoning, объявляйте настроенный defaultEffort только когда он есть, и уважайте необязательный AbortSignal резолвера. Уровни рассуждений (reasoning effort) — упорядоченные непрозрачные идентификаторы, которые адаптер сам сопоставляет с запросами к провайдеру. Сохраняйте авторитетный список вариантов адаптера, включая определённый адаптером off, если он поддерживается, не раскрывая итоговых сетевых написаний и не подрезая неподдерживаемые значения; идентификатор не обязан совпадать со своим представлением на проводе.
Структура реализации
Держите сетевые типы, сериализацию запроса, разбор транспорта, перевод чанков и класс адаптера как отдельные обязанности; эталонная раскладка — llm-deepseek.
Проверка
Следуйте политике тестирования репозитория: она владеет покрытием адаптеров, проверками на реальных провайдерах и требованиями к публикуемым точкам входа.