javascript

vue-llm: AI SDK для Vue и Nuxt — стриминговый чат, Generative UI и модели прямо в браузере

  • пятница, 30 октября 2026 г. в 00:00:10
https://habr.com/ru/articles/1101308/

Если вы делали AI-фичу на Vue, то, скорее всего, сталкивались с одним и тем же: большинство готовых инструментов для LLM-интерфейсов написаны с прицелом на React, а во Vue их приходится адаптировать или писать стриминг, tool calling и состояние чата вручную. Я сделал vue-llm — open-source библиотеку, которая закрывает это для Vue 3, Nuxt и даже Vue 2.7.


Сразу ссылки, чтобы можно было пощупать:

Ниже — что умеет библиотека и как это выглядит в коде.

Чат за десять строк

Основа — композабл useChat. Он стримит ответ модели токен за токеном в реактивное состояние: сообщения, статус, ошибка, отмена, перегенерация.

<script setup lang="ts">
import { useChat } from 'vue-llm'
import { createProxyProvider } from 'vue-llm/server'

const { messages, input, handleSubmit, status, stop } = useChat({
  provider: createProxyProvider('/api/chat'),
  model: 'gpt-4o',
})
</script>

<template>
  <div v-for="m in messages" :key="m.id">
    <b>{{ m.role }}:</b>
    <template v-for="(part, i) in m.parts" :key="i">
      <span v-if="part.type === 'text'">{{ part.text }}</span>
    </template>
  </div>
  <form @submit.prevent="handleSubmit">
    <input v-model="input">
    <button v-if="status === 'streaming'" type="button" @click="stop">Стоп</button>
  </form>
</template>

Сообщения состоят из «частей» (parts): текст, рассуждения модели, вызовы инструментов, файлы, источники. Поэтому один и тот же шаблон показывает и обычный ответ, и reasoning у Claude или Qwen3, и вызов tool.

API-ключ при этом остаётся на сервере. Серверная часть — обычный Web-стандартный обработчик Request → Response, который работает в Nuxt/Nitro, Node, Bun, Deno и edge-рантаймах:

// server/api/chat.post.ts (Nuxt)
import { createLLMHandler } from 'vue-llm/server'
import { createAnthropic } from 'vue-llm/providers'

const handler = createLLMHandler({
  provider: createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }),
  model: 'claude-sonnet-4-5',
  rateLimit: { maxRequests: 30, interval: 60_000 },
})

export default defineEventHandler(event => handler(toWebRequest(event)))

Провайдеры из коробки: OpenAI (и любой OpenAI-совместимый API), Anthropic, Google Gemini, Ollama. Если у вас уже есть свой бэкенд со стримингом, createSSEProvider подключает useChat к нему: вы описываете, как ваши SSE-события превращаются в куски ответа, и всё остальное работает как есть.

Generative UI: модель отвечает компонентами

Самая интересная для меня часть. Когда модель вызывает инструмент, вместо текста можно показать ваш Vue-компонент. Аргументы стримятся в компонент по мере генерации, то есть карточка начинает заполняться ещё до того, как модель дописала вызов.

import { defineTool } from 'vue-llm'

export const getWeather = defineTool<{ city: string }, Weather>({
  name: 'get_weather',
  description: 'Погода в городе',
  parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
  execute: ({ city }) => fetch(`/api/weather?city=${city}`).then(r => r.json()),
})
<MessageParts
  v-for="m in messages"
  :key="m.id"
  :message="m"
  :components="{ get_weather: WeatherCard }"
  @tool-result="addToolResult"
/>

Инструмент с execute выполняется сам. Инструмент без execute ставит чат на паузу и ждёт ответа из интерфейса — так делается human-in-the-loop: модель предлагает забронировать столик, пользователь жмёт «Подтвердить» в карточке, addToolResult передаёт ответ, и разговор продолжается. Живое демо (работает без сети): https://vue-llm.js.org/ru/guide/generative-ui

Под капотом для этого пришлось написать парсер «недописанного» JSON: {"city":"Мос превращается в { city: 'Мос' }, а незаконченные ключи отбрасываются до тех пор, пока не допишутся.

Модели прямо в браузере

createWebLLM запускает открытые модели (Llama 3.2, Qwen, SmolLM, Phi) на WebGPU через WebLLM. Никакого сервера и ключа, данные не покидают устройство.

import { CreateMLCEngine } from '@mlc-ai/web-llm'
import { createWebLLM } from 'vue-llm/providers'

const ai = createWebLLM({
  createEngine: CreateMLCEngine,
  onProgress: ({ progress }) => console.log(`${Math.round(progress * 100)}%`),
})

useChat({ provider: ai, model: 'Llama-3.2-1B-Instruct-q4f16_1-MLC' })

Это тот же useChat, что и с OpenAI, — меняется только провайдер. Можно, например, давать пользователям без WebGPU серверную модель, а остальным — локальную. Первая загрузка модели занимает от 400 МБ, дальше она берётся из кеша браузера за секунду-две.

Готовый интерфейс чата в стиле shadcn

Писать окно чата с нуля утомительно: автоскролл, который не дёргает пользователя, листающего вверх; Enter для отправки, который не срабатывает при наборе на китайском или японском; вставка картинок из буфера; drag-and-drop файлов. Поэтому библиотека устроена гибридно:

  • в npm лежит headless-логика: usePromptInput и useAutoScroll;

  • готовые компоненты копируются в ваш проект как в shadcn — одной командой, после чего код ваш:

npx shadcn-vue@latest add https://vue-llm.js.org/r/chat.json

Компоненты используют токены темы shadcn, поэтому сразу выглядят как остальной ваш интерфейс, включая тёмную тему.

Отладка в Vue DevTools

vue-llm/devtools добавляет в Vue DevTools вкладку LLM: список всех useChat, токены, время до первого токена, скорость генерации, вызовы инструментов и таймлайн каждого запроса. Без плагина библиотека ничего не трассирует и не тратит на это ресурсы.

Nuxt

Модуль подключается одной командой:

npx nuxi module add vue-llm

Он делает auto-import композаблов и провайдеров, регистрирует компоненты и в dev-режиме сам подключает вкладку в DevTools. Проверено на Nuxt 3 и 4.

Почему не взять готовый AI SDK

Хороший вопрос, и я сам с него начинал. vue-llm сделан Vue-first: Nuxt-модуль, Vue-компоненты для Generative UI, интеграция с Vue DevTools, провайдер для моделей в браузере, адаптер под существующий SSE-бэкенд и поддержка Vue 2.7 для композаблов (у многих легаси-админок до сих пор Vue 2). Зависимостей в рантайме нет, кроме самого Vue.

Что дальше

Библиотека под MIT, версия 1.3, покрыта тестами на Vue 3 и Vue 2.7. В планах — MCP-клиент (подключать MCP-серверы как инструменты), ответы с цитатами для RAG и голосовой ввод.

Буду рад обратной связи: что неудобно, чего не хватает, где документация непонятна. Issues и PR приветствуются, а если проект пригодится — поставьте звёздочку на GitHub, это правда помогает его продвигать.