A full-stack TypeScript application where eight distinct AI personalities answer a question, anonymously judge the other answers, and eliminate one competitor each round until a single winner remains.
The backend is provider-agnostic. Game logic depends on an internal LlmClient interface instead of a vendor SDK, while adapters connect that interface to the Vercel AI SDK and supported model providers.
apps/web
React UI
│
▼
packages/contracts
Zod schemas + inferred TypeScript types
│
▼
apps/api
Fastify routes
│
▼
Application services
AnswerService / VoteService
│
▼
LlmClient interface
│
├── OpenAI adapter
├── Anthropic adapter
├── Google adapter
├── OpenAI-compatible adapter
└── Deterministic mock adapter
Routes, services, tests, and the frontend do not import provider SDKs. Only the provider registry knows how a configured provider is constructed.
REQUEST_TRACKING_MODE=redisnpm install
cp .env.example .env
Edit .env and choose a provider configuration.
Start both applications:
npm run dev
You can also run them independently with npm run dev:api and npm run dev:web.
The default local addresses are:
http://localhost:5173http://localhost:5000LLM_PROVIDER=openai
LLM_MODEL=gpt-5-mini
LLM_API_KEY=replace-me
LLM_PROVIDER=anthropic
LLM_MODEL=claude-sonnet-4-5
LLM_API_KEY=replace-me
LLM_PROVIDER=google
LLM_MODEL=gemini-2.5-flash
LLM_API_KEY=replace-me
LLM_PROVIDER=openai-compatible
LLM_PROVIDER_NAME=huggingface
LLM_BASE_URL=https://router.huggingface.co/v1
LLM_MODEL=mistralai/Mistral-7B-Instruct-v0.2
LLM_API_KEY=replace-me
LLM_SUPPORTS_STRUCTURED_OUTPUTS=true
LLM_PROVIDER=openai-compatible
LLM_PROVIDER_NAME=my-provider
LLM_BASE_URL=https://provider.example/v1
LLM_MODEL=model-id
LLM_API_KEY=replace-me
LLM_SUPPORTS_STRUCTURED_OUTPUTS=false
For a local Ollama or LM Studio endpoint, use its OpenAI-compatible base URL. LLM_API_KEY may be omitted when the local server does not require it.
LLM_PROVIDER=mock
LLM_MODEL=deterministic-local-model
Mock mode is useful for UI development and does not make external requests.
| Variable | Purpose | Default |
|---|---|---|
NODE_ENV |
development, test, or production |
development |
HOST |
API bind address | 0.0.0.0 |
PORT |
API port | 5000 |
WEB_ORIGINS |
Comma-separated browser origins | http://localhost:5173 |
TRUST_PROXY |
Trust reverse-proxy headers | false |
LLM_PROVIDER |
Provider adapter | openai-compatible |
LLM_PROVIDER_NAME |
Identifier for compatible providers | custom |
LLM_MODEL |
Provider model ID | Mistral example |
LLM_API_KEY |
Provider credential | none |
LLM_BASE_URL |
Required by compatible providers | none |
LLM_SUPPORTS_STRUCTURED_OUTPUTS |
Advertise strict structured-output support | false |
AI_TIMEOUT_MS |
Maximum duration of each model call | 30000 |
AI_MAX_RETRIES |
SDK retries for transient provider failures | 2 |
AI_CONCURRENCY |
Maximum concurrent generation calls | 3 |
REQUEST_TRACKING_MODE |
disabled, memory, or redis |
memory |
REQUEST_LIMIT |
Logical API-call budget | 200 |
REDIS_URL |
Redis connection URL | none |
COUNTER_FAILURE_MODE |
open or closed on Redis failure |
closed |
ADMIN_KEY |
Secret for resetting the global counter | none |
HTTP_RATE_LIMIT |
Per-IP requests per window | 60 |
HTTP_RATE_WINDOW_MS |
Per-IP rate-limit window | 60000 |
VITE_API_URL |
Browser-facing API URL | same origin |
In production, ADMIN_KEY is required whenever request tracking is enabled.
No global usage count is recorded. Requests remain protected by the per-IP HTTP rate limiter.
REQUEST_TRACKING_MODE=disabled
The count lives in the API process and resets whenever the process restarts. This is suitable for local development and single-instance demonstrations.
REQUEST_TRACKING_MODE=memory
REQUEST_LIMIT=200
The count is shared across API instances. A Lua script makes the consume-and-disable decision atomic.
REQUEST_TRACKING_MODE=redis
REQUEST_LIMIT=200
REDIS_URL=redis://localhost:6379
COUNTER_FAILURE_MODE=closed
COUNTER_FAILURE_MODE=open permits AI calls when Redis is unavailable. closed rejects them with 503 so cost controls cannot silently disappear.
POST /api/answers
Content-Type: application/json
{
"question": "What makes a decision fair?",
"personalities": [
{ "id": 1, "name": "The Philosopher", "trait": "Questions assumptions" },
{ "id": 2, "name": "The Pragmatist", "trait": "Focuses on results" }
]
}
POST /api/vote
Content-Type: application/json
{
"question": "What makes a decision fair?",
"responses": [
{ "id": 1, "answer": "Answer one" },
{ "id": 2, "answer": "Answer two" }
]
}
GET /api/status
POST /api/reset-counter
X-Admin-Key: your-admin-key
GET /health
GET /ready
GET /ping
/health is a lightweight liveness check. /ready checks required application dependencies without exposing credentials.
npm run dev # API and frontend development servers
npm run dev:api # API development server
npm run dev:web # frontend development server
npm run format # format all supported files
npm run lint # lint TypeScript and TSX
npm run typecheck # type-check every workspace
npm run test # run all tests
npm run build # build contracts, API, and frontend
npm run verify # run all quality gates
npm install
npm run verify
npm run start --workspace @ai-hunger-games/api
Serve apps/web/dist through a static host or reverse proxy and set VITE_API_URL before building when the API is deployed at a different origin.
ADMIN_KEY to the browser.WEB_ORIGINS explicitly in production.COUNTER_FAILURE_MODE=closed when the global request budget is a hard cost boundary.MIT. See LICENSE for the required notices covering the supplied source and this TypeScript rewrite.