LoginSignup
0
0

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

Last updated at Posted at 2024-04-11

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

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

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);
}

GoogleのstreamText版

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

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内で吸収してくれるようで、特別な呼び換えは不要です。

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の現行インターフェース

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);
}

AnthropicのstreamText版

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

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

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

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

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

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.というエラーになってしまいます。未実装のようです。

              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)が呼び出せず例外が発生してしまっているようです。

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

OpenAI、Mistral他

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

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

参照情報

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

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

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

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

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

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

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


追記

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0