---
title: "Building the Foundation of an AI Chatbot with Cloudflare Pages, Workers, and AI Gateway"
description: "A small implementation experiment combining Cloudflare Pages, Pages Functions, Workers AI bindings, and AI Gateway to build an AI chatbot without exposing model credentials to the browser."
lang: "en"
canonical: "https://llm-lab.dev/en/posts/cloudflare-pages-workers-ai-gateway-chatbot/"
source: "https://llm-lab.dev/en/posts/cloudflare-pages-workers-ai-gateway-chatbot.md"
publishedAt: "2026-07-05"
updatedAt: "2026-07-05"
category: "実装検証"
tags:
  - "cloudflare"
  - "cloudflare-pages"
  - "workers-ai"
  - "ai-gateway"
  - "chatbot"
  - "agentops"
---

# Building the Foundation of an AI Chatbot with Cloudflare Pages, Workers, and AI Gateway

> [!NOTE]
> What this article confirms
>
> This article builds a small AI chatbot foundation with Cloudflare Pages, Pages Functions, Workers AI bindings, and AI Gateway.
>
> The verified path is to stabilize the UI and `/api/chat` contract in mock mode first, then switch to live mode and confirm a real Workers AI response plus the matching AI Gateway log. Even for a small bot, model credentials should stay out of the browser, Gateway metadata belongs in the server-side call, and the Gateway log should be checked before publishing.

When you start using Cloudflare AI Gateway, the first mental model is often "replace the LLM API URL and logs become visible." That is a reasonable starting point. But once you try to publish an AI chatbot, there are a few more design decisions to make.

Do not put model credentials in the browser. Separate the frontend from the LLM call boundary. Make AI Gateway logs, cache behavior, and metadata visible enough for later operations. Even for a small bot, leaving these points vague tends to create the familiar situation where "it works, but I cannot tell what is happening."

In this article, I build the foundation of an AI chatbot using Cloudflare Pages, Pages Functions, Workers AI bindings, and AI Gateway. The experiment first checks the API boundary and Gateway options in local mock mode, then switches to `CHAT_MODE=live` to verify a real Workers AI model call and AI Gateway log retrieval.

## The Shape of the Experiment

The architecture is intentionally small.

```text
Browser
  ↓
Cloudflare Pages static UI
  ↓ /api/chat
Pages Function on Workers runtime
  ↓ env.AI.run(..., { gateway })
Cloudflare AI Gateway
  ↓
Workers AI model
```

Pages serves the static UI, and only `/api/chat` is handled by a Pages Function. AI Gateway sits in the middle of the LLM call as an observability and control layer. In Cloudflare's documentation, the Workers AI binding can call `env.AI.run()`, and the third argument can include `gateway` options. This is where you pass the Gateway ID, cache controls, log collection setting, and metadata.

The sample uses the Workers AI model `@cf/zai-org/glm-5.2`. Since Workers AI uses Cloudflare account resources even during local development, I did not start by calling the real model. I first checked the UI and API contract in mock mode, then moved to live mode.

## Build the UI and API Boundary First

The Pages UI is a simple chat screen. The browser sends only `message` and `skipCache` to `/api/chat`. The model name, Gateway ID, metadata, and AI binding stay on the server side.

![Chat UI served by Cloudflare Pages, sending to /api/chat and showing the mock response plus Gateway options](/images/posts/cloudflare-pages-workers-ai-gateway-chatbot/01-chat-ui.webp)

The Pages Function itself stays thin and delegates to a shared handler.

```js
import { handleChat, handleOptions } from "../../src/chat.mjs";

export const onRequestPost = (context) => handleChat(context);

export const onRequestOptions = () => handleOptions();

export const onRequestGet = () =>
  Response.json({
    ok: true,
    route: "/api/chat",
    methods: ["POST", "OPTIONS"],
  });
```

With this split, the same `handleChat()` can be used both as a Pages Function and from the local mock server. That keeps input validation and response shape aligned.

## Build AI Gateway Options in the Application

With Cloudflare's AI binding, `env.AI.run()` can receive not only the model input but also Gateway options. In this sample, I attach the Gateway ID, whether to skip cache, whether to collect logs, and metadata for identifying the application.

```js
const raw = await env.AI.run(
  model,
  {
    messages,
  },
  {
    gateway: {
      id: env.CLOUDFLARE_AI_GATEWAY_ID ?? "default",
      skipCache: Boolean(body.skipCache),
      collectLog: env.COLLECT_AI_LOG !== "false",
      metadata: {
        app: "pages-workers-ai-chatbot",
        env: env.ENVIRONMENT ?? "local",
        route: "api-chat",
      },
    },
  },
);
```

