How to build the extension from source and, for anyone who wants a LiteLLM proxy to test against (contributor or not), how to run the repository's local stack: a real LiteLLM proxy in Docker backed by a fake, fully scriptable OpenAI server.
git clone https://github.com/Vivswan/litellm-vscode-chat
cd litellm-vscode-chat
bun install
bun run compilePress F5 to launch the Extension Development Host, or bun run dev to launch it preconfigured against the local stack below. The dev host's state lives in a disposable .dev-profile/ directory at the repository root: deleting the directory, or the seeded server's provider group inside it, is recovered on the next bun run dev, which resets and re-seeds the profile (at the cost of any sign-ins stored there). To avoid reset loops the launcher performs one automatic reset per observed loss; if it reports the group missing again, delete .dev-profile/ by hand. In the dev container (or any other headless Linux machine), run the test suite under a virtual display: xvfb-run -a bun run test.
bun run dev seeds more than the main "Fake LiteLLM" server, so every dashboard surface opens populated:
-
Three demo usage keys with real spend: at stack start the launcher creates the virtual keys
sk-dev-usage-healthy,sk-dev-usage-warning, andsk-dev-usage-over(dev-only; the test fixture keysk-usage-seed-1234is untouched), fires a few deterministic completions through each, and pins their budgets so the Servers page's spend units show roughly 30%, 85%, and 110% spent - plain, warning, and error states, and the same colors on the usage status bar item.- The warning entry gets there via an entry-level
budgetset below the key's own cap (the override case from usage.md); the over key sits past its budget on purpose, so chat requests through that server fail with LiteLLM's budget error - the failure is part of the demo. - Reruns add spend and re-pin the budgets to the same fractions;
bun scripts/dev/seed-demo-usage.tsre-seeds against a running stack without relaunching (the warning entry'sbudgetis re-pinned only by a fullbun run dev), andDEV_NO_USAGE_SEED=1 bun run devskips the seeding for a faster launch.
- The warning entry gets there via an entry-level
-
Demo model records: the seed writes
models.parametersandmodels.capabilitiesrecords (models.md is the grammar) exercising a"*"catch-all with_inheritabledefaults, agpt-5*glob with a_forced field and an_inherit_from: falsebarrier, an exactgpt-5.2-minirecord, a/deepseek.*/iregex record, a_fallbackcontext length (the resolved value onllama-4-scout, which declares none), an_openrouter_modelderivation ondeepseek-r2, and an entry-level record on the main entry that beats the global one - all visible in the dashboard's Resolved models view and the per-model Params/Caps inspectors.- The seed owns exactly these matcher keys and the seeded entry labels: it re-pins them on every run and never touches records or entries you add under other keys.
-
Three error-state servers, so failure presentation is visible without breaking anything:
- "Dev Error (unreachable)" points at a dead local port (
http://localhost:4- IANA-unassigned, outside every OS ephemeral range, and privileged on Unix-likes, so nothing plausibly listens there and the refusal is instant), which fails discovery with a connection error on its row; - "Dev Error (bad key)" points at the real proxy with a key it never issued, which fails with the 401 auth error;
- "Dev Error (misconfigured)" is a healthy server whose entry records carry an invalid regex matcher key (
/[unclosed/) and a misspelled capability field (supports_visoin), which populate the Diagnostics page's Configuration section with a warning and an advisory typo hint.
The two failing rows are intentional, not a broken stack.
DEV_NO_ERROR_SEED=1 bun run devskips them for a clean Servers page; the launcher resets the profile when the flag changes, so the other mode's entries never linger (at the usual cost of the profile's sign-ins). - "Dev Error (unreachable)" points at a dead local port (
To reset the demo state, delete .dev-profile/ (drops the seeded entries and records with the rest of the profile); bun run docker:down additionally zeroes the accrued spend, since the stack's database lives on tmpfs.
| Command | Description |
|---|---|
bun run compile |
Build |
bun run watch |
Watch mode |
bun run lint |
Lint |
bun run format |
Format |
bun run test |
Run tests |
CONTRIBUTING.md covers environment setup, the full check list, and how to submit a pull request.
For local testing you can run a real LiteLLM proxy in Docker, backed by a fake OpenAI server:
cp .env.example .env # optional; only needed for real provider keys or port changes
bun run docker:upThen add a server in the extension with base URL http://localhost:4000 and API key sk-test-1234.
The proxy is backed by a Postgres container (on tmpfs - every recreate starts from an empty database), which turns on LiteLLM's spend and budget endpoints. Stack startup seeds one budgeted virtual key for exercising the usage feature: sk-usage-seed-1234 with a $25 max_budget (identity in src/test/fakeStack/usage.ts). Add a second server with that key to watch spend accrue against a budget.
The fake serves six realistic models and takes its instructions from the chat input itself: a % command on the last line of your message picks the response shape, so one model can play every stream shape the extension handles. (The sigil is % because the obvious choices are both intercepted before they reach the model: Copilot Chat claims /-prefixed input for its own slash commands, and agent CLIs like Claude Code run a leading ! as a shell command, while no chat input surface claims %.)
The model list is deliberately small and shaped like a real deployment (src/test/fakeStack/models.ts):
| Model | What it plays |
|---|---|
claude-opus-4-5 |
Everything on: reasoning, caching, tiered pricing, 1M context |
gpt-5.2 |
A load-balanced pair |
gpt-5.2-mini |
The everyday target |
gpt-5.2-omni |
Audio flags |
deepseek-r2 |
Reasoning without tools |
llama-4-scout |
No limits or pricing declared, tools explicitly off |
codestral-fim |
Declared a text-completion model (mode: completion); the inline-completions (FIM) target on /v1/completions |
gpt-4-turbo |
Blocked in the config; must never appear in the picker - that absence is itself under test |
Pick any of them in the Copilot model picker and type a command as your message. %help lists everything; the ones you will reach for first:
%help list all commands and playback scenarios
%play:thinking-blocks play a canned stream shape (the library lives in src/test/scenarios.ts)
%echo:any text reply with exactly that text
%echon:one\ntwo multi-line echo: \n decodes to a newline, \\ keeps a backslash
%text:200 a deterministic 200-word paragraph
%think:5 reasoning chunks, then a closing text
%tool:get_weather {} call an offered tool, then summarize its result on the next turn
%image, %audio byte-stable generated media carrying their own sha256
%params, %messages, %attachments, %tools inspect what actually reached the backend
%cache, %deployment cache_control marker positions; which upstream served the request
%error:429, %finish:length, %stream:50:100, %delay:2000 error, truncation, pacing shapes
%abort:3, %nodone:5, %stall:3:30000 transport failures: dropped socket, missing [DONE], silent stall
A message without a command gets a fixed reply pointing at %help. Everything is deterministic: the same conversation produces the same bytes.
The proxy config is generated at stack startup (docker/.generated/litellm-config.yaml, gitignored) from src/test/fakeStack/models.ts. On top of the fixed catalog:
- A real provider key set in
.envor the environment makes the generated config also routeopenai/*oranthropic/*model names through the proxy to that provider - the intended way to eyeball real-provider behavior through the same stack. It also turns on LiteLLM'scheck_provider_endpoint, which expands the wildcard into the provider's live catalog on/v1/modelsfor direct API consumers of the proxy; the extension's picker reads/v1/model/info, where a wildcard route appears as its literal entry (openai/*). - Without a key the wildcard route is not emitted at all, so there are no phantom catalog models and no misleading 401s.
- GitHub Copilot works differently (its API takes a device-flow login, not an API key): run
bun run copilot-loginonce, and every stack start fetches your live Copilot catalog and emits agithub_copilot/<model>route per model.- The login needs a GitHub account with a Copilot seat - a plain PAT cannot reach the Copilot token exchange - and it authenticates with the well-known first-party Copilot editor client id; GitHub's Copilot terms can treat use outside a supported client as unauthorized, so running it is your own acceptance of that risk.
- It writes a long-lived OAuth token in plaintext to
docker/.copilot-token/access-token(owner-only permissions, gitignored). To sign out, delete that file but keep the directory (it is a compose mountpoint); re-run the login if GitHub revokes the token.
LITELLM_WILDCARD_ALL=1adds a bare*passthrough for anything else LiteLLM can infer.- The docker test suite always generates without these routes, so local keys never change test results.
bun run test:docker # run the docker test suites against the stack (starts and stops it)
bun run docker:logs # follow container logs
bun run docker:down # stop the stack and remove volumes
bun run generate-config # print the generated LiteLLM config to stdout (never writes; startup writes the real file)
bun run copilot-login # one-time GitHub device flow; stack starts then emit github_copilot/<model> routes- The stack also works with Podman: the scripts try
docker composefirst, thenpodman compose, andCOMPOSE_CMDoverrides the choice. The compose provider must supportup --wait; Podman with the docker-compose provider does, while olderpodman-composereleases may not. - On SELinux hosts, change the bind mounts in
docker/docker-compose.ymlfrom:roto:ro,z. - Always start the stack through
bun run docker:up(ordev/test:docker): those paths generatedocker/.generated/litellm-config.yamlfirst. Invokingdocker compose updirectly is unsupported - without the generation step the read-only directory mount materializes empty and the litellm container exits on a missing config.
The host-fidelity suite runs against a built-in capture server as part of bun run test; to point it at the stack (or any live server) instead, opt in with LITELLM_REAL_LIVE=1 and set its connection variables:
bun run compile && bun run bundle:dev && \
LITELLM_REAL_LIVE=1 LITELLM_REAL_BASE_URL=http://localhost:4000 LITELLM_REAL_API_KEY=sk-test-1234 LITELLM_REAL_MODEL=gpt-5.2-mini \
bunx vscode-test --config .vscode-test.mjs --label host-fidelityOn Windows PowerShell:
bun run compile; bun run bundle:dev
$env:LITELLM_REAL_LIVE = "1"; $env:LITELLM_REAL_BASE_URL = "http://localhost:4000"
$env:LITELLM_REAL_API_KEY = "sk-test-1234"; $env:LITELLM_REAL_MODEL = "gpt-5.2-mini"
bunx vscode-test --config .vscode-test.mjs --label host-fidelityWithout LITELLM_REAL_LIVE=1 the other LITELLM_REAL_* variables are ignored, so exporting them in your shell never turns a regular test run live.