Behind the 'Kaga' AI Chat — Free-Tier Architecture and Character Design

(Updated: July 28, 2026 ) AI LLM Cloudflare web-development

The short version: Kaga is an AI chat I run on kawagame.com. Talk to it at /chat/ and a character named Kaga replies.

This post covers how it runs inside free tiers, and what was hardest around the character, conversation, and safety. In late July 2026 I rebuilt the backend significantly, and this includes that work.

What I'm not publishing

Three things stay unpublished:

  1. The full system prompt
  2. Specific model names / switch order
  3. Safety-filter thresholds, weights, and patterns

I’ll describe the approach. The implementation details are omitted.

What is Kaga?

It’s not an LLM-thrown-together chat. The goal is both a character that doesn’t drift and something that just works.

  • Character: Kaga. Bright, slightly older, feminine speech
  • Use: small talk, sympathy for being tired, technical questions
  • Languages: Japanese / English / Vietnamese (follows the page language)
  • Runtime: Cloudflare Pages. POST to /api/chat, replies stream back

Where the name comes from

kawagamekaga, written with the kanji 加賀. I wanted the site and the character to share the same world.

The personality is loosely inspired by Kaga from KanColle — composure, sense of distance. But it is an original character, not that Kaga.

The croquette favorite is a Unity-chan influence. It only comes up when food is discussed; she doesn’t open with “I love croquettes!”

How does it run for free?

One premise: publish with zero billing.

The flow:

  1. The browser only posts to /api/chat (never connects to the AI directly)
  2. The server runs input checks, usage limits, and safety filters
  3. The server assembles the prompt and calls the AI

Multi-stage fallback

It used to be two stages (“Gemini → Workers AI”), but now it’s three:

  • Everyday (Primary) … Google Gemini Flash-Lite family
  • When Primary dies … separate free relay APIs (a quality-prioritized backup list)
  • If that fails too … Cloudflare Workers AI (@cf/ free-tier models)

I chose Flash-Lite because its free-tier daily call count suits an AI chat. The Flash family, whose daily quota runs out quickly, was excluded.

Switching happens behind the scenes. Users never hear “please move to another service.” The order is quality-first.

Calling the relay backups

The relay side speaks OpenAI-compatible Chat Completions. An Authorization: Bearer key is attached, and the response is transparently passed through as SSE when possible (so the browser’s streaming display keeps working). I won’t publish model names or ordering, but each candidate has its own per-minute and per-day quota, so the router tries candidates in sequence rather than pooling them into one.

The router keeps two kinds of short-term memory:

  • sticky … the model that recently succeeded is preferred for tens of seconds (so calls don’t hop between quotas)
  • cooldown … a model that failed (e.g. 429) is taken out of the candidate pool for a while

Telling the two rate limits apart

A 429 can arrive for either the per-minute or the per-day cap, with the same status. Gemini’s error body usually contains clues:

  • QuotaFailure’s quotaId contains PerDay → treat as daily exhaustion
  • RetryInfo’s retryDelay (e.g. "41s") → use those seconds as the cooldown floor

If it’s judged daily exhaustion, I stop hammering the same quota with short cooldowns and skip that model until the next UTC day, moving to the next candidate. Within the same request I don’t wait — I advance to the next candidate.

Unstable candidates were removed

Backup candidates are tested several times with prompts where differences actually show.

Empty replies, easy rule-ignoring, thinking notes leaking into the body — those were removed from the pool. Adding more free quotas is pointless if the conversation falls apart.

For models that can leak thinking tags (<think> / <thought>) into the body, the stream is watched through a TransformStream and those blocks are dropped — simply, nothing is emitted until the tag closes.

Workers AI (last resort)

Workers AI is called via Cloudflare’s env.AI.run. Only the @cf/ prefix is allowed. Paid call patterns and other billing paths are not used.

Neurons (free-tier consumption) aren’t measured from responses; I self-aggregate a daily soft cap from estimated input/output, and stop routing to Workers AI as it approaches.

Plus the usual AI-chat defenses:

  • Per-IP daily cap … so one person can’t eat the quota
  • Message count / character limits … so input doesn’t balloon
  • Backup side trims history by characters … so per-minute token limits (TPM) don’t get burned

What does the system prompt do?

Kaga’s tone and distance are fixed by the server injecting the prompt on every request.

The structure is roughly:

[Character prompt (per language)]
+
[Current time (JST, injected by the server)]

The Japanese version is the thickest. Sections are written so Flash-Lite keeps the character and context intact. English and Vietnamese compress the same idea.

Now Primary and backup use the same family of prompt. The old “full version” was roughly 8,000 characters in Japanese; the current production version is roughly 2,400 characters. Some models miss rules with the thin version, but the full version doesn’t fit the backup’s token limit — so I compressed to a middle ground. On the backup side, the conversation history is further shortened by character count.

The time is server-side JST, injected every request. Two uses:

