How We Migrated atypica.AI to AI SDK v5 Without Breaking 10M+ Chat Histories
atypica.AI is a multi-agent research platform where AI agents collaborate to conduct user research. Users ask business questions, and the system orchestrates multiple specialized agents—Study Agent plans research, Scout Agent discovers target users from social media, Interview Agent conducts automated interviews with AI personas, and Report Agent generates comprehensive insights. The entire process involves 30+ AI tools and generates extensive chat histories stored in PostgreSQL.
When Vercel released AI SDK v5 with breaking changes to message formats and tool APIs, we faced a challenge: migrate 200 files and 30+ tools while keeping 10 million+ existing chat conversations accessible. This article documents our 3-day migration journey.
Final stats: 200 files changed, 4,206 insertions, 3,094 deletions, 27 commits.
The Core Breaking Change: Message → UIMessage
The most fundamental change in v5 is how messages are structured.
v4: Message with content + parts
In v4, you could use either content (simple string) or parts (structured array). Most of our code used content because it was simpler.
v5: UIMessage with parts only
In v5, parts is the only source of truth. The content field is automatically derived from parts and is read-only.
Migration impact: Every place that accessed message.content needed to be refactored to iterate through message.parts.
Day 1: Understanding the Scope
Running the Codemod
The codemod handled mechanical replacements:
- Type name:
Message→UIMessage - Tool API:
parameters→inputSchema - Added
outputSchemaplaceholders
But it left many FIXME comments because it couldn't understand semantic changes.
The First Major Problem: Content Access
We had code like this everywhere:
v5 migration: We created a utility to extract text from parts:
This pattern appeared in:
- Message persistence (16 files)
- UI components (25 files)
- API routes (15 files)
- Logging and analytics (8 files)
Tool Invocation Structure Changed
This was the second major breaking change.
v4 tool part structure:
v5 tool part structure:
Key changes:
- Flattened structure (no nested
toolInvocationobject) - Type is specific:
"tool-${toolName}"not generic"tool-invocation" - State names:
"call"→"input-available","result"→"output-available" - Field names:
args→input,result→output - Added error state:
"output-error"witherrorText
Reasoning Format Changed
Field name changed from reasoning to text.
Day 2: Backward Compatibility for 10M+ Messages
The Database Challenge
Our PostgreSQL database stores messages in v4 format:
Users expect to:
- Open old chat conversations
- Continue conversations seamlessly
- See tool results from old interactions
We couldn't migrate the database because:
- 10M+ messages across 50+ tables
- Downtime was unacceptable
- Risk of data corruption too high
Solution: Conversion Layer
Created src/ai/v4.ts:
Apply Conversion on Read
Result: Old messages load transparently. New messages save in v5 format. No database migration needed.
Type System Reorganization
We have 5 independent chat systems with different tools:
- Study: 20+ tools (reasoning, search, interviews, reports, social media)
- Interview: 2 tools (endInterview, requestInteractionForm)
- Persona: 1 tool (endInterview)
- NewStudy: 1 tool (endInterview)
- Agents: 3 tools (thanks, hello, scout)
Initial approach: One global UIToolConfigs type.
Problem: Type pollution. Study components got autocomplete for Interview tools. Runtime errors from tool type mismatches.
Solution: Domain-specific tool types.
Each domain gets its own directory structure:
Benefit: Full type safety within each domain. No cross-contamination.
Day 3: UI Components and Model Messages
Component Migration Pattern
Before v5, components accessed content directly:
After v5, components iterate over parts:
Tool detection pattern:
We migrated 25+ UI components with this pattern.
The UIMessage vs ModelMessage Distinction
There's one more important detail: messages to the model have a different format.
UIMessage - From model, for UI:
UserModelMessage - To model:
This only matters when constructing messages for streamText or generateObject:
We had to fix this in 30+ tool definitions and artifact generators.
useChat Hook Changes
Hook method renames:
append→sendMessagereload→regenerateinitialMessages→messages
Transport config:
experimental_prepareRequestBody→prepareSendMessagesRequest
File Attachments
Field renames:
name→filenamecontentType→mediaTypeexperimental_attachments→ part ofpartsarray
Streaming Message Persistence
Key insight: In v5, only check parts.length, not content.
The content field in v5 is derived from parts, so checking it is redundant.
Critical Gotchas
1. Message Content is Read-Only in v5
2. Empty Parts vs Empty Content
3. Tool State Names Changed
Don't forget to update all state checks.
4. Optional Tool Call Names
In edge cases (aborted streams, errors), toolCall.toolName might be undefined:
5. Stream Event Field Names
6. Message ID Handling
Client messages might not have IDs:
Migration Checklist
Phase 1: Dependencies
- Update
aito v5,zodto v4 - Run codemod:
npx @ai-sdk/codemod@latest upgrade - Fix TypeScript errors from
Message→UIMessagerename
Phase 2: Content Access
- Find all
message.contentaccess - Replace with parts iteration or utility function
- Update title generation, logging, analytics
Phase 3: Tool Parts
- Update tool detection:
"tool-invocation"→part.type.startsWith("tool-") - Change field access:
args/result→input/output - Update state checks:
"call"/"result"→"input-available"/"output-available" - Add
"output-error"handling
Phase 4: Backward Compatibility
- Create v4 → v5 conversion utility
- Apply conversion when loading from database
- Test old chat histories thoroughly
Phase 5: UI Components
- Migrate all components to iterate over
parts - Update tool rendering logic
- Update file attachment components
Phase 6: Tool Definitions
- Change
parameters→inputSchema - Add
outputSchema - Import
UserModelMessagefor model messages - Use
content(notparts) when callingstreamText/generateObject
Phase 7: React Hooks
- Update
useChat:append→sendMessage,reload→regenerate - Migrate
experimental_prepareRequestBody→prepareSendMessagesRequest
Phase 8: Edge Cases
- Handle missing message IDs
- Handle undefined
toolCall.toolName - Update stream event handlers
- Remove
content.trim()checks
Key Insights
1. Content is Dead, Long Live Parts
The shift from content as source of truth to parts as source of truth is fundamental. Every message access pattern needs review.
Budget time for:
- Finding all
message.contentreferences (use global search) - Understanding each usage context
- Deciding between iteration or utility function
2. The Codemod is 30% of the Work
The codemod handles:
- Type renames
- Basic API changes
- Obvious field renames
It doesn't handle:
- Semantic changes (
contentaccess patterns) - Complex refactors (tool detection logic)
- Domain-specific decisions (type organization)
Budget 70% of time for post-codemod fixes.
3. Backward Compatibility is Non-Negotiable
Users don't care about your migration. Their old chats must just work.
Strategies:
- Convert on read (what we did)
- Convert on write (gradual migration)
- Dual schema (complex but zero risk)
We chose convert-on-read because:
- Zero downtime
- Simple to implement
- Low risk
- Works transparently
4. Type System Organization Matters
With multiple chat systems, proper type organization prevents:
- Tool type pollution
- Runtime errors from wrong tool usage
- Confusing autocomplete
Invest time in domain-specific types early.
5. Test Incrementally
We migrated systems in order of complexity:
- Agents (simplest, 3 tools) - validate pattern
- NewStudy (1 tool) - test in production
- Persona (1 tool) - confidence building
- Interview (2 tools) - more complex
- Study (20+ tools) - final boss
Each system validated the patterns before applying them broadly.
Results
Before v5:
- Inconsistent access patterns (
contentvsparts) - Loose typing around tools
Messagetype used everywhere
After v5:
- Single source of truth:
partsarray - Full type safety across 30+ tools
- Domain-specific type organization
- Cleaner separation of concerns
Time breakdown:
- Day 1: Dependencies, codemod, content access patterns (8 hours)
- Day 2: Backward compatibility, type system (10 hours)
- Day 3: UI components, tool definitions, edge cases (6 hours)
Total: ~24 hours focused work over 3 days.
Recommendations
- Budget 3-5 days, not 1-2 days
- Start simple: Migrate your simplest system first
- Build backward compatibility early, not as afterthought
- Create utilities:
getTextFromParts(),convertV4ToV5(), etc. - Organize types by domain if you have multiple systems
- Test old data: Load historical chats, verify tool results display
- Document the change: Team needs to understand
partsis source of truth
The migration is substantial but manageable with proper planning. v5's parts-based approach is more flexible and type-safe. The pain of migration is short-term; the benefits are long-term.