> [!IMPORTANT] Design metadata in the application
> Do not treat AI Gateway as something that will somehow make everything understandable later in the dashboard. At minimum, add metadata that shows which application, environment, and API route produced the LLM call.

On the other hand, values such as user IDs or inquiry IDs need care. AI Gateway logs are useful for operations, but in this experiment I confirmed metadata-oriented fields such as provider, model, status, duration, cost, token counts, and metadata. Whether prompt and response bodies should be stored, and who should be able to view them, must be decided separately based on Gateway log settings, payload retention policy, and the application's data classification.

## Check Locally in Mock Mode

I did not connect to the real model first. The reason is simple: the first thing I wanted to verify was not answer quality, but the boundary of the AI chatbot.

The verification commands are:

```bash
npm run check
npm run dev
npm run verify
```

`npm run check` runs `node --check` against the sample JavaScript files. `npm run dev` starts a local mock server for the static Pages UI and `/api/chat`. `npm run verify` is a verification script prepared for this article. It sends requests to `/api/health` and `/api/chat` in the running local preview and checks:

* The health endpoint returns `ok=true`.
* Local preview uses `source=mock`.
* The Gateway ID is built as `default`.
* Metadata includes `app`, `env`, and `route`.
* Empty messages return HTTP 400.

![Local verification screen showing health, mock source, and gateway metadata as OK](/images/posts/cloudflare-pages-workers-ai-gateway-chatbot/02-local-verification.webp)

At this point, I had verified the UI, API, input validation, and Gateway option construction. Stopping here would not be enough to call it an experiment, so the next step was a real Workers AI call and AI Gateway log retrieval.

## Use the Workers AI Binding in Live Mode

To call the real model in Cloudflare or Wrangler live preview, set `CHAT_MODE=live`. With Wrangler, start Pages with the AI binding.

> [!WARNING] Live preview still consumes usage
> The AI binding connects to remote resources, so usage may be incurred even during local development.

```bash
wrangler login
npx wrangler pages dev public \
  --port 8790 \
  --compatibility-date=2026-07-03 \
  --ai AI \
  --binding CHAT_MODE=live \
  --binding CLOUDFLARE_AI_GATEWAY_ID=default \
  --binding AI_MODEL=@cf/zai-org/glm-5.2 \
  --binding ENVIRONMENT=local-live \
  --binding COLLECT_AI_LOG=true \
  --binding ENABLE_LOG_INSPECTION=true
```

The sample `wrangler.jsonc` configures the AI binding as `AI`.

```jsonc
{
  "name": "cloudflare-pages-workers-ai-gateway-chatbot",
  "compatibility_date": "2026-07-03",
  "pages_build_output_dir": "public",
  "ai": {
    "binding": "AI"
  },
  "vars": {
    "CHAT_MODE": "live",
    "CLOUDFLARE_AI_GATEWAY_ID": "default",
    "AI_MODEL": "@cf/zai-org/glm-5.2",
    "ENVIRONMENT": "preview",
    "COLLECT_AI_LOG": "true",
    "ENABLE_LOG_INSPECTION": "false"
  }
}
```

I first tried `compatibility_date: "2026-07-04"`, but Cloudflare rejected it as a future date at the time of verification. Even though the local environment date was July 4, 2026, Cloudflare had not accepted that date yet, so I changed it to `2026-07-03` and continued.

One important point here is that when calling Workers AI through the Workers AI binding, no external provider API key is exposed to the browser. This sample assumes Workers AI inside Cloudflare, so it differs from a design where OpenAI or Gemini keys are stored as Worker secrets.

If you want to call external providers such as OpenAI or Anthropic with your own keys, you need to revisit the AI Gateway provider-native endpoint or REST API design. Cloudflare's documentation explains that third-party models called through the AI binding use Cloudflare Unified Billing, and BYOK is not simply passed into the AI binding.

This is where the old "just replace the OpenAI-compatible URL" mental model gets a little slippery. Honestly, it is easy to get confused here at first.

## Verify the Real Model Call and AI Gateway Log

In live mode, I sent one request to `/api/chat`. The response showed `source=workers-ai`, the model was `@cf/zai-org/glm-5.2`, and the Gateway options included `id: "default"`, `skipCache: true`, `collectLog: true`, and metadata with `app`, `env`, and `route`.

Then I used the returned `aiGatewayLogId` to call the verification-only `/api/ai-gateway-log` endpoint and retrieve a summary of the AI Gateway log. This verifies the same log entry that appears in Cloudflare's AI Gateway logs, using the AI binding's `getLog()`.

