ANTIGRAVITY LABJP
Articles/Integrations
Integrations/2026-03-14Intermediate

Antigravity × Vercel AI SDK Integration — Build AI Chat & Streaming UI Fast

Build AI chat applications with Vercel AI SDK in Antigravity. Covers useChat/useCompletion hooks, streaming responses, tool calling, and multimodal input.

Antigravity224VercelAI SDKstreaming5Next.js4

Antigravity × Vercel AI SDK Integration

Combining Vercel AI SDK with Antigravity enables rapid development of high-performance AI chat applications. This guide covers practical integration techniques in detail.

Vercel AI SDK Overview

Vercel AI SDK is a JavaScript/TypeScript library for building AI-powered user interfaces. It supports multiple LLM providers and offers streaming, tool calling, conversation management, and more.

Key Features

  • Streaming responses: Real-time text generation display
  • Multi-provider support: OpenAI, Anthropic, and more
  • Tool calling: LLM invokes external APIs
  • Conversation management: Automatic history handling
  • Multimodal support: Text, images, file inputs

Setup and Initial Configuration

Project Initialization

# Create Next.js project
npx create-next-app@latest ai-chat-app \
  --typescript \
  --tailwind \
  --eslint
 
cd ai-chat-app
 
# Install Vercel AI SDK and dependencies
npm install ai
 
# Install Antigravity SDK
npm install @antigravitylab/sdk
 
# Setup environment variables
cat > .env.local << 'EOF'
ANTIGRAVITY_API_KEY=your-api-key-here
NEXT_PUBLIC_API_URL=http://localhost:3000
EOF

package.json Configuration

{
  "name": "antigravity-ai-chat",
  "version": "1.0.0",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "ai": "^3.0.0",
    "@antigravitylab/sdk": "^1.0.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "next": "^14.0.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.0.0",
    "typescript": "^5.0.0",
    "tailwindcss": "^3.0.0",
    "autoprefixer": "^10.0.0"
  }
}
ℹ️
Obtain your Antigravity API key from the Antigravity dashboard or environment configuration.

useChat Hook Implementation

Building Basic Chat UI

// app/page.tsx
'use client';
 
import { useChat } from 'ai/react';
import { useState } from 'react';
 
export default function ChatPage() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: '/api/chat',
    initialMessages: [
      {
        id: 'system',
        role: 'assistant',
        content: 'Hello! I\'m Antigravity AI Assistant. How can I help you today?',
      },
    ],
  });
 
  return (
    <div className="flex flex-col h-screen bg-gray-900 text-white">
      {/* Message display area */}
      <div className="flex-1 overflow-y-auto p-6 space-y-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
          >
            <div
              className={`max-w-xs lg:max-w-md xl:max-w-lg px-4 py-2 rounded-lg ${
                message.role === 'user'
                  ? 'bg-blue-600 text-white'
                  : 'bg-gray-700 text-gray-100'
              }`}
            >
              {message.content}
            </div>
          </div>
        ))}
        {isLoading && (
          <div className="flex justify-start">
            <div className="bg-gray-700 px-4 py-2 rounded-lg">
              <div className="flex space-x-2">
                <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
                <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
                <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.4s' }}></div>
              </div>
            </div>
          </div>
        )}
      </div>
 
      {/* Input form */}
      <form onSubmit={handleSubmit} className="border-t border-gray-700 p-4">
        <div className="flex gap-4">
          <input
            type="text"
            value={input}
            onChange={handleInputChange}
            placeholder="Type your message..."
            disabled={isLoading}
            className="flex-1 px-4 py-2 bg-gray-800 border border-gray-600 rounded-lg focus:outline-none focus:border-blue-500 disabled:opacity-50"
          />
          <button
            type="submit"
            disabled={isLoading}
            className="px-6 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  );
}

API Handler Implementation

// app/api/chat/route.ts
import { Message, StreamingTextResponse } from 'ai';
import Antigravity from '@antigravitylab/sdk';
 
const client = new Antigravity({
  apiKey: process.env.ANTIGRAVITY_API_KEY,
});
 
export async function POST(req: Request) {
  const { messages } = await req.json();
 
  // Generate streaming response with Antigravity agent
  const stream = await client.streamCompletion({
    messages: messages.map((msg: Message) => ({
      role: msg.role,
      content: msg.content,
    })),
    model: 'claude-3-opus',
    system: `
      You are a professional Antigravity IDE assistant.
      Provide accurate and practical answers to user questions.
      Include code examples when appropriate.
    `,
    temperature: 0.7,
    maxTokens: 2048,
  });
 
  return new StreamingTextResponse(stream);
}
⚠️
Manage API keys securely via environment variables. Never expose them on the client side.

useCompletion Hook Integration

Text Generation UI

// app/components/TextGeneration.tsx
'use client';
 