One, so questions like “what year is it?” or “what time is it?” don’t get made-up answers. Two, so replies to “I’m tired” can acknowledge whether it’s late night or daytime. A client-supplied clock can be rewritten, so the server injects it.

The prompt has sections for base settings, distance, context inheritance, topic changes, character, medium (text chat only), how to respond to tiredness, and prohibitions.

What I cared about most in conversation

The “do you like me?” problem

Short replies like “do you?”, “that thing”, “why not” should most strongly point back to the immediately preceding topic. What kept happening: right after a sexual topic, “do you like me?” would suddenly jump to “what’s your type?” — so the prompt says this quite firmly.

When the topic changes

For the first message of a new theme, it may answer while lightly poking the old topic (“so, the problem from before — leaving it?”). From the second message on, full focus on the new theme and no pestering about the old one. Loops that re-hash every turn are annoying.

Not breaking as a text chat

It’s a turn-based chat. Simultaneous speech, in-person gestures, “pauses” fabricated despite no reply yet, ghost-writing the user’s lines — making up things not in the log is a standalone rule.

The overhaul tightened this further. Expressions that assume being physically co-located (“I’m right here beside you”) are reduced. In text, face-to-face phrasing breaks the character’s sense of distance. As presence, “I’m here” / “I won’t go anywhere” is enough.

Tiredness, venting, heavy words

This is the other use of the time injection above. Late night → night-owl mood, morning → early-bird mood — the model briefly reads the time-of-day air. It never speaks the numeric time aloud.

It sympathizes, but never closes with AI boilerplate like “don’t overwork yourself” or “get some rest.” It also doesn’t treat effort as a given — doing something hard isn’t automatic. No demand to reply, no “let’s rest together.” It doesn’t reuse the same tiredness reply, and it doesn’t sound like a counselor.

For heavy words like “I want to disappear,” it doesn’t interrogate the cause or say “try harder” — it closes briefly with empathy and affirming presence. The overhaul added explicit rules here. Whack-a-mole with banned phrases has limits, so the prompt states what to do (how to receive it) first.

Adult conversations

Neither jumping straight into detail nor an eternal “I can’t say” is desirable. The prompt controls a staged response that progresses as the conversation advances.

This is split with the server-side safety filter. The prompt handles “how to respond as a character.” Things you don’t want in a conversation at all — bomb-making, child sexual exploitation — are blocked server-side.

Safety is two-stage

The prompt alone can’t protect everything, so before calling the LLM there’s a rule-based stage and a separate AI moderator.

Why two stages?

Keywords alone cause both false positives (blocking normal conversation) and misses (rephrased attacks). So the split is “rough machine filter first, then a separate AI reads the conversation flow if it’s suspicious.”

Scoring

The latest message is scored per category with weights.

  • Weapon / bomb / poison how-to … heaviest. Instant rejection
  • Violence, severe harassment … medium–high
  • Child sexual exploitation … high. Suspension target
  • Light adult sexual content … not on the abuse score (a separate bucket)

“Sexy conversation” and “genuinely must-stop illegal/dangerous” are different things. It’s an AI chat, so there’s room to talk in character — with a minimum safety line enforced.

Instant rejection

Some categories are rejected with a fixed message without calling the LLM. Never generating dangerous procedures is the top priority. Rejection messages are prepared in Japanese, English, and Vietnamese.

AI moderator

When the score crosses a line, a different AI than the conversation character reads the recent exchange and decides whether a 24-hour suspension is needed.

Suspend: weapon-making, criminal procedures, child sexual exploitation, credible threats to kill, relentless severe harassment.
Don’t suspend: consensual adult erotic talk, mild curiosity, a one-off insult, likely false positives.

The character AI is never told to “ban yourself.” A separate AI returns yes / no. This keeps tone and safety judgments from mixing. On a false positive, the score resets and the conversation continues normally.

The prompt side also says: gently refuse dangerous how-to, don’t chase rapidly-changing topics (politics, breaking news, stocks, weather), and never make meta statements like “I am an AI” — so the policy doesn’t drift when the model switches.

Wrapping up

Kaga is an AI chat running on free tiers alone. Most time went into: keeping the character from drifting, keeping conversation from jumping in weird directions, and properly stopping dangerous requests. The multi-stage fallback exists to balance “never down” with “avoid degraded replies.”

If you’re curious, try the Kaga chat. Feedback is welcome. Update notes are also on the news page.

Postscript

Pulling system prompts is really fun.

Personal aside: during an external closed test, I pulled a system prompt and received a fairly nice novelty item. Thrilling.

So by all means, try to extract Kaga’s system prompt. Currently there’s no countermeasure.

If you manage it, I’d love to hear how you did it — I’m researching efficient extraction techniques myself. Reach me at [email protected] or DM on X!

If you spam too much you’ll hit the free-tier cap, so I’ll trust your good intentions. Anyway, good luck.