> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Structured Data

> Learn how to use Vercel AI SDK for structured data generation in Tuturuuu.

<Info>
  **Prerequisite**: You should have followed the [Development](/build/development-tools/development) and
  [Local Supabase Development](/build/development-tools/local-supabase-development) setup guides.
</Info>

## Overview

Tuturuuu leverages the [Vercel AI SDK](https://sdk.vercel.ai/docs/foundations/overview) to generate structured data from large language models (LLMs). This approach enables type-safe AI responses, improved reliability, and consistent data structures for features like flashcards, quizzes, and learning plans.

This guide covers how to use AI structured data generation in the Tuturuuu development workflow.

## Key Concepts

### What is Structured Data Generation?

While text generation can be useful, many applications require generating structured data. For example, you might want to:

* Extract specific information from text
* Generate quizzes or flashcards from learning material
* Create complex objects like learning plans or task lists
* Ensure AI responses follow a consistent format

The AI SDK standardizes structured object generation across model providers with the `generateObject` and `streamObject` functions. You can use Zod schemas to specify the shape of the data that you want, and the AI model will generate data that conforms to that structure.

## Architecture in Tuturuuu

Tuturuuu's AI features follow this high-level architecture:

1. **Frontend UI** - React components that display and interact with AI-generated content
2. **API Routes** - Next.js routes that handle AI requests and responses
3. **AI SDK** - Vercel AI SDK that manages model providers and generates structured data
4. **Supabase** - Backend database for authentication, authorization, and storing AI-generated content

### Mira Chat Attachments

Dashboard chat attachments are uploaded to Supabase Storage before the user
message is sent. New chats may first place files under
`{wsId}/chats/ai/resources/temp/{userId}` and then move them into
`{wsId}/chats/ai/resources/{chatId}` once the chat exists.

Tools that read those attachments, such as `convert_file_to_markdown`, should
resolve bare filenames and stale same-workspace attachment paths against the
current chat folder. They must still reject full paths from another workspace.

### Mira Dashboard Chat Agent Loop

The dashboard Mira chat keeps the main assistant in fast mode by default. New
sessions should ignore stale stored `thinking` preferences unless the user
manually opts into the toolbar's deep-check mode for that active session.

Assistant text should stream as soon as it is useful, then tool calls may run
inline, followed by more assistant text in the same response. Keep every
assistant text surface on the shared Streamdown wrapper, including text rendered
inside compact tool UIs, so code blocks, tables, Mermaid, math, and CJK spacing
stay consistent.

Mira enables Streamdown math with `singleDollarTextMath` because model output
commonly uses `$...$` inline LaTeX. Keep `@streamdown/math` in the Tailwind
source scan alongside the base Streamdown dist files, and keep
`katex/dist/katex.min.css` imported from the chat renderer path.

While the request is submitted but no assistant text has arrived yet, the chat
should render a lightweight assistant activity bubble with rotating status copy.
Hide that placeholder as soon as real assistant text streams, and keep it
separate from markdown/tool rendering so status cycling does not re-render heavy
message content.

Keep the first optional-tool model step lean. Unless a workflow must force a
specific tool before answering, expose only `select_tools` and
`no_action_needed` on the first step so the model can stream direct answers
without carrying every tool schema. Stream smoothing should not add artificial
per-chunk delay on the dashboard chat path; perceived smoothness belongs in the
client activity state, not in delayed server chunks.

Do not force `select_tools` as the universal first step. Force tool selection
only when the workflow cannot answer safely before a tool runs, such as current
web lookups, workspace context switching, workspace member lookups, file
conversion, or writes. Complex verification, risk review, planning checks, or
conflicting evidence should use `run_parallel_checks`, which delegates to
bounded `ToolLoopAgent` subagents in parallel and returns a compact summary to
the main assistant. Any Mira tool that spawns additional model calls must use
`MiraToolContext.creditWsId ?? wsId`, resolve the plan model for that billed
workspace, preflight AI credits, apply the workspace output-token cap, and
deduct the subcall usage before exposing the generated result.

## Schema Definitions

Schemas define the structure of the data that will be generated by the AI models. In Tuturuuu, these are defined in `packages/ai/src/object/types.ts` using [Zod](https://github.com/colinhacks/zod).

Here are some examples of schemas used in Tuturuuu:

### Flashcard Schema

```typescript theme={null}
export const flashcardSchema = z.object({
  flashcards: z.array(
    z.object({
      front: z.string().describe('Question. Do not use emojis or links.'),
      back: z.string().describe('Answer. Do not use emojis or links.'),
    })
  ),
});
```

### Quiz Schema

```typescript theme={null}
export const quizSchema = z.object({
  quizzes: z.array(
    z.object({
      question: z.string().describe('Question. Do not use emojis or links.'),
      quiz_options: z.array(
        z.object({
          value: z.string().describe('Option. Do not use emojis or links.'),
          explanation: z
            .string()
            .describe(
              'Explain why this option is correct or incorrect, if it is incorrect, explain possible misconceptions and what made the option wrong with respect to the question, if it is correct, explain why it is correct. Be as detailed as possible.'
            ),
          is_correct: z.boolean().describe('This option is a correct answer.'),
        })
      ),
    })
  ),
});
```

### Year Plan Schema

```typescript theme={null}
export const yearPlanSchema = z.object({
  yearPlan: z.object({
    overview: z
      .string()
      .describe(
        'A high-level overview of the year plan and how the goals will be achieved'
      ),
    quarters: z
      .array(quarterSchema)
      .describe('List of quarters in the year plan'),
    recommendations: z
      .array(z.string())
      .describe('Additional recommendations, tips, or considerations'),
    start_date: z
      .string()
      .describe('Start date of the year plan (ISO date string)'),
    end_date: z
      .string()
      .describe('End date of the year plan (ISO date string)'),
  }),
});
```

## Creating an API Endpoint

To create an API endpoint that generates structured data, follow these steps:

### 1. Create a new route file

Create a new route file in the appropriate Next.js app, for example:

```typescript theme={null}
// app/api/ai/flashcards/route.ts
import { google } from '@ai-sdk/google';
import { flashcardSchema } from '@tuturuuu/ai/object/types';
import {
  createAdminClient,
  createClient,
} from '@tuturuuu/supabase/next/server';
import { streamObject } from 'ai';

export async function POST(req: Request) {
  // Implementation...
}
```

### 2. Implement authentication and validation

Use Supabase to authenticate the user and validate their permissions:

```typescript theme={null}
// createAdminClient() is synchronous; do not await it.
const sbAdmin = createAdminClient();
const { wsId, context } = await req.json();

// Validate input
if (!wsId) return new Response('Missing workspace ID', { status: 400 });
if (!context) return new Response('Missing context', { status: 400 });

// Authenticate user
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response('Unauthorized', { status: 401 });

// Check feature flag
const { count, error } = await sbAdmin
  .from('workspace_secrets')
  .select('*', { count: 'exact', head: true })
  .eq('ws_id', wsId)
  .eq('name', 'ENABLE_CHAT')
  .eq('value', 'true');

if (error) return new Response(error.message, { status: 500 });
if (count === 0)
  return new Response('You are not allowed to use this feature.', { status: 401 });
```

#### Fast session auth for Mira and assistant routes

Session-authenticated AI routes may accept the `x-tuturuuu-ai-temp-auth` header as an optimization before falling back to Supabase `getUser()`. The browser mints this token through `POST /api/ai/temp-auth/token` after the normal session, workspace normalization, membership, and selected billing workspace checks succeed. Tokens live only in memory on the client, expire after 60 seconds, and are stored in Redis only as SHA-256 digests.

Revocation is version-based: `ai:temp-auth:user-version:{userId}` is bumped before logout or account removal, so any token minted under the old version is rejected. Redis is not authoritative for security; when Redis is unavailable or a token is missing/invalid, routes fall back to the existing Supabase session path. A revoked token returns `401` and does not fall back.

Credit availability snapshots are also Redis-backed under `ai:credits:snapshot:{billingWsId}:{userId}`. They are UI/status hints only and must not authorize model execution. AI preflight must call the authoritative Postgres allowance RPC so daily credit limits, daily request limits, and feature-specific request limits are enforced before every model run. Actual reservations, deductions, and ledger writes remain in Postgres, and successful commits invalidate the snapshot.

### 3. Generate structured data

Use the AI SDK to generate structured data based on the schema:

```typescript theme={null}
const result = streamObject({
  // Use a current default model ID. Active callers in `packages/ai/src`
  // (calendar, chat title generation, search wrapper) use
  // `gemini-3.1-flash-lite`. See "Supported Models" below for how the
  // catalog is resolved dynamically at runtime.
  model: google('gemini-3.1-flash-lite', {
    safetySettings: [
      {
        category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
        threshold: 'BLOCK_NONE',
      },
      // Other safety settings...
    ],
  }),
  prompt: `Generate 10 flashcards with the following context: ${context}`,
  schema: flashcardSchema,
});

// Stream the response to the client
return result.toTextStreamResponse();
```

## Supported Models

Tuturuuu supports multiple AI models through the Vercel AI SDK. There is **no
static model array** anymore — the catalog is sourced dynamically from the AI
gateway and stored in the `ai_gateway_models` table (synced by
`packages/ai/src/credits/sync-gateway-models.ts`). The browsable catalog is
served from `apps/web`:

* **API**: `GET /api/v1/infrastructure/ai/models`
  (`apps/web/src/app/api/v1/infrastructure/ai/models/route.ts`)
* **Docs**: see [Model Catalog](/platform/ai/model-catalog) for how metadata is
  published, browsed, and synced.

The set of provider integrations the SDK can wire up lives in
`packages/ai/src/supported-providers.ts`:

```typescript theme={null}
export const supportedProviders = [
  'google',
  'google-vertex',
  'openai',
  'anthropic',
] as const;
```

For per-workspace plan resolution (which model a billed workspace is allowed to
run), use the helpers under `packages/ai/src/credits/` such as
`resolve-plan-model.ts` and `cap-output-tokens.ts`, rather than hard-coding a
model ID.

To use a different model in your endpoint, change the model reference. Active
callers default to `gemini-3.1-flash-lite`:

```typescript theme={null}
const result = streamObject({
  model: google('gemini-3.1-flash-lite', {
    // Configuration...
  }),
  // Other parameters...
});
```

## Calling from the Frontend

To call your AI endpoint from the frontend, you can use the appropriate hooks or fetch API:

```typescript theme={null}
// Example using fetch
async function generateFlashcards(workspaceId: string, context: string) {
  const response = await fetch('/api/ai/flashcards', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      wsId: workspaceId,
      context,
    }),
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(error);
  }

  // For streaming responses
  const reader = response.body?.getReader();
  const decoder = new TextDecoder();

  while (reader) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    // Process the chunk (partial object)
    console.log(JSON.parse(chunk));
  }
}
```

## Integration with Supabase

Tuturuuu's AI features are tightly integrated with Supabase for several purposes:

### TypeScript Types

Supabase-generated TypeScript types are available at `packages/types/src/supabase.ts`. These types are automatically generated when you run `bun sb:typegen` or `bun sb:reset` and are accessible to all apps that have the `@tuturuuu/types` package installed.

You can use these types to ensure type safety when working with Supabase data in your AI features:

```typescript theme={null}
import type { Database } from '@tuturuuu/types';

// Type-safe access to workspace_secrets table
const { data, error } = await supabase
  .from<
    Database['public']['Tables']['workspace_secrets']['Row']
  >('workspace_secrets')
  .select('*')
  .eq('ws_id', wsId)
  .eq('name', 'ENABLE_AI');

// Type-safe access to specific columns
const { data: workspace } = await supabase
  .from('workspaces')
  .select('id, name, handle')
  .eq('id', wsId)
  .single();

// TypeScript knows the structure of 'workspace' with proper types
const workspaceName: string = workspace?.name;
```

### Short-hand Type Access

For more convenient access to common table types in your AI features, Tuturuuu also provides short-hand type definitions in `packages/types/src/db.ts`. These are easier to use and remember than the full database type paths:

```typescript theme={null}
import type { AIChat, AIPrompt, WorkspaceDocument } from '@tuturuuu/types';

// Use short-hand types directly for AI-related tables
const { data: chats } = await supabase
  .from('ai_chats')
  .select('*')
  .eq('creator_id', user.id);

// Types are properly inferred
chats?.forEach((chat: AIChat) => {
  console.log(chat.id, chat.title);
});

// Store AI-generated content with proper types
const document: WorkspaceDocument = {
  id: uuidv4(),
  ws_id: wsId,
  name: 'AI Generated Document',
  content: generatedContent,
  created_at: new Date().toISOString(),
  creator_id: user.id,
};

await supabase.from('workspace_documents').insert(document);
```

The short-hand types can also include extended client-side properties that aren't in the database schema, making them perfect for your AI feature implementations.

This ensures that your AI features correctly interact with the database schema, reducing runtime errors and improving development experience.

### Authentication and Authorization

Before making AI requests, ensure the user is authenticated and authorized to use the feature:

```typescript theme={null}
// Get the current user
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response('Unauthorized', { status: 401 });

// Check workspace membership
const { data: member, error: memberError } = await sbAdmin
  .from('workspace_members')
  .select('*')
  .eq('ws_id', wsId)
  .eq('user_id', user.id)
  .single();

if (memberError || !member)
  return new Response('You are not a member of this workspace', { status: 403 });
```

### Feature Flags

Use the `workspace_secrets` table to enable or disable AI features for specific workspaces:

```typescript theme={null}
// Check if the AI feature is enabled for this workspace
const { count, error } = await sbAdmin
  .from('workspace_secrets')
  .select('*', { count: 'exact', head: true })
  .eq('ws_id', wsId)
  .eq('name', 'ENABLE_AI')
  .eq('value', 'true');

if (error) return new Response(error.message, { status: 500 });
if (count === 0)
  return new Response('AI features are not enabled for this workspace', { status: 403 });
```

### Storing Results

You can store AI-generated content in Supabase for future use:

```typescript theme={null}
// Store the generated flashcards
const { error } = await sbAdmin
  .from('flashcard_sets')
  .insert({
    ws_id: wsId,
    creator_id: user.id,
    name: 'Generated Flashcards',
    description: context.substring(0, 100) + '...',
    cards: result.object.flashcards,
  });

if (error) return new Response(error.message, { status: 500 });
```

## Best Practices

### Schema Design

When designing schemas for AI-generated content:

1. **Be specific** - Use the `.describe()` method to provide clear instructions to the AI model
2. **Keep it simple** - Break complex schemas into smaller, nested objects
3. **Add validations** - Use Zod's validation methods (`.min()`, `.max()`, `.regex()`, etc.)
4. **Use enums** - For fields with a fixed set of values, use `.enum()`

Example of a well-designed schema:

```typescript theme={null}
const taskSchema = z.object({
  title: z.string().min(3).max(100).describe('A concise title for the task'),
  description: z
    .string()
    .min(10)
    .describe('Detailed description of what needs to be done'),
  priority: z
    .enum(['high', 'medium', 'low'])
    .describe('Priority level of the task'),
  due_date: z.string().describe('Due date in ISO format (YYYY-MM-DD)'),
  estimated_hours: z
    .number()
    .min(0)
    .max(100)
    .describe('Estimated hours to complete'),
});
```

### Error Handling

Implement robust error handling for AI-generated content:

```typescript theme={null}
try {
  const result = streamObject({
    // Configuration...
  });

  return result.toTextStreamResponse();
} catch (error) {
  console.error('AI generation error:', error);

  // Return a friendly error message
  return NextResponse.json(
    {
      message: 'Failed to generate content. Please try again later.',
      error: error.message,
    },
    { status: 500 }
  );
}
```

### Response Processing

For complex AI-generated content, you may need to post-process the response:

```typescript theme={null}
// Example: Filtering out inappropriate content
const filteredFlashcards = result.object.flashcards.filter((card) => {
  // Remove cards containing inappropriate words
  const inappropriateWords = ['inappropriate1', 'inappropriate2'];
  return !inappropriateWords.some(
    (word) => card.front.includes(word) || card.back.includes(word)
  );
});
```

## Local Development and Testing

### Setting Up API Keys

To test AI features locally, you need to set up the appropriate API keys in your environment:

1. Create a `.env.local` file in the root of your Next.js app
2. Add the necessary API keys:

```
GOOGLE_GENERATIVE_AI_API_KEY=your-google-ai-key
OPENAI_API_KEY=your-openai-key
```

3. Restart your development server

### Testing AI Endpoints

You can test your AI endpoints using tools like Postman or simple cURL commands:

```bash theme={null}
curl -X POST http://localhost:3000/api/ai/flashcards \
  -H "Content-Type: application/json" \
  -d '{"wsId":"00000000-0000-0000-0000-000000000000","context":"The process of photosynthesis converts light energy into chemical energy that can be used by plants and other organisms."}'
```

## Troubleshooting

### Common Issues

1. **API Key Issues**: Ensure your API keys are correctly set in your environment
2. **Model Unavailability**: Some models may be unavailable in certain regions
3. **Token Limits**: Large prompts may exceed token limits
4. **Schema Validation Errors**: The AI might generate content that doesn't match your schema

### Debugging Tips

1. **Log the prompt**: Print the full prompt being sent to the AI model
2. **Start with simple schemas**: Begin with simple schemas and gradually increase complexity
3. **Check response format**: Verify the raw response from the AI model before schema validation

## Further Resources

* [Vercel AI SDK Documentation](https://sdk.vercel.ai/docs/foundations/overview)
* [Zod Documentation](https://zod.dev/)
* [Supabase Documentation](https://supabase.com/docs)
* [Google Generative AI Documentation](https://ai.google.dev/docs)
