---
title: "createChatTransport"
description: "Bridge an internal LLM gateway's SSE protocol to the AI SDK transport so Nuxt UI chat components work unchanged."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/api/chat-transport"
---
# createChatTransport

> Bridge an internal LLM gateway's SSE protocol to the AI SDK transport so Nuxt UI chat components work unchanged.

## Usage

`@nuxt/ui`'s `UChat*` components render AI SDK `UIMessage`s; all protocol parsing lives in the `ai` package and only understands the AI SDK's own UI Message Stream format. Internal gateways (MaxKB, Dify, FastGPT, in-house ones) each ship a different chunk shape, and `createChatTransport` fills exactly that gap: **your domain code writes one stateless select, everything else is inherited from the AI SDK's HttpChatTransport.**

> [!NOTE]
> 
> Install 
> 
> ai
> 
>  (an optional peer of 
> 
> @movk/nuxt
> 
> ) and 
> 
> @ai-sdk/vue
> 
> , then import from the 
> 
> @movk/nuxt/ai
> 
>  subpath — the main entry never pulls in the AI SDK.

```vue
<script setup lang="ts">
import { createChatTransport } from '@movk/nuxt/ai'
import { useChat } from '@ai-sdk/vue'

interface GatewayChunk {
  content: string
  node_id: string
  runtime_node_id: string
  node_is_end: boolean
  is_end: boolean
}

const transport = createChatTransport<GatewayChunk>({
  api: '/chat/completions',
  select: raw => ({
    id: raw.runtime_node_id,
    delta: raw.content,
    end: raw.node_is_end,
    finished: raw.is_end
  })
})

const { messages, status, error, sendMessage, stop } = useChat({ transport })
</script>
```

`createChatTransport` reads `$api` internally, so call it inside a setup scope.

## The select mapping

`select` is a pure function: given one backend chunk, it states which UI parts that chunk produces; return `undefined` to produce nothing. The lifecycle is handled by the transport — `start` is emitted once, text blocks are opened and closed per `id`, and every unclosed block plus `finish` is flushed when the stream ends, so a missing end signal or a broken connection never leaves the message stuck in streaming.

**id** (`string`): Text block id; deltas sharing an id are merged into one block. Omitted deltas go to the default block 'text-1'.

**delta** (`string`): Incremental text for this chunk; empty means no text-delta is emitted.

**end** (`boolean`): Closes that text block.

**data** (`{ type: string, value: unknown, transient?: boolean }`): An extra custom data part, emitted as data-${type}. With transient: true it only reaches useChat's onData callback and is never written to message history.

**finished** (`boolean`): Ends the whole stream; later chunks are ignored.

Workflow-style gateways often carry node information alongside the answer. Route it through a `data` part to drive progress without polluting the message history:

```ts
const transport = createChatTransport<GatewayChunk>({
  api: '/chat/completions',
  select: raw => ({
    id: raw.runtime_node_id,
    delta: raw.content,
    end: raw.node_is_end,
    finished: raw.is_end,
    data: { type: 'node', value: { nodeId: raw.node_id, nodeIsEnd: raw.node_is_end }, transient: true }
  })
})

const { messages } = useChat({
  transport,
  onData: (part) => {
    if (part.type === 'data-node') advanceStage(part.data)
  }
})
```

## Request orchestration

`api`, `headers`, `body`, `credentials`, `prepareSendMessagesRequest` and `prepareReconnectToStreamRequest` are the AI SDK `HttpChatTransport` options and behave identically. A private handshake (open a session first, then build the stream URL) belongs in `prepareSendMessagesRequest`, which may be async and may override `api`:

```ts
const transport = createChatTransport<GatewayChunk>({
  prepareSendMessagesRequest: async ({ messages }) => {
    const chatId = await $api<string>('/chat/open')

    return {
      api: `/chat_message/${chatId}`,
      body: { message: getTextFromMessage(messages.at(-1)!) }
    }
  },
  select
})
```

## Why the default fetch goes through $api

The injected default `fetch` is backed by `$api`, so endpoint baseURL, auth headers, business-code validation, error toasts and the `movk:api:*` hooks all apply. That is not only convenience: **some gateways return errors as HTTP 200 with a { code: 500 } envelope**, which any `response.ok` check happily lets through before reading JSON as a stream and spinning forever. Through `$api` such a response throws at the business-code check and lands in `useChat`'s `onError`.

It also takes care of `Accept`: ofetch adds `Accept: application/json` whenever the body is JSON, which is the wrong claim for a stream, so the default is switched to the wildcard. An explicit `headers` entry always wins. See [`useApiStream`](https://nuxt.mhaibaraai.cn/en/docs/composables/use-api-stream#how-accept-and-responsetype-are-handled) for the details.

Passing your own `fetch` overrides the default, `Accept` included.

## API

### createChatTransport()

#### Type Parameters

**Raw** (`unknown`): Type of a single backend chunk.

**UI_MESSAGE** (`UIMessage`): Message type; defaults to the AI SDK UIMessage.

#### Parameters

**options** (`MovkChatTransportOptions<Raw, UI_MESSAGE>`) *required*: Pure mapping from a backend chunk to UI parts.Endpoint name to use; defaults to defaultEndpoint.How to decode a single SSE event's data; return undefined to drop it. Defaults to a fault-tolerant JSON.parse.api, headers, body, credentials, fetch, prepareSendMessagesRequest, prepareReconnectToStreamRequest — same semantics as the AI SDK.

#### Returns

`ChatTransport<UI_MESSAGE>`, ready to hand to `useChat({ transport })`.

## Changelog

See commit history for [src/runtime/domains/ai/create-chat-transport.ts](https://github.com/mhaibaraai/movk-nuxt/commits/main/src/runtime/domains/ai/create-chat-transport.ts).


## Sitemap

See the full [sitemap](https://nuxt.mhaibaraai.cn/sitemap.md) for all pages.
