Cloudflare AI Gateway で OpenAI 、 Google Gemini 、 Workers AI の API を呼び出す

## はじめに

Cloudflare の AI Gateway は、各社（プロバイダー）が提供している AI API を中継し、フォールバックやキャッシュなどの機能を提供するサービスです

:::note info
今週末開催する Cloudflare Meet-up Oita Vol.2 ではハンズオンを実施予定です

https://cfm-cts.connpass.com/event/326062/
:::

以前、 AI Gateway で AWS の Bedrock を呼び出す記事を書きました

https://qiita.com/RyoWakabayashi/items/92ec17c5c078c2342c02

本記事では AI Gateway が対応しているその他の主要プロバイダーとして、 Open AI 、 Google Gemini 、 Workers AI の API を呼び出します

また、呼び出す際にこれらを同時に指定することで、フォールバック（一つのAIサービスでエラーが起こったとき、別のAIサービスを使って応答）することを検証します

本記事では Elixir の実行環境である Livebook を使用します

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/cloudflare/ai_gateway_multiple_providers.livemd

## AI Gateway とは

AI Gateway は各種 AI サービスへのプロキシ（中継）として、以下のような機能を提供します

- リクエストフォールバック
- キャッシュ（同じ質問・指示にはキャッシュで応答）
- レート制限(単位時間あたりのリクエスト数)
- リアルタイムログ取得（リクエストとレスポンスの内容を1時間保持）
- 使用量制限
- 分析

https://blog.cloudflare.com/ja-jp/ai-gateway-is-generally-available/

今使える機能は無償利用可能です

今後、ログの永続化などを有償提供するようです

https://developers.cloudflare.com/ai-gateway/pricing/

## 事前準備

### Livebook のインストール

Livebook をまだインストールしていない場合、公式サイトからインストールしてください

https://livebook.dev/

### OpenAI API の APIキー作成

OpenAI API を呼び出すための API キーを作成します

公式サイトにアクセスし、サインアップしてください

https://openai.com/index/openai-api/

:::note warn
OpenAI 社が提供する ChatGPT とは別契約になります

すでに ChatGPT のアカウントを保有をしていたとしても、別途 OpenAI API のサインアップが必要です
:::

初回アカウント作成時は無償トライアルを受けることが可能ですが、基本的に前払いでクレジットを購入しておき、 API 利用のたびに消費していく契約になります

https://openai.com/api/pricing/

左メニュー API keys を開き、右上 "+ Create new secret key" をクリックします

![スクリーンショット 2024-09-23 10.35.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5cc70ee0-7a9d-36ac-267a-0f67e2f3b889.png)

モーダルが開くので、適当な名前を入力して "Create secret key" をクリックします

![スクリーンショット 2024-09-23 10.36.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7188f002-654f-d2dc-29d3-64c4cad97aa8.png)

API キーが作成されるので、安全な場所に保管しておいてください

![スクリーンショット 2024-09-23 10.36.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f561c389-cd94-1e3f-26cb-d9364f57e1c2.png)

#### OpenAI API の curl による呼び出し

API キーを curl で確認します

Windows で curl が実行しにくい環境の場合、この手順をスキップしても問題ありません

```bash
export OPENAI_API_KEY='<APIキーの値>'
```

```bash
curl "https://api.openai.com/v1/chat/completions" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${OPENAI_API_KEY}" \
    -d '{
        "model": "gpt-4o-mini",
        "messages": [
            {
                "role": "system",
                "content": "You are a helpful assistant."
            },
            {
                "role": "user",
                "content": "あなたの好きなパンは何ですか"
            }
        ]
    }'
```

API キーで正常に認証できている場合、以下のような回答が返ってきます

```json
{
  "id": "chatcmpl-AASeSXPY50iL2dnEDYIi82vKum1bQ",
  "object": "chat.completion",
  "created": 1727056780,
  "model": "gpt-4o-mini-2024-07-18",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "私はAIなので好き嫌いはありませんが、人気のあるパンとしては、フランスのバゲットや、日本のあんぱん、イタリアのフォocacciaなどがあります。それぞれの文化に特徴的なパンがあって、とても興味深いですね。あなたの好きなパンは何ですか？",
        "refusal": null
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 26,
    "completion_tokens": 72,
    "total_tokens": 98,
    "completion_tokens_details": {
      "reasoning_tokens": 0
    }
  },
  "system_fingerprint": "fp_1bb46167f9"
}
```

