GeniffyDocs
Changelog Log In Get a key

Vercel AI SDK

Give your AI SDK chat a memory of each of your users. The route puts what is known about the user in the instructions, streams the reply as before, and saves the exchange when the stream ends. useChat in the browser works as it did.

Install

Terminal
npm install ai @ai-sdk/anthropic geniffy

Set ANTHROPIC_API_KEY, and GENIFFY_API_KEY from API keys in the Geniffy app.

A chat route that remembers

app/api/chat/route.ts
import { anthropic } from "@ai-sdk/anthropic";
import { convertToModelMessages, streamText, type UIMessage } from "ai";
import { Geniffy } from "geniffy";
import { signedInUser } from "@/lib/auth";        // your own sign-in

const geniffy = new Geniffy();                    // reads GENIFFY_API_KEY

// the words of a message, without its files and tool calls
const textOf = (message: UIMessage) =>
  message.parts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n");

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();
  const user = await signedInUser(req);           // never from the request body
  const mem = geniffy.space(`user_${user.id}`);
  const question = textOf(messages[messages.length - 1]);
  const context = await mem.context(question);    // what is known that bears on it

  const result = streamText({
    model: anthropic("claude-opus-5-5"),
    instructions: `You are a helpful assistant.\n\n<memory>\n${context}\n</memory>`,
    messages: await convertToModelMessages(messages),
    onEnd: async ({ text }) => {
      await mem.memories.add({
        messages: [
          { role: "user", content: question },
          { role: "assistant", content: text },
        ],
      });
    },
  });
  return result.toUIMessageStreamResponse();
}

The model sees what is known about this user before every reply, each line with where it came from. When nothing is known, the block says so in one sentence, so the model says it doesn't know instead of guessing.

The stream closes only after onEnd has saved the exchange, so the save is never cut short when the function ends.

Let the model look things up

To let the model decide when to look something up, give it a recall tool, and let it take a few steps. The tool's input is described with zod (npm install zod).

app/api/chat/route.ts
import { anthropic } from "@ai-sdk/anthropic";
import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from "ai";
import { Geniffy } from "geniffy";
import { z } from "zod";
import { signedInUser } from "@/lib/auth";

const geniffy = new Geniffy();
const instructions =
  "You are a helpful assistant. Use recall before answering anything " +
  "that depends on what the user said before.";

const textOf = (message: UIMessage) =>
  message.parts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n");

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();
  const user = await signedInUser(req);
  const mem = geniffy.space(`user_${user.id}`);
  const question = textOf(messages[messages.length - 1]);

  const result = streamText({
    model: anthropic("claude-opus-5-5"),
    instructions,
    messages: await convertToModelMessages(messages),
    tools: {
      recall: tool({
        description: "Look up what is known about the user, with where it came from.",
        inputSchema: z.object({ query: z.string().describe("What to look up") }),
        execute: async ({ query }) => mem.context(query),
      }),
    },
    stopWhen: stepCountIs(5),
    onEnd: async ({ text }) => {
      await mem.memories.add({
        messages: [
          { role: "user", content: question },
          { role: "assistant", content: text },
        ],
      });
    },
  });
  return result.toUIMessageStreamResponse();
}

Any model

The memory is in the route, not in the model, so any provider works the same way. For OpenAI, install @ai-sdk/openai and change the model:

import { openai } from "@ai-sdk/openai";

model: openai("gpt-5.5"),
Last updated October 5, 2026