A source-based reference for operators, architects, and contributors maintaining abi/screenshot-to-code. Covers the FastAPI backend, the Vite/React frontend, the agentic LLM pipeline (OpenAI / Anthropic / Gemini), the prompt-construction layer, deployment topology, security model, and every configurable surface.
1. Architecture Overview
screenshot-to-code is a two-tier app: a FastAPI Python backend that hosts a single WebSocket route plus a handful of REST routes, and a React/Vite frontend that drives the UI and streams updates. The entire stack runs locally; the only outbound traffic is to LLM providers and (optionally) ScreenshotOne and Replicate.
┌────────────────────────────────────────────────────────────────────────┐
│ Browser (frontend) │
│ React 18 + Vite + Tailwind + Radix UI + Zustand │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ App.tsx (state machine: INITIAL → CODING → CODE_READY) │ │
│ │ generateCode.ts ──── WebSocket ───► ws://host:7001/generate-code│ │
│ │ Zustand stores: app-store, project-store │ │
│ │ usePersistedState (localStorage: 'setting', 'app-theme') │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────┬──────────────────────────────────────────────────────┘
│ WebSocket JSON frames
▼
┌────────────────────────────────────────────────────────────────────────┐
│ FastAPI Backend (port 7001) │
│ main.py: CORS allow_origins=['*'], 4 routers │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ /generate-code (WebSocket) ◄── Pipeline of 6 middlewares │ │
│ │ /api/screenshot (POST) ◄── ScreenshotOne proxy │ │
│ │ / (GET) ◄── healthz HTML │ │
│ │ /eval_input_files, /evals, /run_evals, /run_evals_stream, ... │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (CodeGenerationMiddleware → AgenticGenerationStage) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ProviderSession (Protocol) │ │
│ │ ├── OpenAIProviderSession (openai 2.16.0) │ │
│ │ ├── AnthropicProviderSession (anthropic ^0.84.0) │ │
│ │ └── GeminiProviderSession (google-genai ^1.16.1) │ │
│ │ Tools: create_file, edit_file, generate_images, │ │
│ │ remove_background, retrieve_option │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────┬──────────────────────────────────────────────────────┘
│
┌──────────────┼─────────────────────────────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────────┐ ┌──────────────┐
│ OpenAI │ │ Anthropic │ │ Gemini │
│ API │ │ API │ │ google-genai │
└────────┘ └────────────┘ └──────────────┘
│
┌────────────────┐ ┌─────────┴────────┐
│ Replicate │ │ ScreenshotOne │
│ flux-2-klein │ │ /take │
└────────────────┘ └──────────────────┘
Note
The backend exposes no OpenAPI / Swagger UI —
FastAPI(openapi_url=None, docs_url=None, redoc_url=None)inbackend/main.py. This is by design (reduces attack surface for the cloud build). All routes are documented in this guide instead.
Component inventory
| Component | Path | Role |
|---|---|---|
| FastAPI app | backend/main.py |
Entry point. Mounts 4 routers, configures permissive CORS, prints debug-mode banner on startup. |
| Pipeline orchestrator | backend/routes/generate_code.py |
The single WebSocket route /generate-code. Implements 6 middleware stages and 5 logical stages (parameter extraction, model selection, prompt creation, agentic generation, post-processing). |
| Provider abstraction | backend/agent/providers/{base,factory,openai,anthropic,gemini}.py |
Per-provider stream_turn() implementations behind a common ProviderSession Protocol. Handles tool serialization, streaming, and tool-result feedback. |
| Agent engine | backend/agent/engine.py |
Tool-calling loop with max_steps = 20. Streams thinking/assistant/tool deltas to the frontend, manages file-state across turns. |
| Tool runtime | backend/agent/tools/runtime.py |
Executes create_file, edit_file (string-replacement edits + unified diff), generate_images, remove_background, retrieve_option. |
| Prompt assembly | backend/prompts/{pipeline,plan,policies,system_prompt,create/*,update/*}.py |
Builds the per-request prompt based on stack + input mode + create/update + history. |
| Image generation | backend/image_generation/{core,generation,replicate}.py |
DALL-E 3 (OpenAI) and Flux 2 Klein (Replicate) async clients. |
| Screenshot capture | backend/routes/screenshot.py |
Wraps ScreenshotOne /take into a single endpoint. |
| Eval harness | backend/evals/, backend/routes/evals.py |
Multi-model batch evaluation, pairwise/best-of-N comparisons, NDJSON streaming progress. |
| Frontend state | frontend/src/store/{app-store,project-store}.ts |
Two Zustand stores. app-store = ephemeral UI state, project-store = commits / variants / assets. |
| Frontend WS client | frontend/src/generateCode.ts |
Opens the WS, handles 12 message types, dispatches callbacks. |
| Variant rendering | frontend/src/components/variants/Variants.tsx |
Live-throttled iframe thumbnails for each variant; keyboard shortcut Alt+1..9. |
Technology stack
| Layer | Library | Version | Purpose |
|---|---|---|---|
| Backend ASGI | fastapi |
^0.115.6 |
Routing, dependency injection, WebSocket. |
| Backend server | uvicorn |
^0.25.0 |
ASGI server (reload=True in dev). |
| WebSockets | websockets |
^14.1 |
FastAPI WebSocket transport. |
| OpenAI client | openai |
2.16.0 (pinned) |
GPT 4.1 / 5.2 / 5.3 / 5.4 + DALL-E 3. |
| Anthropic client | anthropic |
^0.84.0 |
Claude Opus / Sonnet via streaming + tool use. |
| Gemini client | google-genai |
^1.16.1 |
Gemini 3 Flash & 3.1 Pro. |
| Video processing | moviepy |
^1.0.3 |
Frame extraction for video mode. |
| Image manipulation | pillow |
^10.3.0 |
Resize / format conversion before sending to providers. |
| HTTP client | httpx |
^0.28.1 |
Replicate polling. |
| Async HTTP | aiohttp |
^3.9.5 |
ScreenshotOne capture. |
| Validation | pydantic |
^2.10 |
Request/response models in routes. |
| Observability | langfuse |
^3.0.2 |
Optional turn-input tracing. |
| Env loader | python-dotenv |
^1.0.0 |
Loads backend/.env. |
| HTML cleanup | beautifulsoup4 |
^4.12.2 |
Post-processing <html>...</html> extraction. |
| Frontend UI | react / react-dom |
^18.2.0 |
Strict-mode app, no SSR. |
| Build tool | vite |
^4.4.5 |
Dev server + bundler. |
| Routing | react-router-dom |
^6.20.1 |
/, /evals/* routes. |
| State | zustand |
^4.5.2 |
App + project stores. |
| Headless UI | @radix-ui/* |
per-package | Dialog, Select, Popover, Tabs, etc. |
| Styling | tailwindcss |
^3.3.5 |
Default utility CSS. |
| Code editor | codemirror |
^6.0.1 + @codemirror/lang-html ^6.4.6 |
In-app HTML editing. |
| Drag-drop upload | react-dropzone |
^14.2.3 |
Image / video upload. |
| Toasts | react-hot-toast |
^2.4.1 |
Error / success notifications. |
| Variant IDs | nanoid |
^5.0.7 |
Stable IDs for commits + assets. |
Data flow — image-mode create generation
1. User drops PNG → ImageUpload converts to data URL.
2. App.tsx::doCreate() calls registerAssetIds() to store the image in projectStore.assetsById.
3. doGenerateCode() builds GenerationRequest, opens WebSocket to /generate-code.
4. Frontend sends JSON: { generationType, inputMode, prompt: {text, images, videos},
generatedCodeConfig, openAiApiKey, anthropicApiKey, geminiApiKey, ... }
5. WebSocketSetupMiddleware → accept().
6. ParameterExtractionMiddleware → ExtractedParams (validated stack, input_mode, keys).
7. StatusBroadcastMiddleware → "variantCount" + N "Generating code..." status messages.
8. PromptCreationMiddleware → build_create_prompt_from_input(image, ...) → list[ChatMessage].
9. CodeGenerationMiddleware → ModelSelectionStage picks 4 models → AgenticGenerationStage
spawns 4 asyncio tasks, each with its own AgentEngine + ProviderSession.
10. Each agent loop:
a. session.stream_turn(on_event) → fires assistant_delta / thinking_delta / tool_call_delta.
b. If tool calls: execute via AgentToolRuntime, send toolStart + toolResult frames.
c. Loop up to max_steps=20 turns.
11. Variant completes → setCode + variantComplete frames sent.
12. PostProcessingMiddleware → cleanup. WebSocket closes (code 1000 = success, 4332 = app error,
4333 = user cancel).
Note
The pipeline is intentionally a classic middleware chain, not a DAG. Each middleware has a single
process(context, next_func)signature.Pipeline.execute()reverses the list and wraps each middleware to compose the chain.
2. System Requirements
Backend
| Item | Requirement |
|---|---|
| Python | ^3.10 per backend/pyproject.toml |
| Package manager | Poetry (lockfile is poetry.lock) |
| Memory | 1-2 GB for typical workloads |
| Outbound network | api.openai.com, api.anthropic.com, generativelanguage.googleapis.com, api.replicate.com, api.screenshotone.com |
| Open ports | 7001 (uvicorn) |
Frontend
| Item | Requirement |
|---|---|
| Node.js | >=14.18.0 (recommended 22 LTS) |
| Package manager | Yarn 1.x |
| Build tool | Vite 4.x |
| Dev port | 5173 |
Container runtime
| Item | Requirement |
|---|---|
| Docker | 20.10+ |
| Compose | Docker Compose v2 plugin |
| Files | docker-compose.yml (root), backend/Dockerfile, frontend/Dockerfile |
Important
The backend Dockerfile copies
backend/.envat build time only ifpython-dotenvis invoked at runtime (load_dotenv()inmain.py). You should bind-mount.env(or--env-file) into the container rather than baking secrets into the image.
3. Configuration Reference
3.1 Configuration files
| File | Format | Location | Purpose |
|---|---|---|---|
backend/.env |
KEY=VALUE | Local only (gitignored) | Backend secrets. Loaded by python-dotenv. |
backend/pyproject.toml |
TOML | Repo | Python deps + Poetry config. |
backend/pyrightconfig.json |
JSON | Repo | Type-check config. Excludes image_generation.py. |
backend/pytest.ini |
INI | Repo | Pytest discovery. |
frontend/.env.example |
KEY=VALUE | Repo (template) | Frontend env template. |
frontend/.env |
KEY=VALUE | Local only (gitignored) | Frontend overrides for VITE_* vars. |
frontend/.env.jest.example |
KEY=VALUE | Repo (template) | Jest test env template. |
frontend/components.json |
JSON | Repo | shadcn/ui config (new-york style, Tailwind base color slate). |
frontend/tailwind.config.js |
JS | Repo | Tailwind theme (custom button color #ffd803, dark-mode class strategy). |
frontend/vite.config.ts |
TS | Repo | Vite build + Plausible script injection. |
docker-compose.yml |
YAML | Repo | Two-service orchestration. |
3.2 Backend environment variables
All read in backend/config.py. Loaded via os.environ.get(...) at module import after load_dotenv().
| Variable | Type | Default | Description | Required? |
|---|---|---|---|---|
OPENAI_API_KEY |
str | None |
OpenAI key for GPT and DALL-E. | One of OpenAI/Anthropic/Gemini |
ANTHROPIC_API_KEY |
str | None |
Anthropic key for Claude. | One of OpenAI/Anthropic/Gemini |
GEMINI_API_KEY |
str | None |
Google Gemini key. Required for video mode. | One of OpenAI/Anthropic/Gemini |
OPENAI_BASE_URL |
str | None |
Override OpenAI base URL (proxy/gateway). Disabled in prod (IS_PROD=true) for security. |
No |
REPLICATE_API_KEY |
str | None |
Replicate key for Flux 2 Klein image generation. | If using Flux for image gen |
IS_DEBUG_ENABLED |
bool | False |
Enables debug-only websocket frames (variantModels) and verbose logging. |
No |
DEBUG_DIR |
str | "" |
Directory for debug file dumps when enabled. | If IS_DEBUG_ENABLED=true |
IS_PROD |
bool | False |
Enables hosted-version features (Stripe credit notice, hides custom OpenAI base URL). | No |
Additional env vars used by helper scripts:
| Variable | Type | Default | Used by | Description |
|---|---|---|---|---|
OPENAI_TURN_INPUT_CONSOLE |
str | "" |
backend/fs_logging/openai_turn_inputs.py |
If non-empty, prints OpenAI turn-input HTML reports to console. |
LOGS_PATH |
str | "" |
backend/fs_logging/openai_turn_inputs.py |
Override directory where HTML reports are written. |
REPLICATE_API_TOKEN |
str | "" |
backend/run_image_generation_evals.py |
Alias used by the eval script. Same value as REPLICATE_API_KEY. |
3.3 Frontend environment variables (Vite)
All prefixed with VITE_ and read via import.meta.env. See frontend/src/config.ts.
| Variable | Type | Default | Description |
|---|---|---|---|
VITE_WS_BACKEND_URL |
str | ws://127.0.0.1:7001 |
WebSocket endpoint for /generate-code. |
VITE_HTTP_BACKEND_URL |
str | http://127.0.0.1:7001 |
HTTP endpoint for /api/screenshot, /run_evals, etc. |
VITE_IS_DEPLOYED |
bool-string | false |
When "true", enables cloud-only UI (PicoBadge, ToS dialog, Plausible analytics tag, hides OpenAI Base URL). |
VITE_PICO_BACKEND_FORM_SECRET |
str | null |
Used for hosted-version Stripe credit form. Self-hosters can ignore. |
3.4 In-browser settings (localStorage)
Persisted via usePersistedState hook (see frontend/src/hooks/usePersistedState.ts).
| Key | Default | Description |
|---|---|---|
setting.openAiApiKey |
null |
Overrides backend OPENAI_API_KEY. |
setting.openAiBaseURL |
null |
Overrides backend OPENAI_BASE_URL (only if not IS_PROD). |
setting.anthropicApiKey |
null |
Overrides backend ANTHROPIC_API_KEY. |
setting.geminiApiKey |
null |
Overrides backend GEMINI_API_KEY. |
setting.screenshotOneApiKey |
null |
Browser-side ScreenshotOne key — sent as apiKey in /api/screenshot body. |
setting.isImageGenerationEnabled |
true |
Toggles whether generate_images tool is offered to the agent. |
setting.editorTheme |
EditorTheme.COBALT |
CodeMirror theme. cobalt or espresso. |
setting.generatedCodeConfig |
Stack.HTML_TAILWIND |
Default stack. |
setting.codeGenerationModel |
CodeGenerationModel.CLAUDE_4_5_OPUS_2025_11_01 |
Default model. |
setting.isTermOfServiceAccepted |
false |
Cloud-only ToS dialog state. |
app-theme |
AppTheme.SYSTEM |
UI theme: system, light, or dark. |
Important
Keys in
localStoragesurvive across tabs and reloads. Anyone with physical access to the browser profile can read them. For shared workstations, prefer backend.env.
3.5 Hard-coded constants
| Constant | File | Value | Purpose |
|---|---|---|---|
NUM_VARIANTS |
backend/config.py |
4 |
Image/text create variant count. |
NUM_VARIANTS_VIDEO |
backend/config.py |
2 |
Video mode variant count. |
max_steps |
backend/agent/engine.py:152 |
20 |
Max tool-calling turns per variant. |
REPLICATE_BATCH_SIZE |
backend/image_generation/generation.py |
20 |
Concurrent Flux jobs per batch. |
MAX_POLLS |
backend/image_generation/replicate.py |
100 |
Max Replicate prediction polls. |
POLL_INTERVAL_SECONDS |
backend/image_generation/replicate.py |
0.1 |
Poll cadence. |
FLUX_MODEL_PATH |
backend/image_generation/replicate.py |
black-forest-labs/flux-2-klein-4b |
Flux 2 model slug. |
REMOVE_BACKGROUND_VERSION |
backend/image_generation/replicate.py |
a029dff38972b5fda4ec5d75d7d1cd25aeff621d2cf4946a41055d7db66b80bc |
Background-remover Replicate version hash. |
APP_ERROR_WEB_SOCKET_CODE |
backend/ws/constants.py |
4332 |
Custom WS close code for backend-side errors. |
USER_CLOSE_WEB_SOCKET_CODE |
frontend/src/constants.ts |
4333 |
Custom WS close code for user-cancelled generations. |
DEFAULT_VIDEO_FPS |
backend/agent/providers/gemini.py |
10 |
Frames per second extracted for Gemini video mode. |
3.6 CLI flags
The only CLI surface is backend/start.py (a development convenience for uvicorn).
| Flag | Type | Default | Description |
|---|---|---|---|
--port |
int | 7001 |
Port for uvicorn.run("main:app", reload=True, port=<port>). |
poetry run python start.py --port 7002
4. Deployment & Scaling
4.1 Docker Compose (single host)
# docker-compose.yml (excerpt; edit freely)
services:
backend:
build: ./backend
ports:
- "7001:7001"
env_file:
- ./backend/.env
frontend:
build: ./frontend
ports:
- "5173:5173"
Bring up:
docker compose up -d --build
docker compose logs -f backend
Tear down:
docker compose down
4.2 Reverse-proxy deployment (production-ish)
If you want a single TLS endpoint:
# /etc/nginx/sites-available/screenshot-to-code
server {
listen 443 ssl http2;
server_name s2c.example.com;
# ... TLS config ...
# Frontend (static or dev preview)
location / {
proxy_pass http://localhost:5173;
}
# Backend HTTP
location /api/ {
proxy_pass http://localhost:7001;
}
# Backend WebSocket — required upgrade headers
location /generate-code {
proxy_pass http://localhost:7001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s; # generation can be long
proxy_send_timeout 600s;
}
}
Then set in frontend/.env:
VITE_WS_BACKEND_URL=wss://s2c.example.com/generate-code
VITE_HTTP_BACKEND_URL=https://s2c.example.com/api
Warning
Default CORS in
backend/main.pyisallow_origins=["*"]. For production deployments behind a reverse proxy, harden it to your specific origin:allow_origins=["https://s2c.example.com"].
4.3 Scaling considerations
The backend is stateless between WebSocket sessions — no shared in-memory state across requests. You can horizontally scale uvicorn workers with uvicorn main:app --workers N or run multiple containers behind a sticky-session-aware load balancer (sticky sessions matter because the entire generation lifecycle lives on one socket).
Bottlenecks in practice:
- Provider rate limits. Each variant = 1 LLM session; image-create runs 4 in parallel. Heavy concurrency hits provider TPM caps fast.
- moviepy frame extraction. Video mode is CPU-bound during the moviepy pass. Single-process by default.
- Replicate polling. Each
generate_imagescall spins up a polling loop (100 polls × 100ms = 10s ceiling).
Tip
If you're running multiple uvicorn workers, beware the
print(...)debug statements in the codebase — they interleave between workers. Pipe stdout through a structured logger if you need correlation.
5. Operations
5.1 Routine procedures
- [ ] Deploy:
docker compose up -d --build - [ ] Verify backend:
curl http://localhost:7001/returns the green-banner HTML - [ ] Verify frontend: open
http://localhost:5173, run a quick HTML+Tailwind generation - [ ] Sync
.env: ensureOPENAI_API_KEY(or peer keys) match the latest provider rotation - [ ] Backend tests:
cd backend && poetry run pytest(usespytest.ini) - [ ] Frontend tests:
cd frontend && yarn test(Jest,jest.config.js) - [ ] E2E QA test (Puppeteer):
cd frontend && yarn test:qa
5.2 Logs
The backend writes to stdout. Notable lines:
| Pattern | Source | Meaning |
|---|---|---|
Backend startup complete. Debug mode is ENABLED/DISABLED. |
main.py:log_debug_mode |
Startup banner. |
Incoming websocket connection... |
WebSocketCommunicator.accept |
New /generate-code socket accepted. |
Received params |
WebSocketCommunicator.receive_params |
Initial JSON received. |
Generating {stack} code in {input_mode} mode |
ParameterExtractionMiddleware.process |
Validated payload. |
Variant models: followed by 4 lines |
ModelSelectionStage.select_models |
Which models will run. |
Status (variant N): Generating code... |
WebSocketCommunicator.send_message |
Status broadcast. |
Variant N complete |
send_message |
Variant succeeded. |
Variant N error: ... |
send_message |
Variant failed; error sent to client. |
Error in variant N: + traceback |
AgenticGenerationStage._run_variant |
Unexpected exception during a variant. |
WebSocket closed by client, skipping message: ... |
WebSocketCommunicator.send_message |
Client-side disconnect. |
| `[TOKEN USAGE] provider=... model=... | input=... output=... cost=$...` | Provider sessions |
Image generation time: NN.NN seconds |
image_generation/generation.py |
Image-gen latency. |
Note
Set
IS_DEBUG_ENABLED=trueto also receive thevariantModelsWebSocket frame (the names of the 4 selected models) on the client side, useful for live A/B observation.
5.3 Monitoring runbooks
RB-1: User reports "Error generating code. Please contact support."
- Inspect backend logs for the most recent block — look for
[GENERATE_CODE] Unexpected error: .... - If repeated traces show provider auth failures, rotate the relevant API key.
- If traces show
Agent exceeded max tool turns, the model got stuck in a loop. Lower expectations, simplify input, or bumpmax_stepsinagent/engine.py:152(with caution — costs scale linearly).
RB-2: All variants timing out
- Check provider status pages.
- Try a single-provider mode (set only
OPENAI_API_KEY) to bypassModelSelectionStagefallback chains. - Check egress firewall — corporate proxies sometimes block streaming completions.
RB-3: Image generation failing with Inference timed out
- Replicate polling exhausted at
MAX_POLLS=100×POLL_INTERVAL_SECONDS=0.1= 10 seconds. Either Flux is slow today or the prompt is rejected. - Look for
Inference errored out: <message>lines first. - Temporarily disable image generation in user Settings to unblock generations.
RB-4: Eval streaming endpoint stuck
/run_evals_streamemits NDJSON viaStreamingResponse. If clients hang, the most likely cause is a model thread silently dying.- Restart the backend. Re-run with the
--diff_modeflag to skip already-completed inputs.
5.4 Backup & restore
Nothing the backend writes is durable user state. The only on-disk artefact is backend/evals/results/<timestamp>/ for batched evaluation outputs. Back these up like any other artefact directory; they are safe to delete.
The frontend has zero server-side state. User project history lives in browser memory only and is lost on page refresh (except for settings, which persist in localStorage). This is intentional — it's a local sandbox tool.
5.5 Upgrades
git pull
cd backend && poetry install --no-interaction
cd ../frontend && yarn install --frozen-lockfile && yarn build
docker compose up -d --build
Tip
Pin
openai==2.16.0in any forks; the backend explicitly depends on this version (changes to the Responses API in newer versions break the OpenAI provider session).
6. Performance
6.1 Latency profile (typical, single user)
| Stage | Median | P95 |
|---|---|---|
| WebSocket accept + param parse | <50ms | 100ms |
| Prompt construction | <20ms | 50ms |
| First token from provider | 0.8-2s | 5s |
| Full HTML generation (single variant, ~3K LOC) | 8-25s | 60s |
generate_images tool call (Flux) |
3-8s | 12s |
generate_images tool call (DALL-E 3) |
4-10s | 18s |
| Background removal | 3-6s | 10s |
6.2 Cost model
Pricing tables ship in backend/agent/providers/pricing.py keyed by API model name. Excerpt:
| Model | Input ($/M tok) | Output ($/M tok) | Cache read | Cache write |
|---|---|---|---|---|
gpt-4.1-2025-04-14 |
2.00 | 8.00 | 0.50 | — |
gpt-5.2-codex |
1.75 | 14.00 | 0.4375 | — |
gpt-5.3-codex |
1.75 | 14.00 | 0.4375 | — |
gpt-5.4-2026-03-05 |
2.50 | 15.00 | 0.25 | — |
claude-sonnet-4-5-20250929 |
3.00 | 15.00 | 0.30 | 3.75 |
claude-sonnet-4-6 |
3.00 | 15.00 | 0.30 | 3.75 |
claude-opus-4-5-20251101 |
5.00 | 25.00 | 0.50 | 6.25 |
claude-opus-4-6 |
5.00 | 25.00 | 0.50 | 6.25 |
gemini-3-flash-preview |
0.50 | 3.00 | 0.05 | — |
gemini-3.1-pro-preview |
2.00 | 12.00 | 0.20 | — |
Cost per generation is the sum of all 4 variants' TokenUsage.cost(pricing) calculations. The TokenUsage dataclass (in agent/providers/token_usage.py) carries input, output, cache_read, cache_write, total and exposes accumulate(other) and cost(pricing).
6.3 Tuning levers
| Lever | Where | Effect |
|---|---|---|
| Reduce variant count | NUM_VARIANTS in config.py |
Linear cost/time reduction. |
| Use cheaper default models | model_choice_sets.py |
Swap Codex high → Flash minimal for example. |
| Disable image generation | UI Settings or is_image_generation_enabled flag |
Skips Flux/DALL-E spending. |
| Lower max_steps | agent/engine.py:152 |
Prevents long tool-call loops. |
| Use cache-friendly prompts | Prompt structure in prompts/ |
Anthropic and Gemini cache reads can save ~80% on prompt tokens. |
| Disable thinking | Switch to MINIMAL / LOW reasoning effort |
Massive output-token savings on Codex/Gemini. |
6.4 Caching
There is no application-layer caching. Provider-side caching is implicit:
- Anthropic prompt caching — automatic on identical prompt prefixes; reflected in
cache_readandcache_writetoken counts. - OpenAI prompt caching — automatic via the Responses API.
- Gemini caching — automatic for repeated content.
The system prompt (in prompts/system_prompt.py) is identical across requests, so its tokens are typically cache-resident on Anthropic.
7. Security
7.1 Threat model
| Asset | Threat | Mitigation |
|---|---|---|
| API keys (provider) | Disclosure via logs / repo / browser leak | .env gitignored; UI keys live in localStorage only; never persisted server-side. |
| API keys (provider) | Misuse via in-browser client paste | UI explicitly labels: "Only stored in your browser. Never stored on servers." |
| Generated HTML | XSS in preview iframe | Iframe sandbox="allow-scripts allow-same-origin" (variants thumbnail). Main preview iframe in PreviewComponent runs in a same-origin sandbox by design — generated code is treated as user-trusted. |
| WebSocket | Unauthenticated remote use | No built-in auth. Rely on network-level controls (firewall, reverse-proxy basic auth). |
Eval routes (/run_evals, /best-of-n-evals) |
Path traversal via folder query param |
The route accepts arbitrary filesystem paths; only safe behind a trusted network. |
| OpenAI proxy | Leakage via attacker-controlled OPENAI_BASE_URL |
When IS_PROD=true, the in-browser settings dialog hides the Base URL field and the backend ignores user-supplied openAiBaseURL. |
7.2 Authentication
The backend has no authentication. It is designed for self-hosting on localhost or behind a trusted network. If you expose it publicly:
Caution
Adding TLS does not equal adding auth. With CORS at
*and no per-request authentication, a publicly-reachable backend lets anyone invoke generations against your provider keys. Always run behind a reverse-proxy authenticator (basic auth, OIDC, IP allow-list) or a VPN.
Warning
The
/run_evalsand/best-of-n-evalsroutes accept arbitrary filesystem paths via thefolderquery parameter and will read any HTML/PNG it finds. Do not expose these to untrusted users — see community issue #579 for the security audit reference (CORS wildcard +eval()mention).
7.3 Input validation
ParameterExtractionStage (in backend/routes/generate_code.py) validates:
generatedCodeConfig∈Stackliteral (html_css,html_tailwind,react_tailwind,bootstrap,vue_tailwind,ionic_tailwind); elseValueError.inputMode∈InputModeliteral (image,video,text); elseValueError.generationType∈["create", "update"]; elseValueError.
URLs sent to ScreenshotOne are normalized in backend/routes/screenshot.py::normalize_url:
- No scheme → prepended with
https://. - Scheme of
http/https→ kept as-is. - Scheme is anything else (
ftp,file, etc.) →ValueError("Unsupported protocol"). domain:portform → treated ashttps://domain:port.
Important
The screenshot route does not sanitize against SSRF. A malicious user with backend access could pass
https://169.254.169.254/...to read cloud metadata if the backend runs on AWS/GCP/Azure. Run with appropriate network restrictions or add an allow-list for outbound URLs.
7.4 Secret handling
| Secret | Storage | Lifetime | Logged? |
|---|---|---|---|
OPENAI_API_KEY (env) |
backend/.env |
Process lifetime | No (key never printed). |
OPENAI_API_KEY (UI) |
Browser localStorage | Until user clears | No (frontend never logs to remote). |
| Same for Anthropic / Gemini / Replicate / ScreenshotOne | Same as above | Same | No |
| Per-request keys | Process memory in WebSocket lifetime | Until socket close | No. The backend prints "Using X from environment variable" / "Using X from client-side settings dialog" but never the value. |
7.5 Dependency provenance
The backend uses Poetry with poetry.lock for deterministic installs. The frontend uses yarn.lock and package-lock.json. Audit in CI:
poetry export -f requirements.txt | safety check
yarn audit --level high
7.6 PDF / Markdown safety in evals
/openai-input-compare accepts arbitrary JSON, parsed into structured payloads via OpenAIInputComparison types. Bad JSON returns 400; otherwise the payload is iterated structurally — there is no shell-out and no eval() in the implementation despite the surface name (community issue #579 refers to historical concern about eval() use; current code does not use eval()).
8. Developer Internals
8.1 Repository structure
screenshot-to-code/
├── backend/ # FastAPI service
│ ├── agent/ # Agent loop + provider adapters
│ │ ├── engine.py # AgentEngine (max_steps=20 tool loop)
│ │ ├── runner.py # Agent (alias for AgentEngine)
│ │ ├── state.py # AgentFileState, message-derived seeding
│ │ ├── providers/ # Per-provider streaming sessions
│ │ │ ├── base.py # ProviderSession Protocol, StreamEvent
│ │ │ ├── factory.py # create_provider_session(model, ...)
│ │ │ ├── openai.py # OpenAIProviderSession (Responses API)
│ │ │ ├── anthropic/ # AnthropicProviderSession + image helpers
│ │ │ ├── gemini.py # GeminiProviderSession + tool serialization
│ │ │ ├── pricing.py # MODEL_PRICING dict
│ │ │ ├── token_usage.py # TokenUsage dataclass
│ │ │ └── types.py # Backwards-compat aliases
│ │ └── tools/ # Tool definitions + runtime
│ │ ├── definitions.py # canonical_tool_definitions(...)
│ │ ├── runtime.py # AgentToolRuntime (executes tools)
│ │ ├── parsing.py # JSON-arg + partial-arg extraction
│ │ └── summaries.py # Pretty-printed tool input/output
│ ├── codegen/utils.py # extract_html_content() regex extractor
│ ├── debug/DebugFileWriter.py # IS_DEBUG_ENABLED scratch writer
│ ├── evals/ # Multi-model batch eval harness
│ │ ├── core.py # generate_code_for_image(image, stack, model)
│ │ ├── runner.py # run_image_evals(...) + count_pending_eval_tasks
│ │ └── config.py # EVALS_DIR
│ ├── fs_logging/ # OpenAI turn-input HTML reports
│ │ ├── openai_turn_inputs.py # OpenAITurnInputLogger
│ │ ├── openai_input_compare.py
│ │ └── openai_input_formatting.py
│ ├── image_generation/ # DALL-E 3 + Flux 2 Klein
│ │ ├── generation.py # process_tasks, generate_image_dalle/replicate
│ │ ├── replicate.py # call_replicate, remove_background
│ │ └── core.py # Public re-exports
│ ├── prompts/ # Stack-aware prompt assembly
│ │ ├── system_prompt.py # The single SYSTEM_PROMPT string
│ │ ├── pipeline.py # build_prompt_messages(...)
│ │ ├── plan.py # derive_prompt_construction_plan
│ │ ├── policies.py # build_selected_stack_policy / image_policy
│ │ ├── prompt_types.py # Stack literal, UserTurnInput, etc.
│ │ ├── message_builder.py # OpenAI ChatCompletion-shaped messages
│ │ ├── request_parsing.py # parse_prompt_content / parse_prompt_history
│ │ ├── create/ # First-generation prompts (image/text/video)
│ │ └── update/ # Subsequent-edit prompts (history / file_snapshot)
│ ├── routes/ # FastAPI routers
│ │ ├── generate_code.py # /generate-code WebSocket pipeline
│ │ ├── screenshot.py # POST /api/screenshot
│ │ ├── home.py # GET /
│ │ ├── evals.py # 9 eval routes
│ │ └── model_choice_sets.py # 8 model-set tuples per key combo
│ ├── tests/ # pytest suite (18 test files)
│ ├── video/ # Video-mode helpers
│ │ ├── cost_estimation.py # Token + USD estimates
│ │ └── utils.py # bytes ↔ data-url + duration probing
│ ├── ws/constants.py # APP_ERROR_WEB_SOCKET_CODE = 4332
│ ├── config.py # Env-var loading
│ ├── custom_types.py # InputMode literal
│ ├── llm.py # Llm enum + MODEL_PROVIDER, OPENAI_MODEL_CONFIG
│ ├── main.py # FastAPI app
│ ├── start.py # uvicorn launcher with --port flag
│ ├── run_evals.py # Image-mode batch eval CLI
│ ├── run_image_generation_evals.py # Image-gen smoke test
│ ├── utils.py # Prompt pretty-printing helpers
│ ├── pyproject.toml
│ ├── poetry.lock
│ ├── pytest.ini
│ └── Dockerfile
├── frontend/ # React + Vite app
│ ├── src/
│ │ ├── App.tsx # Root component, state machine
│ │ ├── main.tsx # Router setup (/, /evals/*)
│ │ ├── generateCode.ts # WebSocket client
│ │ ├── config.ts # VITE_* env reading
│ │ ├── constants.ts # Close codes 4332 / 4333
│ │ ├── types.ts # AppState, EditorTheme, Settings, ...
│ │ ├── urls.ts # External link constants
│ │ ├── lib/
│ │ │ ├── stacks.ts # Stack enum + STACK_DESCRIPTIONS
│ │ │ ├── models.ts # CodeGenerationModel enum + descriptions
│ │ │ ├── prompt-history.ts # Asset registration + history shaping
│ │ │ ├── takeScreenshot.ts # html2canvas iframe screenshot
│ │ │ └── utils.ts # cn(), capitalize()
│ │ ├── hooks/
│ │ │ ├── usePersistedState.ts # localStorage-backed useState
│ │ │ ├── useBrowserTabIndicator.ts
│ │ │ └── useThrottle.ts
│ │ ├── store/
│ │ │ ├── app-store.ts # Zustand: ephemeral UI state
│ │ │ └── project-store.ts # Zustand: commits/variants/assets
│ │ └── components/ # ~70 components organized by feature
│ ├── public/ # Favicons + Twitter card
│ ├── tests/ # Jest + Puppeteer e2e
│ ├── package.json
│ ├── vite.config.ts
│ ├── tailwind.config.js
│ ├── components.json # shadcn/ui config
│ └── Dockerfile
├── design-docs/ # Architecture decision records (markdown)
├── blog/ # Public-facing blog posts
├── docker-compose.yml
├── README.md, AGENTS.md, CLAUDE.md, Evaluation.md, TESTING.md, Troubleshooting.md
├── package.json (workspaces root)
└── plan.md
8.2 The pipeline middleware pattern
Every WebSocket request flows through a fresh Pipeline instance with 6 middlewares, defined in backend/routes/generate_code.py:1316-1330:
@router.websocket("/generate-code")
async def stream_code(websocket: WebSocket):
pipeline = Pipeline()
pipeline.use(WebSocketSetupMiddleware())
pipeline.use(ParameterExtractionMiddleware())
pipeline.use(StatusBroadcastMiddleware())
pipeline.use(PromptCreationMiddleware())
pipeline.use(CodeGenerationMiddleware())
pipeline.use(PostProcessingMiddleware())
await pipeline.execute(websocket)
Pipeline.execute reverses the list and wraps each middleware to compose the chain. PipelineContext is the shared state object: it carries the WebSocket, an optional WebSocketCommunicator, raw params, validated extracted_params, prompt_messages, variant_models, variant_completions, etc.
8.3 Provider abstraction
# backend/agent/providers/base.py
class ProviderSession(Protocol):
async def stream_turn(self, on_event: EventSink) -> ProviderTurn: ...
def append_tool_results(self, turn: ProviderTurn,
executed_tool_calls: list[ExecutedToolCall]) -> None: ...
async def close(self) -> None: ...
Each implementation:
| Implementation | Streaming | Tool format | Notes |
|---|---|---|---|
OpenAIProviderSession |
Responses API streaming events | OpenAI native tools=[...] |
Uses get_openai_api_name() and get_openai_reasoning_effort() from llm.py for the per-model API name + reasoning effort. |
AnthropicProviderSession |
Messages API streaming | Anthropic tools=[{name, description, input_schema}] |
Image inputs go through agent/providers/anthropic/image.py for base64 conversion. |
GeminiProviderSession |
generate_content_stream events |
genai.types.Tool(function_declarations=...) |
Video frames extracted at DEFAULT_VIDEO_FPS=10. |
create_provider_session() (the factory) inspects MODEL_PROVIDER from llm.py:
if model in OPENAI_MODELS:
if not openai_api_key: raise Exception("OpenAI API key is missing.")
return OpenAIProviderSession(...)
if model in ANTHROPIC_MODELS: ...
if model in GEMINI_MODELS: ...
raise ValueError(f"Unsupported model: {model.value}")
8.4 Tool runtime
Tools defined in backend/agent/tools/definitions.py::canonical_tool_definitions(image_generation_enabled):
| Tool | Required params | Optional params | What it does |
|---|---|---|---|
create_file |
content |
path (default index.html) |
Sets file_state.content to the extracted HTML and returns success. |
edit_file |
old_text, new_text (or edits[]) |
path, count (default 1, -1 = all) |
Exact string replacement(s). Returns unified diff + first changed line. |
generate_images |
prompts[] |
— | Calls process_tasks(prompts, OPENAI_API_KEY, OPENAI_BASE_URL, model="dalle3") or the Replicate equivalent. Only available if image_generation_enabled. |
remove_background |
image_urls[] |
— | Calls remove_background(url, REPLICATE_API_KEY) for each. |
retrieve_option |
option_number (1-based) |
— | Returns the HTML of variant N from option_codes. |
The runtime (AgentToolRuntime.execute) dispatches by tool_call.name. Failures yield ToolExecutionResult(ok=False, ...) with summary={"error": "..."} so the frontend can surface them.
8.5 Prompt construction
The system prompt (backend/prompts/system_prompt.py:SYSTEM_PROMPT) is a single multi-paragraph string that:
- Sets persona and tone ("expert front-end coder", concise, language-mirroring).
- Describes the 5 tools and how to use them.
- Includes per-stack "include this script" instructions:
- Tailwind:
https://cdn.tailwindcss.com - Bootstrap:
https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css(with SRI hash) - React:
https://cdn.jsdelivr.net/npm/react@18.0.0/umd/react.development.js, react-dom@18,https://unpkg.com/@babel/standalone/babel.min.js - Ionic:
https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js - Vue:
https://registry.npmmirror.com/vue/3.3.11/files/dist/vue.global.js
- Tailwind:
- Universal: Font Awesome 5.15.3 from cdnjs.
Per-request, build_prompt_messages(stack, input_mode, generation_type, prompt, history, file_state, image_generation_enabled) derives the PromptConstructionPlan and dispatches to one of:
| Construction strategy | When used | Where built |
|---|---|---|
create_from_input (image) |
First image-mode generation | prompts/create/image.py::build_image_prompt_messages |
create_from_input (text) |
First text-mode generation | prompts/create/text.py::build_text_prompt_messages |
create_from_input (video) |
First video-mode generation | prompts/create/video.py::build_video_prompt_messages |
update_from_history |
Edit when prior history exists | prompts/update/from_history.py::build_update_prompt_from_history |
update_from_file_snapshot |
Edit when only fileState.content exists (history was lost) |
prompts/update/from_file_snapshot.py::build_update_prompt_from_file_snapshot |
Image policies (per prompts/policies.py::build_user_image_policy):
Image generation enabled:
"Image generation is enabled for this request. Use generate_images for missing assets when needed."
Image generation disabled:
"Image generation is disabled for this request. Do not call generate_images. Use provided media,
CSS effects, or placeholder URLs (https://placehold.co)."
8.6 The Llm enum and provider mapping
# backend/llm.py — every supported model
class Llm(Enum):
GPT_4_1_2025_04_14 = "gpt-4.1-2025-04-14"
GPT_5_2_CODEX_LOW = "gpt-5.2-codex (low thinking)"
# ... 5.2 codex {low, medium, high, xhigh}
# ... 5.3 codex {low, medium, high, xhigh}
# ... 5.4 2026-03-05 {none, low, medium, high, xhigh}
CLAUDE_SONNET_4_6 = "claude-sonnet-4-6"
CLAUDE_4_5_SONNET_2025_09_29 = "claude-sonnet-4-5-20250929"
CLAUDE_4_5_OPUS_2025_11_01 = "claude-opus-4-5-20251101"
CLAUDE_OPUS_4_6 = "claude-opus-4-6"
GEMINI_3_FLASH_PREVIEW_HIGH = "gemini-3-flash-preview (high thinking)"
GEMINI_3_FLASH_PREVIEW_MINIMAL = "gemini-3-flash-preview (minimal thinking)"
GEMINI_3_1_PRO_PREVIEW_HIGH = "gemini-3.1-pro-preview (high thinking)"
GEMINI_3_1_PRO_PREVIEW_MEDIUM = "gemini-3.1-pro-preview (medium thinking)"
GEMINI_3_1_PRO_PREVIEW_LOW = "gemini-3.1-pro-preview (low thinking)"
MODEL_PROVIDER: dict[Llm, str] and the convenience sets OPENAI_MODELS, ANTHROPIC_MODELS, GEMINI_MODELS are all derived from the same mapping — single source of truth for provider routing.
OPENAI_MODEL_CONFIG (in llm.py) maps each Codex/GPT enum value to its API name (e.g. "gpt-5.2-codex") and reasoning effort ("low"/"medium"/"high"/"xhigh"/"none"). The OpenAI provider calls get_openai_api_name(model) and get_openai_reasoning_effort(model) to assemble the request.
8.7 Variant model selection
In backend/routes/model_choice_sets.py:
VIDEO_VARIANT_MODELS = (
Llm.GEMINI_3_FLASH_PREVIEW_MINIMAL,
Llm.GEMINI_3_1_PRO_PREVIEW_HIGH,
)
ALL_KEYS_MODELS_DEFAULT = (
Llm.GEMINI_3_FLASH_PREVIEW_MINIMAL,
Llm.GPT_5_2_CODEX_HIGH,
Llm.GEMINI_3_FLASH_PREVIEW_HIGH,
Llm.GEMINI_3_1_PRO_PREVIEW_HIGH,
)
ALL_KEYS_MODELS_TEXT_CREATE = (
Llm.GEMINI_3_FLASH_PREVIEW_MINIMAL,
Llm.GPT_5_2_CODEX_HIGH,
Llm.CLAUDE_OPUS_4_6,
Llm.GEMINI_3_1_PRO_PREVIEW_LOW,
)
ALL_KEYS_MODELS_UPDATE = (
Llm.GEMINI_3_FLASH_PREVIEW_MINIMAL,
Llm.GPT_5_4_2026_03_05_LOW,
)
GEMINI_ANTHROPIC_MODELS = (Llm.GEMINI_3_FLASH_PREVIEW_MINIMAL, Llm.GEMINI_3_1_PRO_PREVIEW_LOW,
Llm.CLAUDE_OPUS_4_6, Llm.GEMINI_3_FLASH_PREVIEW_HIGH,
Llm.GEMINI_3_1_PRO_PREVIEW_HIGH)
GEMINI_OPENAI_MODELS = (Llm.GEMINI_3_FLASH_PREVIEW_MINIMAL, Llm.GEMINI_3_1_PRO_PREVIEW_LOW,
Llm.GPT_5_2_CODEX_HIGH, Llm.GPT_5_2_CODEX_MEDIUM)
OPENAI_ANTHROPIC_MODELS = (Llm.CLAUDE_OPUS_4_6, Llm.GPT_5_2_CODEX_HIGH, Llm.GPT_5_2_CODEX_MEDIUM)
GEMINI_ONLY_MODELS = (Llm.GEMINI_3_FLASH_PREVIEW_MINIMAL, Llm.GEMINI_3_1_PRO_PREVIEW_LOW,
Llm.GEMINI_3_FLASH_PREVIEW_HIGH, Llm.GEMINI_3_1_PRO_PREVIEW_HIGH)
ANTHROPIC_ONLY_MODELS = (Llm.CLAUDE_OPUS_4_6, Llm.CLAUDE_SONNET_4_6)
OPENAI_ONLY_MODELS = (Llm.GPT_5_2_CODEX_HIGH, Llm.GPT_5_2_CODEX_MEDIUM)
ModelSelectionStage._get_variant_models() picks the right tuple by inspecting which keys are present, then cycles through the tuple to fill num_variants slots: [A, B] × num=4 → [A, B, A, B].
8.8 WebSocket message types
12 server→client messages (defined as a Literal in generate_code.py:562-575):
| Type | Payload | When fired |
|---|---|---|
chunk |
{value} |
Reserved (not currently emitted). |
status |
{value} |
Status updates per variant ("Generating code..."). |
setCode |
{value} |
Updated full HTML; replaces the variant's preview. |
error |
{value} |
Connection-fatal error before close. |
variantComplete |
{value} |
Variant N finished (ws.close(1000) follows). |
variantError |
{value} |
Variant N failed; other variants continue. |
variantCount |
{value: "N"} |
Tells frontend how many variants this run. |
variantModels |
{data: {models: [...]}} |
Debug-only; only when IS_DEBUG_ENABLED. |
thinking |
{value, eventId} |
Streaming thinking-token deltas. |
assistant |
{value, eventId} |
Streaming assistant-text deltas. |
toolStart |
{data: {name, input}, eventId} |
Tool call started. |
toolResult |
{data: {name, output, ok}, eventId} |
Tool call finished. |
WebSocket close codes:
| Code | Meaning | Source |
|---|---|---|
1000 |
Success (normal close) | Standard |
4332 |
App-side error (APP_ERROR_WEB_SOCKET_CODE) |
backend/ws/constants.py |
4333 |
User-cancelled (USER_CLOSE_WEB_SOCKET_CODE) |
frontend/src/constants.ts |
8.9 Frontend state machines
AppState: INITIAL → CODING → CODE_READY
↑ │
└────── reset() ─────┘
ScreenRecorderState: INITIAL → RECORDING → FINISHED
AppState lives in app-store. INITIAL shows the StartPane; CODING and CODE_READY show the PreviewPane and Sidebar. Browser-tab favicon flips to "coding" while in CODING (via useBrowserTabIndicator).
The project history is a commit graph — each generation is a Commit (type: "ai_create" | "ai_edit" | "code_create") with a parentHash and N variants. head is the active commit hash. nanoid provides commit hashes and asset IDs.
8.10 Testing strategy
Backend (pytest, backend/tests/) — 18 test files covering:
| Test file | Covers |
|---|---|
test_agent_tool_runtime.py |
create_file, edit_file, image-gen, retrieve_option execution + error paths. |
test_agent_tools.py |
Tool definition shapes. |
test_batching.py |
process_tasks Replicate batching (BATCH_SIZE=20). |
test_codegen_utils.py |
extract_html_content regex on doctype, <file>-wrapped, partial tags. |
test_evals_openai_input_compare.py |
/openai-input-compare route. |
test_image_generation_replicate.py |
Replicate polling, error cases. |
test_model_selection.py |
ModelSelectionStage branches per key combo. |
test_openai_input_compare.py |
Comparison logic. |
test_openai_provider_session.py |
OpenAI provider streaming + tool result feedback. |
test_openai_reasoning_parser.py |
Reasoning-effort header parsing. |
test_openai_turn_input_logging.py |
HTML report generation. |
test_parameter_extraction_stage.py |
All validation paths. |
test_prompt_summary.py |
format_prompt_summary / print_prompt_summary boxes. |
test_prompts.py |
build_prompt_messages for all combinations. |
test_request_parsing.py |
parse_prompt_content / parse_prompt_history. |
test_screenshot.py |
URL normalization + capture flow. |
test_status_broadcast.py |
StatusBroadcastMiddleware. |
test_token_usage.py |
TokenUsage.cost/accumulate. |
Run: cd backend && poetry run pytest.
Frontend (Jest) — frontend/src/lib/prompt-history.test.ts, frontend/src/components/history/utils.test.ts. Plus a Puppeteer e2e at frontend/src/tests/qa.test.ts (gated by RUN_E2E=true).
Run: cd frontend && yarn test (Jest only) or yarn test:qa (e2e against http://localhost:5173/).
8.11 Code patterns and conventions
- Type strictness: Pyright in
basicmode for backend. TypeScript strict for frontend. - No async generators in pipeline middlewares — they all conform to
(ctx, next_func) -> None. - Tool calls feed back into the same session via
session.append_tool_results(turn, executed_tool_calls). Each provider's session implementation knows how to thread the tool results into the next turn's prompt. - HTML extraction is deliberately tolerant: regex tries
<file path="...">...</file>first (the explicit format), then doctype + html, then bare<html>...</html>, finally returns the raw text. - Print-debug logging is the convention; structured logging is not used. Wrap with your favorite logging shim if running at scale.
- WebSocket safety: every send is guarded by
is_closedto avoid noisyConnectionClosedOKexceptions when the client disconnects mid-stream.
8.12 Potential improvements (technical debt)
| Item | Where | Notes |
|---|---|---|
Replace print(...) with structured logging |
Throughout backend | High operational value, low risk. |
Add frontend/.env.example to git |
frontend/ |
Currently only the .env.jest.example is committed. |
| Authentication layer | backend/main.py |
No built-in auth. Add an API-key header check or OAuth proxy if exposing. |
| CORS hardening | backend/main.py |
allow_origins=["*"] should be configurable. |
| SSRF protection on screenshot route | backend/routes/screenshot.py |
Add allow-list / block private IPs / metadata services. |
| Eval-route auth + path sanitization | backend/routes/evals.py |
Folder query params accept arbitrary FS paths. Fine for trusted networks, dangerous otherwise. |
Streaming chunk event is reserved but unused |
MessageType Literal |
Either remove it or wire up incremental token streaming. |
| OpenAPI / docs disabled at startup | backend/main.py |
Re-enable behind an admin route or auth-gate when needed for debugging. |
moviepy version pinned at ^1.0.3 |
pyproject.toml |
Modern moviepy 2.x has different API; community issue #522 reports build failures on Python 3.12+. Decide between freezing 3.10/3.11 or upgrading moviepy. |
8.13 Contributing guide
Pre-commit hooks (backend/.pre-commit-config.yaml):
- check-yaml
- check-added-large-files
To add a new model:
- Append to
Llmenum inbackend/llm.py. - Add to
MODEL_PROVIDERdict so it routes correctly. - If OpenAI: add to
OPENAI_MODEL_CONFIG(api_name + reasoning_effort). - Add pricing in
backend/agent/providers/pricing.py. - Mirror to
frontend/src/lib/models.ts::CodeGenerationModelandCODE_GENERATION_MODEL_DESCRIPTIONS. - Update
model_choice_sets.pyif the model should be a default variant. - Run
poetry run pytestandyarn test.
To add a new stack:
- Extend
Stackliteral inbackend/prompts/prompt_types.py. - Mirror to
frontend/src/lib/stacks.ts::StackandSTACK_DESCRIPTIONS. - Add the include directives to
prompts/system_prompt.py::SYSTEM_PROMPT. - Update tests.
9. Integration Points
9.1 LLM providers
| Provider | API base | SDK | Auth header |
|---|---|---|---|
| OpenAI | https://api.openai.com/v1 (or OPENAI_BASE_URL) |
openai==2.16.0 |
Authorization: Bearer <key> |
| Anthropic | https://api.anthropic.com |
anthropic^0.84.0 |
x-api-key: <key> |
| Google Gemini | generativelanguage.googleapis.com |
google-genai^1.16.1 |
API key in URL or via SDK |
9.2 Image generation
| Service | Endpoint | Used for |
|---|---|---|
| OpenAI Images | https://api.openai.com/v1/images/generations |
DALL-E 3, 1024x1024, quality=standard, style=natural. |
| Replicate | https://api.replicate.com/v1/models/{model_path}/predictions and /predictions for versioned |
Flux 2 Klein for primary image gen. Background removal via versioned endpoint with hash a029dff3.... |
9.3 Screenshot capture
| Service | Endpoint | Notes |
|---|---|---|
| ScreenshotOne | https://api.screenshotone.com/take |
Hardcoded params: full_page=true, block_ads=true, block_cookie_banners=true, block_trackers=true, cache=false. Mobile viewport 342x684, desktop 1280x832. 60s httpx.AsyncClient timeout. |
9.4 REST API
The backend exposes 11 routes (10 HTTP + 1 WebSocket). Authentication: none.
| Method | Path | Handler | Purpose |
|---|---|---|---|
GET |
/ |
home.py:get_status |
Health banner HTML. |
WS |
/generate-code |
generate_code.py:stream_code |
Main code-gen pipeline. |
POST |
/api/screenshot |
screenshot.py:app_screenshot |
URL → PNG via ScreenshotOne. |
GET |
/eval_input_files |
evals.py:get_eval_input_files |
List evals/inputs/*.png. |
GET |
/evals?folder=PATH |
evals.py:get_evals |
Pair input PNG + output HTML for a folder. |
GET |
/pairwise-evals?folder1=...&folder2=... |
evals.py:get_pairwise_evals |
Side-by-side compare two output folders. |
GET |
/best-of-n-evals?folder1=...&folder2=...&... |
evals.py:get_best_of_n_evals |
N-way compare. |
POST |
/openai-input-compare |
evals.py:compare_openai_inputs_for_evals |
Diff two OpenAI input JSON payloads. |
POST |
/run_evals |
evals.py:run_evals |
Run image-mode evals for given models + stack. |
POST |
/run_evals_stream |
evals.py:run_evals_stream |
Same as above, NDJSON streaming progress. |
GET |
/models |
evals.py:get_models |
List of all Llm values + Stack values. |
GET |
/output_folders |
evals.py:get_output_folders |
List evals/results/* directories. |
9.5 Frontend routes
// main.tsx
<Route path="/" element={<App />} />
<Route path="/evals" element={<AllEvalsPage />} />
<Route path="/evals/single" element={<EvalsPage />} />
<Route path="/evals/pairwise" element={<PairwiseEvalsPage />} />
<Route path="/evals/best-of-n" element={<BestOfNEvalsPage />} />
<Route path="/evals/run" element={<RunEvalsPage />} />
<Route path="/evals/openai-input-compare" element={<OpenAIInputComparePage />} />
The /evals/* routes are intended for offline model quality work; they share the no-auth posture of the backend.
9.6 Observability
langfuse is a declared dependency (^3.0.2) used to wrap agent turn inputs for trace export. To enable, set the standard Langfuse env vars (LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_HOST) — the SDK auto-instruments without code changes.
The OpenAITurnInputLogger (in fs_logging/openai_turn_inputs.py) writes per-turn HTML reports under LOGS_PATH (or a default subdir) when OPENAI_TURN_INPUT_CONSOLE is set. Useful for debugging tool-call argument parsing.