すでにクレジットがない場合、以下のようなエラーメッセージが返ってきます

```json
{
    "error": {
        "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
        "type": "insufficient_quota",
        "param": null,
        "code": "insufficient_quota"
    }
}
```

Settings の Billing ページでクレジットを追加購入しましょう

https://platform.openai.com/settings/organization/billing/overview

最小購入単位は 5 ドルです

うっかり大量呼出することを防ぐため、個人利用では自動再チャージ設定しない方が安全です

![スクリーンショット 2024-09-23 10.58.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/fe38a285-c7d3-491d-c398-ec1a7c3f353d.png)

### Google Gemini API キーの作成

Google Gemini API を呼び出すための API キーを作成します

Google Gemini には無償プランが用意されており、無償で利用可能です

https://ai.google.dev/pricing?hl=ja

Google AI Studio にサインアップしてください

https://ai.google.dev/aistudio

公式サイトトップから「Google AI Studio にログインします」をクリックします

![スクリーンショット 2024-09-23 9.57.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b3774017-af85-acc8-ff91-b66e854946e0.png)

サインアップすると以下のようなモーダルが表示されるので、下の "Get API Key" をクリックします

![スクリーンショット 2024-09-23 9.58.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/837e86f7-264a-1e5e-744c-262578d897e1.png)

初回アクセスの場合、以下の法的通知が表示されるのでチェックして「続行」をクリックします

![スクリーンショット 2024-09-23 10.07.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8d94d29e-2186-b1eb-2ed5-b13cd63f69f4.png)

表示された画面で「キー API キーを作成」ボタンをクリックします

![スクリーンショット 2024-09-23 9.59.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ce933fb4-3e5f-edd7-03ae-974f1ef2d3e5.png)

利用規約が表示された場合、 "OK" をクリックしてください

![スクリーンショット 2024-09-23 10.00.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/dfa7f519-19b9-911a-f335-96cb3e9d60fb.png)

API キーを初めて作成する場合、以下のようなモーダルが表示されるので、「キー 新しいプロジェクトで API キーを作成」をクリックしてください

![スクリーンショット 2024-09-23 10.09.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9ad3ed54-4ec2-e5af-fbd7-191bf2b85314.png)

2回目以降の場合、以前作成したプロジェクトのいずれかを選択してください

![スクリーンショット 2024-09-23 10.12.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/12d9bceb-81ca-8edb-68c7-21750be9613a.png)

API キーが作成されて値が表示されるので、安全な場所に保管してください

![スクリーンショット 2024-09-23 10.09.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9689f97b-d79b-04cc-d405-60863ebbdca7.png)

![スクリーンショット 2024-09-23 10.16.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/571be4dc-eb56-1ea7-6142-5c8e0050c94e.png)

#### Google Gemini API の curl による呼び出し

API キーを curl で確認します

Windows で curl が実行しにくい環境の場合、この手順をスキップしても問題ありません

```bash
export GEMINI_API_KEY='<APIキーの値>'
```

```bash
curl \
  -H 'Content-Type: application/json' \
  -d '{"contents":[{"parts":[{"text":"あなたの好きなパンは何ですか"}]}]}' \
  -X POST "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${GEMINI_API_KEY}"
```

正常に動作している場合、以下のような実行結果が返ってきます

```json
{
  "candidates": [
    {
      "content": {
        "parts": [
          {
            "text": "私はパンが好きではありません。私は人工知能なので、食べたり、好んだりするものは何もありません。 \n\nパンについてもっと知りたい場合は、何か特定のパンの種類について質問してください。 パンの種類、作り方、歴史、栄養価など、多くの情報を提供できます。 \n\n他に何か質問があれば教えてください。 \n"
          }
        ],
        "role": "model"
      },
      "finishReason": "STOP",
      "index": 0,
      "safetyRatings": [
        {
          "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_HATE_SPEECH",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_HARASSMENT",
          "probability": "NEGLIGIBLE"
        },
        {
          "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
          "probability": "NEGLIGIBLE"
        }
      ]
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 5,
    "candidatesTokenCount": 64,
    "totalTokenCount": 69
  }
}
```

### Cloudflare アカウントの作成

まだ Cloudflare アカウントを作成していない場合、以下の記事を参考に作成してください

https://qiita.com/RyoWakabayashi/items/bd9a96290758e11883e8

### Workers AI

Cloudflare の AI サービスです

