AI

A thin wrapper over the AI SDK, with chat primitives.

@kreogen/ai re-exports the AI SDK and adds two things: a configured model registry, and the minimum UI needed to render a conversation.

It is deliberately thin. There is no chat abstraction of kreogen's own to learn or work around — everything the SDK documents works here unchanged.

Entry points

ImportProvides
@kreogen/aiEverything from aistreamText, generateObject, DefaultChatTransport, types
@kreogen/ai/lib/reactEverything from @ai-sdk/reactuseChat, useCompletion
@kreogen/ai/lib/modelsThe configured models object
@kreogen/ai/components/thread<Thread>, a scrolling conversation container
@kreogen/ai/components/message<Message>, one bubble, markdown-aware

This package is on AI SDK v6 (ai@^6, @ai-sdk/react@^3). Guides written for v4 will not work: useChat no longer owns the input, and toDataStreamResponse() is toUIMessageStreamResponse(). The chatbot example has the full mapping.

Models

packages/ai/lib/models.ts
const { OPENAI_API_KEY } = keys();

const openai = OPENAI_API_KEY
  ? createOpenAI({ apiKey: OPENAI_API_KEY })
  : undefined;

export const models:
  | {
      chat: LanguageModel;
      embeddings: EmbeddingModel;
    }
  | undefined = openai
  ? {
      chat: openai('gpt-4o-mini'),
      embeddings: openai.textEmbeddingModel('text-embedding-3-small'),
    }
  : undefined;

Naming the models in one place is what lets you change provider or tier in one edit rather than across every call site. Swap @ai-sdk/openai for another provider package and the rest of the repository is unaffected — the SDK's model interface is the boundary.

The type annotation is not decorative. Without it TypeScript infers a type naming the hoisted @ai-sdk/provider path, which is not portable across installs and breaks in a way that has nothing obviously to do with models.

models is undefined without OPENAI_API_KEY, like every other optional integration here. The guard is not defensive tidiness: the provider constructs happily without a key — it only reads one per request — so before it existed the first model call failed with a LoadAPIKeyError at runtime while the type insisted chat was always there, leaving a caller no signal that it had to degrade.

Configuration

VariableRequiredPurpose
OPENAI_API_KEYnoMust start with sk-

Without the key, models is undefined and every AI feature has to say so. Guard with models?. at the seam, or answer 503 from the route handler — see the chatbot example.

Components

<Thread> is a flex column that scrolls. <Message> takes a UIMessage and renders it, aligned by role, with the assistant's markdown streamed through Streamdown so partial output does not flicker as syntax completes.

<Thread>
  {messages.map((message) => (
    <Message data={message} key={message.id} />
  ))}
</Thread>

<Message> reads message.parts, filtering to the text ones. In v6 a message is an array of typed parts — text, reasoning, tool calls, files — and message.content no longer exists. A component reading it renders nothing rather than failing, which is a slow bug to find.

Things worth doing per project

Authenticate the route. A handler that calls a paid model on request is somebody else's bill if it is public. Route handlers are not covered by the page-level auth check.

Rate limit it. A limiter keyed on the user id bounds the damage from a stuck client as much as from an attacker.

Bound the tool loop. stopWhen: stepCountIs(n) on streamText caps how many round trips one request may make. Without it, a model that keeps calling tools keeps spending.