import { useCompletion } from 'ai/react';
import { useState } from 'react';
 
export function TextGenerationPage() {
  const { completion, input, handleInputChange, handleSubmit, isLoading } = useCompletion({
    api: '/api/completion',
    onFinish: (prompt, completion) => {
      console.log('Generation complete:', completion);
    },
  });
 
  const [generatedText, setGeneratedText] = useState('');
 
  const handleGenerateCustom = async (prompt: string) => {
    const result = await fetch('/api/completion', {
      method: 'POST',
      body: JSON.stringify({ prompt }),
    });
 
    const { text } = await result.json();
    setGeneratedText(text);
  };
 
  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">Text Generation Tool</h1>
 
      {/* Preset templates */}
      <div className="grid grid-cols-2 gap-4 mb-8">
        <button
          onClick={() => handleGenerateCustom('Generate 5 blog post title ideas')}
          className="p-4 bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
        >
          Blog Title Generator
        </button>
        <button
          onClick={() => handleGenerateCustom('Write a product description')}
          className="p-4 bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors"
        >
          Product Description
        </button>
      </div>
 
      {/* Input form */}
      <form onSubmit={handleSubmit} className="mb-8">
        <textarea
          value={input}
          onChange={handleInputChange}
          placeholder="Enter custom prompt..."
          className="w-full px-4 py-2 bg-gray-800 border border-gray-600 rounded-lg mb-4"
          rows={4}
        />
        <button
          type="submit"
          disabled={isLoading}
          className="px-6 py-2 bg-green-600 rounded-lg hover:bg-green-700 disabled:opacity-50"
        >
          Generate
        </button>
      </form>
 
      {/* Generated result */}
      {completion && (
        <div className="bg-gray-800 p-6 rounded-lg border border-gray-700">
          <h2 className="text-xl font-bold mb-4">Generated Result</h2>
          <p className="text-gray-200 whitespace-pre-wrap">{completion}</p>
        </div>
      )}
    </div>
  );
}

Completion API Handler

// app/api/completion/route.ts
import { StreamingTextResponse } from 'ai';
import Antigravity from '@antigravitylab/sdk';
 
const client = new Antigravity({
  apiKey: process.env.ANTIGRAVITY_API_KEY,
});
 
export async function POST(req: Request) {
  const { prompt } = await req.json();
 
  const stream = await client.streamCompletion({
    messages: [
      {
        role: 'user',
        content: prompt,
      },
    ],
    model: 'claude-3-opus',
    maxTokens: 1024,
  });
 
  return new StreamingTextResponse(stream);
}

Tool Calling (Function Calling)

Tool Definition and Handlers

// lib/tools.ts
export const tools = {
  getCurrentWeather: {
    description: 'Get current weather for a location',
    parameters: {
      type: 'object',
      properties: {
        location: {
          type: 'string',
          description: 'City name',
        },
        unit: {
          type: 'string',
          enum: ['celsius', 'fahrenheit'],
          description: 'Temperature unit',
        },
      },
      required: ['location'],
    },
  },
 
  getCodeReview: {
    description: 'Review code and provide improvement suggestions',
    parameters: {
      type: 'object',
      properties: {
        code: {
          type: 'string',
          description: 'Code to review',
        },
        language: {
          type: 'string',
          description: 'Programming language',
        },
      },
      required: ['code', 'language'],
    },
  },
 
  executeCommand: {
    description: 'Execute shell command',
    parameters: {
      type: 'object',
      properties: {
        command: {
          type: 'string',
          description: 'Command to execute',
        },
        cwd: {
          type: 'string',
          description: 'Working directory',
        },
      },
      required: ['command'],
    },
  },
};
 
// Tool execution function
export async function executeTool(
  toolName: string,
  toolInput: Record<string, unknown>
): Promise<string> {
  switch (toolName) {
    case 'getCurrentWeather':
      return await getWeather(toolInput.location as string);
    case 'getCodeReview':
      return await reviewCode(
        toolInput.code as string,
        toolInput.language as string
      );
    case 'executeCommand':
      return await executeShellCommand(
        toolInput.command as string,
        toolInput.cwd as string
      );
    default:
      return `Unknown tool: ${toolName}`;
  }
}
 
async function getWeather(location: string): Promise<string> {
  // Implementation: call weather API
  return `Current temperature in ${location}: 20°C`;
}
 
async function reviewCode(code: string, language: string): Promise<string> {
  // Implementation: code review logic
  return `${language} code review: improvements needed`;
}
 
async function executeShellCommand(
  command: string,
  cwd?: string
): Promise<string> {
  // Implementation: shell command execution
  return `Command execution result: ${command}`;
}

Tool Calling Integration

// app/api/chat-with-tools/route.ts
import { Message, StreamingTextResponse } from 'ai';
import Antigravity from '@antigravitylab/sdk';
import { tools, executeTool } from '@/lib/tools';
 
