2

AI SDK Integration

Wire Fable tools into a Vercel AI SDK route and render tool parts safely.

Fable tool files export Zod schemas and AI SDK tool definitions. For example, metric-card installs lib/fable-ui/tools/show-metric-tool.ts with:

export const showMetricInputSchema = z.object({ ... })
export function createShowMetricTool() { ... }
export const showMetric = defineFableComponent({ ... })

For the full schema, AI SDK tool, renderer, state, and props contract, read Tool Definitions.

Register Tools

import { streamText } from "ai"
import { showMetric } from "@/lib/fable-ui/tools/show-metric-tool"
 
const tools = {
  show_metric: showMetric.tool,
}
 
const result = streamText({
  model,
  messages,
  tools,
  toolChoice: "auto",
})

Only pass AI SDK tool objects into the route. A Fable definition includes both tool and renderer; the route should map just showMetric.tool, showChart.tool, or showDataBrowser.tool. Keep the full Fable registry in client/UI code where streamed tool parts are rendered.

Heavy renderers are lazy-loaded from their definition files. For example, importing the chart definition lets the route register showChart.tool without eagerly loading the chart component or Recharts. The UI renderer loads those pieces only when it actually renders the tool part.

Render Tool Parts

Install core or any item that depends on it, then provide the full Fable definition map to the renderer:

import { FableToolPart } from "@/lib/fable-ui/core/tool-renderer"
import { showMetric } from "@/lib/fable-ui/tools/show-metric-tool"
 
const registry = {
  show_metric: showMetric,
}
 
export function ToolPart({ part }: { part: unknown }) {
  return <FableToolPart part={part} registry={registry} />
}

Unknown tool parts render a small fallback. Invalid payloads are parsed with the item schema and render the component error state.

collect_input is a UI handoff: it renders from the tool input and waits for the host app to handle user submission. The playground mock mode can stream valid and invalid form tool parts without calling a provider, which is useful for verifying renderer safety.

Wire Interactive Tool Parts

Display-only tools can render without handlers. Interactive surfaces need the host chat to pass callbacks into FableToolPart.

For show_next_actions, a click should become a normal user message:

"use client"
 
import { useCallback } from "react"
import { useChat } from "@ai-sdk/react"
import { DefaultChatTransport } from "ai"
import { FableToolPart } from "@/lib/fable-ui/core/tool-renderer"
import { showNextActions } from "@/lib/fable-ui/tools/show-next-actions-tool"
 
const registry = {
  show_next_actions: showNextActions,
}
 
export function ChatToolPart({ part }: { part: unknown }) {
  const { sendMessage, status } = useChat({
    transport: new DefaultChatTransport({ api: "/api/chat" }),
  })
  const isBusy = status === "submitted" || status === "streaming"
 
  const onSuggestedAction = useCallback(
    (action: { prompt: string }) => {
      if (isBusy) {
        return
      }
 
      void sendMessage({ text: action.prompt })
    },
    [isBusy, sendMessage],
  )
 
  return (
    <FableToolPart
      part={part}
      registry={registry}
      handlers={{
        onSuggestedAction: isBusy ? undefined : onSuggestedAction,
      }}
    />
  )
}

In a full chat shell, prefer defining this handler next to the composer and passing it down through message rendering. That keeps suggested actions and manual composer sends on the same path, including request options:

await sendMessage(
  { text: action.prompt },
  {
    body: {
      provider,
      model,
      selectedKeyId,
      apiKey,
    },
  },
)

The selected action is user intent, not an automatic side effect. The model receives the prompt, then decides whether to answer in text or call another registered Fable tool.

DataBrowser Resources

show_data_browser should receive a resourceId, not database details. Register resources in host code, then add the safe manifest to your system prompt:

show_chart renders static model-provided chart data only. Fetch private or live data in host code, validate the rows, then pass display-ready data into the chart payload.

import { describeAvailableResources } from "@/lib/fable-ui/tools/show-data-browser-tool"
 
const result = streamText({
  model,
  system: [
    "Use Fable UI tools for structured UI.",
    describeAvailableResources(),
  ].join("\n\n"),
  messages,
  tools,
})

The model can select a resource id from that manifest. It must not pass raw SQL, Firestore paths, collection names, REST URLs, secrets, or permission decisions.

Quickstart

The quickstart installs a working example at /fable-chat and /api/fable-chat. Configure FABLE_AI_PROVIDER, FABLE_AI_MODEL, and FABLE_AI_API_KEY in .env.local; when those variables are missing, the chat responds with setup guidance instead of rendering placeholder data.

Provider mode is opt-in. Keep provider keys, data access, authorization, writes, and validation in your host app.