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
| Import | Provides |
|---|---|
@kreogen/ai | Everything from ai — streamText, generateObject, DefaultChatTransport, types |
@kreogen/ai/lib/react | Everything from @ai-sdk/react — useChat, useCompletion |
@kreogen/ai/lib/models | The 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
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
| Variable | Required | Purpose |
|---|---|---|
OPENAI_API_KEY | no | Must 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.