Solve LeetCode problems hands-off with a local LLM driving a real Chrome.
A small web console: paste a problem URL (or hit Auto) and it opens the problem, writes a solution, runs the sample tests, repairs itself if they fail, submits, reads the verdict, and saves a Markdown record -- with no input from you.
navigate -> pick language -> read problem -> [LLM writes code] -> type into editor
-> Run samples -> [LLM repairs on failure] -> Submit -> read verdict -> save
Everything runs on your machine. No API keys, no cloud, no Node, no browser extension.
The whole stack: the console on the left, LM Studio serving the model at the bottom, and the Chrome window the solver is driving on the right -- mid-submission, samples already passed.
- Why this works with small local models
- Requirements
- Setup
- Using the console
- Thinking & context length
- How it works
- Configuration
- API
- Troubleshooting
- Notes & limits
- Project layout
- Architecture notes -- how the model and Chrome layers actually work, how this differs from an MCP browser agent, and what it takes to point it at other sites
- Project brief -- measured facts, design decisions and honest limitations, compiled for resume/portfolio review
Most "AI solves LeetCode" attempts hand the whole job to an agent: pick tools, emit tool-call JSON, hold a plan across dozens of rounds. Local models fall over doing that -- they malform tool calls or lose the plan and loop.
Here, code owns the control flow. The model is a worker with exactly two jobs:
| Slot | Input | Output |
|---|---|---|
| Solve | problem text + the editor's exact stub | a solution |
| Repair | the failing testcase (input / expected / actual) | corrected solution |
It is never asked to choose a tool or emit tool-call JSON -- only to return Python. That deletes both classic failure modes, which makes the whole thing model-agnostic: anything that can write a correct function works.
| Python 3.10+ | Runs the harness itself |
| Google Chrome (or Chromium) | Driven over the DevTools Protocol -- it must be a real Chrome you can log into |
| LM Studio | Serves the model. Any OpenAI-compatible server works (Ollama, vLLM, llama.cpp), but the context-length control is LM Studio-only |
| A LeetCode account | You log in yourself, once, in the browser window the solver drives |
Any instruction-following model that writes decent Python. That's the whole requirement -- the harness sends plain text and asks for plain code back.
Things you might expect to need but don't:
- Tool use / function calling -- never used. Code drives the browser; the model only writes code. That's the core design decision, so a model with no tool training is fine.
- Vision -- never used. The problem description is scraped as text and sent as text. A problem with a diagram in it just loses the diagram, for a vision model and a text model alike.
- Reasoning / thinking -- optional, and often counterproductive here. See Thinking & context length. A 7B coding model with thinking off will usually beat a big thinker that spends its whole token budget deliberating.
What actually matters, in order: Python quality -> speed -> context length. A coding-tuned model in the 7B-30B range is the sweet spot; larger helps on Hard problems and costs you wall-clock time.
Whatever your model needs, plus headroom for the context you allocate. A 27B Q4 model at 8K context wants roughly 16 GB of VRAM/unified memory; the same model at 64K wants noticeably more, because KV cache scales with context. If loads start failing, lower the context length first.
git clone <your-repo-url> leetcode-solver
cd leetcode-solver
python -m venv .venvActivate it -- Windows .venv\Scripts\activate, macOS/Linux source .venv/bin/activate -- then:
pip install -r requirements.txtIn LM Studio: load a model, open the Developer tab, and Start Server (default port
1234). The solver talks to http://127.0.0.1:1234/v1 unless you tell it otherwise.
Verify it from a shell:
curl http://127.0.0.1:1234/v1/modelsThe solver needs a Chrome started with --remote-debugging-port. It uses a dedicated profile
(chrome-debug-profile/, git-ignored) so your everyday browser and its cookies are untouched.
- Windows:
scripts\start-chrome-debug.bat - macOS/Linux:
./scripts/start-chrome-debug.sh - ...or just click Start debug Chrome in the console once it's running.
Log into LeetCode in that window. The solver never types credentials -- it drives the session you established. You only do this once; the profile persists.
python -m leetcode_solver...or run.bat / ./run.sh. Then open http://127.0.0.1:8765/.
Port already in use? Set
LEETCODE_PORT=...in.envor the environment and restart.
Pick a model from the dropdown (it lists whatever your server advertises), then either:
- Solve queue -- paste one or more
https://leetcode.com/problems/<slug>/URLs, one per line. They're solved in order. - Auto-solve down the list -- walks the entire problem list in ID order, using LeetCode's own GraphQL through your logged-in session. Start anywhere with from #N, tick skip solved, and Stop ends it after the current problem. Premium-locked and non-Python problems are skipped automatically.
Two toggles worth knowing:
- Max attempts -- how many solve/repair rounds before giving up on a problem (1-6).
- Iterative repair -- off by default. On, every retry sees the full history of failed attempts and is told not to repeat an approach that already failed. Costs context; helps on Hard problems.
The log pane shows every step with timings, the verdict, runtime/memory/beats%, and where the record was saved:
Every line is a real state transition, not a status guess -- run-result is the sample run,
verdict is the actual submission result read back off the page.
The Thinking & context button exposes two knobs that look similar and work completely differently.
Sent as the standard reasoning_effort field on every completion. LM Studio accepts
none / minimal / low / medium / high / xhigh and maps those onto whatever the loaded model
actually supports -- a plain on/off thinker reads none as off, while a graded one like gpt-oss uses
low/medium/high directly. The panel shows each model's real capability underneath.
Leave it on server default to send nothing at all. A server that doesn't understand the field gets one automatic retry without it, so this stays safe on any backend.
WARNING: Not every model obeys it. Some ignore the API-level setting and think anyway -- the giveaway is a slow first attempt or code that arrives truncated. When that happens, turn thinking off in LM Studio itself (the model's settings -> Reasoning, or the chat sidebar's thinking toggle) and reload the model. That setting always wins over the request field.
Thinking tokens come out of the same budget as the answer. If you raise the effort, raise Max tokens too, or a long deliberation will cut the solution off mid-function. The harness detects that specific failure and tells you, rather than feeding you a truncated function.
For this harness, thinking off is a reasonable default: the model gets a complete problem statement and an exact stub, which is the easy case, and repair rounds already provide the "think again" loop.
Context is allocated when a model enters memory, so there is no per-request setting. Changing it
means reloading the model, which is exactly what Apply & reload does (via LM Studio's native
/api/v1/models/load). The status line shows loaded at 8192 - max 262144 so you know where you
stand.
Other backends don't expose this, and the control disables itself with an explanation -- set context wherever that server is started instead.
How much do you need? A solve prompt is the problem description (capped at 12,000 characters) plus the stub, so 8K is enough for a single-shot solve. Turn it up if you use Iterative repair on long problems, since every prior attempt is replayed into the prompt.
- Navigate to the problem and wait for the Monaco editor to exist.
- Pick a language -- prefer Python3; if unavailable, switch to Pandas; if neither, mark the job
skippedand move on. (Monaco reportspython3for both Python3 and Pandas problems, so the language-button text is what actually distinguishes them.) - Scrape the title, number, description and the exact function stub.
- Solve -- the model returns code; fences and stray prose are stripped.
- Inject the code into the editor.
- Run the sample tests. The verdict is read from
[data-e2e-locator="console-result"]-- scoped, because a solved problem already displays "Accepted" in its status badge and a page-wide scan would false-positive. - Repair on failure using the actual failing case, then retry (bounded by max attempts).
- Submit only after the samples pass; read the verdict + runtime/memory/beats%.
- Save a Markdown record under
solutions/and add it to a runningINDEX.md.
Anything that isn't an explicit Accepted on the samples goes back for repair rather than being submitted -- a submission is never spent on code that hasn't already passed.
Everything is environment variables (or a .env -- see .env.example). All optional:
| Variable | Default | Meaning |
|---|---|---|
LEETCODE_LLM_BASE |
http://127.0.0.1:1234/v1 |
OpenAI-compatible base URL |
LEETCODE_LLM_API_KEY |
(none) | Only if your server needs one |
LEETCODE_LLM_TIMEOUT |
180 |
Seconds; covers a cold model load |
LEETCODE_LLM_TEMPERATURE |
0.2 |
Low = deterministic code |
LEETCODE_LLM_MAX_TOKENS |
2048 |
Per completion |
LEETCODE_LLM_REASONING_EFFORT |
(none) | none...xhigh; blank leaves the server's setting alone |
LEETCODE_CDP_URL |
http://127.0.0.1:9222 |
Chrome DevTools endpoint |
LEETCODE_CHROME_EXE |
(auto-detect) | Chrome path if detection fails |
LEETCODE_CHROME_PROFILE |
./chrome-debug-profile |
Dedicated browser profile |
LEETCODE_SOLUTIONS_DIR |
./solutions |
Where records are written |
LEETCODE_HOST / LEETCODE_PORT |
127.0.0.1 / 8765 |
Console bind address |
The console is just a client for these:
| Method / Path | Purpose |
|---|---|
POST /api/solve |
{url, model?, max_attempts?, iterative?, reasoning_effort?, max_tokens?} -> {job_id, model} |
GET /api/status/{job_id} |
{status, events[], result, error} -- poll ~1.5 s |
GET /api/models |
Models advertised by your LLM server, plus the thinking levels it accepts |
GET /api/llm/model?model= |
One model's context length (loaded vs max) and thinking capability |
POST /api/llm/context |
{model, context_length} -> reloads that model at a new context length |
GET /api/problems?offset&limit |
The LeetCode problem list (id order, with solved/Premium flags) |
GET /api/chrome/status |
Is the debug Chrome reachable? |
POST /api/chrome/launch |
Launch the debug Chrome |
status is running / completed / failed / skipped.
"the code editor never loaded -- is this tab logged into LeetCode?" The debug Chrome isn't logged in, or the tab it attached to isn't a problem page. Open leetcode.com in that window and sign in.
"x not running -- click Start" and Start does nothing
Chrome wasn't found. Set LEETCODE_CHROME_EXE to your Chrome binary. Note that a Chrome already
running under your normal profile can swallow the launch -- close it, or rely on the dedicated
profile that the scripts use.
"cannot reach the model server"
LM Studio's server isn't started (Developer tab -> Start Server), or it's on a different port than
LEETCODE_LLM_BASE.
The model returns prose instead of code It's ignoring the system prompt. Lower the temperature, or use a coding-tuned model. The harness strips fences and leading prose, but it can't rescue an essay.
Solutions arrive truncated / the log says the model spent its budget thinking
Thinking is on and eating max_tokens. Lower the effort, raise Max tokens, or turn thinking off
in LM Studio directly (see the warning above).
"a solve is already running" (HTTP 409) One solve at a time by design -- the browser is a shared resource. Wait, or stop Auto mode.
Selectors suddenly break after a LeetCode redesign
Every selector lives in one file: leetcode_solver/leetcode.py. That's the only place to look.
- Keep it on loopback. The console drives your logged-in browser and spends local compute; there's no authentication by design.
- It really submits. Every accepted solve is a real submission on your account. Auto mode over the whole list is thousands of problems -- that's what from #N and Stop are for.
- One solve at a time -- a second request gets
409. - Language support is Python3 and Pandas. SQL/Bash/etc. are skipped, not failed.
- Changing context length reloads the model, which is blocked while a solve is running.
- LeetCode's DOM can change. See
leetcode_solver/leetcode.py.
leetcode_solver/
|-- config.py env-driven settings (cross-platform)
|-- cdp.py minimal Chrome DevTools client + Chrome launcher
|-- llm.py OpenAI-compatible chat client + LM Studio model/context control
|-- leetcode.py ALL page automation / selectors (change here if LeetCode moves)
|-- solver.py the deterministic state machine + job store
|-- library.py saves solution records + INDEX.md
|-- app.py FastAPI routes
`-- static/ the console (index.html + app.js)
scripts/ start-chrome-debug (.bat / .sh)
solutions/ your saved records + INDEX.md
docs/
|-- ARCHITECTURE.md internals: the model layer, the CDP layer, extending to other sites
`-- images/ README screenshots
For how any of it actually works under the hood -- the two-layer model API, the raw CDP client, the comparison with MCP browser agents, and what a "recipe" for another website would look like -- see docs/ARCHITECTURE.md.




