RemixでVercel AI SDKを使う時はzodを添える
この記事では、RemixアプリケーションでVercel AI SDKを使用する際に発生する「z8.string(…).base64 is not a function」エラーの原因と解決策を解説しています。エラーの原因はzodライブラリの依存関係が明示的にインストールされていないことでした。zodをインストールすることで問題が解決し、Anthropicの言語モデルを使ったストリーミングレスポンスの実装例も紹介されています。
目次
最近のプロジェクトでRemixとVercel AI SDKを組み合わせて実装していたところ、一見シンプルなコードなのに「z8.string(…).base64 is not a function」というエラーが発生しました。この記事では、Remix環境でVercel AI SDKを使用する際に必要な依存関係とエラー解決方法について解説します。
前提知識と環境構築
この記事では以下の環境を前提としています:
- Remix v2系(@remix-run/node)
 - Vercel AI SDK v3系
 - Node.js 18以上
 
まず、必要なパッケージをインストールします:
npm install @vercel/ai @ai-sdk/anthropic
エラー詳細と原因
さっそく以下のようなコードを実装してみました:
import { getAuth } from '@clerk/remix/ssr.server'
import type { ActionFunctionArgs } from '@remix-run/node'
import { streamText } from 'ai';
import { createAnthropic } from '@ai-sdk/anthropic';
export async function action(args: ActionFunctionArgs) {
    const { CLAUDE_API_KEY } = args.context.cloudflare.env
    const { userId } = await getAuth(args)
    console.log(userId)
    
    const model = createAnthropic({
        apiKey: CLAUDE_API_KEY
    })('claude-3-5-sonnet-20241022')
    
    const result = streamText({
      model,
      messages: [
        {
            role: 'user',
            content: 'Hello, how are you?'
        }
      ],
    })
    
    return result.toDataStreamResponse();
}
しかし実行すると、以下のエラーが発生しました:
13:14:26 [vite] Internal server error: z8.string(...).base64 is not a function
このエラーメッセージからは直感的に原因を把握しづらいですが、実はVercel AI SDKはzodという型検証ライブラリに依存しており、この依存関係が明示的にインストールされていないことが原因でした。
解決策:zodのインストール
解決策は非常にシンプルです。zodパッケージをインストールするだけです:
npm install zod
このコマンドを実行した後、先ほどと同じコードを実行すると、エラーが解消されます。
なぜこのエラーが発生するのか?
Vercel AI SDKは内部でzodを使用して型の検証を行っています。特に、ストリーミングレスポンスの処理やメッセージの検証などでzodが活用されています。npmやyarnなどのパッケージマネージャは、依存関係を自動的にインストールしますが、peerDependencyとして宣言されているパッケージは明示的にインストールする必要があります。
Vercel AI SDKではzodがこのpeerDependencyとして扱われているため、明示的なインストールが必要になります。
完全な実装例
では、zodを追加した上での完全な実装例を見ていきましょう。この例では、Cloudflare Workersを使ったRemixアプリケーションでClerk認証とAnthropicのAPIを使用しています:
import { getAuth } from '@clerk/remix/ssr.server'
import type { ActionFunctionArgs } from '@remix-run/node'
import { streamText } from 'ai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { z } from 'zod'; // zodをインポート
// 入力データのバリデーションスキーマ
const inputSchema = z.object({
  message: z.string().min(1).max(1000)
});
export async function action(args: ActionFunctionArgs) {
    const { CLAUDE_API_KEY } = args.context.cloudflare.env
    const { userId } = await getAuth(args)
    
    // 未認証ユーザーのチェック
    if (!userId) {
      return new Response('Unauthorized', { status: 401 });
    }
    
    // フォームデータの取得とバリデーション
    const formData = await args.request.formData();
    const userMessage = formData.get('message')?.toString() || '';
    
    try {
      // 入力データの検証
      const { message } = inputSchema.parse({ message: userMessage });
      
      // Anthropicモデルの初期化
      const model = createAnthropic({
          apiKey: CLAUDE_API_KEY
      })('claude-3-5-sonnet-20241022')
      
      // ストリーミングレスポンスの作成
      const result = streamText({
        model,
        messages: [
          {
              role: 'user',
              content: message
          }
        ],
      })
      
      return result.toDataStreamResponse();
    } catch (error) {
      console.error('Error processing AI request:', error);
      return new Response('Error processing request', { status: 500 });
    }
}
フロントエンド側では、以下のようにストリーミングレスポンスを受け取って表示できます:
import { useAIStream } from 'ai/react';
import { Form, useActionData } from '@remix-run/react';
import { useEffect, useState } from 'react';
export default function AIChat() {
  const actionData = useActionData<typeof action>();
  const [message, setMessage] = useState('');
  const [streamedText, setStreamedText] = useState('');
  
  // ストリーミングデータの処理
  useEffect(() => {
    if (actionData?.stream) {
      const reader = actionData.stream.getReader();
      
      const readStream = async () => {
        let done = false;
        let accumulatedText = '';
        
        while (!done) {
          const { value, done: doneReading } = await reader.read();
          done = doneReading;
          
          if (value) {
            const text = new TextDecoder().decode(value);
            accumulatedText += text;
            setStreamedText(accumulatedText);
          }
        }
      };
      
      readStream();
    }
  }, [actionData]);
  
  return (
    <div className="p-4 max-w-3xl mx-auto">
      <h1 className="text-2xl font-bold mb-4">AIチャット</h1>
      
      <Form method="post" className="mb-4">
        <div className="flex gap-2">
          <input
            type="text"
            name="message"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            className="flex-1 p-2 border rounded"
            placeholder="メッセージを入力..."
          />
          <button
            type="submit"
            className="px-4 py-2 bg-blue-500 text-white rounded"
          >
            送信
          </button>
        </div>
      </Form>
      
      {streamedText && (
        <div className="p-4 border rounded bg-gray-50">
          <h2 className="font-bold mb-2">AI応答:</h2>
          <div className="whitespace-pre-wrap">{streamedText}</div>
        </div>
      )}
    </div>
  );
}
まとめと発展
RemixとVercel AI SDKを組み合わせることで、インタラクティブなAIアプリケーションを効率的に開発できます。今回のエラー「z8.string(…).base64 is not a function」はzodをインストールするだけで解決しましたが、このようなエラーメッセージは直感的に原因を理解しづらいことがあります。
エラーメッセージから原因を特定できない場合は、ライブラリの依存関係を確認することが重要です。また、GitHubのIssuesも参考になります:
今後の発展として、以下のような取り組みも検討してみてください:
- Remixのロードの最適化: AIレスポンスが生成される間のローディング状態の改善
 - エラーハンドリングの強化: AIモデルからのエラーを適切に処理する仕組み
 - 複数のモデルの切り替え: ユーザーが異なるAIモデルを選択できる機能の実装
 - メッセージ履歴の管理: チャット履歴を保存して文脈を維持する機能
 
今回のような小さなトラブルは開発過程では頻繁に発生しますが、原因を理解して適切に対処することで、より堅牢なアプリケーション開発につながります。Remixの強力なサーバーサイド機能とVercel AI SDKの柔軟性を組み合わせて、ぜひ素晴らしいAIアプリケーションを開発してみてください。