はじめに
2024/9/28 大分で Cloudflare Meet-up 開催します!
テーマは「AI Gateway」
というわけで、 Elixir Livebook から AI Gateway を使ってみます
実装したノートブックはこちら
AI Gateway とは
AI Gateway は各種 AI サービスへのプロキシ(中継)として、以下のような機能を提供します
- キャッシュ(同じ質問・指示にはキャッシュで応答)
- レート制限(単位時間あたりのリクエスト数)
- リアルタイムログ取得(リクエストとレスポンスの内容を1時間保持)
- 使用量制限
- 分析
- リクエストフォールバック(一つのAIサービスでエラーが起こったとき、別のAIサービスを使って応答)
今使える機能は無償利用可能です
今後、ログの永続化などを有償提供するようです
セットアップ
新しいノートブックを開き、セットアップセルで以下のコードを実行します
Mix.install([ {:aws, "~> 1.0"}, {:hackney, "~> 1.20"}, {:kino, "~> 0.13"}, {:req, "~> 0.5"}])
直接 Amazon Bedrock を呼び出す場合、 AWS モジュールを使用します
また、 AI Gateway 経由で呼ぶ場合の署名にも利用します
事前準備
AWS
Amazon Bedrock を使うための準備をしておきます
- Amazon Bedrock で Claude 3.5 のアクセス要求
- Amazon Bedrock を呼び出す権限を付与した IAM ユーザーとその認証情報を作成
手順は以下の記事を参照
Cloudflare
Cloudflare のアカウントを作成し、 AI Gateway のゲートウェイを作成しておきます
アカウント作成手順は以下の記事を参照
アカウント作成後、ホーム画面で左メニューから AI を開き、 AI Gateway をクリック
「ゲートウェイの作成」をクリックして開いたモーダルにゲートウェイの名前を入力
(本記事ではゲートウェイ名を「bedrock」に指定)
ゲートウェイが作成されるので、右上(赤枠)の API ボタンをクリック
開いたモーダルに AI Gateway のエンドポイントが表示されます
エンドポイントは以下のような構成になっています
https://gateway.ai.cloudflare.com/v1/<Cloudflare のアカウントID>/<ゲートウェイ名>/
「Cloudflare のアカウントID」と「ゲートウェイ名」を後の手順で使用します
認証情報の設定
テキスト入力を作成し、 AWS の認証情報とリージョン、 Cloudflare で用意しておいたアカウント ID、ゲートウェイ名を入力します
リージョンはアクセス要求を出したリージョンを指定します
aws_access_key_id_input = Kino.Input.password("AWS ACCESS_KEY_ID")aws_secret_access_key_input = Kino.Input.password("AWS SECRET_ACCESS_KEY")aws_region_input = Kino.Input.text("AWS REGION")cf_account_id_input = Kino.Input.password("CLOUDFLARE ACCOUNT_ID")cf_gateway_name_input = Kino.Input.text("CLOUDFLARE GATEWAY_NAME")[ aws_access_key_id_input, aws_secret_access_key_input, aws_region_input, cf_account_id_input, cf_gateway_name_input]|> Kino.Layout.grid(columns: 3)
認証情報を利用して、 AWS にアクセスするためのクライアントを作成します
aws_client = AWS.Client.create( Kino.Input.read(aws_access_key_id_input), Kino.Input.read(aws_secret_access_key_input), Kino.Input.read(aws_region_input) )
AWS クライアントからの Bedrock 呼び出し
まずは通常通り、 AWS クライアントから Bedrock を呼び出してみます
やっていることは以下の記事と同じです
モデルIDで Claude 3.5 Sonnet を指定します
model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0"
生成AIへの指示を用意します
input = "あなたの名前を教えてください。"payload = %{ "messages" => [%{ "role" => "user", "content" => [%{"text" => input}] }]}
返答に少し時間がかかるため、リクエストのタイムアウトに 60 秒を指定します
options = [recv_timeout: 60_000]
AWS.BedrockRuntime.converse
で Claude 3.5 にリクエストを送ります
result = aws_client |> AWS.BedrockRuntime.converse(model_id, payload, options) |> elem(1)
レスポンスは以下のようになります
%{ "metrics" => %{"latencyMs" => 1095}, "output" => %{ "message" => %{ "content" => [ %{ "text" => "私の名前はClaudeです。AIアシスタントとして作られました。どうぞよろしくお願いします。" } ], "role" => "assistant" } }, "stopReason" => "end_turn", "usage" => %{"inputTokens" => 19, "outputTokens" => 39, "totalTokens" => 58}}
Livebook ではレスポンスからテキストを取り出し、マークダウンとして表示できます
result|> Map.get("output")|> Map.get("message")|> Map.get("content")|> hd()|> Map.get("text")|> Kino.Markdown.new()
実行結果
ここまでは通常通りの Bedrock の使い方です
AWS.Client.request による Bedrock の呼び出し
AI Gateway から Amazon Bedrock を呼び出す場合、リクエストへの署名が必要です
AWS モジュールでは内部的に署名と AWS のAPI 呼び出しを実施してくれています
まずはその実装を参考に署名と AWS API 呼び出しを実行してみましょう
まず、 API 呼び出しのヘッダーと URL を用意します
aws_region = Kino.Input.read(aws_region_input)host = "bedrock-runtime.#{aws_region}.amazonaws.com"headers = [ {"Host", host}, {"Content-Type", "application/json"}]encoded_path = "/model/#{AWS.Util.encode_uri(model_id)}/converse"url = "https://#{host}#{encoded_path}"
AWS モジュールの実装では "Content-Type" を "application/x-amz-json-1.1" に指定していますが、この指定だと AI Gateway のログ上にリクエスト内容が表示されません
https://github.com/aws-beam/aws-elixir/blob/master/lib/aws/generated/bedrock_runtime.ex#L886
"Content-Type" は "application/json" を指定します
Bedrock 自体はどちらでも応答してくれます
URL は以下のような値になります
"https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-3-5-sonnet-20240620-v1%3A0/converse"
AWS.Util.encode_uri
を通すことで :
が %3A
になっていることが肝ですね
署名に必要な現在時刻を取得します
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
実行結果(例)
~N[2024-08-28 04:53:09]
署名には有効期間があるため、この時刻から一定時間経過すると使えなくなります(エラーメッセージに Signature expired
が返ってくる)
リクエスト内容を JSON 文字列に変換します
encoded_payload = AWS.Client.encode!(aws_client, payload, :json)
実行結果
"{\"messages\":[{\"content\":[{\"text\":\"あなたの名前を教えてください。\"}],\"role\":\"user\"}]}"
AWS.Signature.sign_v4
関数を利用して署名付ヘッダーを生成します
siged_headers = AWS.Signature.sign_v4( %{aws_client | service: "bedrock"}, now, :post, url, headers, encoded_payload )
実行結果
[ {"Authorization", "AWS4-HMAC-SHA256 Credential=xxx/20240828/us-east-1/bedrock/aws4_request,SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date,Signature=999"}, {"X-Amz-Content-SHA256", "aaa"}, {"X-Amz-Date", "20240828T045309Z"}, {"Host", "bedrock-runtime.us-east-1.amazonaws.com"}, {"Content-Type", "application/json"}]
署名付ヘッダーを指定して AWS API を呼び出します
AWS.Client.request(aws_client, :post, url, encoded_payload, siged_headers, options)
実行結果
{:ok, %{ body: "{\"metrics\":{\"latencyMs\":706},\"output\":{\"message\":{\"content\":[{\"text\":\"私の名前はClaudeです。よろしくお願いします。\"}],\"role\":\"assistant\"}},\"stopReason\":\"end_turn\",\"usage\":{\"inputTokens\":19,\"outputTokens\":23,\"totalTokens\":42}}", headers: [ {"Date", "Wed, 28 Aug 2024 07:40:18 GMT"}, {"Content-Type", "application/json"}, {"Content-Length", "244"}, {"Connection", "keep-alive"}, {"x-amzn-RequestId", "07b47719-ae2d-4822-93cd-158d2a9c7853"} ], status_code: 200 }}
AI Gateway 経由で Amazon Bedrock を呼び出す
AI Gateway のエンドポイント URL を組み立てます
cf_account_id = Kino.Input.read(cf_account_id_input)cf_gateway_name = Kino.Input.read(cf_gateway_name_input)cf_host = "gateway.ai.cloudflare.com"gw_url = "https://#{cf_host}/v1/#{cf_account_id}/#{cf_gateway_name}/aws-bedrock/bedrock-runtime/#{aws_region}#{encoded_path}"
署名付ヘッダーのうち、 "Host" の値を AI Gateway のホストに変換します
gw_header = siged_headers |> Enum.map(fn {key, value} -> if key == "Host" do {key, cf_host} else {key, value} end end)
実行結果
[ {"Authorization", "AWS4-HMAC-SHA256 Credential=xxx/20240828/us-east-1/bedrock/aws4_request,SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date,Signature=999"}, {"X-Amz-Content-SHA256", "aaa"}, {"X-Amz-Date", "20240828T045309Z"}, {"Host", "gateway.ai.cloudflare.com"}, {"Content-Type", "application/json"}]
Req
を使って、普通の REST API のようにリクエストを投げます
result = Req.new(url: gw_url, headers: gw_header, body: encoded_payload) |> Req.post!(connect_options: [timeout: 60_000]) |> Map.get(:body)
実行結果
%{ "metrics" => %{"latencyMs" => 1008}, "output" => %{ "message" => %{ "content" => [ %{ "text" => "私の名前はClaudeです。人工知能アシスタントとして作られました。よろしくお願いします。" } ], "role" => "assistant" } }, "stopReason" => "end_turn", "usage" => %{"inputTokens" => 19, "outputTokens" => 38, "totalTokens" => 57}}
無事、 AI Gateway 経由で呼び出すことができました
AI Gateway のコンソールを見ると、リスエストとレスポンスのログが取得できています
モジュール化
ここまでの処理を整理し、 AI Gateway 経由で Bedrock を呼び出すためのモジュールを定義しましょう
defmodule AIGateway do @cf_host "gateway.ai.cloudflare.com" def invoke(aws_client, model_id, cf_account_id, cf_gateway_name, input) do payload = %{ "messages" => [%{ "role" => "user", "content" => [%{"text" => input}] }] } host = "bedrock-runtime.#{aws_client.region}.amazonaws.com" headers = [ {"Host", host}, {"Content-Type", "application/json"} ] encoded_path = "/model/#{AWS.Util.encode_uri(model_id)}/converse" encoded_payload = AWS.Client.encode!(aws_client, payload, :json) gw_headers = sign_headers( aws_client, "https://#{host}#{encoded_path}", headers, encoded_payload ) gw_url = "https://#{@cf_host}/v1/#{cf_account_id}/#{cf_gateway_name}/aws-bedrock/bedrock-runtime/#{aws_client.region}#{encoded_path}" Req.new(url: gw_url, headers: gw_headers, body: encoded_payload) |> Req.post!(connect_options: [timeout: 60_000]) |> Map.get(:body) end defp sign_headers(aws_client, url, headers, payload) do now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) %{aws_client | service: "bedrock"} |> AWS.Signature.sign_v4( now, :post, url, headers, payload ) |> Enum.map(fn {key, value} -> if key == "Host" do {key, @cf_host} else {key, value} end end) endend
キャッシュの確認
キャッシュ機能を試してみたいので、 AI Gateway でキャッシュを有効にします
AI Gateway の画面から「設定」タブを開き、「キャッシュ レスポンス」のトグルを ON にしてください
Livebook で任意の質問・指示ができるように、テキストエリアを用意します
instruction_input = Kino.Input.textarea("INSTRUCTION")
定義したモジュールを使って AI Gateway を呼び出し、結果を表示します
instruction = Kino.Input.read(instruction_input)aws_client|> AIGateway.invoke(model_id, cf_account_id, cf_gateway_name, instruction)|> Map.get("output")|> Map.get("message")|> Map.get("content")|> hd()|> Map.get("text")|> Kino.Markdown.new()
実行結果
約8秒程でちゃんと答えが返ってきました
正確な源泉数や湧出量はこちらを参照してください
このまま質問を変えずにもう一度セルを実行すると、今度は1秒未満でなじ結果が返ってきます
AI Gateway の画面で確認すると、キャッシュで返答したことが分かります
レート制限
「設定」タブのレート制限から、1分間に1回のレート制限を設定してみます
この状態で立て続けに呼び出すと、 "Rate limited" というメッセージが返ってきます
AI Gateway のログ上でもエラーになっていることが確認できます
分析
AI Gayeway の「分析」タグを開くと、使用状況がグラフ化されているのが確認できます
API からのログ取得
Cloudflare API から AI Gateway のログを取得できます
API トークンの作成
Cloudflare の右上アイコンをクリックし、表示されるドロップダウンから「マイプロフィール」を開きます
左メニューの「API トークン」を開き、「トークンを作成する」をクリックします
遷移した画面の一番下「カスタムトークンを作成する」の右にある「始める」をクリックします
トークン名を適当な値にして、アクセス許可で「AI Gateway」の「読み取り」を選択します
画面一番下の「概要に進む」をクリックします
内容を確認し、「トークンを作成する」をクリックします
トークンの値が表示されるので、この値を Livebook で使用します
API の呼び出し
Livebook にトークン用の入力を作成し、トークンの値を設定します
cf_token_input = Kino.Input.password("CLOUDFLARE TOKEN")
トークンを認証ヘッダーに設定し、 API を呼び出します
cf_api_url = "https://api.cloudflare.com/client/v4/accounts/#{cf_account_id}/ai-gateway/gateways/#{cf_gateway_name}/logs"cf_token = Kino.Input.read(cf_token_input)cf_api_headers = [ {"Authorization", "Bearer #{cf_token}"}, {"Content-Type", "application/json"}]result = Req.new(url: cf_api_url, headers: cf_api_headers) |> Req.get!() |> Map.get(:body)
実行結果
%{ "result" => [ %{ "cached" => false, "cost" => 0, "created_at" => "2024-08-28 09:05:19", "duration" => 8346, "id" => "01J6C3PX50TTFCQDE9NCXJDCFS", "metadata" => "", "model" => "", "path" => "bedrock-runtime/us-east-1/model/anthropic.claude-3-5-sonnet-20240620-v1%3A0/converse", "provider" => "aws-bedrock", "request" => "{\"messages\":[{\"content\":[{\"text\":\"「やせうま」とはどういう食べ物ですか\"}],\"role\":\"user\"}]}", "request_content_type" => "application/json", "request_type" => "provider", "response" => "{\"metrics\":{\"latencyMs\":7112},\"output\":{\"message\":{\"content\":[{\"text\":\"「やせうま」は、日本の伝統的な菓子の一種です。以下に「やせうま」の特徴をまとめます:\\n\\n1. 形状:細長い棒状", "response_content_type" => "application/json", "status_code" => 200, "step" => 0, "success" => true, "tokens_in" => 0, "tokens_out" => 0 }, ... ], "result_info" => %{"count" => 19, "page" => 1, "per_page" => 20, "total_count" => 19}, "success" => true}
リクエストやレスポンスの内容、時間、実行結果、キャッシュなどの情報が取得できました
まとめ
AI Gateway を使うことで、 Bedrock のログ取得やキャッシュ設定、レート制限などが簡単に実装できました
生成AIの利用実績を分析したい場合などには有効そうです