billion-context is an HTTP proxy that injects ACP (Active Context Pruning) context compression into LLM API streams. Every option below lives in a single JSON config file (or an equivalent environment variable / CLI flag).
| Scope | Path | Notes |
|---|---|---|
| Config file (Linux) | ~/.config/billion-context/billion-context.json |
XDG Base Directory — the canonical, user-editable config |
| Config file (override) | value of XDG_CONFIG_HOME |
relocates the whole config dir |
| Config file (explicit) | value of BILI_CONFIG_FILE |
points directly at any JSON file |
| CLI flag | --config <FILE> |
same as BILI_CONFIG_FILE; highest precedence for the file path |
| Session data | ~/.local/share/billion-context/sessions/ |
persisted compression state, grows over time |
On first run, billion-context seeds an empty template ({ "providers": {} }) at the config path so you have something to edit. It never overwrites an existing file.
The config file is a pure override layer — every field is optional. Anything unset falls through to the built-in default.
Status legend: ACTIVE = currently used | DEPRECATED = accepted but no effect | EXPERIMENTAL = may change
Top-level keys that control how the proxy listens and behaves globally.
- Type:
number - Default:
8787 - Status: ACTIVE
- Description: TCP port the proxy listens on. Must be an integer between 1 and 65535. Overridden by the
ACP_PORT(orPORT) environment variable, or the--portCLI flag. An invalid value aborts startup.
- Type:
string - Default:
127.0.0.1 - Status: ACTIVE
- Description: Network interface the proxy binds to.
127.0.0.1(default) listens only on localhost — safe for a local sidecar. Use::for IPv4 + IPv6 dual-stack. Use0.0.0.0(or a LAN IP) to expose the proxy to other machines — typically inside a container or on a trusted LAN: remote agents then point their modelbaseURLathttp://<this-host>:<port>/bili/…, and MITM-modeCONNECTaccepts remote clients for whitelisted model hosts only (blind tunnels stay loopback-only, and/__bili/management endpoints remain loopback-only). There is no authentication — ensure the surrounding network is trusted. Overridden byACP_HOST/--host.
- Type:
string - Default:
x-acp-session - Status: ACTIVE
- Description: Name of the HTTP request header clients may send to identify a conversation. Requests carrying the same value share compression state across calls. Overridden by
ACP_SESSION_HEADER.
- Type:
boolean - Default:
true - Status: ACTIVE
- Description: Enable per-request logging. Set
false(orACP_LOG=0) to silence the standard request log.
- Type:
boolean - Default:
false - Status: ACTIVE
- Description: Verbose logging — equivalent to setting
ACP_DEBUG=1. Useful for diagnosing routing or compression behaviour.
- Type:
boolean - Default:
false - Status: ACTIVE
- Description: Forward every request to the upstream without compression, tool injection, or nudging. Equivalent to
ACP_PASSTHROUGH=1. Handy for A/B comparison against the uncompressed baseline.
- Type:
{ roles?: Record<string, string> } - Default:
{}(disabled) - Status: ACTIVE
- Description: Global wire-compat role map.
rolesmaps message roles to the role name your upstream accepts, e.g.{"compat":{"roles":{"developer":"system"}}}rewritesdeveloper→systemon the final forwarded body for upstreams that reject thedeveloperrole (#552, newer codex clients). Applies toopenaichat-completions andresponsesrequests; exact-match roles only, everything else in the body is untouched; re-sent compress-retry bodies carry the same rewrite. Per-providercompat.rolesentries (see Providers) win per key. Default{}forwards bodies byte-for-byte unchanged. - Learn-on-failure: with no compat configured, an upstream
400 Invalid role: …is auto-fixed — bili rewrites the offending role tosystem, retries once, and remembers the mapping session-scoped (in-memory on the session; never written to config). Later requests in that session skip the 400 round-trip. The info log emitted when the fix fires carries the permanent per-provider snippet.
- Type:
string - Default: (none — no upstream proxy)
- Status: ACTIVE
- Description: Upstream HTTP proxy (
http://host:port) used for the proxy's own outbound connections to model providers. SOCKS5 is not supported. A per-URLproxyset inside aprovidersentry overrides this for that provider. An empty string means "explicitly direct" — it disables any environment/system proxy fallback for all providers.
The providers block maps upstream URLs to per-provider configuration. Each key is a URL prefix; each value can declare model context windows, a per-provider proxy, a compression protocol, compression overrides, and a per-route passthrough.
{
"providers": {
"https://api.anthropic.com": {
"models": {
"claude-sonnet-4-5": { "context": 200000, "output": 8192 }
},
"proxy": "http://10.0.0.1:7890",
"compressProtocol": "tools",
"compress": { "maxContextLimit": "70%" }
}
}
}Keys are matched against the request's upstream URL by longest-prefix wins. A key matches if the request URL equals the key, or starts with key + "/". This makes matching boundary-safe: a key https://api.example.com matches https://api.example.com/v1/chat but does not match https://api.example.com.evil (an attacker-controlled lookalike host).
A shallow key (https://open.bigmodel.cn) matches every path on that host. A deep key (https://open.bigmodel.cn/api/anthropic) matches only that endpoint. When two keys both match, the longest (most specific) one wins. Trailing slashes on keys are stripped automatically.
-
Type:
Record<string, { context?: number; output?: number; compress?: CompressSettings }> -
Default: (none)
-
Status: ACTIVE
-
Description: Maps a model name to its context-window declaration. The LLM
/modelsendpoint does not return context windows (verified across OpenAI, Anthropic, zhipu, comfly), so the proxy cannot discover them at runtime.contextis the model's context window in tokens;outputis the max output size.Resolution order (first match wins): (1) per-request sources — the client's
anthropic-betalarger-context negotiation, a cooperative plugin's report, and the launcher's per-model windows; (2) this per-modelcontextdeclaration; (3) the warm models.dev registry cache, when the model is listed (relay/private hosts match the bare model name against the registry's provider-prefixed entries); (4) the built-in context table. So this per-modelcontextdeclaration outranks the registry — set it to the window your relay/private deployment actually serves, and it wins even when models.dev lists a different (usually larger) window for the model.compress.modelContextLimitremains the highest-priority source (always wins) when you want to pin the window across every route. Each model entry may also carry a per-modelcompressblock (see Compression Tuning).
- Type:
string - Default: (inherits top-level
proxy) - Status: ACTIVE
- Description: Per-provider upstream HTTP proxy (
http://host:port). Overrides the top-levelproxyfor this provider only. An empty string means "explicitly direct" — override the global proxy with no proxy for this one provider.
- Type:
"tools" | "marker" - Default:
"tools" - Status: ACTIVE
- Description: How compression tools are injected into the request.
"tools"(default) injects them as native function-call tools."marker"uses a text-trigger protocol instead — use this for upstreams that cannot coexist with a declaredtoolsfield.
- Type:
CompressSettings - Default: (inherits global
compress) - Status: ACTIVE
- Description: Per-provider compression overrides. This is level 2 of 3 in the merge hierarchy — see Compression Tuning.
- Type:
{ roles?: Record<string, string> } - Default:
{}(disabled) - Status: ACTIVE
- Description: Per-provider wire-compat overrides.
rolesmaps message roles to the role name this upstream accepts, e.g.{"developer": "system"}for upstreams that reject thedeveloperrole newer codex clients send (#552). Applied to the final forwardedopenai/responsesbody — client-sent roles and bili's own injected prompt alike — and to every body the compress-retry loops re-send. Wins per key over the globalcompatblock (see Server Settings). Default{}forwards byte-for-byte unchanged.
-
Type:
boolean -
Default: (none — compression active)
-
Status: ACTIVE
-
Description: Per-route override of the global
passthroughsetting. Whentrue, every request matching this route is forwarded byte-for-byte: no kernel round-trip (no message re-serialization, no ACP render tags, noprompt_cache_keyremoval), the response is piped through untouched, and no session state is created for that route. Use this for upstreams whose anti-fraud fingerprinting rejects bili's rewritten bodies — e.g. ZCode's405 / 3012("request has been blocked due to unusual activity") on the kernel-rebuiltmessagesbody (#661). Amitm://key targets only the MITM (login-client) traffic of that host, while a plainhttps://key covers both MITM and/bili/(API-key) traffic:{ "providers": { "mitm://zcode.z.ai": { "passthrough": true } } }
Compression behaviour is controlled by the compress block, which can appear at three levels. They merge per-field, deepest wins: a field set at a deeper level overrides the same field higher up, but an unset field at a deeper level never clears a value set higher up. In other words, the child covers the parent field-by-field — it never replaces the whole object.
The three levels, from broadest to most specific:
- Global — a top-level
"compress": { … }key. Applies to every request. This is the only level where theinjectTool/injectNudgetoggles are honoured. - Per-provider — a
"compress": { … }block inside aproviders[url]entry. - Per-model — a
"compress": { … }block inside aproviders[url].models[model]entry.
For each request, the proxy resolves the settings by longest-URL-prefix match (to find the provider) and the request's model name (to find the model entry), then merges global → provider → model.
- Type:
number | string - Default: (the model's native window)
- Status: ACTIVE
- Description: The context window size, in tokens. This is the denominator the engine uses for its usage ratio (
usage = tokens / modelContextLimit) — it is not a truncation cap. Accepts an absolute number (200000) or a percent string ("80%"= 80% of the model's native window, resolved from the built-in table or models.dev registry). When omitted at every level, the native window is used. This is the highest-priority source for the model limit; it overrides the built-in table, the legacy per-modelcontextfield, and the top-levelmodelContextLimit.
- Type:
number | string - Default:
"75%" - Status: ACTIVE
- Description: Context-usage threshold that triggers forced compression nudges. Once usage crosses this ratio, the engine fires a nudge that bypasses the growth-gate and cadence checks. Accepts a ratio (
0.75) or a percent string ("75%"). Lower values compress earlier. Maps to the kernel fieldnudge.maxContextLimitPct.
- Type:
number | string - Default:
"95%" - Status: ACTIVE
- Description: Context-usage threshold that triggers emergency truncation of large tool outputs. Accepts a ratio or a percent string. Must be greater than or equal to
maxContextLimit. Maps to the kernel fieldsnudge.emergencyThresholdPctandtruncate.threshold.
- Type:
number - Default:
50000 - Status: ACTIVE
- Description: Token-growth step for soft compression nudges. A nudge fires roughly every time this many tokens become compressible. Lower values produce more frequent nudges. Maps to the kernel fields
nudge.growthFloorandnudge.growthCap(it flattens the engine's adaptive band to this fixed step).
- Type:
number - Default: (kernel default, typically
5) - Status: ACTIVE
- Description: Number of most-recent messages that are never offered for compression. Protects the active working set so the model retains the latest turns verbatim. Maps to the kernel field
preserveRecentMessages.
- Type:
number - Default: (kernel default, typically
5000) - Status: ACTIVE
- Description: Token budget reserved for recent-message protection. Maps to the kernel field
preserveRecentTokens.
- Type:
number - Default: (kernel default, typically
5000) - Status: ACTIVE
- Description: Minimum range size, in characters (not tokens), for a message range to be eligible for compression; smaller ranges are skipped. English/code averages ~4 chars per token, CJK ~1-2, so the same number reads ~4× more permissive for English text than a token-based mental model. Maps to the kernel field
compress.minCompressRange.
- Type:
number - Status: DEPRECATED (alias of
minCompressRangeChars, kept for backward compatibility) - Description: Legacy name for
minCompressRangeChars— same kernel mapping (compress.minCompressRange), same unit (characters). When both keys are set at the same level the canonical name wins; across levels the deeper level wins regardless of which name it uses.
- Type:
boolean - Default:
true - Status: ACTIVE
- Description: Enable multi-tier compression — tier-2 distillation of old summaries and tier-3 condensation. Set
falseto run in tier-1-only mode (every summary is a flat tier-1 summary). Maps to the kernel fieldtiers.enabled.
- Type:
object({ compressPhilosophy?, howToCompressRules?, tier2DistillRules?, tier3CondenseRules? }, all strings) - Default: (kernel defaults — see
acp-kerneldefaultPrompts) - Status: ACTIVE
- Description: Override the compression prompt text injected into the system prompt and nudge messages. Every field is load-bearing: the kernel rules were tuned over months of production use, and overriding them can degrade summary quality (lost paths / signatures / decisions → broken retrieval). Overrides only take effect when
acknowledgePromptsRisk: trueis set at the same (winning) level; otherwise they are ignored and a one-time warning is logged. Non-string fields are silently dropped (a malformed partial never clobbers a good default). Useful mainly for non-English or small-model tuning — see issue #156.
- Type:
boolean - Default:
false - Status: ACTIVE
- Description: Must be
trueforpromptsoverrides to take effect. Setting it acknowledges the summary-quality risk documented above.
- Type:
object({ enabled?, minToolTokens?, contextThresholdPct?, excludeTools?, toolName? }) - Default: (disabled — the feature is off unless you set
enabled: true) - Status: ACTIVE
- Description: Opt-in instant tool-result compression (issue #605, via the
acp-kernelabsorb API). When enabled, large tool results get a forced[ACP absorb]instruction at result time; the model distills the result into a compact summary via theabsorbtool, and the original tool-call/tool-result pair is hidden from the wire from the next turn on — keeping mid-session pressure lower between fold rounds. Sub-fields (merged deepest-wins like every other CompressSettings field):enabled: boolean— master switch; anything other thantruekeeps the feature fully off (no tool, no prompt, no markers).minToolTokens: number— only results at or above this many tokens are prompted (kernel default 1000).contextThresholdPct: number|percent-string— only prompt once usage reaches this fraction ofmodelContextLimit(0= size gate alone;"75%"is accepted).excludeTools: string[]— tool-name patterns never absorbed. Known limitation: a no-op on tool results until ranxianglei/acp-kernel#213 ships (wire projections don't carrytoolNameon results, so the kernel's name guard can't fire).toolName: string— rename the injected tool (default"absorb"); the schema, system-prompt section and per-session adjudication all follow the name. Injection follows the wire's native-tool surface: proxy mode injects the tool + a static system-prompt section on the anthropic/openai/responses native-tools wires, plugin mode advertises it in the plugin manifest (the MCP shell picks it up for free). Responses marker/text-protocol routes are not supported (no native tool surface — the REQUIRED absorb instruction would be unsatisfiable), and title-generation requests (max_tokens ≤ 200) skip injection like the compress prompt does. Absorbed pairs stay hidden across restarts (persisted in the session state).
- Type:
object({ drop?, threshold? }) - Default:
drop: true,threshold: 2048— on - Status: ACTIVE
- Description: Compress-reasoning hygiene (issue #651, the proxy-side twin of
billion-context-pi#339/#348 /opencode-acp#377). Models that keep theirreasoning/thinkingtraces on the wire accumulate a permanent uncompressible floor: the anchor of a fold is acompresscall, and any reasoning messages sitting before that call survive every fold as part of the protected prefix — they can never be re-summarized, only stripped. In the storm sessions this floor reached ~50% of the visible context. When on, the proxy removes the reasoning run that immediately precedes a closedcompresscall — closed on round evidence: the call's tool result (contentType: "tool-result", matchingtoolCallId) has arrived at a later index and at least one message exists after it. No user message is required, so long agentic sessions close rounds too [#348 twin]. Safety gates: the in-flight round (result missing, or result still the last message) is never touched; runs of ordinary tool calls (read,bash, …) keep their reasoning; a run is judged by its summed length so a 2×1200-char run still trips a 2048 gate; non-contiguous reasoning (text between the fragments) is left alone. Sub-fields (merged deepest-wins like every other CompressSettings field):drop: boolean— kill-switch;falserestores the old wire verbatim. Required per-provider for thinking models that mandatereasoninground-trip while the request carriestools— DeepSeek, GLM thinking and Qwen-QwQ return HTTP 400 when a priorreasoning_contentis not echoed back:"providers": { "https://api.deepseek.com": { "compress": { "reasoning": { "drop": false } } } }
threshold: number— character gate; runs strictly greater than this are dropped (0= drop any non-empty run). Invalid values fall back to the default instead of throwing.
- Type:
boolean - Default:
false - Status: ACTIVE
- Description: Opt-in removal of historical image payloads. When
true, every message except the most recentstripImagesKeepRecenthas its image parts dropped before the wire rebuild; an image-only message collapses to a single[image]text placeholder (mixed text+image messages keep their text). Recent-N images are forwarded verbatim, and a freshly-sent image always falls inside that window on the turn it arrives. Off by default — while off, the #488 image-token floor and its overflow502stay the opt-in signal for image-heavy payloads. Applies to both compression modes (in plugin mode the agent's own history is untouched; only the upstream-bound wire is slimmed). See issue #617.
- Type:
number - Default:
5 - Status: ACTIVE
- Description: With
stripImages: true, how many trailing messages keep their images verbatim. Ignored unlessstripImagesis enabled.
These two toggles are honoured only at the global level. Setting them inside a per-provider or per-model compress block has no effect.
- Type:
boolean - Default:
true - Status: ACTIVE
- Description: Inject the
compress/decompress/search_contexttools and the compression system prompt into each request. Setfalse(orACP_COMPRESS_TOOL=0) to disable tool injection entirely.
- Type:
boolean - Default:
true - Status: ACTIVE
- Description: Inject automatic compression-nudge messages when usage thresholds are crossed. Set
false(orACP_COMPRESS_NUDGE=0) to disable nudge injection. Disabling bothinjectToolandinjectNudgeis functionally similar topassthrough, except the proxy still tracks token usage.
This example shows global defaults, a per-provider override, and a per-model override all stacking per-field:
{
// Level 1 — global: applies to every request
"compress": {
"maxContextLimit": "75%",
"emergencyThresholdPercent": "95%",
"nudgeGrowthTokens": 50000,
"tiers": true,
"injectTool": true,
"injectNudge": true
},
"providers": {
"https://api.anthropic.com": {
// Level 2 — per-provider: overrides global fields for this provider
"compress": {
"maxContextLimit": "70%", // compress a bit earlier here
"preserveRecentMessages": 8 // keep more recent turns
},
"models": {
"claude-sonnet-4-5": {
"context": 200000,
// Level 3 — per-model: the deepest, highest priority
"compress": {
"modelContextLimit": 180000, // treat window as 180k (leaves headroom)
"emergencyThresholdPercent": "90%"
}
}
}
}
}
}For a request to https://api.anthropic.com/v1/messages with model claude-sonnet-4-5, the resolved settings are:
| Field | Resolved from | Value |
|---|---|---|
maxContextLimit |
provider (level 2) | "70%" |
emergencyThresholdPercent |
model (level 3) | "90%" |
nudgeGrowthTokens |
global (level 1) | 50000 |
preserveRecentMessages |
provider (level 2) | 8 |
modelContextLimit |
model (level 3) | 180000 |
tiers |
global (level 1) | true |
Environment variables take precedence over the config file. They are useful for environment-specific overrides (CI, containers) without editing the file.
| Variable | Effect |
|---|---|
ACP_DEBUG |
Set to 1 for verbose logging (same as "debug": true). |
ACP_PASSTHROUGH |
Set to 1 to forward without compression (same as "passthrough": true). |
ACP_COMPRESS_TOOL |
Set to 0 to disable tool injection (same as "compress.injectTool": false). |
ACP_COMPRESS_NUDGE |
Set to 0 to disable nudge injection (same as "compress.injectNudge": false). |
ACP_MODEL_CONTEXT_LIMIT |
Override the context limit globally (absolute token count). |
BILI_IMAGE_TOKEN_CAP |
Cap the per-image token estimate used by the preflight size gate and output clamp (#488/#496). By default an inline data: image counts as base64 length / 4 tokens with no cap — correct for byte-billing relays, but a large over-estimate for pixel-tile upstreams (official Anthropic/OpenAI), which bill each image at roughly 1.1K–1.6K tokens regardless of byte size. Set this to your upstream's per-image tile cost so the gate reflects real billing; unset = no cap (current default). |
BILI_PREFLIGHT_HOLD_MS |
Grace period (ms) before a long preflight compression starts holding the client with keep-alive bytes (default 30000; see #568 / README "Preflight hold"). |
BILI_CONFIG_FILE |
Override the config file path (point at any JSON file). |
ACP_PORT / PORT |
Override the listen port. |
ACP_HOST |
Override the listen host. |
ACP_UPSTREAM |
Override the default upstream base URL. |
ACP_LOG |
Set to 0 to disable request logging. |
ACP_AUTO_UPDATE |
Set to 0 to disable auto-update checks. |
ACP_UPDATE_TAG |
Dist-tag channel the auto-updater follows (default latest, e.g. dev). File-config key: updateTag. A pr-N preview tag is only followed when explicitly configured. |
BILI_HOST_USAGE_CREDIT |
#408 host-usage backfill mode (file-config key: hostUsageCredit). auto (default) = the uncompressed-baseline backfill is armed for plain proxy clients (the bili-launched pi/omp extensions are exempted — their host-side compaction is cancelled, so the baseline drives nothing there). off = never backfill — the usage reported to the host is the actually-forwarded (folded) request, matching [acp-usage] input=. Use off for plain anthropic proxy clients (e.g. ZCode) whose UI would otherwise show the cumulative, drifting baseline as inflated context (#648). |
ACP_PROVIDERS |
Path to an external providers.json (legacy / shared file). |
BILI_REPLAY_RETRY_BASE_MS |
Base backoff delay (ms) for acp-loop replay retries after a transient upstream rejection (default 1500; set 0 to disable the delay). See #189. |
BILI_REPLAY_RETRY_MAX |
Total attempts for acp-loop replay retries (default 3; set 1 to disable retries entirely — legacy fail-fast behavior). See #189. |
BILI_UPSTREAM_TIMEOUT_MS |
Idle budget (ms) for upstream requests: time-to-first-byte and time between body chunks (default 720000 = 12 min). A healthy stream that keeps producing chunks is never cut mid-flight; a silent one is. The same value drives the underlying HTTP client's transport timeouts, so this single knob bounds long local-model prefills end-to-end (#551). |
ACP_SESSION_HEADER |
Conversation-id header name (default x-acp-session). |
ACP_REASONING_KEEP |
Responses API only: set none to drop all reasoning items. Default routes reasoning through the compression pipeline so it is hidden automatically once its turn is summarized (prevents the unbounded accumulation that broke Codex's prompt-cache prefix). |
ACP_LOG_FILE |
Log file path (default XDG state path; off disables the file, keeps stderr). Auto-rotates at 10 MB. |
ACP_DUMP_SSE |
Directory to dump raw SSE frames for debugging. |
BILI_UPSTREAM_PROXY |
Upstream proxy for the proxy's own outbound connections — highest priority, above per-URL/per-provider config. See the README Upstream proxy section. |
BILI_PERSIST |
Set 0 to disable session persistence (in-memory only, lost on restart). |
BILI_PERSIST_DEBOUNCE_MS |
Debounce window for persistence writes to disk, in ms (default 500). |
BILI_PERSIST_TAIL_TOKENS |
Token budget for the persisted conversation snapshot (#401). The disk record stores the folded view (block summaries in place of compressed ranges) truncated to the newest messages within this budget — never the raw full history. Default 16384; 0 disables message persistence entirely (block summaries and compressed originals still persist; bili export falls back to block-only rendering). Live in-memory sessions are unaffected — bili export of a live session is always complete. |
BILI_PERSIST_EPERM_ALERT_THRESHOLD |
N consecutive persist write failures (EPERM/EBUSY/EACCES) on one session before the one-time "add this dir to antivirus exclusions" alert fires (default 5). Windows only. See Windows: exclude the sessions dir. |
BILI_PERSIST_EPERM_ALERT_REPEAT_MS |
Re-alert window for the persist EPERM alert, in ms. 0 (default) = alert once then stay silent; >0 = re-alert at most every that many ms while the failures continue. |
BILI_TUNNEL_ALLOWED_HOSTS |
/bili/<absolute-url> tunnel admission for remote clients (#409): comma-separated host or host:port entries that unlock loopback/private destinations (e.g. a LAN relay or the machine's own sglang) for non-loopback clients. The proxy itself and link-local/metadata addresses are always denied; local (loopback) clients always pass. |
BILI_MAX_SESSIONS |
Max sessions held in memory (default 256; LRU eviction — disk is the source of truth). |
BILI_SESSIONS_DIR |
Directory for persisted session state (default XDG data dir). |
BILLION_CONTEXT_PROXY |
Exported by the launcher; client-side bili plugins/extensions detect it and self-disable their own compression (no double compression). |
BILLION_CONTEXT_PLUGIN |
Set 0 to disable plugin mode entirely (wire-level tool injection resumes). |
BILI_LAUNCHER_MODEL_WINDOWS |
Internal: the launcher hands the client's own per-model context windows (pi models.json, omp models.yml, opencode models.<id>.limit, codex model_context_window) to the spawned proxy as JSON, so the nudge denominator matches the real window for self-hosted models. Only the launcher sets it — no user configuration. |
BILI_LAUNCHER_PLUGIN |
Set 0 to disable the launcher's bili MCP server injection for claude/codex (pure wire mode); 1 forces plugin mode. Default: injected — except codex with a local/private upstream (sglang/vllm/ollama cannot parse codex's namespace tool type, so bili auto-falls back to wire tools there). See Launcher Reference. |
BILI_LAUNCHER_DIRECT |
Set 1 for direct-URL routing in the launcher (drop MITM/CA trust). See Launcher Reference. |
BILI_CLAUDE_UPSTREAM |
claude direct mode: your relay endpoint, when ANTHROPIC_BASE_URL already points at a relay the launcher would otherwise bypass. |
BILI_CODEX_COMPACT |
Codex native-compaction handling. Default intercept: bili intercepts codex's compaction requests and forges a local handoff to the ACP state when the safety gate passes (transform ok + steady-state usage < 90% of the window + at least one active compressed block) — trigger form forges a 2-frame SSE, endpoint form forges {output} — and never contacts upstream. Forged ACP summaries are re-injected as a history-borne handoff message (developer-message fallback) so compressed content stays visible after codex truncates its history. Set pass to opt out and forward codex's compaction requests upstream (native compaction backstops). On any gate failure the request passes through untouched. |
Full command surface (bili --help prints an abridged version). Precedence everywhere: CLI flag > env var > config file > built-in default.
| Command | What it does |
|---|---|
bili [start] [options] |
Start the proxy (reads the XDG config file by default) |
bili pi [opts --] [args] |
Start a proxy + launch pi against it |
bili pi-test [opts --] [args] |
Like bili pi, but adds --no-extensions (clean-room test — the proxy owns compression) |
bili codex [opts --] [args] |
Proxy + codex |
bili claude [opts --] [args] |
Proxy + claude (Claude Code CLI) |
bili omp [opts --] [args] |
Proxy + omp (pi-based) |
bili opencode [opts --] [args] |
Proxy + opencode |
bili hermes [opts --] [args] |
Proxy + hermes-agent (/bili/ rewrite) |
bili dsh [opts --] [args] |
Proxy + deepseek-harness (non-loopback upstreams via proxy envs, loopback via /bili/ rewrite — #535; args like --profile web "task" pass through) |
bili test pi |
Non-polluting end-to-end smoke test of the pi path |
bili export [session] [--full] [--output FILE] |
List persisted sessions / export one as a Markdown handoff — see Sessions & Migration |
bili update |
Check for & install a newer version now (bypasses the 3-minute throttle) |
bili plugin install <agent> |
Install the native-tool plugin / MCP bridge into a host — see Plugin Mode |
bili plugin remove <agent> |
Remove it again |
bili plugin list |
Show install status for every host |
bili mcp |
Run the bili MCP server standalone on stdio |
bili plugin-register <id> [--origin URL] [--agent name] |
Pre-bind a conversation id to plugin mode (advanced) |
bili --version / bili --help |
Print version / help |
Anything after -- in a launcher command is passed through to the client verbatim (bili pi -- print "hi").
| Flag | Effect |
|---|---|
--port <N> |
Listen port (default 8787) |
--host <ADDR> |
Listen host (default 127.0.0.1) |
--config <FILE> |
Path to config JSON (default: XDG location) |
--debug |
Verbose logging |
--passthrough |
Forward without compression |
--no-passthrough |
Force compression on (overrides config) |
--no-auto-update |
Disable background self-update for this run |
--mitm-domain <domain> |
Extra MITM whitelist entry (repeatable; launcher only) |
Two ways to point a client at the proxy without the launcher: the /bili/ prefix (API-key clients) and MITM transparent mode (login clients with hardcoded endpoints).
Clients you configure with an API key (not a login) let you change the upstream URL. Prepend the proxy origin + /bili/ to it — that's the only change. The API key stays in the client config and is passed through untouched.
OpenCode — edit ~/.config/opencode/opencode.json, change the provider's baseURL:
// before:
"baseURL": "https://open.bigmodel.cn/api/coding/paas/v4"
// after (prepend the proxy origin + /bili/):
"baseURL": "http://localhost:8787/bili/https://open.bigmodel.cn/api/coding/paas/v4"Codex (API key) — edit ~/.codex/config.toml, change the provider's base_url:
# before:
base_url = "https://api.openai.com/v1"
# after:
base_url = "http://localhost:8787/bili/https://api.openai.com/v1"Codex (ChatGPT login) — set the top-level openai_base_url field (keeps model_provider = "openai" and OAuth login intact):
# ~/.codex/config.toml (top-level field, not a section)
model_provider = "openai"
openai_base_url = "http://localhost:8787/bili/https://chatgpt.com/backend-api/codex"Run codex login as usual; the OAuth token travels in the Authorization header, which the proxy forwards untouched.
Pi — edit ~/.pi/agent/models.json, change the provider's baseUrl:
// before:
"baseUrl": "https://api.anthropic.com"
// after:
"baseUrl": "http://localhost:8787/bili/https://api.anthropic.com"Claude Code — set the ANTHROPIC_BASE_URL env var to the /bili/ URL. (claude's undici fetch ignores HTTPS_PROXY, so the /bili/ URL form is the only manual option — cert MITM cannot intercept it.)
export ANTHROPIC_BASE_URL="http://localhost:8787/bili/https://api.anthropic.com"Auto-compact alignment (manual mode only). The
bili claudelauncher automatically setsCLAUDE_CODE_AUTO_COMPACT_WINDOWto bili's effective window for your model, so claude's own auto-compact threshold lines up with bili's compression budget. In manual/bili/mode you must do this yourself — otherwise claude may run its own local auto-compact (a "summarize the conversation" turn) on a threshold that doesn't match bili's window. That is usually harmless (same session-id, so bili re-derives state from the truncation) but noisier than needed. Set it to bili's effective window for your model:export CLAUDE_CODE_AUTO_COMPACT_WINDOW=<bili effective window in tokens>claude clamps this value down to the window it perceives for the model (never up), so over-setting is safe. You can also set it persistently via claude's settings (
autoCompactWindow).
Other API-key clients (Cursor / Aider / Continue …) — wherever the upstream URL is configured, prepend http://localhost:8787/bili/. Nothing else changes.
The /bili/ prefix doubles as a self-detection signal: billion-context client extensions (billion-context-pi / opencode-acp) recognize it in their own baseUrl and self-disable, so you never get double compression.
Clients you sign into an account (ChatGPT Plus/Pro, Claude, ZCode coding plan, …) authenticate via OAuth and often hardcode the endpoint — if you can't change the baseURL, the prefix trick doesn't work. These use MITM mode instead.
How it works: the client only offers an HTTP proxy setting, so it sends CONNECT <host>:443; billion-context terminates the TLS locally (with a locally-generated root CA), injects compression into the cleartext, re-encrypts and forwards. The OAuth token travels in the client's Authorization header, which is forwarded untouched — so the subscription discount is preserved.
Supported MITM clients:
| Client | Login | Endpoint hardcoded | Status |
|---|---|---|---|
| ZCode | bigmodel coding plan (OAuth) | open.bigmodel.cn (builtin provider) |
✅ tested |
| Claude Code | Claude subscription (OAuth) | api.anthropic.com |
❓ untested (may not work — needs verification) |
Codex exception: Codex exposes a top-level
openai_base_urlconfig field, so the ChatGPT login version CAN use the/bili/prefix (see above). MITM is not needed for Codex.
MITM is scoped to a whitelist of model hosts (open.bigmodel.cn, api.anthropic.com, api.openai.com, chatgpt.com). All other HTTPS hosts are blind-tunnelled — billion-context never decrypts non-model traffic.
One-time setup (trust the root CA in the client):
-
Start the proxy once to generate the root CA:
bili start ls ~/.local/share/billion-context/ca/root-ca.pem # exists now
-
In the client's Settings → Network / Proxy set:
- HTTP Proxy:
http://127.0.0.1:8787 - Proxy CA certificate path: the CA file bili actually generated on this machine —
~/.local/share/billion-context/ca/root-ca.pemon Linux/macOS,%USERPROFILE%\.local\share\billion-context\ca\root-ca.pemon Windows. The ZCode card on the web UI's routing page shows the real path on this machine with a copy button — just paste it. - (optional) No-proxy list:
localhost,127.0.0.1 - (For ZCode specifically: Settings → Network. For Claude Code, set the
HTTPS_PROXYenv var andNODE_EXTRA_CA_CERTSto the CA path.)
Windows note: ZCode on Windows does not expand
~— a~/...path is not found (independent of the current working directory). Enter the full absolute path, e.g.C:\Users\<user>\.local\share\billion-context\ca\root-ca.pem(#342). - HTTP Proxy:
-
Restart the client. Its model traffic now flows through billion-context with compression injected. Send a message and check the proxy log (
~/.local/state/billion-context/bili.log) formitm <host>:443 tunnel established.
The root CA is generated locally and lives only on this machine; it is not a system-wide install. Only the client you configure (via its CA-path setting) trusts it, so no other app is affected. Deleting the CA files and restarting the proxy regenerates them.
To give a MITM login client its own upstream proxy (firewall/GFW) without affecting API-key clients on the same host, use the mitm:// scheme key — see the README's Upstream proxy section.
bili <client> brings up a proxy on an independent port (a fresh instance every launch — an already-running bili start is never reused, #216), then runs the client pointed at it. No config-file edits: the client's own config is read (never edited) to discover which upstream hosts it talks to; those hosts are auto-whitelisted for MITM so the proxy TLS-terminates exactly the hosts the client uses. When the client exits, a proxy the launcher started is stopped.
Both upstream schemes are covered automatically, with no config edits:
- HTTPS upstreams → cert MITM. The client is pointed at the proxy via
HTTPS_PROXYand trusts the proxy's MITM root CA (~/.local/share/billion-context/ca/root-ca.pem, generated lazily). Compression is injected on the intercepted TLS stream. - HTTP / localhost upstreams →
/bili/baseURL rewrite (plaintext can't be MITM'd). The launcher rewrites the client's base URL through the client's own mechanism, via an isolated temp copy of its config — the real config files are never touched (details below).
How each client is pointed at the proxy (set automatically in the child env):
| Client | Redirect | CA trust |
|---|---|---|
| pi | HTTPS_PROXY + BILI_PROVIDER_REWRITES env manifest (extension registerProvider) |
NODE_EXTRA_CA_CERTS |
| omp | HTTPS_PROXY + BILI_PROVIDER_REWRITES env manifest (extension registerProvider) |
NODE_EXTRA_CA_CERTS |
| codex | HTTPS_PROXY + -c key=value overrides |
SSL_CERT_FILE → combined-ca.pem |
| claude | ANTHROPIC_BASE_URL = /bili/ URL |
none needed |
| opencode | HTTPS_PROXY + isolated OPENCODE_CONFIG |
NODE_EXTRA_CA_CERTS |
| hermes | HTTPS_PROXY (plain-http rides absolute-form forward-proxy requests) |
HERMES_CA_BUNDLE → root-ca.pem |
| dsh | HTTPS_PROXY (+ HTTP_PROXY for plain-http) + DEEPSEEK_BASE_URL; loopback-only isolated DSH_HOME |
SSL_CERT_FILE → combined-ca.pem |
NODE_EXTRA_CA_CERTS appends to the built-in trust store, so it points at the MITM root alone (root-ca.pem). SSL_CERT_FILE replaces the default CA bundle, so for codex it points at combined-ca.pem — a bundle containing the MITM root plus the system/Node public roots — keeping pip/git/curl style TLS (blind-tunnelled, real certificates) working inside the child env (#152).
Claude Code's undici fetch ignores HTTPS_PROXY, so cert MITM cannot intercept it. Every claude upstream — including a pre-configured ANTHROPIC_BASE_URL relay — is routed through the /bili/ URL form via ANTHROPIC_BASE_URL instead; no CA trust is required.
Where upstreams are discovered from (read-only):
| Client | Read from |
|---|---|
| Pi | ~/.pi/agent/models.json — each provider's baseUrl |
| omp | ~/.omp/agent/models.yml — each provider's baseUrl |
| Codex | ~/.codex/config.toml — each [model_providers.<name>] base_url (+ top-level openai_base_url) |
| Claude Code | ANTHROPIC_BASE_URL env var, else hardcoded api.anthropic.com |
| OpenCode | ~/.config/opencode/opencode.json — each provider's baseURL |
| hermes | ~/.hermes/config.yaml — each provider's endpoint lines |
| dsh | ~/.dsh/settings.yaml — every baseURL/baseUrl/base_url value, split by destination (loopback → /bili/ rewrite; non-loopback https → MITM whitelist; non-loopback http → HTTP_PROXY); plus the built-in deepseek-official route via $DEEPSEEK_BASE_URL |
The launcher prefers file-free injection (env vars > CLI flags/extension APIs > generated files; see README, “Injection priority” section). Where a file is unavoidable it is a copy — the real config is never edited:
- pi / omp — nothing is written (#535): provider baseUrls ride the
BILI_PROVIDER_REWRITESenv manifest consumed by the bili extension at load (registerProvider), and native compaction is cancelled in-extension (session_before_compact). The real~/.pi/~/.omphomes are untouched. - opencode — a temp
opencode.jsonpointed at byOPENCODE_CONFIG(removed when the client exits), with/bili/-rewritten plaintext baseURLs plus the thin/acpplugin appended (native tools out of the box; the standaloneopencode-acpplugin self-disables viaBILLION_CONTEXT_PROXY). - hermes — nothing is written (#535): its httpx stack rides
HTTPS_PROXY(+HERMES_CA_BUNDLE) — https via CONNECT cert-MITM, plain-http via absolute-form forward-proxy requests. If no providers are configured, the launcher prints a warning and hermes runs unproxied (compression off). - dsh — split by destination (#535): dsh's fetch stack honors proxy envs except for an unconditional loopback bypass, so non-loopback upstreams ride
HTTPS_PROXY(cert MITM) /HTTP_PROXY(absolute-form forward-proxy requests) withSSL_CERT_FILE→combined-ca.pem; only loopback upstreams keep the persistent overlayDSH_HOME(~/.dsh-bili) with a rewrittensettings.yamlrouting them through/bili/.profiles/, credentials and sessions are symlinked through; the real~/.dshis never touched. The built-indeepseek-officialroute is captured separately via$DEEPSEEK_BASE_URL(dsh resolvessettings llm-deepseek.baseURL?? env ?? default, so a user setting wins and the env is the zero-config fallback) — with no custom providers the deepseek route is still proxied out of the box.
- pi — if the plugin is NOT installed, the launcher rides pi's
-e <file>flag to loaddist/agent/pi.jsfor that run only (nothing is written): native tools + the/acpcommand out of the box. If it IS installed, the symlinkedsettings.jsonalready loads it — no-eis added. - omp — does NOT ship the plugin; the launcher auto-injects
-e dist/agent/omp.jswhen the config carries no loadable bili entry (same zero-config ride as pi). Two omp-specific mechanics make the plugin fully native there: omp 17.x mounts extension tools that omitloadModeunder itsxd://device URLs (invisible to the model's main turn), so the plugin registers its tools withloadMode: "essential"— the model gets the four ACP tools natively; and since omp's fork emits nobefore_provider_headers, the plugin binds the conversation via the launcher identity register (POST /__bili/plugin/register, keyed by omp's session id =prompt_cache_key/x-session-id) — bound sessions run in plugin mode (wire injection suppressed) with the native/acpcommand. - opencode — the temp config appends the thin plugin automatically.
- claude / codex — on by default: the launcher injects a single
biliMCP server (--mcp-configfor claude,-c mcp_servers.bili.*for codex — both ephemeral, nothing written to host config), so the host gets native tools out of the box (verified with claude 2.1.227 / codex 0.147.0).BILI_LAUNCHER_PLUGIN=0falls back to plain wire mode — for hosts older than the verified builds that have not been tested against the injection flags. - codex + self-hosted upstream auto-fallback — codex 0.147 ships MCP tools to the model as a
namespacetool type; self-hosted servers (sglang/vllm/ollama/llama.cpp) do not parse it, leaving the tools silently invisible. When the codex upstream host is loopback/private (127.0.0.1, RFC1918, ULA,.local, …) andBILI_LAUNCHER_PLUGINis unset, bili automatically uses wire mode instead (flat tools every server understands) and says so on stderr.BILI_LAUNCHER_PLUGIN=1forces plugin mode regardless. - hermes — no plugin API; always wire mode.
- dsh — the launcher always splices a
--patch <file>flag into dsh's argv (written to~/.dsh-bili/.bili-acp.patch.yml), insertingdist/agent/dsh-acp.jsinto the profile's loader tree: the native/acpcommand, same shape as dsh's own/compact. Works on every profile that composes the commands service (web/tui interactive surfaces; theheadlessone-shot driver sends its task straight to the model and parses no commands —/compactbehaves the same there). Subcommand forms are handled:dsh webgets the flag afterweb,dsh plugin/--dump-default-configtake none.
Launcher-mode matrix:
| Mode | Tools surface | Setup |
|---|---|---|
| Launcher + MCP (default for claude/codex) | native MCP tools | none — just bili claude / bili codex |
Launcher wire mode (claude/codex, BILI_LAUNCHER_PLUGIN=0) |
proxy-injected wire tools | one env var |
Launcher -e / auto-plugin (pi, opencode; omp built-in) |
native plugin tools | none |
Manual plugin (bili plugin install) |
agent-side plugin | run install |
Manual baseURL (/bili/ prefix) |
proxy-injected wire tools | edit client config |
BILI_LAUNCHER_DIRECT=1 drops MITM/CA trust entirely — claude's ANTHROPIC_BASE_URL / codex's provider base_url point at the /bili/ prefix directly. Warnings:
- codex direct mode: the LLM traffic does not go through the proxy, so compression is not applied — only the bili MCP tool calls do. For full compression use the default MITM mode (unset
BILI_LAUNCHER_DIRECT). - claude direct mode:
ANTHROPIC_BASE_URLis overridden to the proxy; a pre-configured relay is bypassed unlessBILI_CLAUDE_UPSTREAM=<relay>is set. OAuth-subscription traffic requires the default MITM mode.
--mitm-domain <domain> (repeatable) adds extra domains to the MITM whitelist beyond what auto-discovery finds — useful for hosts the client fetches at runtime rather than from its config file. The launcher picks a free port automatically if the default is taken; --passthrough / --debug / --no-auto-update work like plain bili.
For a native-plugin experience, an agent can run a small cooperative plugin alongside the proxy: the plugin registers the four ACP tools (compress / decompress / search_context / acp_status) natively with the agent and drives the agent's own tool loop, while the proxy stays the compression authority (state, history folding, philosophy prompt, nudges). Tool schemas are served by the proxy itself (GET /__bili/plugin/manifest), so plugin and proxy can never drift. Protocol spec: PLUGIN.md.
Plugin-equipped sessions are detected automatically via request headers — wire-level tool injection is then suppressed for them (no double compression, native tool UX). Works in both proxy modes: the /bili/ prefix baseURL and MITM transparent mode. The plugin can also report the agent's own model context window (x-bili-plugin-context-window) and read live context usage via GET /__bili/plugin/status.
bili plugin install pi # add this billion-context install to pi's settings.json (packages)
bili plugin install omp # same for omp (config.yml extensions)
bili plugin install claude # register the bili MCP server (claude mcp add, user scope)
bili plugin install codex # append [mcp_servers.bili] to ~/.codex/config.toml
bili plugin install opencode # add mcp.bili to ~/.config/opencode/opencode.json
bili plugin list # install status for every supported host
bili plugin remove pi # undo (original files backed up to *.bili-bak once)install pi also replaces any legacy billion-context entries (old npm:billion-context-pi references, stale npm:billion-context@x.y.z, leftover dev-checkout paths) so exactly one bili plugin stays live.
The installed plugin is a thin one (~5 KB, zero runtime deps): it detects the proxy (from the /bili/ baseURL or BILLION_CONTEXT_PROXY), fetches tool schemas from the proxy, registers native tools, and forwards executions — the proxy remains the single compression authority, so plugin and proxy always match versions. Hosts without a plugin API (claude, codex, opencode) install the MCP bridge (dist/mcp.js) instead — same protocol underneath, though MCP has no slash-commands (no /acp panel command).
Kill switch: BILLION_CONTEXT_PLUGIN=0 disables plugin mode entirely (wire-level injection resumes).
When do you need plugin install at all? Launcher users mostly don't (see Launcher Reference — pi/omp get -e auto-injected, opencode auto-injects, claude/codex get the MCP server auto-injected, dsh gets the native /acp command via --patch, hermes is wire-only). It's for a manually-configured client (/bili/ prefix or MITM) where you want the native panel: pi/omp/opencode get native tools + /acp (on omp the fork hides extension tools from the model — the plugin's value there is the /acp command); claude/codex get native MCP tools (no /acp); dsh gets /acp through the launcher's --patch (a manually-configured dsh can add the same patch itself); hermes can't (wire only). Without any plugin everything still works — compression runs via wire-injected tools, and the model can be asked to call acp_status to check live usage.
Compression state (blocks, summaries, original message cache) lives in the proxy, not in the client. The client's own local history is the full uncompressed view. Two consequences:
- If you point the client back at the real upstream (or stop the proxy), the client replays its full local history every turn. After a long compressed session this can exceed the model's context window (
context_window_exceeded). - There is no way to "unpack" a compression block into the client's local history — the client never saw the compressed form.
The proxy boots session storage with a single directory walk+parse (load and the one-time #286 identity migration run over the same parsed map). Sessions are kept permanently — the design goal is that a year-long conversation is never lost — so there is no retention or size-budget pruning. The #286 migration writes a .bili-migration-286.done marker after its first pass, so it never re-scans or re-logs on later boots.
Export the session and paste it into a fresh conversation as a handoff:
bili export # list persisted sessions (id, label, blocks)
bili export <id|label> # print a Markdown handoff (block summaries)
bili export <id> --full # include the original messages per block
bili export <id> --full --output handoff.mdThen start a new conversation in the client (direct to upstream) and paste the handoff doc as the opening context.
Codex subagents (e.g. the guardian_subagent approval reviewer) reuse the main conversation's session_id, so on the wire they look like the same session. Without care their requests inherit the main conversation's compression state — a subagent turn can get its context folded (losing the verbatim user authorization it must read back) and the two roles' usage estimates pollute each other.
billion-context detects this via the instructions field: subagent requests carry their own role prompt. The first instructions seen for a conversation anchor the main namespace (stable even if the main prompt drifts); any other instructions value maps to a separate |sub: namespace with its own empty compression state. Subagent requests are self-contained replays, so the fresh namespace is lossless — and the web UI's session list shows the two namespaces as separate sessions sharing the same client label.
billion-context persists each session's compression state to one JSON file per session under the sessions dir (%USERPROFILE%\.local\share\billion-context\ by default) and rewrites that file on every turn of a long session. On Windows, real-time antivirus (Windows Defender), the search indexer, or a sync tool (OneDrive) can lock that directory mid-write. When the lock holds across several writes, the rename fails with EPERM and every persist for that session fails until the lock is cleared.
When the same session fails N consecutive writes (default 5, tunable via BILI_PERSIST_EPERM_ALERT_THRESHOLD), the proxy logs a one-time, actionable alert naming the exact directory to exclude. It does not repeat (set BILI_PERSIST_EPERM_ALERT_REPEAT_MS > 0 to re-alert at most every M minutes while the failures continue).
To stop the failures at the root, add the sessions dir to your antivirus exclusions and keep it out of any sync folder:
- Windows Defender exclusions: Settings → Privacy & security → Windows Security → Virus & threat protection → Manage settings → Exclusions → Add an exclusion → Folder → select
%USERPROFILE%\.local\share\billion-context\. - Do not sync this directory. Make sure OneDrive (or Dropbox / Google Drive / similar) is not syncing
%USERPROFILE%\.local\share\billion-context\. If it lives under a synced folder, relocate it withBILI_SESSIONS_DIRto a non-synced path.
High-frequency persist writes otherwise re-trigger the real-time scan on every turn — which is what produces the EPERM write failures. Once the directory is excluded, the alerts stop.