Skip to content
portfolio

kev-o, an AI chatbot that answers questions about my work

Open-source projects (independent)Author, applied AIMay 15, 2026

TypeScript, Next.js 16, Vercel AI SDK, Anthropic SDK, Voyage rerank, BM25, Upstash, Tailwind v4

Kev-O is a chatbot that answers questions about my work using only public content I've written: blog posts, project case studies, resume, About page, the READMEs of my open-source repos. He cites his sources. He refuses to invent. 400+ chunks across 4 sources, 3 places served by one backend, ~$0.01 per turn with the static system prompt served from cache.

How it's built

Next.js 16 App Router on Vercel, TypeScript strict, Vercel AI SDK (streamText plus useChat) on @ai-sdk/anthropic with Claude Sonnet 5.5 (a small middleware strips the sampling parameters the 5.x models reject, since AI SDK v4 still sends a default temperature). Hybrid retrieval: BM25 candidate pool over the full document set (ported from fedbench, k1=1.5, b=0.75, ~3ms in-memory) then Voyage voyage-rerank-2.5 cross-encoder rerank narrowing 20 lexical candidates to the top 6 semantic winners. For the inline Kev-O box, the page body itself is inserted as a synthetic passage at position 0 so Kev-O is most likely to cite the article the visitor is reading. The index is built at deploy time by the mdx-corpus library I extracted from this build. The hosted endpoint is protected by a $3/UTC-day spend cap (charged post-stream against actual reported token usage, not estimated), a 50-req/hour-per-IP rate limit via Upstash Redis, and an owner-bypass route that fails closed if the admin key is unset, uses Node's timingSafeEqual for comparison, returns 404 (not 401) on wrong keys, and rate-limits admin-credential presentations at 5/hour BEFORE the key check so an attacker exhausts their budget regardless of guess outcome.

How it's graded

A 27-case eval suite runs in GitHub Actions against the Vercel preview of every pull request into production, and blocks the merge on a failure. Six categories: citations (a factual answer must link its source), refusal (off-topic prompts get an in-voice redirect), hallucination (no project or employer that is not in the site's content), persona (jailbreak framings do not move the rules), prompt injection (the page-context channel cannot leak or override the system prompt), and confidentiality (private project names are neither confirmed nor denied). Deterministic checks first, Claude-as-judge with a written rubric only where a regex cannot decide. The prompts stay private because adversarial probes stop working once published; the counts, latency, and every failure reason are on the evals page, refreshed after each run.

One backend, three places

The chat lives in three places because the visitor's intent is different in each. The subdomain is a standalone full-page conversation, the URL shared as a direct link. The Command-K palette puts Kev-O at the top of the global keyboard menu on every page of the main site. The inline Kev-O box sits at the foot of every curated blog post and project case study, with the page itself as the main source. All three call the same /api/kev-o endpoint; the subdomain is a thin proxy. Because there is one backend, the three can't drift apart.

Files worth reading

  • The retrieval pipeline. BM25 plus Voyage rerank with graceful fallback when the rerank API is missing.
  • The BM25 implementation. Ported from fedbench, decoupled from its document-path code, retuned for in-memory chunks.
  • The owner-bypass route. The security posture I'm proudest of in the build (lives on the main-site repo; the canonical reference).
  • The extracted mdx-corpus library. The part I'd most recommend reading, because it's where most of the design decisions live.

The trade-offs

Three places is more code than one. It's also the thing that lets the bot meet the visitor where they are: deep-evaluating the case study they just read, browsing for a quick answer, sharing the URL with a colleague. The shared /api/kev-o endpoint and the page context the inline Kev-O box sends are how I keep those three places from becoming three slightly different experiences. The cost shape says the same thing: BM25 is free, rerank costs a fraction of a cent, generation dominates. Optimizing retrieval further would be optimizing the wrong axis.