Vercel AI SDKのstreamTextでGoogleやAnthropicのモデルを統合的に扱う

VercelのAI SDKは、OpenAIをはじめ様々なベンダー（プロバイダー）の生成AIソリューションを統合的に呼び出せるところが売りですが、GoogleやAnthropicについては、これまでの公式ドキュメントの解説でも以下のようにGoogleのSDKを直接呼び出していてあまり共通化ができていませんでした。最近、まだexperimental扱いですがstreamText系のインターフェースが出てきて、シンプルに書けるようになったので紹介します。

# Googleの現行インターフェース

```TypeScript
import { GoogleGenerativeAI } from '@google/generative-ai';
import { GoogleGenerativeAIStream, Message, StreamingTextResponse } from 'ai';
 
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY || '');
 
// IMPORTANT! Set the runtime to edge
export const runtime = 'edge';
 
// convert messages from the Vercel AI SDK Format to the format
// that is expected by the Google GenAI SDK
const buildGoogleGenAIPrompt = (messages: Message[]) => ({
  contents: messages
    .filter(message => message.role === 'user' || message.role === 'assistant')
    .map(message => ({
      role: message.role === 'user' ? 'user' : 'model',
      parts: [{ text: message.content }],
    })),
});
 
export async function POST(req: Request) {
  // Extract the `prompt` from the body of the request
  const { messages } = await req.json();
 
  const geminiStream = await genAI
    .getGenerativeModel({ model: 'gemini-pro' })
    .generateContentStream(buildGoogleGenAIPrompt(messages));
 
  // Convert the response into a friendly text-stream
  const stream = GoogleGenerativeAIStream(geminiStream);
 
  // Respond with the stream
  return new StreamingTextResponse(stream);
}
```

https://sdk.vercel.ai/docs/api-reference/providers/google-generative-ai-stream

# GoogleのstreamText版

AI SDKのインターフェースだけを使い、とてもシンプルに書くことができるようになります。

```TypeScript
import { ExperimentalMessage, experimental_streamText, StreamingTextResponse } from 'ai';
import { Google } from 'ai/google';

const google = new Google({ apiKey: process.env.GOOGLE_API_KEY || '' })

export async function POST(req: Request) {
  // Extract the `prompt` from the body of the request
  const { messages } = await req.json();
  
  const result = await experimental_streamText({
    model: google.generativeAI(model),
    messages: messages as ExperimentalMessage[],
  });
  
  // Convert the response into a friendly text-stream
  const stream = result.toAIStream();
  
  // Respond with the stream
  return new StreamingTextResponse(stream);
}
```

ここで`model`は、下記のように定義されているのでいずれかを指定すればOKです。1.5などのベータ版モデルを指定する時、現行インターフェースでは`getGenerativeModel({model}, {apiVersion: "v1beta"})`のようにする必要がありますが、streamText版ではSDK内で吸収してくれるようで、特別な呼び換えは不要です。

```TypeScript
type GoogleGenerativeAIModelId = 'models/gemini-1.5-pro-latest' | 'models/gemini-pro' | 'models/gemini-pro-vision' | (string & {});
```

また、環境変数`GOOGLE_GENERATIVE_AI_API_KEY`にAPIキーを設定した場合、`import { google } from 'ai/google';`として直接インスタンスを使えます。

# Anthropicの現行インターフェース

```TypeScript
import Anthropic from '@anthropic-ai/sdk';
import { AnthropicStream, StreamingTextResponse } from 'ai';
import { experimental_buildAnthropicPrompt } from 'ai/prompts';
 
// Create an Anthropic API client (that's edge friendly??)
const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY || '',
});
 
// IMPORTANT! Set the runtime to edge
export const runtime = 'edge';
 
export async function POST(req: Request) {
  // Extract the `prompt` from the body of the request
  const { messages } = await req.json();
 
  // Ask Claude for a streaming chat completion given the prompt
  const response = await anthropic.completions.create({
    prompt: experimental_buildAnthropicPrompt(messages),
    model: 'claude-2',
    stream: true,
    max_tokens_to_sample: 300,
  });
  
  // Convert the response into a friendly text-stream
  const stream = AnthropicStream(response);
  
  // Respond with the stream
  return new StreamingTextResponse(stream);
}
```

https://sdk.vercel.ai/docs/api-reference/providers/anthropic-stream

# AnthropicのstreamText版

同じくAI SDKのインターフェースだけを使い、Googleとほぼ同じ記述でとてもシンプルに書くことができるようになりました。モデルの指定はなぜか`anthropic.chat(modelId)`ではなく`anthropic.messages(modelId)`と指定します。指定可能なIDは`AnthropicMessagesModelId`を確認しましょう。

ただし、後述しますが、画像には未対応のようなので、ご注意ください。

```TypeScript
import { ExperimentalMessage, experimental_streamText, StreamingTextResponse } from 'ai';
import { Anthropic } from 'ai/anthropic';

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY || '' })

export async function POST(req: Request) {
  // Extract the `prompt` from the body of the request
  const { messages } = await req.json();
 
  const result = await experimental_streamText({
    model: anthropic.messages('claude-3-sonnet-20240229'),
    messages: messages as ExperimentalMessage[],
  });
  
  // Convert the response into a friendly text-stream
  const stream = result.toAIStream();
  
  // Respond with the stream
  return new StreamingTextResponse(stream);
}
```

## 画像データの処理

Anthropicの現行インターフェースでは、画像の解釈などを質問する時、データは下記のようなフォーマットで渡す必要がありました。

```TypeScript
const messages = [
  { type: "text", text: message },
  { type: "image",
    source: {
      type: "base64",
      media_type: image_media_type,
      data: image_data,
    }
  }
];
```