const client = new Antigravity({
  apiKey: process.env.ANTIGRAVITY_API_KEY,
});
 
export async function POST(req: Request) {
  const { messages } = await req.json();
 
  // Stream with tool calling support
  const response = await client.streamChatWithTools({
    messages,
    tools,
    onToolCall: async (toolName, toolInput) => {
      const result = await executeTool(toolName, toolInput);
      return {
        toolName,
        result,
      };
    },
    model: 'claude-3-opus',
  });
 
  return new StreamingTextResponse(response);
}
ℹ️
Tool calling enables LLMs to integrate with external APIs and system commands.

Multimodal Input

Image and File Input Processing

// app/components/MultimodalChat.tsx
'use client';
 
import { useState, useRef } from 'react';
import { useChat } from 'ai/react';
 
export function MultimodalChat() {
  const fileInputRef = useRef<HTMLInputElement>(null);
  const [attachedFiles, setAttachedFiles] = useState<File[]>([]);
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: '/api/chat-multimodal',
  });
 
  const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(event.target.files || []);
    setAttachedFiles(prev => [...prev, ...files]);
  };
 
  const handleRemoveFile = (index: number) => {
    setAttachedFiles(prev => prev.filter((_, i) => i !== index));
  };
 
  const handleSubmitWithFiles = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
 
    // Encode files to Base64
    const filesData = await Promise.all(
      attachedFiles.map(async file => ({
        name: file.name,
        type: file.type,
        data: await fileToBase64(file),
      }))
    );
 
    // Include files in form data
    const formData = new FormData(e.currentTarget);
    formData.append('files', JSON.stringify(filesData));
 
    // Submit logic here
    setAttachedFiles([]);
  };
 
  const fileToBase64 = (file: File): Promise<string> => {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result as string);
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  };
 
  return (
    <div className="flex flex-col h-screen bg-gray-900 text-white">
      <div className="flex-1 overflow-y-auto p-6">
        {messages.map(msg => (
          <div key={msg.id} className="mb-4">
            <span className="font-bold">{msg.role}:</span> {msg.content}
          </div>
        ))}
      </div>
 
      <form onSubmit={handleSubmitWithFiles} className="border-t border-gray-700 p-4">
        {/* Attached files list */}
        {attachedFiles.length > 0 && (
          <div className="mb-4 space-y-2">
            {attachedFiles.map((file, index) => (
              <div key={index} className="flex items-center justify-between bg-gray-800 p-2 rounded">
                <span className="text-sm">{file.name}</span>
                <button
                  type="button"
                  onClick={() => handleRemoveFile(index)}
                  className="text-red-400 hover:text-red-300"
                >
                  Remove
                </button>
              </div>
            ))}
          </div>
        )}
 
        {/* Input field */}
        <div className="flex gap-4">
          <button
            type="button"
            onClick={() => fileInputRef.current?.click()}
            className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600"
          >
            Add Files
          </button>
          <input
            ref={fileInputRef}
            type="file"
            multiple
            onChange={handleFileSelect}
            accept="image/*,.pdf,.txt,.md"
            className="hidden"
          />
          <input
            type="text"
            value={input}
            onChange={handleInputChange}
            placeholder="Type message..."
            className="flex-1 px-4 py-2 bg-gray-800 border border-gray-600 rounded"
          />
          <button
            type="submit"
            disabled={isLoading}
            className="px-6 py-2 bg-blue-600 rounded hover:bg-blue-700 disabled:opacity-50"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  );
}

Multimodal API Handler

// app/api/chat-multimodal/route.ts
import { StreamingTextResponse } from 'ai';
import Antigravity from '@antigravitylab/sdk';
 
const client = new Antigravity({
  apiKey: process.env.ANTIGRAVITY_API_KEY,
});
 
export async function POST(req: Request) {
  const formData = await req.formData();
  const message = formData.get('message') as string;
  const filesJson = formData.get('files') as string;
  const files = JSON.parse(filesJson || '[]');
 
  const stream = await client.streamCompletion({
    messages: [
      {
        role: 'user',
        content: message,
        attachments: files.map(f => ({
          type: 'document',
          mimeType: f.type,
          data: f.data,
        })),
      },
    ],
    model: 'claude-3-opus',
  });
 
  return new StreamingTextResponse(stream);
}

Advanced Features

Chat History Persistence

// lib/db.ts
import { Message } from 'ai';
 
export class ChatHistoryDB {
  static async saveMessage(conversationId: string, message: Message) {
    // Save to database
    const response = await fetch('/api/history', {
      method: 'POST',
      body: JSON.stringify({ conversationId, message }),
    });
    return response.json();
  }
 
  static async loadHistory(conversationId: string): Promise<Message[]> {
    const response = await fetch(`/api/history?conversationId=${conversationId}`);
    return response.json();
  }
 
