RORK LABJP
MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode requiredSTACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decisionFOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generationBUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebaseFUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)PRICING — It is free to start, with paid plans from $25/month, so you can try before committingMAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode requiredSTACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decisionFOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generationBUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebaseFUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)PRICING — It is free to start, with paid plans from $25/month, so you can try before committing
Articles/AI Models
AI Models/2026-04-15Intermediate

Rork × Vercel AI SDK: Implement Streaming, Tool Calling, and Structured Outputs with Full Type Safety

Learn how to integrate the Vercel AI SDK into your Rork app for streaming chat, tool calling, and structured outputs — all with complete TypeScript type safety using Cloudflare Workers and Hono.

Vercel AI SDKstreaming2tool callingReact Native160Cloudflare Workers18OpenAI5TypeScript7useChat

Fetching from the OpenAI API directly isn't that hard. But the two or three seconds of silence between a user tapping "Send" and the first character appearing on screen can quietly destroy the feel of your app. Rolling your own streaming implementation means parsing Server-Sent Events, accumulating delta tokens, wiring error handling, and keeping your state in sync with the UI — none of which has anything to do with your actual product.

The Vercel AI SDK abstracts all of that away. It gives Rork (React Native / Expo) developers a type-safe, provider-agnostic way to add AI features in hours instead of days. In this guide, I'll walk through three practical patterns: streaming chat, tool calling, and structured outputs — all backed by real, copy-pasteable code.

Why Vercel AI SDK Over a Direct Integration

There are a few libraries that promise to simplify AI integration in React Native, but Vercel AI SDK stands out for three reasons.

End-to-end type safety. The SDK is TypeScript-first, and response types flow through your stack automatically. When you use generateObject(), the return value is fully inferred from your Zod schema — no manual casting, no as any.

Provider switching at zero cost. OpenAI, Anthropic, Google Gemini, Mistral — they all share the same interface. Switching models to compare cost and output quality means changing one import, not rewriting your backend.

Proven React Native compatibility. The useChat hook works in Expo and React Native with minor adjustments (more on this below). The backend side needs a Node.js or Edge Runtime environment, and Cloudflare Workers fits perfectly.

If you've previously tried building manual streaming from scratch — like the pattern described in our LLM Streaming Implementation Guide — the SDK will feel almost unfairly simple by comparison.

Backend Setup: Cloudflare Workers + Hono

The SDK handles the frontend, but you still need a backend to serve the streaming API. Here's the setup using Cloudflare Workers and Hono. For Hono configuration details, see the Hono × Cloudflare Workers REST API Guide.

# In your backend project
npm install ai @ai-sdk/openai hono zod

The streaming chat endpoint looks like this:

// src/index.ts (Cloudflare Worker + Hono)
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
 
const app = new Hono<{ Bindings: { OPENAI_API_KEY: string } }>()
 
app.use('/*', cors())
 
app.post('/api/chat', async (c) => {
  const { messages } = await c.req.json()
 
  // streamText handles all SSE delta sending automatically
  const result = streamText({
    model: openai('gpt-4o-mini'),
    system: 'You are a helpful assistant.',
    messages,
    // Always cap tokens to manage costs
    maxTokens: 1000,
  })
 
  // toDataStreamResponse() makes this compatible with the useChat hook
  return result.toDataStreamResponse()
})
 
export default app

Wrapping streamText() output with toDataStreamResponse() produces a stream that useChat understands natively. All the low-level work — chunked SSE parsing, reconnect handling, error propagation — disappears behind that single method call.

Store your API key as a Cloudflare Workers secret, not in source code:

wrangler secret put OPENAI_API_KEY

Hardcoding keys in frontend or backend code risks GitHub Secret Scanning alerts and, more importantly, key exposure if your repository is ever public.

Streaming Chat in Your Rork App

On the Rork side, install the SDK:

npm install ai

Here's a full chat screen using useChat:

// screens/ChatScreen.tsx
import { useChat } from 'ai/react'
import {
  View, TextInput, FlatList, Text,
  TouchableOpacity, StyleSheet, KeyboardAvoidingView, Platform
} from 'react-native'
 
const API_URL = 'https://your-worker.workers.dev/api/chat'
 
export default function ChatScreen() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: API_URL,
    onError: (error) => {
      // Notify the user on error rather than silently retrying
      console.error('Chat error:', error.message)
    },
  })
 
  return (
    <KeyboardAvoidingView
      style={styles.container}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    >
      <FlatList
        data={messages}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={[
            styles.bubble,
            item.role === 'user' ? styles.userBubble : styles.aiBubble,
          ]}>
            {/* Text updates in real time as tokens stream in */}
            <Text style={styles.messageText}>{item.content}</Text>
          </View>
        )}
      />
 
      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          value={input}
          // React Native's onChangeText returns a plain string.
          // useChat expects a web-style onChange event, so we shim it.
          onChangeText={(text) =>
            handleInputChange({ target: { value: text } } as any)
          }
          placeholder="Type a message..."
          editable={\!isLoading}
          multiline
        />
        <TouchableOpacity
          style={[styles.sendButton, isLoading && styles.disabled]}
          onPress={() => handleSubmit()}
          disabled={isLoading}
        >
          <Text style={styles.sendLabel}>{isLoading ? '…' : 'Send'}</Text>
        </TouchableOpacity>
      </View>
    </KeyboardAvoidingView>
  )
}
 
