This is a local proxy that exposes the GitHub Copilot API as both an OpenAI-compatible and Anthropic-compatible HTTP service. It uses GitHub Copilot the same way Opencode's built-in Copilot provider does: authenticate with the user's own Copilot license, route requests to the Copilot endpoint, translate the response shape. The entry point is src/main.ts (CLI via citty), which dispatches to subcommands: start, auth, check-usage, debug.
src/routes/messages/handler.ts is the core dispatch logic:
- Rate limit check
- Parse Anthropic payload
- Detect subagent marker (
__SUBAGENT_MARKER__in<system-reminder>) → setsx-initiator: agent - Detect compact requests (Claude Code context compaction)
- Force
smallModelfor tool-less warmup/probe requests (defaultgpt-5-mini; warmup only — distinct from the Claude Code haiku tier, which carries subagent tool calls and must stay tool-competent: seesrc/lib/models/small-model.tsresolveSmallToolModel) - Merge mixed
tool_result+ text blocks to avoid fresh premium request - Normalize model ID → look up Copilot model
- Route to one of three upstream flows:
handleWithMessagesApi— Copilot native/v1/messages(Claude models, preferred)handleWithResponsesApi— Copilot/responses(GPT models)handleWithChatCompletions— fallback for everything else
| Path | Purpose |
|---|---|
src/server.ts |
Hono app, middleware stack, route registration |
src/lib/ |
Shared utilities: config, state, auth, tokens, rate-limit, models, tokenizer, trace |
src/routes/ |
Route handlers grouped by endpoint family |
src/services/ |
Upstream API clients (Copilot, GitHub, providers) |
tests/ |
All test files (*.test.ts), Bun built-in runner |
shell/ |
Tauri menu-bar app (Vite frontend + src-tauri/ Rust shell) wrapping the proxy as a sidecar |
traceIdMiddleware → logger() → cors() → createAuthMiddleware (API key validation via x-api-key or Authorization: Bearer; unauthenticated paths: /, /ui/*)
src/lib/models/models.ts normalizes Claude model IDs via 5 regex patterns (handles variants like claude-opus-4-6, claude-opus-4.6). The useMessagesApi config flag (default true) controls whether Claude-family models use the native Messages API or fall back to Chat Completions.
src/lib/config/config.ts—AppConfigshape, disk read/write from~/.local/share/maximal/config.json(Linux/macOS) or%USERPROFILE%\.local\share\maximal\config.json(Windows). Also respectsCOPILOT_API_HOMEenv var.src/lib/config/config-schema.ts— zod runtime validation. Bad config → exit non-zero with key path. Unknown keys → warning, kept via.loose().src/lib/runtime-state/state.ts— singleton mutable state: tokens, accountType, rate-limit, models cache.src/lib/auth/github-token-store.ts— the GitHub identity store. Multi-account registry (schema v2) ataccounts.jsonbeside the legacygithub_token:{ activeKey, accounts: Record<"login@host", AccountRecord> }, atomic temp+rename writes. Boot reads the active account; the legacy single-record file is migrated in once (gated, offline→unknown@host) and kept as a rollback fallback. The three sign-in producers (device-code, CLI, gh-reuse) all persist a typedAccountRecord; switch/remove + the/settings/api/accountsroutes drive quick-switch (set active → reboot the sidecar into it). Sign-out forgets the active account; Remove forgets a specific one; both touch only maximal's own copy — nevergh. RMW takes no lock (safe on the single Bun sidecar; see the comment aboveaddAccountToDefaultRegistry).src/lib/auth/secrets.ts— file-based provider keys at~/.local/share/maximal/secrets/<name>(mode 0600). Env wins; file fills in unset values.src/lib/runtime-state/cache.ts—Cache<K,V>LRU wrapper with hit/miss/eviction metrics. Wrapped instances register globally for/_debug/state.
maximal debug(and--json) — effective config, executor selection (whichExecutorselectExecutor()would pick), secret sources (env/file/config/unset, never values), paths.GET /_debug/state— live equivalent on a running proxy. 404 by default; gated onstate.verbose. Useful when restart isn't an option.- Daily log at
~/.local/share/maximal/logs/messages-handler-<date>.log— request payloads, translated SSE events, web-tools agent traces. 7-day retention.
This repo can collide on a shared working tree (lint-staged stash + concurrent merge ate a turn already). For parallel agents:
- Spawned subagents: pass
isolation: "worktree"to the Agent tool. - Sessions: create a worktree manually with
git worktree add ../maximal-<task> -b agent/<task>; clean up withgit worktree remove ../maximal-<task>after merging back.git worktree adddoes not runbun install, so the gitignoredsrc/generated/ui-embed.tsstub won't exist — the gates now self-heal it (check:fastand the test preload callensure:ui-embed), but runbun installin the new tree anyway if you need its node_modules. - Never run
git stash popin a shared working tree. It silently merges another in-flight worker's stash into your tree, and on conflict it leaves an inconsistent state that's easy to "clean up" byrm-ing files that aren't yours. We lost a session's worth of React-shell work to this exact path: a subagent rangit stash popto bisect a test failure, hit a conflict, andrm'd untracked files it didn't recognize. If you need an isolated bisect, use a worktree (see above). If you must inspect a stash, usegit stash show -p stash@{N}(read-only) and neverpop/applyoutside an isolated tree.
See also: docs/codegen-feedback-loops-practices.md → Dispatch and review loops.
mock.modulepersists forward across files in a run — now lint-enforced. Bun does not reset module mocks between test files, and CI orders files differently than local, so an unrestored mock leaks its stub into a sibling file that then reads stale state — the classic "green locally, red on CI" (or vice-versa) failure. This bit us four times (the last cost a long #229 debugging loop), so an ESLint rule (mockModuleLeakGuardineslint.config.js, scoped totests/**) now bans the fire-and-forget forms:void mock.module(...)and a baremock.module(...)expression statement both error.- Awaited is not automatically safe.
await mock.module(...)passes the lint rule, but an awaitedafterAllrestore does not reliably land before the next file's static imports on CI — so a module mock that intercepts a shared module still leaks across files even when restored. The rule catches the common footgun, not this one. - The durable fix is to not mock a shared module across files at all.
Prefer the real module (the test preload redirects
COPILOT_API_HOMEto a temp dir, andgetClaudeCodeSettingsPath()honorsCLAUDE_CONFIG_DIR— so config/settings round-trips are already isolated), or injectable function options. Only stub a module with no env/injection seam, keep the wrapper behaviorally identical (spread...actual, forward...rest), and prove via a sequential-import repro that it can't break a later file.
- Awaited is not automatically safe.
- Green tests can still test nothing. Mutation testing (
bun run mutate) caught classification tests that passed without exercising the branch they claimed to cover (the fixture hit a different code path that returned the same value). For security-critical or branchy logic, run Stryker and confirm the targeted mutants actually die — don't trust a passing assertion alone. Every surviving mutant must land in exactly one of three buckets — killable (write the test), dead (delete it / encode the impossibility in types), or proven-equivalent (written proof + reason to keep). Seedev/testing-strategy.md§6 for the disposition rule and the hot-path modules that warrant periodic sweeps.
- Release is driven by Conventional Commit types. release-please
scans commits since the last tag; only
feat:(minor) andfix:(patch) cut a release.test:/chore:/ci:/docs:/refactor:are release-silent. If release-please "isn't doing anything," it almost certainly found nofeat/fixcommit — check therelease-prstep log forNo user facing commits found ... skippingbefore assuming it's broken. - Squash-merge uses the PR title as the commit subject. So the PR
title must be a single valid Conventional Commit (
fix: …, nottest+fix: …). A non-standard type liketest+fixparses as one unrecognized token and release-please skips it — even if the diff contains a realfix:. Title PRs accordingly; the body's individual commit messages don't reachmainthrough a squash.
shell/ is a Tauri 2 menu-bar app that wraps the proxy for non-CLI users. bun run app:sidecar builds the UI (bun run build:ui), regenerates the embed manifest, and compiles the standalone proxy binary into shell/src-tauri/binaries/. Tauri launches it as a sidecar bound to 127.0.0.1:4141. The settings (React, Bun-bundled) and dashboard (vanilla) UIs live in shell/ui/{settings,dashboard} and are embedded in the sidecar binary, served by the proxy at /ui/settings and /ui/dashboard (src/routes/ui/route.ts) — from shell/dist on disk in dev, from $bunfs in the compiled binary. The webview windows point at those /ui/* URLs; legacy /settings and /usage-viewer 301-redirect to them. No Vite — Bun is the bundler.
/v1/messages/count_tokens: when anthropicApiKey is configured, forwards Claude model requests to Anthropic's free /v1/messages/count_tokens endpoint for exact counts. Otherwise falls back to GPT o200k_base tokenizer with 1.15x multiplier (src/lib/models/tokenizer.ts).