Klippy can say hi. Now it gets a brain. In this lesson you'll add POST
/ask: send it a question, and it answers with Claude. The call goes through AI Gateway, Cloudflare's front door for AI models, so your Anthropic key never sits in your code.
Verified wrangler 4.157.0agents 0.33.0ai 7.0.162@ai-sdk/provider 4.0.35
Start from a-03 on GitHub. Starting here, or behind? Get the code at that tag:
git clone https://github.com/ryanyogan/klippy cd klippy git checkout a-03
1. Sign in to Cloudflare
The model runs on Cloudflare's side, even while Klippy runs on your laptop, so wrangler needs to know who you are. Make a free account at cloudflare.com if you don't have one, then from klippy/:
pnpm --filter agent exec wrangler loginA browser tab asks you to allow wrangler. Say yes, and the terminal prints "Successfully logged in".
2. Install the AI packages
pnpm --filter agent add --save-exact agents@0.33.0 ai@7.0.162 @ai-sdk/provider@4.0.35aiis the AI SDK: one way to call any model, withgenerateTextfor a single answer.agentsis Cloudflare's Agents SDK. You'll use one piece of it today:createAI, which connects the AI SDK to AI Gateway.@ai-sdk/providerholds the shared types both need.
--save-exact writes 0.33.0, not ^0.33.0, so you get the versions this lesson was checked with.
3. A binding for AI
A Worker doesn't hold passwords to reach Cloudflare services. It gets a binding: a named connection that Cloudflare hands your code on env. Add one to apps/agent/wrangler.jsonc, after upload_source_maps (note the new comma after true):
binding: "AI" means your code reaches it as env.AI. remote: true means that even under pnpm dev, model calls go to the real service on your account, since there's no model on your laptop. Then refresh the types, so TypeScript knows about env.AI:
pnpm --filter agent run cf-typegenThis is why no key lives in the Worker. The binding proves the call comes from your account. Your Anthropic key, if you add one, is stored in your gateway, not in your code or your repo.
4. Pick the model
Make apps/agent/src/model.ts:
createAIconnects to the AI Gateway calleddefaultthrough the binding. Cloudflare creates that gateway the first time you use it.anthropic/claude-haiku-5.5is Claude Haiku 5.5: Anthropic's fast, low-cost model, and plenty for Klippy.fallbackis the plan B. If the Claude call fails, the same request goes to@cf/zai-org/glm-4.7-flash, a free Workers AI model that runs on Cloudflare.reasoningEffort: nullturns its thinking step off, for short, quick answers.
The one-line switch. Claude needs your Anthropic key in your gateway (or credits added to the gateway itself), and that costs money per call. Without a key, the Claude call fails and the free model answers instead, so Klippy works either way. To use only the free model, make line 6 return ai('@cf/zai-org/glm-4.7-flash', {
reasoningEffort: null }); (and drop the fallback). To use Claude, store your key, as below.
To store your key: in the Cloudflare dashboard, go to AI → AI Gateway, open the default gateway (or create one with that name), find Provider Keys, choose Add API Key, pick Anthropic and paste your key from the Claude Console. That's BYOK, bring your own key. Course F covers spend limits on the gateway.
5. The /ask route
/ask takes a question as JSON. Anyone could send anything, so check it first. In apps/agent/src/index.ts, add the imports and a length limit at the top:
Then add the route under the / one, still inside fetch. Change async fetch(request) to async fetch(request, env) too, since the route needs env. First, the checks:
Never trust the body. request.json() turns the body into an object, or fails, and .catch(() => null) turns a failure into null. The type { question?: unknown } is only a label for TypeScript; it checks nothing when the code runs. So typeof body?.question === 'string' checks for real. A missing, blank or over-long question gets a 400 Bad Request and never reaches the model.
Then the call itself, right after the checks:
systemis the system prompt: who Klippy is and how long to answer.The question goes in as a
usermessage, on its own. It's never pasted into your system prompt, so a question can't rewrite Klippy's instructions as easily.maxOutputTokens: 300caps the reply. Every call spends your money, and a public endpoint can be called by anyone, again and again. That's called denial of wallet. The cap on the question and the cap on the reply bound what one call can cost. Before you deploy, you'll add a lock so that only you can call it.providerMetadata.cloudflare.modelsays which model answered: Claude or the fallback.
6. Ask it
With pnpm dev running, ask Klippy something:
curl -s -X POST http://localhost:8787/ask -d '{"question":"Who are you, in one sentence?"}'Look at model. Our recording has no Anthropic key in its gateway, so the free fallback answered, not Claude. With your key stored, you'll see anthropic/claude-haiku-5.5 there. Every recorded answer in these courses comes from the fallback, and we label it each time.
Now send something that isn't a question:
If you get a 500 and the dev log says "Insufficient balance; add money to your gateway or use BYOK", the fallback line is missing from model.ts. If it says you're not logged in, run step 1 again.
7. Test it without spending a cent
Tests shouldn't call a real model: that's slow, costs money and gives a different answer each time. Klippy's tests swap in a fake model from ai/test that always says the same thing. Copy test/fake-model.ts and test/ask.spec.ts from the repo into apps/agent/test/. Then add one line to apps/agent/vitest.config.mts, so tests never reach your account:
Here's the part of ask.spec.ts that checks the refusals:
it.each runs the same test once per row: five bad bodies, five 400s. The other test checks that the reply cap really reaches the model call.
Commit your work. Stuck? Compare with tag a-04 (changes from a-03).