  static async deleteHistory(conversationId: string) {
    await fetch(`/api/history/${conversationId}`, { method: 'DELETE' });
  }
}

Custom Hook

// hooks/useCustomChat.ts
import { useChat } from 'ai/react';
import { useEffect, useState } from 'react';
import { ChatHistoryDB } from '@/lib/db';
 
export function useCustomChat(conversationId: string) {
  const [isLoading, setIsLoading] = useState(true);
  const chat = useChat({
    api: '/api/chat',
  });
 
  useEffect(() => {
    // Load conversation history
    ChatHistoryDB.loadHistory(conversationId)
      .then(messages => {
        // Restore messages
        setIsLoading(false);
      });
  }, [conversationId]);
 
  // Save messages on submission
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    await chat.handleSubmit(e);
    // Save latest message
    if (chat.messages.length > 0) {
      const lastMessage = chat.messages[chat.messages.length - 1];
      await ChatHistoryDB.saveMessage(conversationId, lastMessage);
    }
  };
 
  return {
    ...chat,
    isLoading: isLoading || chat.isLoading,
    handleSubmit,
  };
}

Deployment and Best Practices

Deploy to Vercel

# Initialize repository
git init
git add .
git commit -m "Initial commit"
 
# Deploy to Vercel
vercel deploy
 
# Configure environment variables
vercel env add ANTIGRAVITY_API_KEY

Security and Performance

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
 
export function middleware(request: NextRequest) {
  // API key validation
  const apiKey = request.headers.get('Authorization');
  if (!apiKey) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }
 
  // Rate limiting
  const ip = request.ip || 'unknown';
  const rateLimit = checkRateLimit(ip);
  if (!rateLimit) {
    return NextResponse.json(
      { error: 'Rate limit exceeded' },
      { status: 429 }
    );
  }
 
  return NextResponse.next();
}
 
function checkRateLimit(ip: string): boolean {
  // Implementation: rate limit checking
  return true;
}
 
export const config = {
  matcher: '/api/:path*',
};

Practical Example: Custom AI Assistant

// app/components/AntgravityAssistant.tsx
'use client';
 
import { useChat } from 'ai/react';
 
export function AntgravityAssistant() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: '/api/assistant',
    system: `
      You are Antigravity AI Assistant.
      Help users with:
      - Antigravity IDE usage
      - Code analysis and suggestions
      - Project setup guidance
      - Performance optimization tips
    `,
  });
 
  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">Antigravity AI Assistant</h1>
 
      <div className="space-y-4 mb-6 h-96 overflow-y-auto bg-gray-800 p-4 rounded-lg">
        {messages.map(msg => (
          <div key={msg.id} className={msg.role === 'user' ? 'text-right' : 'text-left'}>
            <div className={`inline-block px-4 py-2 rounded-lg max-w-xs ${
              msg.role === 'user' ? 'bg-blue-600' : 'bg-gray-700'
            }`}>
              {msg.content}
            </div>
          </div>
        ))}
      </div>
 
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          type="text"
          value={input}
          onChange={handleInputChange}
          placeholder="Ask a question..."
          className="flex-1 px-4 py-2 bg-gray-800 border border-gray-600 rounded"
          disabled={isLoading}
        />
        <button
          type="submit"
          disabled={isLoading}
          className="px-6 py-2 bg-blue-600 rounded hover:bg-blue-700 disabled:opacity-50"
        >
          {isLoading ? 'Sending...' : 'Send'}
        </button>
      </form>
    </div>
  );
}

Looking back

Combining Antigravity with Vercel AI SDK enables:

  • Real-time streaming conversation UIs
  • Multi-provider LLM support
  • Advanced tool calling capabilities
  • Multimodal input handling
  • Scalable deployment

Explore more advanced AI applications with these powerful tools.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Integrations2026-04-24
Fixing Mid-Stream Cutoffs and Long-Run Freezes When Antigravity Talks to Ollama
When Antigravity connects to Ollama just fine but responses keep dying mid-stream or long refactors hang forever, the fix usually isn't at the connection layer. Here's a focused triage guide for cutoff-class symptoms.
Integrations2026-06-09
Rebuilding Wallpaper Image Delivery Around Resolution Buckets — Letting an Antigravity Agent Own Conversion and Validation
Every new device resolution quietly makes a wallpaper app heavier. I stopped shipping one master image to every device and rebuilt delivery around resolution buckets, WebP/AVIF, and an edge redirect — then handed conversion and validation to an Antigravity agent. Real code and thresholds included.
Integrations2026-06-02
Fixing self-signed certificate in chain When Antigravity Can't Connect
On networks with a corporate proxy or antivirus TLS inspection, Antigravity may log self-signed certificate in chain or unable to verify the first certificate and fail to reach the model. Here is what causes it and how to fix it.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →