Рецепт: добавляем адаптер 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.

Проверка

Следуйте политике тестирования репозитория: она владеет покрытием адаптеров, проверками на реальных провайдерах и требованиями к публикуемым точкам входа.

перевод и редактура:

обновлено

⌕ ESC