DEV Community

Riley Zhang
Riley Zhang

Posted on

My AI Bot Choked on a 9,000-Word README. I Built a Token Gate.

A 9,000-word README broke my AI bot in under a second. The output was confident, detailed, and completely wrong. The root cause was boring: I never measured the input before sending it. The README blew past the model's context window, and the model quietly invented the rest. The fix was a small token gate that checks every request before it reaches the API.

I was building a docs summarizer for a side project. It worked beautifully on the two sample files I tested. Then I pointed it at a real repository. The first call returned a summary that mixed real features with ones I never wrote. A second call returned a different hallucination. The variance was the clue: the model was not reading the whole document.

I built the bot on MonkeyCode's free models, and I exposed it through a public endpoint hosted on MonkeyCode's free server. Both fit the budget of a $0 side project. But free access to a model does not remove the model's context limit. The model reads tokens, not prices. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The lesson: context is the real budget. Quotas reset monthly. The context window resets on every request, and most developers never look at it until the bot starts lying.

Count tokens before you send

The simplest fix is measuring the request on the client side. Token counting is a solved problem. The tiktoken library handles most common encodings, and the call is short:

import tiktoken

def count_tokens(text: str, encoding: str = "cl100k_base") -> int:
    return len(tiktoken.get_encoding(encoding).encode(text))
Enter fullscreen mode Exit fullscreen mode

A full count costs CPU time on every request. For a gate, a rough estimate is usually enough. Prose sits near four characters per token. Code sits closer to two. I used the cheap estimate for the decision and the exact count for logging.

def quick_count(text: str) -> int:
    return max(1, len(text) // 4)
Enter fullscreen mode Exit fullscreen mode

The gate in front of the model

I wrapped the counting logic in a small class. It takes the parts of a request, a priority order, and a hard limit. It returns what fits and what did not.

class TokenGate:
    def __init__(self, limit: int, reserve: int = 500):
        self.limit = limit
        self.reserve = reserve

    def budget(self) -> int:
        return self.limit - self.reserve

    def decide(self, parts: dict, order: list[str]) -> tuple[int, dict]:
        used = 0
        kept = {}
        for name in order:
            tokens = count_tokens(parts[name])
            if used + tokens > self.budget():
                return used, kept
            kept[name] = parts[name]
            used += tokens
        return used, kept
Enter fullscreen mode Exit fullscreen mode

The reserve is the safety margin. Model outputs cost tokens too. A response can easily eat five hundred of them. If the response overshoots, the whole call fails. Keeping part of the window unused is not waste; it is the only plan that survives real usage.

Priority order matters. My requests had three parts: a system prompt, a question, and a document. The system prompt is core behavior, so it stays. The question is short, so it stays. The document is the first thing to cut. My order reflected that: ["system", "question", "doc"].

When the document does not fit, cutting the middle rarely hurts as much as people expect. The first part of a README explains what the project is. The last part usually covers configuration or license. The middle holds history and examples. A trim that keeps both ends preserves most of the signal:

def trim_to_budget(text: str, room: int) -> str:
    head = text[: room * 4]
    tail = text[-200:]
    return f"{head}\n...[trimmed in the middle]...\n{tail}"
Enter fullscreen mode Exit fullscreen mode

This is a heuristic, not a guarantee. A README with a critical install command in the middle will lose it. When that matters, truncation is the wrong tool.

When trimming is not enough

Some documents are structured, and both ends are not enough. The correct fallback is chunking: split the document into pieces, summarize each piece, then answer from the summaries. I kept this as a pseudocode spec for the cases the gate could not handle alone:

for chunk in split(document, chunk_size=1500, overlap=200):
    summary = summarize(chunk)
bucket = join(summaries)
answer = generate(system_prompt + bucket + question)
Enter fullscreen mode Exit fullscreen mode

Each chunk fits comfortably inside the budget. The cost is latency: one call per chunk plus one final call. The benefit is a full read of a long document. Cap the chunk count. Without a cap, a pathological document turns into a hundred calls and a very patient user.

Hosting the gate on the free server

A gate is only useful if every request goes through it. I put it in front of the same public endpoint that was already running on MonkeyCode's free server. The wrapper is small:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post("/ask")
def ask():
    payload = request.get_json(silent=True) or {}
    question = str(payload.get("question", ""))[:400]
    doc = str(payload.get("doc", ""))
    used, kept = gate.decide(
        {"system": SYSTEM, "question": question, "doc": doc},
        ["system", "question", "doc"],
    )
    if "doc" not in kept:
        return jsonify({"warning": "document trimmed", "answer": answer(question, SYSTEM)}), 200
    return jsonify({"answer": answer(question, SYSTEM + kept["doc"])}), 200

app.run(port=8080)
Enter fullscreen mode Exit fullscreen mode

The question length cap is not politeness. It is protection. A four-thousand-character question would eat the whole budget and leave nothing for the answer. The free server handled this workload because the requests are small and stateless. A free server is best-effort. I kept a local fallback and treated the endpoint as an experiment, not an SLA.

The full strategy fits in one table:

Input size Strategy
fits in the budget send as-is
document too long, edges carry the signal trim the middle
structure matters across the whole doc chunk and summarize
still too big after both reject with a clear error

Reject early. An empty response is the worst failure mode.

Who should not adopt this

Small projects with tiny inputs do not need a gate. If your documents are five paragraphs, the gate is overhead with no payoff. Teams with a real uptime requirement should not build their critical path on a free server, no matter how convenient. Lossless pipelines should never truncate; the chunking path is the only honest option.

Free models changed the economics of side projects. Free servers removed the last excuse to keep tools local. But nobody removed the context window. I stopped trusting my bot the day it failed silently. I started trusting it again after I added a counter that refuses to guess.

Before you ship your next AI helper, feed it one real input by hand. If it lies, measure what you sent. The meter is ten lines of code, and it will save you a much stranger debugging session.

Top comments (0)