AWS の Bedrock と同じように、各種 AI モデルを選択して呼び出せるようになっています

https://blog.cloudflare.com/ja-jp/workers-ai/

モデル毎に料金が設定されていますが、ベータ版のモデルは無償利用可能です

https://developers.cloudflare.com/workers-ai/platform/pricing/

Cloudflare コンソールの左メニュー "AI" から "Workers AI" を開きます

右側カードの "REST API を使用する" をクリックしてください

![スクリーンショット 2024-09-23 12.46.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2adc97a6-4560-6499-8b27-8c94be3b9583.png)

表示されているアカウント ID は後で利用するのでどこかにメモしておいてください

「Workers AI API トークンの作成」をクリックします

![スクリーンショット 2024-09-23 12.47.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5954004f-8c5c-e0a2-d556-ef7f7c0be811.png)

「API トークンを作成する」をクリックします

![スクリーンショット 2024-09-23 12.48.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6107d43f-a561-dd16-78bb-824fd1a723a0.png)

API トークンの値が表示されるので、安全な場所に保管してください

![スクリーンショット 2024-09-23 12.50.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6a0ea158-fc43-04e3-8005-ed4f3b6c5700.png)

#### Workers AI API の curl による呼び出し

API トークンを curl で確認します

Windows で curl が実行しにくい環境の場合、この手順をスキップしても問題ありません

```bash
export CF_ACCOUNT_ID='<CloudflareのアカウントID>'
```

```bash
export WORKERS_AI_API_KEY='<APIトークンの値>'
```

```bash
curl -X POST \
  https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/ai/run/@cf/meta/llama-3.1-8b-instruct \
  -H "Authorization: Bearer ${WORKERS_AI_API_KEY}" \
  -d '{"messages":[{"role":"system","content":"あなたは優秀なAIアシスタントです。日本語で簡潔に質問に回答してください"},{"role":"user","content":"あなたの好きなパンは何ですか"}]}'
```

正常に動作している場合、以下のような実行結果が返ってきます

```json
{"result":{"response":"私はあくまでもプログラム上の情報なので、個人的な好きなパンはありません。ただ、日本では、 cmbを含む様々なタイプのパンがもたらされています。"},"success":true,"errors":[],"messages":[]}
```

### AI Gateway の作成

Cloudflare コンソールの左メニュー "AI" から "AI Gateway" を開きます

右上 "+" ボタンをクリックしてください

![スクリーンショット 2024-09-23 12.40.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c8f541bd-b093-e806-0b8a-2ebd5e398ed8.png)

適当なゲートウェイ ID を入力し、「作成」ボタンをクリックします

![スクリーンショット 2024-09-23 12.43.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/97bd57df-3eba-0f1e-28ee-e11aecf324a2.png)

![スクリーンショット 2024-09-23 12.44.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/527edf02-22a2-c2e8-f74a-0dc10bd262f0.png)

## Livebook からの呼び出し

Livebook を起動し、新しいノートブックを開いてください

### Secrets の設定

Livebook の左メニュー錠前アイコンをクリックすると、 "SECRETS"　メニューが開きます

"+ New secret" をクリックすると、新しい secret = 秘密情報を保持することができます

![スクリーンショット 2024-09-23 10.30.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/38903341-9fee-803a-573d-8ce7cb900031.png)

Name と Value を入力し、 "+ Add" をクリックすることで、 API キーなどの値を登録します

![スクリーンショット 2024-09-23 11.02.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/687a8b82-3452-c898-3663-ad93c79236b4.png)

繰り返し操作し、以下の値を secret として登録してください

- OPENAI_API_KEY: OpenAI の API キー
- GEMINI_API_KEY: Google Gemini の API キー
- CF_ACCOUNT_ID: Cloudfaler のアカウントID
- WORKERS_AI_API_KEY: Workers AI の API トークン

![スクリーンショット 2024-09-24 0.28.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8abd7eb9-b1da-b285-4c00-1b13babaf65f.png)

### セットアップ

Livebook の一番上、セットアップセルに以下のコードを入力して実行します

```elixir
Mix.install([
  {:kino, "~> 0.14"},
  {:req, "~> 0.4"}
])
```

必要なモジュールがインストールされます