AI SDKでは、下記のようになります。mimeTypeは省略することもできます。

```TypeScript
const messages = [
  { type: "text", text: message },
  { type: "image", image: image_data, mimeType: image_media_type }
];
```

なお、`image: URL`の形でも渡せるはずですが`AI_UnsupportedFunctionalityError: \'URL image parts\' functionality not supported.`というエラーになってしまいます。未実装のようです。

https://github.com/vercel/ai/blob/51f25a7f7e7b10ab2d46c96ea36220f927adf6b4/packages/core/anthropic/convert-to-anthropic-messages-prompt.ts#L33
```TypeScript
              case 'image': {
                if (part.image instanceof URL) {
                  throw new UnsupportedFunctionalityError({
                    functionality: 'URL image parts',
                  });
                } else {
                  return {
                    type: 'image',
                    source: {
                      type: 'base64',
                      media_type: part.mimeType ?? 'image/jpeg',
                      data: convertUint8ArrayToBase64(part.image),
                    },
                  };
                }
              }
```

ではimage_dataをバイナリーで渡せば上記のようにbase64に変換して動作するかと思いきや、これはローカル環境では動作しますが、残念ながらEdge Runtimeではこれも下記のように**エラー**になってしまいます。

> undefined 'TypeError: Illegal invocation' TypeError: Illegal invocation
    at (node_modules/.pnpm/ai@3.0.21_react@18.2.0_solid-js@1.8.16_svelte@4.2.13_vue@3.4.21_zod@3.22.4/node_modules/ai/dist/index.mjs:78:34)
    at (node_modules/.pnpm/ai@3.0.21_react@18.2.0_solid-js@1.8.16_svelte@4.2.13_vue@3.4.21_zod@3.22.4/node_modules/ai/dist/index.mjs:130:37)
    at (node_modules/.pnpm/ai@3.0.21_react@18.2.0_solid-js@1.8.16_svelte@4.2.13_vue@3.4.21_zod@3.22.4/node_modules/ai/dist/index.mjs:181:26)
    at (node_modules/.pnpm/ai@3.0.21_react@18.2.0_solid-js@1.8.16_svelte@4.2.13_vue@3.4.21_zod@3.22.4/node_modules/ai/dist/index.mjs:165:41)
    at (node_modules/.pnpm/ai@3.0.21_react@18.2.0_solid-js@1.8.16_svelte@4.2.13_vue@3.4.21_zod@3.22.4/node_modules/ai/dist/index.mjs:154:27)
    at (node_modules/.pnpm/ai@3.0.21_react@18.2.0_solid-js@1.8.16_svelte@4.2.13_vue@3.4.21_zod@3.22.4/node_modules/ai/dist/index.mjs:1429:43)
    at (node_modules/.pnpm/ai@3.0.21_react@18.2.0_solid-js@1.8.16_svelte@4.2.13_vue@3.4.21_zod@3.22.4/node_modules/ai/dist/index.mjs:405:17)
    at (node_modules/.pnpm/ai@3.0.21_react@18.2.0_solid-js@1.8.16_svelte@4.2.13_vue@3.4.21_zod@3.22.4/node_modules/ai/dist/index.mjs:394:24)

該当部分が下記ですが、base64になった画像データを再度UInt8Arrayにしようとして、globalThis.atob(base64Url)が呼び出せず例外が発生してしまっているようです。

https://github.com/vercel/ai/blob/51f25a7f7e7b10ab2d46c96ea36220f927adf6b4/packages/core/core/prompt/convert-to-language-model-prompt.ts#L61

なので、画像の解釈が必要な場合は、今のところは古い方のインターフェースを使うしかないようです。これについてはGitHubにissueを書いてみました。私の勘違いだったらごめんなさい。

https://github.com/vercel/ai/issues/1333

# OpenAI、Mistral他

OpenAI、MistralもGoogleやAnthropicと同様の記述で対応可能です。`OpenAIChatModelId`や`MistralChatModelId`に、受け渡し可能なモデルIDも定義されているので安心です。OpenIDの場合の例をこちらの記事に記述しました。

https://qiita.com/twk/items/ef3d90362048c5cf292f

他のプロバイダーには対応していないので、OpenAIとの互換性がないAWS BedrockやCohere、Hugginfaceなどではこの方法は利用できません。対応を待ちましょう。

# 参照情報

ドキュメントはこちらをご覧ください。

https://sdk.vercel.ai/docs/ai-core/stream-text

ただし、ドキュメントに記述されていない内容もあります。SDKのソースにあるプログラム例もシンプルでわかりやすいです。関数呼び出し版のサンプルもあります。

https://github.com/vercel/ai/tree/main/examples/ai-core/src/stream-text

うまく動かない場合は`pnpm upgrade ai`または`pnpm install ai`として、SDKを最新版にしてみてください。

最近作られたReact Server Component版のサンプルアプリケーションでもrenderの代わりにstreamTextが使われていたので、今後experimentalも取れてこちらが本筋になっていくかもしれません。

https://github.com/vercel-labs/gemini-chatbot/blob/c19fd66c7dae5e04fb7519be12bbdcfed9be882a/lib/chat/actions.tsx#L164

experimentalなのでまだ本番利用は避けた方が良いかも知れません。

動作するデモはこちらです。ただし、Google, Anthropic, Mistralはこちらで紹介した`experimental_streamText`を使っていますが、OpenAIについては現行インターフェースを使っています。

https://mulai.vercel.app/

ソースコードも参照してみてください。

https://github.com/twxxk/mulai/blob/main/app/api/chat/route.ts

----

# 追記

また新しいインターフェースができたようなので別記事にしました。

https://qiita.com/twk/items/3ef3aa6ca0c61d2c4fac