> [!WARNING] The log inspection endpoint is for verification only
> This endpoint only works when `ENABLE_LOG_INSPECTION=true`. It should not be left as-is in a public chatbot.

![Live verification screen comparing the Workers AI response with the AI Gateway log summary](/images/posts/cloudflare-pages-workers-ai-gateway-chatbot/03-live-ai-gateway-log.webp)

The AI Gateway log summary I retrieved was:

| Field | Result |
|---|---|
| provider | `workers-ai` |
| model | `@cf/zai-org/glm-5.2` |
| status | `200` |
| cached | `false` |
| duration | `17182` ms |
| cost | `0.0028052000000000003` |
| tokens in | `74` |
| tokens out | `614` |
| metadata | Confirmed `app`, `env`, and `route` |
| prompt / response body | Not included in this `getLog()` summary |

This confirmed that the request did not merely return a model response from the Worker. It was also recorded as an AI Gateway log, and I could later inspect provider, model, status, duration, cost, token counts, and metadata. On the other hand, this artifact did not include the prompt body or response body. If you plan to use payload bodies operationally, you need to verify the AI Gateway log settings and payload retention policy separately.

## Do Not Oversell Cache-Based Cost Reduction

AI Gateway caching looks attractive for chatbots. If the same question can be answered without calling the upstream model, it may reduce both latency and cost.

> [!CAUTION] Cache is not a universal fix for free-form chat
> Cloudflare's documentation describes cache as applying to identical requests. In free-form chat, even small differences in wording or punctuation create different requests. So saying "chatbots can greatly reduce cost with cache" too casually is risky.

Caching is better suited to FAQs, fixed menus, templated questions, or repeated calls with the same system prompt and user prompt. For personalized consultation-style bots, metadata, logs, rate limits, and payload retention policy usually matter before cache does.

The `skip cache` control in this UI is not there because cache should always be enabled. It is there to force the question: can this request be cached safely?

## Even a Small Bot Surfaces Operational Questions Early

The biggest takeaway from this experiment is that implementation and operations cannot be completely separated, even in the smallest AI chatbot setup.

Putting the UI on Pages and routing `/api/chat` to a Function is straightforward. Calling a model with `env.AI.run()` is also not difficult. But once you think of it as a public bot, these questions appear immediately:

* Should prompt and response bodies be stored in Gateway logs?
* What granularity of identifiers should be placed in metadata?
* Which questions can use cache, and which should always call the model?
* Should local preview call the real model, or should it only verify the API contract with mocks?
* Should the app use Workers AI, or call external providers through provider-native endpoints?

If you postpone these questions, the first build may be faster. But when a user says the answer is strange, slow, or expensive, you will not know where to look.

Personally, I think Cloudflare Pages + Workers + AI Gateway is a strong fit for individual projects and small AI applications. Static UI, API boundary, inference, and the first observability layer can all live on the Cloudflare side. At the same time, AI Gateway alone does not complete AgentOps.

AI Gateway is an entry point for seeing the boundary of LLM calls. If you need to know which documents a RAG system retrieved, which tools an agent called, or how user feedback changed over time, you still need an application-level tracing design such as Langfuse.

## Summary

By combining Cloudflare Pages, Pages Functions, Workers AI bindings, and AI Gateway, you can build the foundation of an AI chatbot without exposing model credentials to the browser. In this experiment, I first sent requests from the static UI to `/api/chat` and verified input validation, mock responses, Gateway ID, and metadata with the same handler. Then I switched to live mode, received a real response from GLM-5.2 on Workers AI, and confirmed that AI Gateway logs exposed provider, model, status, duration, cost, token counts, and metadata.

There are still follow-up checks, especially cache HIT/MISS behavior, payload retention policy, and BYOK designs for external providers. Cache in particular is not a general solution for all free-form chat. It works best when requests are identical or highly templated.

Starting with mock mode to stabilize the API boundary, then moving to Wrangler live mode to verify Workers AI and AI Gateway logs, keeps model quality, Cloudflare bindings, and UI issues from becoming one large debugging problem.

## References

* [Cloudflare Pages Functions](https://developers.cloudflare.com/pages/functions/)
* [Cloudflare Pages Functions Bindings](https://developers.cloudflare.com/pages/functions/bindings/)
* [Cloudflare AI Gateway Workers Bindings](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/)
* [Cloudflare AI Gateway Logging](https://developers.cloudflare.com/ai-gateway/observability/logging/)
* [Cloudflare AI Gateway Caching](https://developers.cloudflare.com/ai-gateway/features/caching/)
* [Cloudflare AI Gateway REST API](https://developers.cloudflare.com/ai-gateway/usage/rest-api/)
