# Quick Start

You want a streaming chat in your app. TanStack AI streams from a server route. The hook for your framework renders the tokens.

> [!TIP]
> If you do not want a key per provider, [OpenRouter](../adapters/openrouter) gives you 300+ models with one API key.

React Native or Expo needs an absolute server URL and an XHR transport. See [Quick Start: React Native](./quick-start-react-native).

No UI: see [Quick Start: Server Only](./quick-start-server).

## 1. Install

```sh
npm i @tanstack/ai @tanstack/ai-svelte @tanstack/ai-openai
```

## 2. Stream from the server

Call `chat()`. Then wrap the result with `toServerSentEventsResponse`.

```typescript
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

export async function POST(request: Request) {
  const { messages, threadId, runId } = await chatParamsFromRequest(request);

  const stream = chat({
    adapter: openaiText("gpt-5.6"),
    messages,
    threadId,
    runId,
  });

  return toServerSentEventsResponse(stream);
}
```

This works with TanStack Start, Next.js, SvelteKit, Hono, and any host that returns a Web `Response`.

If your server is Node streams (Express), see [Quick Start: Server Only](./quick-start-server).

Put the API key on the server:

```bash
OPENAI_API_KEY=your-openai-api-key
```

The adapter reads `OPENAI_API_KEY` at runtime. Do not send this key to the browser.

If you do not want a server key, see [Bring Your Own Key](../advanced/byok).

## 3. Render the chat

Call `createChat` from `@tanstack/ai-svelte`. The return object uses reactive getters. Read `chat.messages` and `chat.isLoading` with no extra wrapper.

```svelte
<script lang="ts">
import { createChat, fetchServerSentEvents } from '@tanstack/ai-svelte'

let input = $state('')

const chat = createChat({
  connection: fetchServerSentEvents('/api/chat'),
})

function handleSubmit() {
  if (input.trim() && !chat.isLoading) {
    chat.sendMessage(input)
    input = ''
  }
}
</script>

<div>
  {#each chat.messages as message (message.id)}
    <div>
      {#each message.parts as part}
        {#if part.type === 'text'}
          <p>{part.content}</p>
        {/if}
      {/each}
    </div>
  {/each}

  <form onsubmit={handleSubmit}>
    <input bind:value={input} disabled={chat.isLoading} />
    {#if chat.isLoading}
      <button type="button" onclick={() => chat.stop()}>Stop</button>
    {:else}
      <button type="submit" disabled={!input.trim()}>Send</button>
    {/if}
  </form>
</div>
```

If the component can unmount while a response streams, call `chat.stop()` in `onDestroy`.

See the [Svelte API](../api/ai-svelte).

Send a message. Tokens show up in the UI.

## Later

- [Tools](../tools/tools) for function calling
- [Streaming](../chat/streaming) for cancel, callbacks, and transports
- [Adapters](../adapters/openai) for other providers
