ai
98 linesSource
Use when adding LLM features (chat, streamed text, structured output, tools, media generation)
Mechanics are covered by TanStack's own @tanstack/ai skills. This skill adds the wcz-layout conventions on top of that.
Rules
- The gateway needs three server-side inputs:
- Ensure that required env are present: AI_BASE_URL (z.url()) and AI_SUBSCRIPTION_KEY (z.string()) in src/env.ts.
- Pass getAppToken("cognitiveServices") as the API key.
- The subscription key sent as the Ocp-Apim-Subscription-Key header.
- Build the adapter in one place, src/server/ai.ts, and call it per request. getAppToken caches and refreshes the token.
- Build every chat with createLayoutChatHook(chatFn) from wcz-layout/hooks.
- The model renders MUI components itself (show_ui) and asks the user with MUI forms (ask_user). The client sends both tools with every request, so the server function only merges them: tools: mergeAgentTools(serverTools, tools). It must also forward parentRunId and resume, or tool results and approvals never continue the run.
- App tools are server tools (toolDefinition(...).server(...)) in serverTools; they check the signed-in user themselves. Give every tool that writes needsApproval: true: the chat shows an Approve / Deny card for it. Their calls show only the progress bar in the transcript.
- Structured output (one typed object, e.g. a generated document) stays raw useChat with outputSchema and partial, not the chat.
- Render streamed text outside the chat with @tanstack/markdown (install if missing): <Markdown> from @tanstack/markdown/react.
Example
ts
// src/server/ai.ts
import { createOpenaiChat } from "@tanstack/ai-openai";
import { getAppToken } from "wcz-layout/auth";
import { serverEnv } from "~/env";
export const aiText = async () =>
createOpenaiChat("some-model", await getAppToken("cognitiveServices"), {
baseURL: serverEnv.AI_BASE_URL,
defaultHeaders: { "Ocp-Apim-Subscription-Key": serverEnv.AI_SUBSCRIPTION_KEY },
});
// src/server/actions/<feature>.ts
import {
chat,
chatParamsFromRequestBody,
mergeAgentTools,
toServerSentEventsResponse,
toolDefinition,
} from "@tanstack/ai";
import type { RunAgentInput } from "@tanstack/ai/client";
import { createServerFn } from "@tanstack/react-start";
import z from "zod";
import { authMiddleware } from "wcz-layout/middleware";
import { aiText } from "~/server/ai";
const getOrders = toolDefinition({
name: "get_orders",
description: "Orders of the signed-in user",
inputSchema: z.object({ status: z.enum(["open", "closed"]) }),
}).server(async ({ status }) => findOrders(status));
export const chatFn = createServerFn({ method: "POST" })
.validator(z.custom<RunAgentInput>())
.middleware([authMiddleware()])
.handler(async ({ data }) => {
const { messages, threadId, runId, parentRunId, resume, tools } =
await chatParamsFromRequestBody(data);
const abortController = new AbortController();
const stream = chat({
adapter: await aiText(),
threadId,
runId,
parentRunId,
resume,
systemPrompts: [SYSTEM_PROMPT],
messages,
tools: mergeAgentTools([getOrders], tools),
abortController,
});
return toServerSentEventsResponse(stream, { abortController });
});tsx
// client component
import { createLayoutChatHook } from "wcz-layout/hooks";
import { chatFn } from "~/server/actions/chat";
const { useLayoutChat, LayoutChat } = createLayoutChatHook(chatFn);
const chat = useLayoutChat({ threadId: "assistant" });