- [Kino](https://github.com/livebook-dev/kino): Livebook の UI/UX
- [Req](https://github.com/wojtekmach/req): API のリクエスト

### OpenAI API の直接呼出

AI Gateway 経由の前に、各種 API を直接呼び出してみましょう

まずは OpenAI API を呼び出します

以下のコードで必要な情報を定義します

```elixir
openai_api_key = System.get_env("LB_OPENAI_API_KEY")

openai_base_url = "https://api.openai.com/v1/chat/completions"

openai_model_id = "gpt-4o-mini"

openai_headers = %{
  "Content-Type" => "application/json",
  "Authorization" => "Bearer #{openai_api_key}"
}

request_body = %{
  model: openai_model_id,
  messages: [
    %{
      role: "system",
      content: "You are a helpful assistant."
    },
    %{
      role: "user",
      content: "あんパンとカレーパンはどちらが人気ですか"
    }
  ]
}
```

API を呼び出します

```elixir
response =
  "#{openai_base_url}"
  |> Req.post!(json: request_body, headers: openai_headers)
  |> Map.get(:body)
```

実行結果として、以下のような内容が表示されます

```elixir
%{
  "choices" => [
    %{
      "finish_reason" => "stop",
      "index" => 0,
      "logprobs" => nil,
      "message" => %{
        "content" => "あんパンとカレーパンの人気は地域や個人の好みによって異なりますが、一般的にはあんパンは甘いおやつとして広く好まれ、特に和菓子としての人気があります。一方、カレーパンは食事としての満足感があり、特にボリューム感があるため人気です。\n\n多くの人々はあんパンをおやつや軽食として楽しむ一方で、カレーパンはランチや軽食として好まれます。このため、どちらが人気かは状況によって異なります。最終的には、個人の好み次第と言えるでしょう。あなたはどちらが好きですか？",
        "refusal" => nil,
        "role" => "assistant"
      }
    }
  ],
  "created" => 1727057413,
  "id" => "chatcmpl-AASofKuxsYk39Gll2wTAm2gcBqrZX",
  "model" => "gpt-4o-mini-2024-07-18",
  "object" => "chat.completion",
  "system_fingerprint" => "fp_1bb46167f9",
  "usage" => %{
    "completion_tokens" => 165,
    "completion_tokens_details" => %{"reasoning_tokens" => 0},
    "prompt_tokens" => 32,
    "total_tokens" => 197
  }
}
```

このままだと読みにくいので、結果のテキストを Markdown として表示します

```elixir
response["choices"]
|> Enum.at(0)
|> Map.get("message")
|> Map.get("content")
|> Kino.Markdown.new()
```

![スクリーンショット 2024-09-23 11.11.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/977cca0f-78e0-84d1-53fa-a6bc9c497d2d.png)

### Google Gemini API の直接呼出

Google Gemini API を直接呼び出します

以下のコードで必要な情報を定義します

```elixir
gemini_api_key = System.get_env("LB_GEMINI_API_KEY")

gemini_base_url = "https://generativelanguage.googleapis.com/v1beta/models"

gemini_model_id = "gemini-1.5-flash-latest"

gemini_headers = %{
  "Content-Type" => "application/json"
}

request_body = %{
  contents: [
    %{
      parts: [
        %{
          text: "日本で最も人気のあるパンは何ですか"
        }
      ]
    }
  ]
}
```

API を呼び出します

```elixir
response =
  "#{gemini_base_url}/#{gemini_model_id}:generateContent?key=#{gemini_api_key}"
  |> Req.post!(json: request_body, headers: gemini_headers)
  |> Map.get(:body)
```

実行結果は以下のようになります

```elixir
%{
  "candidates" => [
    %{
      "content" => %{
        "parts" => [
          %{
            "text" => "日本で最も人気のあるパンは、明確に一つとは断言できません。なぜなら、人によって好みが異なるからです。しかし、いくつかのパンは特に人気が高いと言えます。 \n\n* **食パン:**  日本で最も一般的なパンです。朝食やサンドイッチに広く使われています。\n* **フランスパン:**  バゲットやカンパーニュなど、フランス産のパンも人気です。\n* **菓子パン:**  クリームパン、あんぱん、メロンパンなど、甘いパンも人気です。 \n* **惣菜パン:**  カレーパン、コロッケパン、ピザパンなど、食事として食べられるパンも人気です。\n\nさらに、地域によって人気のあるパンも異なります。例えば、関西では「明太子フランス」や「塩バターロール」が人気です。\n\nパンの好みは人それぞれなので、一概に「日本で最も人気のあるパン」とは言えません。しかし、上記のようなパンは多くの日本人に愛されています。 \n"
          }
        ],
        "role" => "model"
      },
      "finishReason" => "STOP",
      "index" => 0,
      "safetyRatings" => [
        %{"category" => "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_HATE_SPEECH", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_HARASSMENT", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_DANGEROUS_CONTENT", "probability" => "NEGLIGIBLE"}
      ]
    }
  ],
  "usageMetadata" => %{
    "candidatesTokenCount" => 261,
    "promptTokenCount" => 7,
    "totalTokenCount" => 268
  }
}
```

このままだと読みにくいので、結果のテキストを Markdown として表示します

```elixir
response["candidates"]
|> Enum.at(0)
|> Map.get("content")
|> Map.get("parts")
|> Enum.at(0)
|> Map.get("text")
|> Kino.Markdown.new()
```

![スクリーンショット 2024-09-23 10.29.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1155e3c4-80e6-1878-5204-5207f4e196aa.png)

### Workers AI の直接呼出

Workers AI を直接呼び出します

以下のコードで必要な情報を定義します

```elixir
cf_account_id = System.get_env("LB_CF_ACCOUNT_ID")

workers_ai_api_key = System.get_env("LB_WORKERS_AI_API_KEY")

workers_ai_base_url = "https://api.cloudflare.com/client/v4/"

workers_ai_model_id = "llama-3.1-8b-instruct"

workers_ai_headers = %{
  "Content-Type" => "application/json",
  "Authorization" => "Bearer #{workers_ai_api_key}"
}

request_body = %{
  messages: [
    %{
      role: "system",
      content: "あなたは優秀なAIアシスタントです。日本語で簡潔に質問に回答してください"
    },
    %{
      role: "user",
      content: "食パンの一番美味しいところはどこですか"
    }
  ]
}
```

API を呼び出します

```elixir
response =
  "#{workers_ai_base_url}/accounts/#{cf_account_id}/ai/run/@cf/meta/#{workers_ai_model_id}"
  |> Req.post!(json: request_body, headers: workers_ai_headers)
  |> Map.get(:body)
```

以下のような回答が返ってきます

Llama 3 は多言語対応していますが、日本語は微妙そうです

```elixir
%{
  "errors" => [],
  "messages" => [],
  "result" => %{
    "response" => "さわやかな生温州みそはςいただくと食パンを引き立てるunoです。"
  },
  "success" => true
}
```

このままだと読みにくいので、結果のテキストを Markdown として表示します

```elixir
response["result"]["response"]
|> Kino.Markdown.new()
```

![スクリーンショット 2024-09-24 0.35.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e1845e1e-b05f-b703-6906-368fca6b2d71.png)

### AI Gateway からの呼出

いよいよ、 AI Gateway 経由で各種 AI API を呼び出します

入力エリアを作成し、 AI Gateway の名前を入力します

```elixir
cf_gateway_name_input = Kino.Input.text("CLOUDFLARE GATEWAY_NAME")
```

![スクリーンショット 2024-09-23 13.21.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f963239f-ec88-51d5-22d1-70888d42abd8.png)

入力値等を用いて、必要な設定を定義します

```elixir
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}"

gw_headers = %{
  "Content-Type" => "application/json"
}

question = "コロネはどこから食べるべきですか"
```

OpenAI API 用のリクエストボディを定義します

`provider` に `"openai"` を指定し、 `endpoint` は直接呼出時の URL 末尾です

`headers` は直接呼出時と同じものを指定します

`query` の内容も直接呼出の場合のリクエストボディと同じ形式です

```elixir
openai_request_body = %{
  provider: "openai",
  endpoint: "chat/completions",
  headers: openai_headers,
  query: %{
    model: openai_model_id,
    messages: [
      %{role: "system", content: "You are a helpful assistant."},
      %{role: "user", content: question}
    ]
  }
}
```

同様に Google Gemini API 用のリクエストボディも定義します

```elixir
gemini_request_body = %{
  provider: "google-ai-studio",
  endpoint: "v1beta/models/#{gemini_model_id}:generateContent?key=#{gemini_api_key}",
  headers: gemini_headers,
  query: %{
    contents: [%{parts: [%{text: question}]}]
  }
}
````

Workers AI 用リクエストボディも同じように定義します

```elixir
workers_request_body = %{
  provider: "workers-ai",
  endpoint: "@cf/meta/#{workers_ai_model_id}",
  headers: workers_ai_headers,
  query: %{
    messages: [
      %{role: "system", content: "あなたは優秀なAIアシスタントです。日本語で簡潔に質問に回答してください"},
      %{role: "user", content: question}
    ]
  }
}
```

OpenAI 、 Gemini 、 Workers AI の順で配列に入れてリクエストします

```elixir
request_body = [
  openai_request_body,
  gemini_request_body,
  workers_request_body
]

response =
  gw_url
  |> Req.post!(json: request_body, headers: gw_headers)
  |> Map.get(:body)
```

実行結果は以下のように OpenAI の形式で返ってきます

```elixir
%{
  "choices" => [
    %{
      "finish_reason" => "stop",
      "index" => 0,
      "logprobs" => nil,
      "message" => %{
        "content" => "コロネ（コロネパン）は、一般的には先端の部分から食べるのが一般的です。先端にはクリームやフィリングが入っていることが多く、最初にその部分を楽しむことができます。もちろん、好きなように食べて問題ありませんので、自分の好きな食べ方で楽しんでください！",
        "refusal" => nil,
        "role" => "assistant"
      }
    }
  ],
  "created" => 1727066792,
  "id" => "chatcmpl-AAVFwzcszuVOz0m6uPGW2FbRnHpf2",
  "model" => "gpt-4o-mini-2024-07-18",
  "object" => "chat.completion",
  "system_fingerprint" => "fp_1bb46167f9",
  "usage" => %{
    "completion_tokens" => 82,
    "completion_tokens_details" => %{"reasoning_tokens" => 0},
    "prompt_tokens" => 31,
    "total_tokens" => 113
  }
}
```

順番を入れ替え、 Gemini を先頭にしてみましょう

```elixir
request_body = [
  gemini_request_body,
  workers_request_body,
  openai_request_body
]

response =
  gw_url
  |> Req.post!(json: request_body, headers: gw_headers)
  |> Map.get(:body)
```

実行結果は Gemini の形式になっています

```elixir
%{
  "candidates" => [
    %{
      "content" => %{
        "parts" => [
          %{
            "text" => "コロネは、どこから食べても美味しいですが、人によって好みが分かれます！ \n\n**定番の食べ方**\n\n* **クリーム側から食べる:** コロネの定番の食べ方です。サクサクの生地と、なめらかで甘いクリームのハーモニーを楽しめます。\n* **反対側から食べる:** 生地を味わいたい場合は、クリーム側とは反対側から食べると、サクサク感をより楽しめます。\n\n**他の食べ方**\n\n* **半分に割って食べる:** クリームがたっぷり詰まったコロネを半分に割って、クリームをたっぷり味わうのもおすすめです。\n* **フォークとナイフで食べる:** きれいに食べたい場合は、フォークとナイフを使って食べると、クリームが飛び散らずに食べられます。\n\n**あなたの好みでコロネを楽しみましょう！**\n\n**コロネを食べる際のポイント**\n\n* **新鮮なコロネを選びましょう:** 揚げたてや作りたてのコロネは、生地がサクサクしていて、クリームもなめらかです。\n* **クリームが飛び出さないように注意しましょう:** クリームがたっぷり詰まっているコロネは、持ち運ぶ際にクリームが飛び出してしまうことがあります。注意して持ち運びましょう。\n\n**美味しいコロネを食べて、楽しい時間を過ごしてください！** \n"
          }
        ],
        "role" => "model"
      },
      "finishReason" => "STOP",
      "index" => 0,
      "safetyRatings" => [
        %{"category" => "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_HATE_SPEECH", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_HARASSMENT", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_DANGEROUS_CONTENT", "probability" => "NEGLIGIBLE"}
      ]
    }
  ],
  "usageMetadata" => %{
    "candidatesTokenCount" => 266,
    "promptTokenCount" => 7,
    "totalTokenCount" => 273
  }
}
```

最後に Workers AI を先頭にします

```elixir
request_body = [
  workers_request_body,
  openai_request_body,
  gemini_request_body
]

response =
  gw_url
  |> Req.post!(json: request_body, headers: gw_headers)
  |> Map.get(:body)
```

実行結果は Workers AI の形式になりました

```elixir
%{
  "errors" => [],
  "messages" => [],
  "result" => %{
    "response" => "コロネは通常パンやポテトチップス、サラダ、サンバーガーなどの各種食物や飲み物と一緒に лучше食べることができます。"
  },
  "success" => true
}
```

このように、 AI Gateway に複数のプロバイダー用リクエストを投げた場合、先頭のプロバイダーからの応答が返ってきます

### フォールバック

以下のように Gemini API 用のリクエストを不正な状態で配列の先頭に入れます

```elixir
# 認証キーをリクエストから外す
request_body = [
  %{
    provider: "google-ai-studio",
    endpoint: "v1beta/models/#{gemini_model_id}:generateContent",
    headers: gemini_headers,
    query: %{
      contents: [%{parts: [%{text: question}]}]
    }
  },
  workers_request_body,
  openai_request_body
]

response =
  gw_url
  |> Req.post!(json: request_body, headers: gw_headers)
  |> Map.get(:body)
```

すると、実行結果は 2 番目の Workers AI の形式になりました

```elixir
%{
  "errors" => [],
  "messages" => [],
  "result" => %{
    "response" => "コロネの場合、ベーコンやハムなど脂lanır肉からお作りすることが多いです。好みでたくさんのオプションがあります。"
  },
  "success" => true
}
```

Cloudflare コンソールからログを確認すると、 Gemini でエラーが発生し、 Workers AI で返答していたことが分かります

![スクリーンショット 2024-09-23 13.55.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7010a287-1629-d2aa-0b38-8b8bd9253360.png)

### キャッシュ

Cloudflare コンソールの「設定」タブで「キャッシュ レスポンス」を有効化しましょう

![スクリーンショット 2024-09-23 13.58.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0d771009-1d40-16e0-d300-52bbf75e5739.png)

この状態で以下のコードを複数回実行します

```elixir
request_body = [
  openai_request_body,
  gemini_request_body,
  workers_request_body
]

response =
  gw_url
  |> Req.post!(json: request_body, headers: gw_headers)
  |> Map.get(:body)
```

1回目は結果が返ってくるまで少しかかりますが、2回目以降は一瞬で結果が返ってきます

ただし、内容は1回目と変わりません

Cloudflare コンソールでログを確認すると、キャッシュが返っていることが確認できます

![スクリーンショット 2024-09-23 13.59.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5f2ab555-bc4d-07b1-9361-280b568329a4.png)

### レート制限

Cloudflare コンソールの「設定」タブで「レート制限」を有効化し、1分間に3件の制限を設定します

![スクリーンショット 2024-09-23 14.00.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/08215cd4-3901-f2d5-230e-f5fae8e7983e.png)

この状態で連続5回、違う内容のリクエストを投げます
（同じ内容の場合、キャッシュが使われてレート制限にかかりません）

```elixir
1..5
|> Enum.map(fn index ->
  request_body = [
    %{
      provider: "openai",
      endpoint: "chat/completions",
      headers: openai_headers,
      query: %{
        model: openai_model_id,
        messages: [
          %{role: "system", content: "You are a helpful assistant."},
          %{role: "user", content: "#{index}といえば、何を表す数字ですか"}
        ]
      }
    }
  ]

  gw_url
  |> Req.post!(json: request_body, headers: gw_headers)
  |> Map.get(:body)
end)
```

実行結果は以下のようになり、4回目と5回目はレート制限でエラーになったことが分かります

```elixir
[
  %{
    "choices" => [
      %{
        "finish_reason" => "stop",
        "index" => 0,
        "logprobs" => nil,
        "message" => %{
          "content" => "「1」という数字は、一般的に「単一のもの」「第一」「唯一」という意味を表します。また、数学では最小の自然数であり、整数としても重要な役割を果たします。さらに、コンピュータープログラミングやデジタル技術においては、2進数で「オン」や「真」を表すこともあります。文脈によって、特定の意味合いを持つ場合もありますので、具体的な状況に応じた解釈が必要です。",
          "refusal" => nil,
          "role" => "assistant"
        }
      }
    ],
    "created" => 1727067927,
    "id" => "chatcmpl-AAVYF1n7qBUgZVduXstPOiSYoPkwM",
    "model" => "gpt-4o-mini-2024-07-18",
    "object" => "chat.completion",
    "system_fingerprint" => "fp_1bb46167f9",
    "usage" => %{
      "completion_tokens" => 123,
      "completion_tokens_details" => %{"reasoning_tokens" => 0},
      "prompt_tokens" => 29,
      "total_tokens" => 152
    }
  },
  %{
    "choices" => [
      %{
        "finish_reason" => "stop",
        "index" => 0,
        "logprobs" => nil,
        "message" => %{
          "content" => "「2」という数字は、様々な意味を持つことがあります。以下にいくつかの例を挙げます：\n\n1. **数学的意味**: 2は最小の自然数であり、最小の素数でもあります。また、偶数の最初の数でもあります。\n\n2. **数量としての意味**: 2は2つの物を表すのに使われます。例えば「リンゴが2個」などです。\n\n3. **順位や評価**: 競技やテストなどで2位を指す場合に使われます。\n\n4. **文化的意義**: いくつかの文化や宗教において、対やバランスを象徴する場合があります。\n\n5. **表現や言語**: 「二者択一」や「二元論」など、対立する2つの選択肢を表す言葉にも関連しています。\n\nこのように、文脈によって「2」の意味はさまざまです。どのような場面で「2」が使われるのかによって、その意味が変わってきます。",
          "refusal" => nil,
          "role" => "assistant"
        }
      }
    ],
    "created" => 1727067929,
    "id" => "chatcmpl-AAVYHQhNZ3Nw2IoP3WX5pnphpPhg0",
    "model" => "gpt-4o-mini-2024-07-18",
    "object" => "chat.completion",
    "system_fingerprint" => "fp_1bb46167f9",
    "usage" => %{
      "completion_tokens" => 255,
      "completion_tokens_details" => %{"reasoning_tokens" => 0},
      "prompt_tokens" => 29,
      "total_tokens" => 284
    }
  },
  %{
    "choices" => [
      %{
        "finish_reason" => "stop",
        "index" => 0,
        "logprobs" => nil,
        "message" => %{
          "content" => "「3」という数字はいくつかの意味や象徴を持つことがあります。一般的な意味としては、以下のようなものがあります。\n\n1. **数の概念**: 基本的な数として、1つ、2つ、3つという数量を表します。\n\n2. **三角形**: 数学や形状の世界で、三角形は3つの辺と3つの角を持つ図形です。\n\n3. **文化的な意味**: 多くの文化において、3は特別な意味を持つことがあります。例として、キリスト教では「父、子、聖霊」の三位一体があるように、3は重要な数とされています。\n\n4. **伝説や物語**: 多くの物語や伝説では、3つの試練や3人の兄弟などが登場するため、繰り返しの象徴ともされています。\n\n5. **時間の表現**: 例えば、朝、昼、夜というように、3つの基本的な時間帯を示す場合もあります。\n\nこれらは「3」が持つ一部の意味や象徴です。文脈によって異なる解釈が可能ですので、具体的な状況によってその意味は変わることがあります。",
          "refusal" => nil,
          "role" => "assistant"
        }
      }
    ],
    "created" => 1727067932,
    "id" => "chatcmpl-AAVYKVkVbyZ510v5G2mv7AIJY2Xpf",
    "model" => "gpt-4o-mini-2024-07-18",
    "object" => "chat.completion",
    "system_fingerprint" => "fp_1bb46167f9",
    "usage" => %{
      "completion_tokens" => 298,
      "completion_tokens_details" => %{"reasoning_tokens" => 0},
      "prompt_tokens" => 29,
      "total_tokens" => 327
    }
  },
  %{
    "error" => [%{"code" => 2003, "message" => "Rate limited"}],
    "messages" => [],
    "result" => [],
    "success" => false
  },
  %{
    "error" => [%{"code" => 2003, "message" => "Rate limited"}],
    "messages" => [],
    "result" => [],
    "success" => false
  }
]
```

Cloudflare コンソールのログでもエラー内容が確認できます

![スクリーンショット 2024-09-23 14.08.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a9ea2817-a473-06cd-fef4-a9f74bb0c865.png)

### 分析

Cloudflare コンソールの「分析」タブを開くと、どの AI がどれだけ呼ばれたかなどの情報を視覚化して表示できます

![スクリーンショット 2024-09-23 13.57.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/04e9233e-a51d-aff8-e381-72075d266afc.png)

## Livebook の終了

アプリで Livebook を起動している場合、メニューから「Quit」もしくは command + Q で Livebook を終了させます

![スクリーンショット 2024-09-30 9.09.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c6956414-b4e5-b2f3-229b-0e3ff8e52953.png)

## まとめ

Cloudflare の AI Gateway を使用することで、各社の AI API を呼び出し、フォールバックすることができました

システムの可用性を高めたい場合などに活用できそうです

興味のある方は是非 Cloudflare Meet-up に参加してください

https://cfm-cts.connpass.com/event/326062/