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アプリケーションを開発してみてください。