Skip to content

Server & Proxy ​

vue-llm/server turns any provider into a streaming HTTP endpoint and gives the browser a provider that talks to it — API keys never leave the server.

createLLMHandler ​

Returns a Web-standard (request: Request) => Promise<Response> handler, so it runs on Node (via your framework), Bun, Deno, Cloudflare Workers and Nuxt/Nitro.

ts
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',
  system: 'You are a helpful assistant.',
  maxMessages: 50,
  rateLimit: { maxRequests: 30, interval: 60_000 },
  onRequest: body => ({ ...body, model: 'claude-sonnet-4-5' }), // pin the model
})

// Bun
Bun.serve({ port: 3000, fetch: req => new URL(req.url).pathname === '/api/chat' ? handler(req) : new Response('Not found', { status: 404 }) })
OptionTypeDescription
providerLLMProviderRequired
modelstringDefault model (the body may override it)
systemstringDefault system prompt (the body may override it)
toolsServerToolDefinition[]Tools offered to the model; those with execute run on the server
maxToolRoundtripsnumberMax server tool rounds per request (default 5)
maxMessagesnumberKeep only the last N messages
rateLimit{ maxRequests, interval?, key? }Per-client limit, responds 429 + Retry-After
onRequest(body) => bodyValidate or rewrite the request body

The request body is { messages: ChatMessage[], model?: string, system?: string }. Responses are Server-Sent Events, one ChatStreamChunk per event, ending with data: [DONE]. Errors before streaming return JSON with status 400, 405, 429 or 500.

Server tools ​

Give a tool an execute function and the handler runs it on the server, feeds the result back to the model and keeps streaming — the browser never sees your database or API keys:

ts
const handler = createLLMHandler({
  provider,
  tools: [
    {
      name: 'get_standings',
      description: 'Current standings of a tournament',
      parameters: { type: 'object', properties: { tournament: { type: 'number' } }, required: ['tournament'] },
      async execute(args, { request }) {
        const { tournament } = args as { tournament: number }
        await assertCanRead(request, tournament)
        return db.standings(tournament)
      },
    },
  ],
})

The client receives the tool call followed by a tool-result chunk, so the tool-call part goes straight to result without a client roundtrip. A thrown error becomes { type: 'tool-result', error } and is also reported to the model, which can recover or explain.

Tools without execute are left to the client: the stream ends with finishReason: 'tool-calls' and useChat runs onToolCall as usual. Server and client tools can be mixed in one request.

createProxyProvider ​

ts
import { useChat } from 'vue-llm'
import { createProxyProvider } from 'vue-llm/server'

const { messages, sendMessage } = useChat({
  provider: createProxyProvider('/api/chat'),
  model: 'claude-sonnet-4-5',
})

Attachments, tool calls, server tool results and reasoning stream through the proxy unchanged. It is a thin wrapper around createSSEProvider. Non-2xx responses become LLMAPIErrors with provider: 'proxy'.

streamToSSE ​

Lower-level helper if you build the response yourself:

ts
import { streamToSSE } from 'vue-llm/server'

const { stream } = await provider.chat({ model, messages })
return new Response(streamToSSE(stream), { headers: { 'Content-Type': 'text/event-stream' } })

Released under the MIT License.