const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#fff' },
  bubble: { margin: 8, padding: 12, borderRadius: 16, maxWidth: '80%' },
  userBubble: { alignSelf: 'flex-end', backgroundColor: '#007AFF' },
  aiBubble: { alignSelf: 'flex-start', backgroundColor: '#F2F2F7' },
  messageText: { fontSize: 15, color: '#000' },
  inputRow: {
    flexDirection: 'row', padding: 8,
    borderTopWidth: 1, borderColor: '#E5E5EA'
  },
  input: {
    flex: 1, borderWidth: 1, borderColor: '#C7C7CC',
    borderRadius: 20, paddingHorizontal: 16, paddingVertical: 8, marginRight: 8
  },
  sendButton: {
    backgroundColor: '#007AFF', borderRadius: 20,
    paddingHorizontal: 20, justifyContent: 'center'
  },
  disabled: { opacity: 0.5 },
  sendLabel: { color: '#fff', fontWeight: '600' },
})

One thing the official docs don't spell out: handleInputChange expects a web onChange event object, not a plain string. The shim { target: { value: text } } as any is the standard workaround when using useChat in React Native. It's a known friction point and an easy one to miss.

Everything else — message history, streaming token accumulation, loading state — is managed by the hook automatically.

Tool Calling: Letting AI Fetch Live Data

Tool calling lets the AI decide, at inference time, to reach out to an external API and incorporate that data into its answer. Instead of "Tokyo weather is whatever was in the training data," the AI calls your weather API and answers with current conditions.

Add tool definitions to your backend:

// Add to src/index.ts
import { streamText, tool } from 'ai'
import { z } from 'zod'
 
app.post('/api/chat-with-tools', async (c) => {
  const { messages } = await c.req.json()
 
  const result = streamText({
    model: openai('gpt-4o'),
    messages,
    tools: {
      getWeather: tool({
        description: 'Fetch current weather for a given city',
        // Zod schema prevents the model from passing wrong argument types
        parameters: z.object({
          city: z.string().describe('City name, e.g. Tokyo, London'),
        }),
        execute: async ({ city }) => {
          // Replace with your real API call in production
          // const res = await fetch(`https://api.openweathermap.org/...`)
          return { city, temperature: 22, condition: 'Sunny', humidity: 55 }
        },
      }),
    },
    // Without maxSteps, the model stops after calling the tool
    // and never delivers the final answer to the user
    maxSteps: 3,
  })
 
  return result.toDataStreamResponse()
})

The maxSteps setting is easy to overlook, and forgetting it produces a silent failure: the model calls the tool, receives the result, then stops — leaving the user with no response. Setting it to 3 allows: call tool → receive result → generate answer. Deeper multi-step reasoning is possible too; just increase the value.

For more advanced patterns including multiple parallel tools and error handling, see the AI Function Calling Guide.

Structured Outputs with generateObject

Not every AI feature is a chatbot. Sometimes you need the model to return a specific data shape — sentiment analysis, information extraction from free-text input, automatic content tagging. generateObject() is designed exactly for this.

// src/index.ts
import { generateObject } from 'ai'
 
app.post('/api/analyze-review', async (c) => {
  const { reviewText } = await c.req.json()
 
  const { object } = await generateObject({
    model: openai('gpt-4o-mini'),
    schema: z.object({
      sentiment: z.enum(['positive', 'neutral', 'negative']),
      score: z.number().min(1).max(5).describe('Rating from 1 to 5'),
      summary: z.string().max(60).describe('One-sentence summary'),
      keywords: z.array(z.string()).max(3).describe('Up to 3 key topics'),
    }),
    prompt: `Analyze this review:\n\n${reviewText}`,
  })
 
  // object is fully typed — object.sentiment is 'positive' | 'neutral' | 'negative'
  // TypeScript catches any typo or wrong property access at compile time
  return c.json(object)
})

The return value is typed exactly from your Zod schema. When you consume it in your Rork app, autocomplete works, switch statements are exhaustive-checked, and accessing a nonexistent property is a compile error. Compare that to JSON.parse(response) as any — the difference in refactoring confidence alone is worth it.


The Vercel AI SDK earns its place in a Rork stack not because it's magic, but because it eliminates an entire category of accidental complexity. Start with useChat for conversational features, reach for tool calling when the AI needs live data, and use generateObject whenever you need structured output from unstructured input. Each pattern builds on the same foundation, so adding capabilities later is additive rather than a rewrite. Give it a try on your next Rork project.

Share

Thank You for Reading

Rork 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

AI Models2026-04-07
Rork Max × OpenAI Responses API: to Building Stateful AI Agents in Mobile Apps 2026
A complete guide to implementing stateful AI agents in Rork Max apps using the OpenAI Responses API. Learn how to integrate built-in tools like web search, file search, and code interpreter via Cloudflare Workers, with practical monetization strategies for indie developers.
Dev Tools2026-04-13
LLM Streaming in Rork Apps: Building ChatGPT-Style Real-Time AI Responses with Expo and SSE
A complete guide to implementing LLM streaming (SSE) in React Native and Expo apps. Covers Anthropic and OpenAI streaming APIs, AbortController cancellation, error retry, Cloudflare Workers proxy, and multi-provider abstraction — with production-ready code throughout.
AI Models2026-06-14
Calling Apple Foundation Models from a Rork (Expo) App: Bridging On-Device AI Through a Native Module
Rork generates Expo (React Native) apps, but Apple Foundation Models ships as a Swift framework you can't touch from JavaScript. Here's how to write an Expo Modules API bridge, gate it by availability, and fall back to the cloud on unsupported devices.
📚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 →