Merge branch 'main' into docs/fix-stale-and-incorrect-docs

This commit is contained in:
Tejas Chopra 2026-06-04 10:57:40 -07:00 committed by GitHub
commit 3599de8e7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 2361 additions and 1537 deletions

View file

@ -0,0 +1,54 @@
---
name: Copilot Subscription Test Report
about: Report results of testing `headroom wrap copilot --subscription` on Linux/Windows/macOS
title: '[COPILOT-SUB] <OS> test report'
labels: copilot-subscription, testing
assignees: ''
---
<!--
Thanks for helping verify Copilot subscription mode across platforms!
See TESTING-copilot-subscription.md for the step-by-step flows.
Redact your actual token everywhere.
-->
## Environment
- **OS + version**: (e.g., Ubuntu 24.04, Windows 11 23H2, macOS 14.5)
- **Architecture**: (x86_64 / arm64)
- **How you installed headroom**: (pipx/pip `--pre` wheel · Docker install.sh/ps1 · built from source)
- **headroom version**: (`headroom --version`)
- **Copilot CLI version**: (`copilot --version`)
- **Was plain `copilot` logged in before the test?**: yes / no
## Result
- **Command run**:
```
headroom wrap copilot --subscription -- --model gpt-4o -p "Reply with exactly: HEADROOM_OK"
```
- **Did it print `HEADROOM_OK`?**: yes / no
- **Worked WITHOUT `GITHUB_COPILOT_TOKEN` (auto-discovery)?**: yes / no / didn't try
- **Worked WITH `GITHUB_COPILOT_TOKEN` set?**: yes / no / didn't try
## Error output (if any)
```
paste any error here
```
## Token storage schema (only if auto-discovery failed)
Helps us fix auto-discovery. **Redact the secret value.**
- Linux: `secret-tool search --all 2>/dev/null | sed -E 's/^secret = .*/secret = <redacted>/'`
- Windows: `cmd /c "cmdkey /list"` (paste the Copilot-related `Target:` line)
- macOS (reference): service `copilot-cli`
```
paste the attribute / Target lines here (secret redacted)
```
## Anything else
(logs from `~/.headroom/logs/proxy.log`, surprises, etc.)

View file

@ -8,6 +8,7 @@
"draft": false,
"prerelease": false,
"separate-pull-requests": false,
"pull-request-title-pattern": "chore: release ${version}",
"packages": {
".": {
"package-name": "headroom-ai",

View file

@ -1,3 +1,3 @@
{
".": "0.22.4"
".": "0.23.0"
}

View file

@ -5,29 +5,48 @@ All notable changes to Headroom will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Documentation
## [0.23.0](https://github.com/chopratejas/headroom/compare/v0.22.4...v0.23.0) (2026-06-04)
- Fix stale API references: removed `IntelligentContextConfig`, `ScoringWeights`, and `RollingWindowConfig` from all docs (these classes no longer exist); context management is now automatic.
- Fix `SmartCrusherConfig` field names: removed fabricated fields (`keep_first`, `keep_last`, `relevance_threshold`, `anomaly_std_threshold`, `preserve_errors`, `relevance_tier`); replaced with real fields from source (`variance_threshold`, `uniqueness_threshold`, `similarity_threshold`, `preserve_change_points`, `first_fraction`, `last_fraction`, `dedup_identical_items`, `use_feedback_hints`).
- Fix `EmbeddingScorer` constructor: `model=` -> `model_name=`, default model updated to `BAAI/bge-small-en-v1.5`.
- Fix `memory.add()` examples: remove non-existent `category=` parameter.
- Fix `headroom learn` CLI reference: remove non-existent `--claude-dir PATH`; add actual `--agent`, `--model`, `--workers` flags.
- Fix import path: `from headroom.compression import compress` -> `from headroom import compress`; mark `UniversalCompressor` examples as Advanced/Internal API.
- Mark "Message-level CCR via IntelligentContext" section as retired in `ccr.mdx`.
- Fix CHANGELOG footer links: `headroom-sdk/headroom` -> `chopratejas/headroom`.
- Note `pip install "headroom-ai[llmlingua]"` extra was removed in 0.9.x.
- Fix proxy command: `python -m headroom.proxy.server --port 8787` -> `headroom proxy --port 8787`.
- Add "opt-in; disabled by default" caveat to source code 40-70% savings claims.
- Add missing extras to installation docs: `memory`, `relevance`, `image`, `reports`, `otel`, `voice`.
- Update `README.md` extras mention to include `[code]`, `[memory]`, `[relevance]`, `[image]`.
- Fix telemetry description in `llms.txt`: "No telemetry by default" -> "Anonymous telemetry enabled by default; set `HEADROOM_TELEMETRY=off` to disable".
- Update `EmbedderBackend.LOCAL` recommendation to `EmbedderBackend.ONNX` with note that LOCAL requires PyTorch (~2 GB).
- Fix CCR acronym: "Cross-Conversation Retrieval" -> "Compress-Cache-Retrieve" in `api-reference.mdx` (including `ccrHashes` field description).
- Remove `IntelligentContext` from pipeline diagram in `how-compression-works.mdx`; update to 2-stage pipeline: `CacheAligner` -> `ContentRouter`.
- Remove retired `rolling_window_config` field from `HeadroomClient` Python constructor table in `api-reference.mdx`; add note that `rolling_window` was removed in 0.9.x.
- docs: fix stale API references, retired class imports, wrong field names, and incomplete extras table
### Features
* **copilot:** GitHub Copilot subscription mode through Headroom ([f4dff9b](https://github.com/chopratejas/headroom/commit/f4dff9b4885b5c62d79396bbb0847ae3e39a9bd9))
### Bug Fixes
* **ccr:** scope proactive expansion by workspace (cross-project leak) ([197601b](https://github.com/chopratejas/headroom/commit/197601bc64ee72e786bf6b94cd90efcac4269bcf))
* **ccr:** scope proactive expansion by workspace (cross-project leak) ([1bc163f](https://github.com/chopratejas/headroom/commit/1bc163f5bc1a8422f9ad659061e1fdd8cfeb077b))
* **codex:** keep init model_provider at config root ([#260](https://github.com/chopratejas/headroom/issues/260)) ([304dcc7](https://github.com/chopratejas/headroom/commit/304dcc78047bc744fc2f7656b484ec54dc271354))
* **codex:** keep init model_provider at config root ([#260](https://github.com/chopratejas/headroom/issues/260)) ([849b46d](https://github.com/chopratejas/headroom/commit/849b46de5934a88369af2fd7f7d52e9af0536a7e))
* **copilot:** deterministic subscription token handoff to the proxy ([72da461](https://github.com/chopratejas/headroom/commit/72da46121726074515e0c1eb9745498457a1a8d5))
* **copilot:** support subscription auth through Headroom ([ff4a0c6](https://github.com/chopratejas/headroom/commit/ff4a0c6bc64e5e68ab76c38047a36a3c7a6aaacf))
* correct tiktoken encoding for unknown gpt-4 model snapshots ([#552](https://github.com/chopratejas/headroom/issues/552)) ([0e551de](https://github.com/chopratejas/headroom/commit/0e551de9d81021bb7f0dde1857a2341408606969))
* decode/encode owned config, state and template assets as UTF-8 ([2f1538a](https://github.com/chopratejas/headroom/commit/2f1538a641dd0e60a7be3de85646a70c4bf7e287))
* decode/encode owned config, state and template assets as UTF-8 (fixes [#533](https://github.com/chopratejas/headroom/issues/533)) ([92075b9](https://github.com/chopratejas/headroom/commit/92075b95af799951c90a305a08ec4e958473967a))
* **docker:** upgrade base images to Python 3.13 / debian13 ([e6bf7a0](https://github.com/chopratejas/headroom/commit/e6bf7a03fef8a9f2e4802d63afdafb40627c7ad9))
* **docker:** upgrade base images to Python 3.13 / debian13, drop digest pinning ([08a2197](https://github.com/chopratejas/headroom/commit/08a219708c97dcdc678483a0e6891306624a1fad))
* **docs:** bump next.js to 16.2.6 for GHSA-h64f-5h5j-jqjh (CVE-2026-44577) ([a6a09e6](https://github.com/chopratejas/headroom/commit/a6a09e6cfbe6962a70a6fb2e4bebeee80756e304))
* **docs:** mkdocs configuration to build with correct folder ([#543](https://github.com/chopratejas/headroom/issues/543)) ([5557944](https://github.com/chopratejas/headroom/commit/55579445f84c363219f45dc5358599a04d4263ed))
* **docs:** update brace-expansion to 5.0.6 to remediate GHSA-jxxr-4gwj-5jf2 (CVE-2026-45149) ([6eb6fb5](https://github.com/chopratejas/headroom/commit/6eb6fb5941adfbd056daa1689c3fa0c3755fd298))
* **docs:** update bun.lock to next 16.2.6 for GHSA-h64f-5h5j-jqjh (CVE-2026-44577) ([91e0937](https://github.com/chopratejas/headroom/commit/91e0937243c801fa5f1021b4c47debef2444650c))
* ignore brackets inside JSON strings when splitting mixed content ([#553](https://github.com/chopratejas/headroom/issues/553)) ([bdcfc32](https://github.com/chopratejas/headroom/commit/bdcfc322da0c4cde69931d641cfa18c76ddb138b))
* **learn:** decode Unix home dirs whose username contains '.', '-' or '_' ([211daae](https://github.com/chopratejas/headroom/commit/211daae25687901d1f893714d877b25606d0ef69))
* **learn:** decode Unix home dirs whose username contains '.', '-' or '_' ([491a8b3](https://github.com/chopratejas/headroom/commit/491a8b3a1b260f42f503b3553a04c578c18e1cc0))
* **learn:** finish gemini-flash-latest default model sweep ([982d01b](https://github.com/chopratejas/headroom/commit/982d01b9c996fd5fe26154dc2f94d567192f6ff6))
* **learn:** finish gemini-flash-latest default model sweep ([#532](https://github.com/chopratejas/headroom/issues/532)) ([d797366](https://github.com/chopratejas/headroom/commit/d7973665f4e2f40f2b3acadd0ec584609fb33c6c))
* **memory:** READ-ONLY framing + fail-closed unresolved-project fallback ([a178249](https://github.com/chopratejas/headroom/commit/a178249fc0af4a1b6f212decb4f6d2793d57fae8))
* **memory:** READ-ONLY framing + fail-closed unresolved-project fallback ([482f80e](https://github.com/chopratejas/headroom/commit/482f80e735f124ee6860f6854255c77170b862e7))
* update dashboard doc link ([#544](https://github.com/chopratejas/headroom/issues/544)) ([378d77e](https://github.com/chopratejas/headroom/commit/378d77e79d0020ca7fba3de8df7aaf910056ad2a))
* Update Next.js to 16.2.4 in docs/bun.lock to address GHSA-gx5p-jg67-6x7h (CVE-2026-44580) ([0b9f11a](https://github.com/chopratejas/headroom/commit/0b9f11a223bb6e6a6c1660ff1dfc1df6d67dfa84))
* Update Next.js to 16.2.6 in docs/package.json and package-lock.json to address GHSA-h64f-5h5j-jqjh (CVE-2026-44577) ([db5d15f](https://github.com/chopratejas/headroom/commit/db5d15f99e71b69a369eb9c161e04dbffb9b5d4a))
* Upgrade litellm to 1.86.2 to remediate CVE-2026-42271 ([07581b9](https://github.com/chopratejas/headroom/commit/07581b9e8075b833a6b543149008547260fe9dc0))
### Code Refactoring
* **cli:** factor shared wrap-subcommand scaffolding ([8eeb926](https://github.com/chopratejas/headroom/commit/8eeb9261680dd071654a87204521ccd3703ef77d))
* **cli:** factor shared wrap-subcommand scaffolding ([c74ad11](https://github.com/chopratejas/headroom/commit/c74ad113a4ced9968e45cad1077e6a020dc6a401))
## [0.22.4](https://github.com/chopratejas/headroom/compare/v0.22.3...v0.22.4) (2026-05-26)

View file

@ -34,6 +34,7 @@
</sub></p>
---
<a href="https://trendshift.io/repositories/20881" target="_blank"><img src="https://trendshift.io/api/badge/repositories/20881" alt="chopratejas%2Fheadroom | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
> Headroom compresses everything your AI agent reads — tool outputs, logs, RAG chunks, files, and conversation history — before it reaches the LLM. Same answers, fraction of the tokens.
@ -94,7 +95,7 @@ headroom proxy --port 8787 # drop-in proxy, zero code changes
# or: from headroom import compress # inline library
# 3 — See the savings
headroom stats
headroom perf
```
Granular extras: `[proxy]`, `[mcp]`, `[ml]`, `[code]`, `[memory]`, `[relevance]`, `[image]`, `[agno]`, `[langchain]`, `[evals]`. Requires **Python 3.10+**.
@ -134,6 +135,18 @@ Reproduce: `python -m headroom.evals suite --tier 1` · [Full benchmarks & metho
Any OpenAI-compatible client works via `headroom proxy`. MCP-native: `headroom mcp install`.
### GitHub Copilot CLI subscription mode
Headroom can route GitHub Copilot CLI subscription traffic through the local proxy:
```bash
headroom wrap copilot --subscription -- --model gpt-4o
```
This lets Headroom intercept OpenAI-compatible Copilot CLI requests and apply the same proxy compression pipeline before forwarding to GitHub Copilot's hosted API. The wrapper resolves the account-specific Copilot API endpoint and prints it as `COPILOT_PROVIDER_API_URL=...` during launch.
Platform support note: macOS auth reuse via Copilot CLI Keychain storage has been smoke-tested. Windows Credential Manager, Linux Secret Service / `secret-tool`, and Docker/CI token-injection paths are implemented or planned as auth-discovery paths, but still need real OS validation before they should be considered fully vetted. For Docker and CI, prefer passing an explicit `GITHUB_COPILOT_TOKEN` or `GITHUB_COPILOT_GITHUB_TOKEN` rather than relying on host keychain access.
## When to use · When to skip
**Great fit if you…**
@ -262,7 +275,6 @@ Devcontainers in `.devcontainer/` (default + `memory-stack` with Qdrant & Neo4j)
## Community
- **[Live leaderboard](https://headroomlabs.ai/dashboard)** — 60B+ tokens saved and counting.
- **[Discord](https://discord.gg/yRmaUNpsPJ)** — questions, feedback, war stories.
- **[Kompress-base on HuggingFace](https://huggingface.co/chopratejas/kompress-base)** — the model behind our text compression.

View file

@ -0,0 +1,122 @@
# Testing: GitHub Copilot subscription mode (`headroom wrap copilot --subscription`)
This is an **experimental** feature and we need help verifying it on **Linux and
Windows**. It already works on macOS; the cross-platform gap is small and
specific (see [Status](#status)). If you have a GitHub Copilot subscription and
10 minutes, please run one of the flows below and
[file a report](https://github.com/chopratejas/headroom/issues/new?template=copilot-subscription-test-report.md).
> ⚠️ This is experimental, and it reads your Copilot login token + routes your
> Copilot CLI traffic through a local Headroom proxy. Only run it if you're
> comfortable with that. The branch is open for inspection.
## What it does (and what "subscription" means here)
Normally `headroom wrap copilot` is **BYOK** — you bring an Anthropic/OpenAI API
key and pay that vendor. `--subscription` is different: it lets you use the
**Copilot seat you already pay GitHub for**, with **no separate API key**, while
still routing through Headroom so your context gets compressed.
Mechanically: the Copilot CLI's only interposition hook is its provider-override
(the "BYOK transport"), so Headroom uses that knob but supplies **your
subscription token** and points back at **GitHub's own Copilot API**. So the CLI
may print "BYOK" and require an explicit `--model`, but you are **not** paying a
third party — it's your subscription, just compressed. (Proof it's working: the
proxy forwards to `https://api.*.githubcopilot.com` with your token.)
## Status
| Platform | Mechanism (compress + forward) | Token **auto-discovery** from the OS secret store |
|----------|:---:|:---:|
| macOS (Keychain) | ✅ verified | ✅ verified (`copilot-cli`) |
| Linux (`secret-tool`/libsecret) | ✅ expected | ❓ **needs testing** |
| Windows (Credential Manager) | ✅ expected | ❓ **needs testing** |
| Any OS via `GITHUB_COPILOT_TOKEN` env var | ✅ verified by tests | n/a (bypasses discovery) |
The two things we want to learn:
1. **Does it work end to end on your OS?**
2. **Does it find your Copilot token automatically**, or do you have to set
`GITHUB_COPILOT_TOKEN`? If it can't find it, we need the **storage schema**
(see each flow) so we can fix auto-discovery.
## Prerequisites (all platforms)
1. A **GitHub Copilot subscription**.
2. The **GitHub Copilot CLI**: `npm install -g @github/copilot`
3. **Log in once**: run `copilot`, complete the device-code login in your
browser, then type `/exit`.
---
## Linux — the flow we most need (tests auto-discovery)
Auto-discovery only works with a **host-native** install (a container can't read
your host secret store). Linux has prebuilt wheels, so:
```bash
pipx install --pip-args='--pre' headroom-ai # or: pip install --pre headroom-ai
# (no separate API key needed — that's the point)
headroom wrap copilot --subscription -- --model gpt-4o -p "Reply with exactly: HEADROOM_OK"
```
- **If it prints `HEADROOM_OK`** → auto-discovery works on your Linux. 🎉 Report success.
- **If it errors with "no reusable bearer token"** → discovery missed your token. Please grab the **schema** so we can fix it (redact the secret), then confirm the mechanism works via the env var:
```bash
secret-tool search --all 2>/dev/null | sed -E 's/^secret = .*/secret = <redacted>/'
# then retry, supplying the token explicitly:
GITHUB_COPILOT_TOKEN='<your-token>' headroom wrap copilot --subscription -- --model gpt-4o -p "Reply with: HEADROOM_OK"
```
Report the `attribute.*` lines from `secret-tool` and whether the env-var retry worked.
---
## Windows
There is **no native Windows wheel yet**, so pick one:
**A. Mechanism test (easiest — Docker Desktop or WSL2):**
```powershell
$env:HEADROOM_DOCKER_IMAGE = "ghcr.io/chopratejas/headroom:<branch-tag>" # ask the maintainer for the tag
# run the Docker-native installer (scripts/install.ps1), then:
$env:GITHUB_COPILOT_TOKEN = "<your-token>"
headroom wrap copilot --subscription -- --model gpt-4o -p "Reply with: HEADROOM_OK"
```
Report whether it prints `HEADROOM_OK`.
**B. Native auto-discovery schema (even without a working install):** after
`copilot` login, tell us where Windows stored the token:
```cmd
cmd /c "cmdkey /list"
```
Report the `Target:` line that looks Copilot-related (it shows the target name,
not the secret). That single fact lets us make native Windows discovery work.
> Native Windows auto-discovery becomes fully testable once we add a Windows
> wheel to the build matrix — tracked separately.
---
## macOS (already proven — a second data point still helps)
```bash
pipx install --pip-args='--pre' headroom-ai
headroom wrap copilot --subscription -- --model gpt-4o -p "Reply with exactly: HEADROOM_OK"
```
Schema, for reference: Keychain generic password, service `copilot-cli`
(`security find-generic-password -s copilot-cli -w`).
---
## What to report
Please open a
[Copilot subscription test report](https://github.com/chopratejas/headroom/issues/new?template=copilot-subscription-test-report.md)
with:
- **OS + version** and **how you installed** (pipx/pip wheel, Docker, source).
- Was plain `copilot` logged in?
- Did `wrap copilot --subscription` print **`HEADROOM_OK`**? Paste any error.
- Did it work **without** setting `GITHUB_COPILOT_TOKEN` (auto-discovery), or
only **with** it?
- The **storage schema** if discovery failed (`secret-tool search --all` /
`cmdkey /list`), with the secret redacted.

View file

@ -13,9 +13,10 @@
"fumadocs-ui": "16.7.10",
"headroom-ai": "file:../sdk/typescript",
"lucide-react": "^1.7.0",
"next": "16.2.2",
"next": "16.2.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"recharts": "^3.8.1",
"tailwind-merge": "^3.5.0",
},
"devDependencies": {
@ -28,7 +29,7 @@
"@types/react-dom": "^19.2.3",
"ai": "^6.0.149",
"openai": "^6.33.0",
"postcss": "^8.5.8",
"postcss": "^8.5.10",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3",
},
@ -181,23 +182,23 @@
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@next/env": ["@next/env@16.2.2", "", {}, "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ=="],
"@next/env": ["@next/env@16.2.4", "", {}, "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@ -271,6 +272,8 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
@ -345,6 +348,8 @@
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
@ -385,6 +390,24 @@
"@turf/invariant": ["@turf/invariant@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-88Eo4va4rce9sNZs6XiMJowWkikM3cS2TBhaCKlU+GFHdNf8PFEpiU42VDU8q5tOF6/fu21Rvlke5odgOGW4AQ=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@ -411,6 +434,8 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@ -511,8 +536,32 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
@ -547,6 +596,8 @@
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="],
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
@ -573,6 +624,8 @@
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
@ -651,8 +704,12 @@
"image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
@ -839,11 +896,11 @@
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"next": ["next@16.2.2", "", { "dependencies": { "@next/env": "16.2.2", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.2", "@next/swc-darwin-x64": "16.2.2", "@next/swc-linux-arm64-gnu": "16.2.2", "@next/swc-linux-arm64-musl": "16.2.2", "@next/swc-linux-x64-gnu": "16.2.2", "@next/swc-linux-x64-musl": "16.2.2", "@next/swc-win32-arm64-msvc": "16.2.2", "@next/swc-win32-x64-msvc": "16.2.2", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A=="],
"next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
@ -883,7 +940,7 @@
"point-in-polygon-hao": ["point-in-polygon-hao@1.2.4", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
@ -897,8 +954,12 @@
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-is": ["react-is@19.2.6", "", {}, "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw=="],
"react-medium-image-zoom": ["react-medium-image-zoom@5.4.3", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cDIwdn35fRUPsGnnj/cG6Pacll+z+Mfv6EWU2wDO5ngbZjg5uLRb2ZhEnh92ufbXCJDFvXHekb8G3+oKqUcv5g=="],
"react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
@ -907,6 +968,8 @@
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
@ -915,6 +978,10 @@
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
@ -937,6 +1004,8 @@
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
@ -985,6 +1054,8 @@
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
@ -1045,6 +1116,8 @@
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
@ -1053,6 +1126,8 @@
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
"vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="],
@ -1083,6 +1158,8 @@
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
@ -1095,6 +1172,8 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/postcss/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"headroom-ai/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"headroom-ai/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="],
@ -1115,12 +1194,18 @@
"vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"@tailwindcss/postcss/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"headroom-ai/@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"headroom-ai/openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"tsup/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
@ -1169,6 +1254,8 @@
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"headroom-ai/@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"headroom-ai/openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],

View file

@ -249,7 +249,13 @@ OPENROUTER_API_KEY=sk-or-... headroom proxy --backend openrouter
export HEADROOM_HOST=0.0.0.0
export HEADROOM_PORT=8787
export HEADROOM_BUDGET=100.0
# Route OpenAI passthrough requests to a custom endpoint
export OPENAI_TARGET_API_URL=https://custom.openai.endpoint.com
# Route Anthropic passthrough requests to a custom endpoint
export ANTHROPIC_TARGET_API_URL=https://litellm.company.internal
headroom proxy
```

1474
docs/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@
"fumadocs-ui": "16.7.10",
"headroom-ai": "file:../sdk/typescript",
"lucide-react": "^1.7.0",
"next": "16.2.4",
"next": "16.2.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"recharts": "^3.8.1",

View file

@ -87,7 +87,7 @@ Use 'auto' (default) to scan all detected agents."""
"--model",
type=str,
default=None,
help="LLM model for analysis (e.g., claude-sonnet-4-6, gpt-4o, gemini/gemini-2.0-flash). "
help="LLM model for analysis (e.g., claude-sonnet-4-6, gpt-4o, gemini/gemini-flash-latest). "
"Auto-detected from API keys if not specified.",
)
@click.option(

View file

@ -37,8 +37,12 @@ if sys.platform == "win32" and hasattr(sys.stdout, "buffer"):
import click
from headroom._version import __version__ as _HEADROOM_VERSION
from headroom.copilot_auth import DEFAULT_API_URL as COPILOT_API_URL
from headroom.copilot_auth import has_oauth_auth, resolve_client_bearer_token
from headroom.copilot_auth import (
has_oauth_auth,
resolve_client_bearer_token,
resolve_copilot_api_url,
resolve_subscription_bearer_token,
)
from headroom.providers.aider import build_launch_env as _build_aider_launch_env
from headroom.providers.claude import proxy_base_url as _claude_proxy_base_url
from headroom.providers.codex import build_launch_env as _build_codex_launch_env
@ -157,6 +161,7 @@ def _start_proxy(
anyllm_provider: str | None = None,
region: str | None = None,
openai_api_url: str | None = None,
copilot_api_token: str | None = None,
) -> subprocess.Popen:
"""Start Headroom proxy as a background subprocess.
@ -210,6 +215,14 @@ def _start_proxy(
if agent_type != "unknown":
proxy_env["HEADROOM_AGENT_TYPE"] = agent_type
proxy_env.setdefault("HEADROOM_STACK", f"wrap_{agent_type}")
if openai_api_url:
proxy_env["OPENAI_TARGET_API_URL"] = openai_api_url
# Pin the wrapper-validated Copilot token for this proxy instance only.
# Injected into the subprocess env here (not the parent's os.environ) so it
# never leaks into shared state. The proxy's CopilotTokenProvider honours
# GITHUB_COPILOT_API_TOKEN directly, making upstream auth deterministic.
if copilot_api_token:
proxy_env["GITHUB_COPILOT_API_TOKEN"] = copilot_api_token
proc = subprocess.Popen(
cmd,
@ -1343,6 +1356,16 @@ def _proxy_active_session_count(payload: dict[str, Any] | None) -> int:
return max(counts, default=0)
def _normalize_proxy_api_url(url: object) -> str | None:
"""Normalize configured upstream URLs for running-proxy comparisons."""
if not isinstance(url, str):
return None
normalized = url.strip().rstrip("/")
if normalized.endswith("/v1"):
normalized = normalized[:-3]
return normalized or None
def _proxy_version(payload: dict[str, Any] | None) -> str | None:
"""Return the running proxy version when it exposes one."""
if payload is None:
@ -1554,8 +1577,11 @@ def _should_use_copilot_oauth(
backend: str | None,
provider_type: str,
env: dict[str, str],
force_subscription: bool = False,
) -> bool:
"""Prefer a reusable Copilot OAuth session when the requested routing supports it."""
if force_subscription:
return True
if env.get("COPILOT_PROVIDER_API_KEY") or env.get("COPILOT_PROVIDER_BEARER_TOKEN"):
return False
if provider_type == "anthropic":
@ -1580,6 +1606,7 @@ def _ensure_proxy(
anyllm_provider: str | None = None,
region: str | None = None,
openai_api_url: str | None = None,
copilot_api_token: str | None = None,
) -> subprocess.Popen | None:
"""Start or verify proxy. Returns process handle if we started it."""
helpers = _live_wrap_module()
@ -1669,10 +1696,19 @@ def _ensure_proxy(
missing.append("learn")
if code_graph and not running_config.get("code_graph"):
missing.append("code_graph")
if openai_api_url:
running_openai_url = _normalize_proxy_api_url(
running_config.get("openai_api_url")
)
requested_openai_url = _normalize_proxy_api_url(openai_api_url)
if running_openai_url != requested_openai_url:
missing.append("openai-api-url")
if missing:
needs_restart = True
flags_str = ", ".join(f"--{f.replace('_', '-')}" for f in missing)
flags_str = ", ".join(
f if f.startswith("--") else f"--{f.replace('_', '-')}" for f in missing
)
click.echo(f" Proxy on port {port} is missing: {flags_str}")
click.echo(" Restarting proxy with upgraded configuration...")
@ -1718,6 +1754,7 @@ def _ensure_proxy(
anyllm_provider=anyllm_provider,
region=region,
openai_api_url=openai_api_url,
copilot_api_token=copilot_api_token,
),
)
click.echo(f" Proxy ready on http://127.0.0.1:{port}")
@ -1793,6 +1830,7 @@ def _launch_tool(
anyllm_provider: str | None = None,
region: str | None = None,
openai_api_url: str | None = None,
copilot_api_token: str | None = None,
) -> None:
"""Common logic: start proxy, launch tool, clean up."""
proxy_holder: list[subprocess.Popen | None] = [None]
@ -1819,6 +1857,7 @@ def _launch_tool(
anyllm_provider=anyllm_provider,
region=region,
openai_api_url=openai_api_url,
copilot_api_token=copilot_api_token,
)
if code_graph:
@ -2360,6 +2399,14 @@ def unwrap_claude(
default=None,
help="OpenAI-compatible Copilot wire API. Defaults to 'completions' when provider-type resolves to openai.",
)
@click.option(
"--subscription",
is_flag=True,
help=(
"Experimental: route GitHub-authenticated Copilot CLI traffic through Headroom "
"without requiring a provider API key."
),
)
@click.option("--memory", is_flag=True, help="Enable persistent cross-session memory")
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
@click.argument("copilot_args", nargs=-1, type=click.UNPROCESSED)
@ -2372,6 +2419,7 @@ def copilot(
region: str | None,
provider_type: str,
wire_api: str | None,
subscription: bool,
memory: bool,
verbose: bool,
copilot_args: tuple[str, ...],
@ -2389,6 +2437,7 @@ def copilot(
headroom wrap copilot -- --model claude-sonnet-4-20250514
headroom wrap copilot --backend anyllm --anyllm-provider groq -- --model gpt-4o
headroom wrap copilot --provider-type openai --wire-api responses -- --model gpt-5.4
headroom wrap copilot --subscription -- --model gpt-4.1
headroom wrap copilot --no-context-tool -- --prompt "explain this file"
"""
copilot_bin = shutil.which("copilot")
@ -2416,6 +2465,17 @@ def copilot(
wire_api=wire_api,
backend=effective_backend,
)
if subscription:
if effective_backend not in (None, "", "anthropic"):
raise click.ClickException(
"--subscription routes to GitHub Copilot's hosted API and cannot be combined "
"with translated backends such as anyllm or litellm-*."
)
if provider_type == "anthropic":
raise click.ClickException(
"--subscription uses Copilot's OpenAI-compatible hosted API path; "
"do not combine it with --provider-type anthropic."
)
if not no_rtk:
if _selected_context_tool() == _CONTEXT_TOOL_LEAN_CTX:
@ -2430,15 +2490,21 @@ def copilot(
env = os.environ.copy()
openai_api_url: str | None = None
copilot_proxy_token: str | None = None
if _should_use_copilot_oauth(
backend=effective_backend,
provider_type=provider_type,
env=env,
force_subscription=subscription,
):
client_bearer = resolve_client_bearer_token()
client_bearer = (
resolve_subscription_bearer_token() if subscription else resolve_client_bearer_token()
)
if not client_bearer:
raise click.ClickException(
"GitHub Copilot auth was detected but no reusable bearer token could be resolved."
"GitHub Copilot subscription mode requires a reusable GitHub/Copilot bearer "
"token, but none could be resolved. Run `copilot auth login` first, or set "
"GITHUB_COPILOT_TOKEN / GITHUB_COPILOT_GITHUB_TOKEN."
)
effective_wire_api = wire_api or "completions"
@ -2446,14 +2512,32 @@ def copilot(
env["COPILOT_PROVIDER_BASE_URL"] = f"http://127.0.0.1:{port}/v1"
env["COPILOT_PROVIDER_WIRE_API"] = effective_wire_api
env["COPILOT_PROVIDER_BEARER_TOKEN"] = client_bearer
env["GITHUB_COPILOT_USE_TOKEN_EXCHANGE"] = "false"
env.pop("COPILOT_PROVIDER_API_KEY", None)
# Hand the exact token we resolved (and, for --subscription, validated
# against GitHub) to the proxy explicitly via copilot_proxy_token below.
# The proxy pins it as GITHUB_COPILOT_API_TOKEN, so upstream auth is
# deterministic instead of the proxy re-running unvalidated discovery
# (read_cached_oauth_token returns the *first* candidate, which may not
# be the one the wrapper approved → environment-dependent 401s). Passing
# it as a launch argument — rather than mutating this process's global
# os.environ — keeps the token off shared state and out of unrelated
# code paths.
copilot_proxy_token = client_bearer
env_vars_display = [
"COPILOT_PROVIDER_TYPE=openai",
f"COPILOT_PROVIDER_BASE_URL=http://127.0.0.1:{port}/v1",
f"COPILOT_PROVIDER_WIRE_API={effective_wire_api}",
"COPILOT_AUTH_MODE=github-oauth",
(
"COPILOT_AUTH_MODE=github-subscription-experimental"
if subscription
else "COPILOT_AUTH_MODE=github-oauth"
),
]
openai_api_url = COPILOT_API_URL
openai_api_url = resolve_copilot_api_url(client_bearer)
env["GITHUB_COPILOT_API_URL"] = openai_api_url
env["OPENAI_TARGET_API_URL"] = openai_api_url
env_vars_display.append(f"COPILOT_PROVIDER_API_URL={openai_api_url}")
else:
env, env_vars_display = _build_copilot_launch_env(
port=port,
@ -2475,7 +2559,7 @@ def copilot(
)
raise SystemExit(1)
if not _copilot_model_configured(copilot_args, env):
if not subscription and not _copilot_model_configured(copilot_args, env):
click.echo(
" Note: Copilot BYOK requires a model. Pass `--model <name>` "
"or set `COPILOT_MODEL` / `COPILOT_PROVIDER_MODEL_ID`."
@ -2496,6 +2580,7 @@ def copilot(
anyllm_provider=anyllm_provider,
region=region,
openai_api_url=openai_api_url,
copilot_api_token=copilot_proxy_token,
)

View file

@ -18,10 +18,14 @@ from urllib import error as urllib_error
from urllib import request as urllib_request
from urllib.parse import urlparse
from headroom.copilot_linux_secret import read_copilot_oauth_token as read_linux_secret_token
from headroom.copilot_macos_keychain import read_copilot_oauth_token as read_macos_keychain_token
logger = logging.getLogger(__name__)
DEFAULT_API_URL = "https://api.githubcopilot.com"
DEFAULT_TOKEN_EXCHANGE_URL = "https://api.github.com/copilot_internal/v2/token"
DEFAULT_USER_INFO_URL = "https://api.github.com/copilot_internal/user"
DEFAULT_GITHUB_HOST = "github.com"
_TOKEN_EXPIRY_BUFFER_S = 60
_DEFAULT_EDITOR_VERSION = "vscode/1.104.1"
@ -31,12 +35,15 @@ _API_TOKEN_ENV_VARS = (
"GITHUB_COPILOT_API_TOKEN",
"COPILOT_PROVIDER_BEARER_TOKEN",
)
_OAUTH_TOKEN_ENV_VARS = (
_COPILOT_OAUTH_TOKEN_ENV_VARS = (
"GITHUB_COPILOT_GITHUB_TOKEN",
"GITHUB_COPILOT_TOKEN",
"GITHUB_TOKEN",
"COPILOT_GITHUB_TOKEN",
)
_GENERIC_GITHUB_TOKEN_ENV_VARS = (
"GH_TOKEN",
"GITHUB_TOKEN",
)
_OAUTH_TOKEN_KEYS = (
"oauth_token",
"oauthToken",
@ -62,6 +69,16 @@ class CopilotAPIToken:
return time.time() < (self.expires_at - _TOKEN_EXPIRY_BUFFER_S)
@dataclass(frozen=True)
class CopilotTokenCandidate:
"""A discovered reusable token plus enough metadata to reason about trust."""
token: str
source: str
confidence: str
validate_for_subscription: bool = True
def _github_host() -> str:
return (os.environ.get("GITHUB_COPILOT_HOST") or DEFAULT_GITHUB_HOST).strip().lower()
@ -70,6 +87,10 @@ def _token_exchange_url() -> str:
return os.environ.get("GITHUB_COPILOT_TOKEN_EXCHANGE_URL", DEFAULT_TOKEN_EXCHANGE_URL).strip()
def _user_info_url() -> str:
return os.environ.get("GITHUB_COPILOT_USER_INFO_URL", DEFAULT_USER_INFO_URL).strip()
def _should_exchange_oauth_token() -> bool:
raw = os.environ.get("GITHUB_COPILOT_USE_TOKEN_EXCHANGE", "").strip().lower()
return raw in {"1", "true", "yes", "on"}
@ -119,6 +140,18 @@ def _read_gh_cli_oauth_token() -> str | None:
return token or None
def _read_macos_keychain_oauth_token() -> str | None:
"""Best-effort Copilot CLI token lookup from macOS Keychain."""
return read_macos_keychain_token(host=_github_host())
def _read_linux_secret_oauth_token() -> str | None:
"""Best-effort Copilot CLI token lookup from Linux Secret Service."""
return read_linux_secret_token(host=_github_host())
def _read_windows_copilot_cli_oauth_token() -> str | None:
if os.name != "nt":
return None
@ -262,19 +295,87 @@ def _iter_file_entries(payload: Any) -> list[tuple[str, dict[str, Any]]]:
def read_cached_oauth_token() -> str | None:
"""Return a GitHub OAuth token for Copilot, if one is available."""
for env_var in _OAUTH_TOKEN_ENV_VARS:
for candidate in iter_oauth_token_candidates():
return candidate.token
return None
def iter_oauth_token_candidates() -> list[CopilotTokenCandidate]:
"""Return reusable token candidates in safest-first discovery order."""
candidates: list[CopilotTokenCandidate] = []
for env_var in _COPILOT_OAUTH_TOKEN_ENV_VARS:
token = os.environ.get(env_var, "").strip()
if token:
return token
candidates.append(
CopilotTokenCandidate(
token=token,
source=f"env:{env_var}",
confidence="explicit",
)
)
windows_copilot_token = _read_windows_copilot_cli_oauth_token()
if windows_copilot_token:
return windows_copilot_token
candidates.append(
CopilotTokenCandidate(
token=windows_copilot_token,
source="windows-credential-manager:copilot-cli",
confidence="high",
)
)
macos_copilot_token = _read_macos_keychain_oauth_token()
if macos_copilot_token:
candidates.append(
CopilotTokenCandidate(
token=macos_copilot_token,
source="macos-keychain:copilot-cli",
confidence="high",
)
)
linux_copilot_token = _read_linux_secret_oauth_token()
if linux_copilot_token:
candidates.append(
CopilotTokenCandidate(
token=linux_copilot_token,
source="linux-secret-service:copilot-cli",
confidence="high",
)
)
candidates.extend(_read_file_oauth_token_candidates())
for env_var in _GENERIC_GITHUB_TOKEN_ENV_VARS:
token = os.environ.get(env_var, "").strip()
if token:
candidates.append(
CopilotTokenCandidate(
token=token,
source=f"env:{env_var}",
confidence="generic-github",
)
)
gh_token = _read_gh_cli_oauth_token()
if gh_token:
return gh_token
candidates.append(
CopilotTokenCandidate(
token=gh_token,
source="gh-cli",
confidence="generic-github",
)
)
return _dedupe_token_candidates(candidates)
def _read_file_oauth_token_candidates() -> list[CopilotTokenCandidate]:
"""Return token candidates from Copilot/GitHub credential files."""
candidates: list[CopilotTokenCandidate] = []
host = _github_host()
for path in _resolve_token_file_paths():
try:
@ -290,9 +391,28 @@ def read_cached_oauth_token() -> str | None:
continue
cached_token = _extract_oauth_token(entry)
if cached_token:
return cached_token
candidates.append(
CopilotTokenCandidate(
token=cached_token,
source=f"file:{path}",
confidence="medium",
)
)
return None
return candidates
def _dedupe_token_candidates(
candidates: list[CopilotTokenCandidate],
) -> list[CopilotTokenCandidate]:
seen: set[str] = set()
deduped: list[CopilotTokenCandidate] = []
for candidate in candidates:
if candidate.token in seen:
continue
seen.add(candidate.token)
deduped.append(candidate)
return deduped
def resolve_client_bearer_token() -> str | None:
@ -305,6 +425,28 @@ def resolve_client_bearer_token() -> str | None:
return read_cached_oauth_token()
def resolve_subscription_bearer_token() -> str | None:
"""Return the first discovered token that GitHub accepts for Copilot subscription APIs."""
for env_var in _API_TOKEN_ENV_VARS:
token = os.environ.get(env_var, "").strip()
if token and _fetch_copilot_user_info(token) is not None:
return token
for candidate in iter_oauth_token_candidates():
if not candidate.validate_for_subscription:
continue
if _fetch_copilot_user_info(candidate.token) is not None:
logger.debug(
"Using Copilot subscription token from %s (%s)",
candidate.source,
candidate.confidence,
)
return candidate.token
return None
def has_oauth_auth() -> bool:
"""Return True when existing Copilot auth can be reused."""
@ -331,6 +473,46 @@ def build_copilot_upstream_url(base_url: str, path: str) -> str:
return f"{normalized_base}{normalized_path}"
def resolve_copilot_api_url(oauth_token: str | None = None) -> str:
"""Return the Copilot API endpoint advertised for the current OAuth token."""
token = (oauth_token or read_cached_oauth_token() or "").strip()
if not token:
return os.environ.get("GITHUB_COPILOT_API_URL", DEFAULT_API_URL).strip() or DEFAULT_API_URL
payload = _fetch_copilot_user_info(token)
if payload is None:
return os.environ.get("GITHUB_COPILOT_API_URL", DEFAULT_API_URL).strip() or DEFAULT_API_URL
endpoints = payload.get("endpoints") if isinstance(payload, dict) else None
api_url = endpoints.get("api") if isinstance(endpoints, dict) else None
if isinstance(api_url, str) and api_url.strip():
return api_url.strip()
return os.environ.get("GITHUB_COPILOT_API_URL", DEFAULT_API_URL).strip() or DEFAULT_API_URL
def _fetch_copilot_user_info(token: str) -> dict[str, Any] | None:
"""Fetch Copilot account metadata for a reusable OAuth-style token."""
token = token.strip()
if not token:
return None
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
}
request = urllib_request.Request(_user_info_url(), headers=headers, method="GET")
try:
with urllib_request.urlopen(request, timeout=10.0) as response:
payload = json.loads(response.read().decode("utf-8"))
except Exception as exc:
logger.debug("Unable to resolve Copilot API URL from user info: %s", exc)
return None
return payload if isinstance(payload, dict) else None
class CopilotTokenProvider:
"""Resolve and cache short-lived Copilot API tokens."""
@ -377,7 +559,7 @@ class CopilotTokenProvider:
async def _exchange_token(self, oauth_token: str) -> CopilotAPIToken:
headers = {
"Authorization": f"token {oauth_token}",
"Authorization": f"Bearer {oauth_token}",
"Accept": "application/json",
"Editor-Version": os.environ.get(
"GITHUB_COPILOT_EDITOR_VERSION", _DEFAULT_EDITOR_VERSION

View file

@ -0,0 +1,106 @@
"""Linux secret-service lookup helpers for GitHub Copilot CLI auth."""
from __future__ import annotations
import json
import logging
import os
import subprocess
import sys
from pathlib import Path
logger = logging.getLogger(__name__)
def read_copilot_oauth_token(*, host: str = "github.com") -> str | None:
"""Return a Copilot CLI OAuth token from Linux Secret Service, if available."""
if not sys.platform.startswith("linux"):
return None
secret_tool = os.environ.get("GITHUB_COPILOT_SECRET_TOOL", "secret-tool").strip()
if not secret_tool:
return None
normalized_host = host.strip().lower() or "github.com"
login = _read_copilot_config_login()
for command in _candidate_secret_tool_commands(secret_tool, normalized_host, login):
token = _run_secret_tool_lookup(command)
if token:
return token
return None
def _read_copilot_config_login() -> str | None:
path = Path(os.environ.get("COPILOT_HOME", str(Path.home() / ".copilot"))) / "config.json"
try:
lines = [
line
for line in path.read_text(encoding="utf-8").splitlines()
if not line.lstrip().startswith("//")
]
payload = json.loads("\n".join(lines))
except Exception:
return None
if not isinstance(payload, dict):
return None
user = payload.get("lastLoggedInUser")
if not isinstance(user, dict):
return None
login = user.get("login")
return login.strip() if isinstance(login, str) and login.strip() else None
def _candidate_secret_tool_commands(
secret_tool: str,
host: str,
login: str | None,
) -> list[list[str]]:
commands: list[list[str]] = []
accounts = [
value
for value in (
f"https://{host}:{login}" if login else None,
f"{host}:{login}" if login else None,
login,
f"https://{host}",
host,
)
if value
]
service_names = ("copilot-cli", "GitHub Copilot CLI", "github-copilot", "copilot")
for service in service_names:
commands.append([secret_tool, "lookup", "service", service])
commands.append([secret_tool, "lookup", "application", service])
for account in accounts:
commands.append([secret_tool, "lookup", "service", service, "account", account])
commands.append([secret_tool, "lookup", "application", service, "account", account])
commands.append([secret_tool, "lookup", "service", service, "username", account])
return commands
def _run_secret_tool_lookup(command: list[str]) -> str | None:
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
timeout=5,
)
except OSError as exc:
logger.debug("Unable to invoke secret-tool for Copilot auth discovery: %s", exc)
return None
except subprocess.TimeoutExpired:
logger.debug("secret-tool lookup timed out for Copilot auth")
return None
if result.returncode != 0:
return None
token = result.stdout.strip()
return token or None

View file

@ -0,0 +1,124 @@
"""macOS Keychain lookup helpers for GitHub Copilot CLI auth."""
from __future__ import annotations
import json
import logging
import os
import subprocess
import sys
from pathlib import Path
logger = logging.getLogger(__name__)
def read_copilot_oauth_token(*, host: str = "github.com") -> str | None:
"""Return a Copilot CLI OAuth token from macOS Keychain, if available."""
if sys.platform != "darwin":
return None
normalized_host = host.strip().lower() or "github.com"
login = _read_copilot_config_login()
services = _split_env_list("GITHUB_COPILOT_KEYCHAIN_SERVICE") or [
"GitHub Copilot",
"GitHub Copilot CLI",
"github-copilot",
"copilot",
"copilot-cli",
"GitHub CLI",
"github.com",
f"https://{normalized_host}",
normalized_host,
]
accounts = _split_env_list("GITHUB_COPILOT_KEYCHAIN_ACCOUNT") or [
value
for value in (
f"https://{normalized_host}:{login}" if login else None,
f"{normalized_host}:{login}" if login else None,
login,
os.environ.get("USER"),
os.environ.get("USERNAME"),
normalized_host,
f"https://{normalized_host}",
)
if value
]
for command in _candidate_security_commands(normalized_host, services, accounts):
token = _run_security_lookup(command)
if token:
return token
return None
def _read_copilot_config_login() -> str | None:
"""Return the last logged-in Copilot CLI username from ~/.copilot/config.json."""
path = Path(os.environ.get("COPILOT_HOME", str(Path.home() / ".copilot"))) / "config.json"
try:
lines = [
line
for line in path.read_text(encoding="utf-8").splitlines()
if not line.lstrip().startswith("//")
]
payload = json.loads("\n".join(lines))
except Exception:
return None
if not isinstance(payload, dict):
return None
user = payload.get("lastLoggedInUser")
if not isinstance(user, dict):
return None
login = user.get("login")
return login.strip() if isinstance(login, str) and login.strip() else None
def _split_env_list(name: str) -> list[str]:
return [part.strip() for part in os.environ.get(name, "").split(",") if part.strip()]
def _candidate_security_commands(
host: str,
services: list[str],
accounts: list[str],
) -> list[list[str]]:
commands: list[list[str]] = []
for service in services:
commands.append(["security", "find-generic-password", "-s", service, "-w"])
for account in accounts:
commands.append(
["security", "find-generic-password", "-s", service, "-a", account, "-w"]
)
for server in (host, f"https://{host}"):
commands.append(["security", "find-internet-password", "-s", server, "-w"])
for account in accounts:
commands.append(
["security", "find-internet-password", "-s", server, "-a", account, "-w"]
)
return commands
def _run_security_lookup(command: list[str]) -> str | None:
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
timeout=5,
)
except OSError as exc:
logger.debug("Unable to invoke macOS Keychain lookup for Copilot auth: %s", exc)
return None
except subprocess.TimeoutExpired:
logger.debug("macOS Keychain lookup timed out for Copilot auth")
return None
if result.returncode != 0:
return None
token = result.stdout.strip()
return token or None

View file

@ -9,4 +9,4 @@ TEMPLATES_DIR = DASHBOARD_DIR / "templates"
def get_dashboard_html() -> str:
"""Load the dashboard HTML template."""
template_path = TEMPLATES_DIR / "dashboard.html"
return template_path.read_text()
return template_path.read_text(encoding="utf-8")

View file

@ -1238,7 +1238,7 @@
Press <kbd class="px-1.5 py-0.5 bg-border rounded text-gray-400">R</kbd> to refresh
</div>
<div>
<a href="https://chopratejas.github.io/headroom/" target="_blank" class="hover:text-gray-300 transition-colors">Documentation</a>
<a href="https://headroom-docs.vercel.app/docs" target="_blank" class="hover:text-gray-300 transition-colors">Documentation</a>
</div>
</div>
</footer>

View file

@ -24,7 +24,7 @@ def save_manifest(manifest: DeploymentManifest) -> None:
root.mkdir(parents=True, exist_ok=True)
manifest.updated_at = iso_utc_now()
path = manifest_path(manifest.profile)
path.write_text(json.dumps(asdict(manifest), indent=2) + "\n")
path.write_text(json.dumps(asdict(manifest), indent=2) + "\n", encoding="utf-8")
except OSError as e:
logger.warning("Cannot save deployment manifest: %s — continuing without persistence", e)
@ -35,7 +35,7 @@ def load_manifest(profile: str = "default") -> DeploymentManifest | None:
path = manifest_path(profile)
if not path.exists():
return None
payload = json.loads(path.read_text())
payload = json.loads(path.read_text(encoding="utf-8"))
payload["mutations"] = [ManagedMutation(**item) for item in payload.get("mutations", [])]
payload["artifacts"] = [ArtifactRecord(**item) for item in payload.get("artifacts", [])]
return DeploymentManifest(**payload)
@ -51,7 +51,7 @@ def list_manifests() -> list[DeploymentManifest]:
manifests: list[DeploymentManifest] = []
for candidate in sorted(root.glob("*/manifest.json")):
try:
payload = json.loads(candidate.read_text())
payload = json.loads(candidate.read_text(encoding="utf-8"))
payload["mutations"] = [
ManagedMutation(**item) for item in payload.get("mutations", [])
]

View file

@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
_MODEL_DEFAULTS: list[tuple[str, str]] = [
("ANTHROPIC_API_KEY", "claude-sonnet-4-6"),
("OPENAI_API_KEY", "gpt-4o"),
("GEMINI_API_KEY", "gemini/gemini-2.0-flash"),
("GEMINI_API_KEY", "gemini/gemini-flash-latest"),
]
_MAX_DIGEST_TOKENS = 80_000 # Budget for the digest (leave room for prompt + output)
@ -94,7 +94,7 @@ def _detect_default_model() -> str:
"No LLM API key found. headroom learn needs one of:\n"
" export ANTHROPIC_API_KEY=sk-ant-... → uses claude-sonnet-4-6\n"
" export OPENAI_API_KEY=sk-... → uses gpt-4o\n"
" export GEMINI_API_KEY=... → uses gemini-2.0-flash\n"
" export GEMINI_API_KEY=... → uses gemini-flash-latest\n"
"Or set HEADROOM_LEARN_CLI to a coding agent CLI (claude, gemini, codex).\n"
"Or install one of those CLIs for auto-detection.\n"
"Or specify a model directly: headroom learn --model <litellm-model-name>"

View file

@ -124,7 +124,7 @@ def _load_sync_state(state_path: Path) -> dict[str, Any]:
"""Load sync state from disk."""
if state_path.exists():
try:
result: dict[str, Any] = json.loads(state_path.read_text())
result: dict[str, Any] = json.loads(state_path.read_text(encoding="utf-8"))
return result
except (json.JSONDecodeError, OSError):
pass
@ -134,7 +134,7 @@ def _load_sync_state(state_path: Path) -> dict[str, Any]:
def _save_sync_state(state_path: Path, state: dict[str, Any]) -> None:
"""Save sync state to disk."""
state_path.parent.mkdir(parents=True, exist_ok=True)
state_path.write_text(json.dumps(state, indent=2))
state_path.write_text(json.dumps(state, indent=2), encoding="utf-8")
def _db_fingerprint(memories: list[Any]) -> str:

View file

@ -26,14 +26,14 @@ def apply_provider_scope(manifest: DeploymentManifest) -> ManagedMutation | None
path.parent.mkdir(parents=True, exist_ok=True)
payload: dict[str, object] = {}
if path.exists():
payload = json.loads(path.read_text())
payload = json.loads(path.read_text(encoding="utf-8"))
env = payload.get("env")
env_map = dict(env) if isinstance(env, dict) else {}
values = manifest.tool_envs.get(ToolTarget.CLAUDE.value, {})
previous = {name: env_map.get(name) for name in values}
env_map.update(values)
payload["env"] = env_map
path.write_text(json.dumps(payload, indent=2) + "\n")
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
return ManagedMutation(
target=ToolTarget.CLAUDE.value,
kind="json-env",
@ -49,7 +49,7 @@ def revert_provider_scope(mutation: ManagedMutation, manifest: DeploymentManifes
path = Path(mutation.path)
if not path.exists():
return
payload = json.loads(path.read_text())
payload = json.loads(path.read_text(encoding="utf-8"))
env = payload.get("env")
env_map = dict(env) if isinstance(env, dict) else {}
previous: dict[str, object] = mutation.data.get("previous", {})
@ -60,4 +60,4 @@ def revert_provider_scope(mutation: ManagedMutation, manifest: DeploymentManifes
else:
env_map[name] = previous[name]
payload["env"] = env_map
path.write_text(json.dumps(payload, indent=2) + "\n")
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")

View file

@ -80,14 +80,14 @@ def apply_provider_scope(manifest: DeploymentManifest) -> ManagedMutation | None
+ f"{_CODEX_MARKER_END}\n"
)
if path.exists():
existing = path.read_text()
existing = path.read_text(encoding="utf-8")
if _CODEX_MARKER_START in existing:
merged = _CODEX_PATTERN.sub(section, existing)
else:
merged = existing.rstrip() + "\n\n" + section + "\n"
else:
merged = section + "\n"
path.write_text(merged)
path.write_text(merged, encoding="utf-8")
return ManagedMutation(target=ToolTarget.CODEX.value, kind="toml-block", path=str(path))
@ -99,7 +99,7 @@ def revert_provider_scope(mutation: ManagedMutation, manifest: DeploymentManifes
path = Path(mutation.path)
if not path.exists():
return
content = path.read_text()
content = path.read_text(encoding="utf-8")
# Remove the managed marker block.
if _CODEX_MARKER_START in content:
content = _CODEX_PATTERN.sub("", content)
@ -108,4 +108,4 @@ def revert_provider_scope(mutation: ManagedMutation, manifest: DeploymentManifes
content = _ORPHAN_MODEL_PROVIDER.sub("", content)
content = _ORPHAN_OPENAI_BASE_URL.sub("", content)
content = _ORPHAN_HEADROOM_TABLE.sub("", content)
path.write_text(content.strip() + "\n")
path.write_text(content.strip() + "\n", encoding="utf-8")

View file

@ -13,8 +13,12 @@ import hashlib
import json
import logging
import os
import threading
import time
import uuid
from collections import OrderedDict
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import replace
from datetime import datetime
from typing import TYPE_CHECKING, Any
@ -46,6 +50,72 @@ from headroom.proxy.outcome import RequestOutcome
logger = logging.getLogger("headroom.proxy")
_OPENAI_RESPONSES_UNIT_CACHE_MAX_ENTRIES = 10_000
_OPENAI_RESPONSES_UNIT_CACHE_VERSION = "openai_responses_unit_v1"
_OPENAI_RESPONSES_UNIT_PARALLELISM_ENV = "HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM"
_OPENAI_RESPONSES_UNIT_PARALLELISM_DEFAULT = 4
_OPENAI_RESPONSES_UNIT_PARALLELISM_MAX = 16
_OPENAI_RESPONSES_UNIT_CACHE_INIT_LOCK = threading.RLock()
_OPENAI_RESPONSES_UNIT_EXECUTOR_LOCK = threading.RLock()
_OPENAI_RESPONSES_UNIT_EXECUTOR: ThreadPoolExecutor | None = None
def _openai_responses_unit_parallelism() -> int:
raw = os.getenv(_OPENAI_RESPONSES_UNIT_PARALLELISM_ENV)
if raw is None or raw.strip() == "":
return _OPENAI_RESPONSES_UNIT_PARALLELISM_DEFAULT
try:
requested = int(raw)
except ValueError:
logger.warning(
"Invalid %s=%r; using default %d",
_OPENAI_RESPONSES_UNIT_PARALLELISM_ENV,
raw,
_OPENAI_RESPONSES_UNIT_PARALLELISM_DEFAULT,
)
return _OPENAI_RESPONSES_UNIT_PARALLELISM_DEFAULT
return max(1, min(_OPENAI_RESPONSES_UNIT_PARALLELISM_MAX, requested))
def _openai_responses_unit_executor() -> ThreadPoolExecutor:
global _OPENAI_RESPONSES_UNIT_EXECUTOR
with _OPENAI_RESPONSES_UNIT_EXECUTOR_LOCK:
if _OPENAI_RESPONSES_UNIT_EXECUTOR is None:
_OPENAI_RESPONSES_UNIT_EXECUTOR = ThreadPoolExecutor(
max_workers=_OPENAI_RESPONSES_UNIT_PARALLELISM_MAX,
thread_name_prefix="headroom-openai-unit",
)
return _OPENAI_RESPONSES_UNIT_EXECUTOR
def _openai_responses_unit_cache_key(unit: Any, *, model: str) -> str:
text_hash = hashlib.sha256(unit.text.encode("utf-8", errors="replace")).hexdigest()
key_payload = {
"version": _OPENAI_RESPONSES_UNIT_CACHE_VERSION,
"model": model,
"provider": unit.provider,
"endpoint": unit.endpoint,
"role": unit.role,
"item_type": unit.item_type,
"cache_zone": unit.cache_zone,
"mutable": unit.mutable,
"min_bytes": unit.min_bytes,
"context": unit.context,
"question": unit.question,
"bias": unit.bias,
"metadata": unit.metadata,
"text_sha256": text_hash,
}
serialized = json.dumps(key_payload, sort_keys=True, separators=(",", ":"), default=str)
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
def _openai_responses_result_with_cache_hit(result: Any) -> Any:
router_result = getattr(result, "router_result", None)
if router_result is None:
return result
return replace(result, router_result=replace(router_result, cache_hit=True))
def _codex_ws_text_shape(text: str) -> str:
stripped = text.strip()
@ -366,6 +436,35 @@ class OpenAIHandlerMixin:
"apply_patch_call_output",
}
def _openai_responses_unit_cache(self) -> tuple[Any, OrderedDict[str, Any]]:
with _OPENAI_RESPONSES_UNIT_CACHE_INIT_LOCK:
lock = getattr(self, "_openai_responses_unit_cache_lock", None)
if lock is None:
lock = threading.RLock()
self._openai_responses_unit_cache_lock = lock
cache = getattr(self, "_openai_responses_unit_result_cache", None)
if cache is None:
cache = OrderedDict()
self._openai_responses_unit_result_cache = cache
return lock, cache
def _get_openai_responses_cached_unit(self, key: str) -> Any | None:
lock, cache = self._openai_responses_unit_cache()
with lock:
result = cache.get(key)
if result is None:
return None
cache.move_to_end(key)
return _openai_responses_result_with_cache_hit(result)
def _store_openai_responses_cached_unit(self, key: str, result: Any) -> None:
lock, cache = self._openai_responses_unit_cache()
with lock:
cache[key] = result
cache.move_to_end(key)
while len(cache) > _OPENAI_RESPONSES_UNIT_CACHE_MAX_ENTRIES:
cache.popitem(last=False)
@staticmethod
def _headroom_bypass_enabled(headers: Any) -> bool:
"""Return True when inbound headers request full passthrough."""
@ -698,18 +797,65 @@ class OpenAIHandlerMixin:
elapsed_ms = (time.perf_counter() - unit_started) * 1000.0
return routed.slot, result, elapsed_ms
# Units run serially within the frame-level worker thread. Frame-
# level parallelism is already provided by
# ``self._compression_executor`` (32 workers, sized
# ``min(32, cpu*4)``), which `_run_compression_in_executor`
# dispatches each frame onto. The prior per-call
# ``ThreadPoolExecutor`` + module-global
# ``threading.BoundedSemaphore(10)`` caused production cascades
# under ≥10 concurrent Codex sessions; both are deleted.
router_total_started = time.perf_counter()
routed_results = [_compress_routed_unit(routed) for routed in routed_units]
routed_results: list[tuple[object, Any, float] | None] = [None] * len(routed_units)
cache_misses: list[tuple[int, str, RoutedCompressionUnit]] = []
cache_miss_followers: dict[str, list[int]] = {}
for unit_idx, routed in enumerate(routed_units):
cache_key = _openai_responses_unit_cache_key(routed.unit, model=model)
cached = self._get_openai_responses_cached_unit(cache_key)
if cached is not None:
routed_results[unit_idx] = (routed.slot, cached, 0.0)
continue
if cache_key in cache_miss_followers:
cache_miss_followers[cache_key].append(unit_idx)
continue
cache_miss_followers[cache_key] = []
cache_misses.append((unit_idx, cache_key, routed))
for _, result, elapsed_ms in routed_results:
def _compress_and_store(
unit_idx: int,
cache_key: str,
routed: RoutedCompressionUnit,
) -> tuple[int, str, tuple[object, Any, float]]:
slot, result, elapsed_ms = _compress_routed_unit(routed)
self._store_openai_responses_cached_unit(cache_key, result)
return unit_idx, cache_key, (slot, result, elapsed_ms)
def _record_routed_result(
unit_idx: int,
cache_key: str,
routed_result: tuple[object, Any, float],
) -> None:
routed_results[unit_idx] = routed_result
_slot, result, _elapsed_ms = routed_result
for follower_idx in cache_miss_followers.get(cache_key, []):
routed_results[follower_idx] = (
routed_units[follower_idx].slot,
_openai_responses_result_with_cache_hit(result),
0.0,
)
parallelism = _openai_responses_unit_parallelism()
if len(cache_misses) > 1 and parallelism > 1:
executor = _openai_responses_unit_executor()
for start in range(0, len(cache_misses), parallelism):
batch = cache_misses[start : start + parallelism]
futures = [executor.submit(_compress_and_store, *item) for item in batch]
for future in as_completed(futures):
unit_idx, cache_key, routed_result = future.result()
_record_routed_result(unit_idx, cache_key, routed_result)
else:
for unit_idx, cache_key, routed in cache_misses:
_record_routed_result(
unit_idx,
cache_key,
_compress_and_store(unit_idx, cache_key, routed)[2],
)
ordered_routed_results = [result for result in routed_results if result is not None]
for _, result, elapsed_ms in ordered_routed_results:
router_chain = list(result.router_result.strategy_chain) if result.router_result else []
router_content_type = (
result.router_result.routing_log[0].content_type.value
@ -766,7 +912,7 @@ class OpenAIHandlerMixin:
_add_timing("compression_units_router_loop", router_total_started)
apply_started = time.perf_counter()
for slot, result, _elapsed_ms in routed_results:
for slot, result, _elapsed_ms in ordered_routed_results:
item_idx, slot_ref = slot
router_chain = list(result.router_result.strategy_chain) if result.router_result else []
for s in router_chain:

View file

@ -1666,6 +1666,10 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
"memory": config.memory_enabled,
"learn": config.traffic_learning_enabled,
"code_graph": config.code_graph_watcher,
"anthropic_api_url": config.anthropic_api_url,
"openai_api_url": config.openai_api_url,
"gemini_api_url": config.gemini_api_url,
"cloudcode_api_url": config.cloudcode_api_url,
"pid": os.getpid(),
}
return payload

View file

@ -357,7 +357,9 @@ class UsageReporter:
return
try:
self._cache_path.parent.mkdir(parents=True, exist_ok=True)
self._cache_path.write_text(json.dumps(self._license_info.to_dict(), indent=2))
self._cache_path.write_text(
json.dumps(self._license_info.to_dict(), indent=2), encoding="utf-8"
)
except OSError:
logger.warning("Could not save license cache to %s", self._cache_path)
@ -365,7 +367,7 @@ class UsageReporter:
"""Load cached license info, or return a default if expired/missing."""
try:
if self._cache_path.exists():
data = json.loads(self._cache_path.read_text())
data = json.loads(self._cache_path.read_text(encoding="utf-8"))
cached = LicenseInfo.from_dict(data)
age = (datetime.now(timezone.utc) - cached.validated_at).total_seconds()
if age < GRACE_PERIOD_SECONDS:

View file

@ -97,13 +97,22 @@ def get_encoding_for_model(model: str) -> str:
if model in MODEL_TO_ENCODING:
return MODEL_TO_ENCODING[model]
# Try prefix matching for versioned models
for prefix in ["gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5", "o1", "o3"]:
# Try prefix matching for versioned models. Ordered most-specific first
# so that, e.g., "gpt-4o-*" resolves before "gpt-4-*". Each prefix maps
# directly to its encoding: scanning MODEL_TO_ENCODING for the first key
# that merely starts with the prefix is order-dependent and wrong — the
# "gpt-4" prefix would match the "gpt-4o" dict entry first and return
# o200k_base instead of cl100k_base for unknown gpt-4 snapshots.
for prefix, encoding in (
("gpt-4o", "o200k_base"),
("gpt-4-turbo", "cl100k_base"),
("gpt-4", "cl100k_base"),
("gpt-3.5", "cl100k_base"),
("o1", "o200k_base"),
("o3", "o200k_base"),
):
if model.startswith(prefix):
# Find any model with this prefix
for known_model, encoding in MODEL_TO_ENCODING.items():
if known_model.startswith(prefix):
return encoding
return encoding
return DEFAULT_ENCODING

View file

@ -658,13 +658,38 @@ def _extract_json_block(lines: list[str], start: int) -> tuple[str | None, int]:
bracket_count = 0
brace_count = 0
json_lines = []
in_string = False
escaped = False
for i in range(start, len(lines)):
line = lines[i]
json_lines.append(line)
bracket_count += line.count("[") - line.count("]")
brace_count += line.count("{") - line.count("}")
# Count brackets/braces, but ignore any that appear inside a JSON
# string literal — a naive line.count() treats e.g. the "]" in
# {"path": "a]b"} as a closing bracket and terminates the block
# early, splitting one array across multiple sections.
for ch in line:
if escaped:
escaped = False
continue
if ch == "\\":
if in_string:
escaped = True
continue
if ch == '"':
in_string = not in_string
continue
if in_string:
continue
if ch == "[":
bracket_count += 1
elif ch == "]":
bracket_count -= 1
elif ch == "{":
brace_count += 1
elif ch == "}":
brace_count -= 1
if bracket_count <= 0 and brace_count <= 0 and json_lines:
return "\n".join(json_lines), i

View file

@ -1,4 +1,5 @@
site_name: Headroom
docs_dir: wiki
site_description: "The Context Optimization Layer for LLM Applications — compress everything your AI agent reads."
site_url: https://chopratejas.github.io/headroom
repo_url: https://github.com/chopratejas/headroom

View file

@ -1,6 +1,6 @@
{
"name": "headroom-openclaw",
"version": "0.22.4",
"version": "0.23.0",
"description": "Headroom context compression plugin for OpenClaw — 70-90% token savings with zero LLM calls",
"type": "module",
"main": "./dist/index.js",

View file

@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "headroom-ai"
version = "0.22.4"
version = "0.23.0"
description = "The Context Optimization Layer for LLM Applications - Cut costs by 50-90%"
readme = "README.md"
license = "Apache-2.0"

View file

@ -1,6 +1,6 @@
{
"name": "headroom-ai",
"version": "0.22.4",
"version": "0.23.0",
"description": "Compress LLM context. Save tokens. Fit more into every request.",
"type": "module",
"main": "./dist/index.cjs",

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import importlib
import os
import sys
import types
from pathlib import Path
@ -212,8 +213,145 @@ def test_wrap_copilot_prefers_existing_oauth_session(
assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
assert env["COPILOT_PROVIDER_BEARER_TOKEN"] == "gho-existing"
assert env["GITHUB_COPILOT_API_URL"] == DEFAULT_API_URL
assert env["OPENAI_TARGET_API_URL"] == DEFAULT_API_URL
assert "COPILOT_PROVIDER_API_KEY" not in env
assert captured["openai_api_url"] == DEFAULT_API_URL
assert f"COPILOT_PROVIDER_API_URL={DEFAULT_API_URL}" in captured["env_vars_display"]
def test_wrap_copilot_subscription_uses_github_auth_without_provider_key(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
monkeypatch: pytest.MonkeyPatch,
) -> None:
_wrap_cli, main = wrap_modules
for var in ("COPILOT_PROVIDER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"):
monkeypatch.delenv(var, raising=False)
captured: dict[str, object] = {}
def fake_launch_tool(**kwargs): # noqa: ANN003
captured.update(kwargs)
with (
patch("headroom.cli.wrap.shutil.which", return_value="copilot"),
patch("headroom.cli.wrap.resolve_subscription_bearer_token", return_value="gho-existing"),
patch("headroom.cli.wrap.has_oauth_auth", return_value=False),
patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool),
):
result = runner.invoke(
main,
["wrap", "copilot", "--subscription", "--no-rtk"],
)
assert result.exit_code == 0, result.output
assert "Copilot BYOK requires a model" not in result.output
env = captured["env"]
assert isinstance(env, dict)
assert env["COPILOT_PROVIDER_TYPE"] == "openai"
assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
assert env["COPILOT_PROVIDER_BEARER_TOKEN"] == "gho-existing"
assert "COPILOT_PROVIDER_API_KEY" not in env
assert captured["openai_api_url"] == DEFAULT_API_URL
def test_wrap_copilot_subscription_pins_validated_token_for_proxy(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""`--subscription` must hand the *validated* token to the proxy.
The proxy honours ``GITHUB_COPILOT_API_TOKEN``; the wrapper passes the
resolved token as the ``copilot_api_token`` launch argument so the proxy
pins exactly it (rather than re-discovering a possibly different,
unvalidated token). The token rides the launch arg, never the child env or
the parent's global ``os.environ``. This guards the deterministic handoff.
"""
_wrap_cli, main = wrap_modules
for var in ("COPILOT_PROVIDER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"):
monkeypatch.delenv(var, raising=False)
business_api = "https://api.business.githubcopilot.com"
captured: dict[str, object] = {}
def fake_launch_tool(**kwargs: object) -> None:
captured.update(kwargs)
with (
patch("headroom.cli.wrap.shutil.which", return_value="copilot"),
patch(
"headroom.cli.wrap.resolve_subscription_bearer_token",
return_value="gho-validated",
),
patch("headroom.cli.wrap.resolve_copilot_api_url", return_value=business_api),
patch("headroom.cli.wrap.has_oauth_auth", return_value=False),
patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool),
):
result = runner.invoke(main, ["wrap", "copilot", "--subscription", "--no-rtk"])
assert result.exit_code == 0, result.output
env = captured["env"]
assert isinstance(env, dict)
# The validated token is handed to the proxy as an explicit launch
# argument — not via the child env, not via the parent's os.environ.
assert captured["copilot_api_token"] == "gho-validated"
assert "GITHUB_COPILOT_API_TOKEN" not in env
assert os.environ.get("GITHUB_COPILOT_API_TOKEN") is None
assert env["COPILOT_PROVIDER_TYPE"] == "openai"
assert env["COPILOT_PROVIDER_BEARER_TOKEN"] == "gho-validated"
assert env["GITHUB_COPILOT_USE_TOKEN_EXCHANGE"] == "false"
assert env["OPENAI_TARGET_API_URL"] == business_api
assert captured["openai_api_url"] == business_api
assert "COPILOT_PROVIDER_API_KEY" not in env
# The secret must never be echoed to the terminal.
assert "gho-validated" not in result.output
def test_wrap_copilot_subscription_requires_reusable_auth(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
) -> None:
_wrap_cli, main = wrap_modules
with (
patch("headroom.cli.wrap.shutil.which", return_value="copilot"),
patch("headroom.cli.wrap.resolve_subscription_bearer_token", return_value=None),
):
result = runner.invoke(main, ["wrap", "copilot", "--subscription", "--no-rtk"])
assert result.exit_code != 0
assert "subscription mode requires a reusable GitHub/Copilot bearer token" in result.output
def test_wrap_copilot_subscription_rejects_translated_backend(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
) -> None:
_wrap_cli, main = wrap_modules
with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
result = runner.invoke(
main,
["wrap", "copilot", "--subscription", "--backend", "anyllm", "--no-rtk"],
)
assert result.exit_code != 0
assert "cannot be combined with translated backends" in result.output
def test_wrap_copilot_subscription_rejects_anthropic_provider_type(
runner: CliRunner,
wrap_modules: tuple[types.ModuleType, click.Group],
) -> None:
_wrap_cli, main = wrap_modules
with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
result = runner.invoke(
main,
["wrap", "copilot", "--subscription", "--provider-type", "anthropic", "--no-rtk"],
)
assert result.exit_code != 0
assert "do not combine it with --provider-type anthropic" in result.output
def test_wrap_copilot_translated_backend_still_requires_byok(

View file

@ -206,6 +206,46 @@ def test_ensure_proxy_restarts_idle_stale_ephemeral_proxy(monkeypatch) -> None:
assert calls[1][0] == "start"
def test_ensure_proxy_restarts_ephemeral_proxy_for_openai_api_url_mismatch(monkeypatch) -> None:
calls: list[object] = []
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {
"pid": "12345",
"memory": False,
"learn": False,
"code_graph": False,
"openai_api_url": "https://api.githubcopilot.com",
},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: len(calls) == 0)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda pid, port: calls.append(("kill", pid, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: calls.append(("start", args, kwargs)),
)
result = wrap_cli._ensure_proxy(
8787,
False,
openai_api_url="https://api.individual.githubcopilot.com",
)
assert result is None
assert calls[0] == ("kill", 12345, 8787)
assert calls[1][0] == "start"
assert calls[1][2]["openai_api_url"] == "https://api.individual.githubcopilot.com"
def test_ensure_proxy_leaves_active_stale_ephemeral_proxy_running(monkeypatch) -> None:
health = {
"version": "0.0.1",

View file

@ -245,7 +245,9 @@ def test_concurrent_compression_has_no_semaphore_tail() -> None:
(27×) or (b) OS-level scheduler noise (23×). 4× sits
comfortably between the two catches the bug, tolerates
hardware. (First iteration tried 5× with mixed sizes, which
let size-variance push CI ratios to 7.4×.)
let size-variance push CI ratios to 7.4×.) The ratio is only
enforced once p99 clears a scheduler-noise floor on very fast
runners p50 rounds to 0ms and the ratio becomes pure jitter.
Marked ``slow`` so a normal ``pytest`` run can skip it via
``-m 'not slow'``. CI matrix runs all marks.
@ -300,9 +302,20 @@ def test_concurrent_compression_has_no_semaphore_tail() -> None:
assert not errors, f"Got {len(errors)} errors; first: {errors[0].error}"
ratio = p99 / max(p50, 1)
assert ratio < 4.0, (
# The p99/p50 ratio only signals contention when the tail is also
# *absolutely* large. On a fast/quiet runner p50 rounds toward 0ms, so the
# ratio collapses to "p99 in ms" and a few milliseconds of ordinary
# scheduler jitter reads as a spurious multiple (e.g. p50=0ms, p99=5ms →
# ~5×) that has nothing to do with the semaphore. The deleted semaphore
# produced a tail of *tens* of milliseconds (and ~27×); a healthy run keeps
# p99 in the single-digit-ms range regardless of ratio. So only treat a high
# ratio as a regression once p99 clears a scheduler-noise floor.
SEMAPHORE_TAIL_FLOOR_MS = 25.0
assert ratio < 4.0 or p99 < SEMAPHORE_TAIL_FLOOR_MS, (
f"p99/p50 ratio is {ratio:.1f}× (p50={p50:.0f}ms, p99={p99:.0f}ms). "
f"Expected < 4× on uniform-size workload — a higher ratio means "
f"the semaphore-induced contention tail is back. Pre-fix baseline "
f"ratio on this same workload shape was ~27× regardless of CPU speed."
f"Expected < 4× on uniform-size workload once p99 clears the "
f"{SEMAPHORE_TAIL_FLOOR_MS:.0f}ms noise floor — a high ratio with a large "
f"absolute tail means the semaphore-induced contention tail is back. "
f"Pre-fix baseline ratio on this same workload shape was ~27× regardless "
f"of CPU speed."
)

View file

@ -17,6 +17,74 @@ def test_read_cached_oauth_token_prefers_env(monkeypatch: pytest.MonkeyPatch) ->
assert copilot_auth.read_cached_oauth_token() == "gho-env"
def test_read_cached_oauth_token_prefers_copilot_cli_before_generic_github_token(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("GITHUB_COPILOT_GITHUB_TOKEN", raising=False)
monkeypatch.delenv("GITHUB_COPILOT_TOKEN", raising=False)
monkeypatch.delenv("COPILOT_GITHUB_TOKEN", raising=False)
monkeypatch.setenv("GITHUB_TOKEN", "ghp-generic")
monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_macos_keychain_oauth_token", lambda: "gho-keychain")
monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: None)
assert copilot_auth.read_cached_oauth_token() == "gho-keychain"
def test_iter_oauth_token_candidates_preserves_sources(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("GITHUB_COPILOT_GITHUB_TOKEN", raising=False)
monkeypatch.delenv("GITHUB_COPILOT_TOKEN", raising=False)
monkeypatch.delenv("COPILOT_GITHUB_TOKEN", raising=False)
monkeypatch.setenv("GITHUB_TOKEN", "ghp-generic")
monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_macos_keychain_oauth_token", lambda: "gho-keychain")
monkeypatch.setattr(copilot_auth, "_read_file_oauth_token_candidates", lambda: [])
monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: None)
candidates = copilot_auth.iter_oauth_token_candidates()
assert [(candidate.source, candidate.token) for candidate in candidates] == [
("macos-keychain:copilot-cli", "gho-keychain"),
("env:GITHUB_TOKEN", "ghp-generic"),
]
def test_resolve_subscription_bearer_token_skips_invalid_generic_token(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("GITHUB_COPILOT_API_TOKEN", raising=False)
monkeypatch.delenv("COPILOT_PROVIDER_BEARER_TOKEN", raising=False)
monkeypatch.setattr(
copilot_auth,
"iter_oauth_token_candidates",
lambda: [
copilot_auth.CopilotTokenCandidate(
token="ghp-generic",
source="env:GITHUB_TOKEN",
confidence="generic-github",
),
copilot_auth.CopilotTokenCandidate(
token="gho-copilot",
source="macos-keychain:copilot-cli",
confidence="high",
),
],
)
monkeypatch.setattr(
copilot_auth,
"_fetch_copilot_user_info",
lambda token: (
{"endpoints": {"api": "https://api.individual.githubcopilot.com"}}
if token == "gho-copilot"
else None
),
)
assert copilot_auth.resolve_subscription_bearer_token() == "gho-copilot"
def test_should_exchange_oauth_token_supports_truthy_values(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@ -59,6 +127,7 @@ def test_read_cached_oauth_token_falls_back_to_gh_cli(monkeypatch: pytest.Monkey
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
monkeypatch.delenv("COPILOT_GITHUB_TOKEN", raising=False)
monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_macos_keychain_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: "gho-gh-cli")
assert copilot_auth.read_cached_oauth_token() == "gho-gh-cli"
@ -79,6 +148,35 @@ def test_read_cached_oauth_token_prefers_copilot_cli_windows_token(
assert copilot_auth.read_cached_oauth_token() == "gho-copilot"
def test_read_cached_oauth_token_prefers_macos_keychain_before_gh(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("GITHUB_COPILOT_GITHUB_TOKEN", raising=False)
monkeypatch.delenv("GITHUB_COPILOT_TOKEN", raising=False)
monkeypatch.delenv("COPILOT_GITHUB_TOKEN", raising=False)
monkeypatch.delenv("GH_TOKEN", raising=False)
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_macos_keychain_oauth_token", lambda: "gho-keychain")
monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: "gho-gh-cli")
assert copilot_auth.read_cached_oauth_token() == "gho-keychain"
def test_read_macos_keychain_oauth_token_uses_security(
monkeypatch: pytest.MonkeyPatch,
) -> None:
calls: list[str] = []
def fake_read(*, host: str) -> str:
calls.append(host)
return "gho-keychain"
monkeypatch.setattr(copilot_auth, "read_macos_keychain_token", fake_read)
assert copilot_auth._read_macos_keychain_oauth_token() == "gho-keychain"
assert calls == ["github.com"]
def test_read_cached_oauth_token_reads_hosts_file(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
@ -97,6 +195,7 @@ def test_read_cached_oauth_token_reads_hosts_file(
monkeypatch.delenv("GITHUB_COPILOT_TOKEN", raising=False)
monkeypatch.setenv("GITHUB_COPILOT_TOKEN_FILE", str(hosts))
monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_macos_keychain_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: None)
assert copilot_auth.read_cached_oauth_token() == "gho-file"
@ -112,6 +211,7 @@ def test_read_cached_oauth_token_skips_expired_entries(
)
monkeypatch.setenv("GITHUB_COPILOT_TOKEN_FILE", str(hosts))
monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_macos_keychain_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: None)
assert copilot_auth.read_cached_oauth_token() is None

View file

@ -0,0 +1,68 @@
from __future__ import annotations
from types import SimpleNamespace
from headroom import copilot_linux_secret
def test_read_copilot_oauth_token_uses_secret_tool(monkeypatch) -> None:
calls: list[list[str]] = []
def fake_run(command, **kwargs): # noqa: ANN001, ANN003
calls.append(command)
assert kwargs["capture_output"] is True
return SimpleNamespace(returncode=0, stdout="gho-secret\n")
monkeypatch.setattr(copilot_linux_secret.sys, "platform", "linux")
monkeypatch.setattr(copilot_linux_secret, "_read_copilot_config_login", lambda: "octo")
monkeypatch.setattr(copilot_linux_secret.subprocess, "run", fake_run)
assert copilot_linux_secret.read_copilot_oauth_token(host="github.com") == "gho-secret"
assert calls[0] == ["secret-tool", "lookup", "service", "copilot-cli"]
def test_read_copilot_oauth_token_returns_none_off_linux(monkeypatch) -> None:
monkeypatch.setattr(copilot_linux_secret.sys, "platform", "darwin")
assert copilot_linux_secret.read_copilot_oauth_token() is None
def test_candidate_secret_tool_commands_include_login_specific_lookup() -> None:
commands = copilot_linux_secret._candidate_secret_tool_commands(
"secret-tool",
"github.com",
"octo",
)
assert [
"secret-tool",
"lookup",
"service",
"copilot-cli",
"account",
"https://github.com:octo",
] in commands
def test_read_copilot_oauth_token_tries_until_match(monkeypatch) -> None:
expected = [
"secret-tool",
"lookup",
"service",
"copilot-cli",
"account",
"https://github.com:octo",
]
calls: list[list[str]] = []
def fake_run(command, **kwargs): # noqa: ANN001, ANN003
calls.append(command)
stdout = "gho-secret\n" if command == expected else ""
return SimpleNamespace(returncode=0, stdout=stdout)
monkeypatch.setattr(copilot_linux_secret.sys, "platform", "linux")
monkeypatch.setattr(copilot_linux_secret, "_read_copilot_config_login", lambda: "octo")
monkeypatch.setattr(copilot_linux_secret.subprocess, "run", fake_run)
assert copilot_linux_secret.read_copilot_oauth_token(host="github.com") == "gho-secret"
assert expected in calls

View file

@ -0,0 +1,90 @@
from __future__ import annotations
from types import SimpleNamespace
import pytest
from headroom import copilot_macos_keychain
def test_read_copilot_oauth_token_uses_security(
monkeypatch: pytest.MonkeyPatch,
) -> None:
calls: list[list[str]] = []
def fake_run(command: list[str], **kwargs: object) -> SimpleNamespace:
calls.append(command)
assert kwargs["capture_output"] is True
assert kwargs["timeout"] == 5
return SimpleNamespace(returncode=0, stdout="gho-keychain\n")
monkeypatch.setattr(copilot_macos_keychain.sys, "platform", "darwin")
monkeypatch.setenv("GITHUB_COPILOT_KEYCHAIN_SERVICE", "GitHub Copilot")
monkeypatch.setenv("GITHUB_COPILOT_KEYCHAIN_ACCOUNT", "chopratejas")
monkeypatch.setattr(copilot_macos_keychain.subprocess, "run", fake_run)
assert copilot_macos_keychain.read_copilot_oauth_token(host="github.com") == "gho-keychain"
assert calls[0] == ["security", "find-generic-password", "-s", "GitHub Copilot", "-w"]
def test_read_copilot_oauth_token_returns_none_off_macos(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(copilot_macos_keychain.sys, "platform", "linux")
assert copilot_macos_keychain.read_copilot_oauth_token() is None
def test_candidate_security_commands_include_account_specific_lookup() -> None:
commands = copilot_macos_keychain._candidate_security_commands(
"github.com",
["GitHub Copilot"],
["chopratejas"],
)
assert ["security", "find-generic-password", "-s", "GitHub Copilot", "-w"] in commands
assert [
"security",
"find-generic-password",
"-s",
"GitHub Copilot",
"-a",
"chopratejas",
"-w",
] in commands
def test_read_copilot_oauth_token_tries_copilot_cli_host_login_account(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
calls: list[list[str]] = []
copilot_home = tmp_path / ".copilot"
copilot_home.mkdir()
(copilot_home / "config.json").write_text(
'{"lastLoggedInUser":{"host":"https://github.com","login":"chopratejas"}}',
encoding="utf-8",
)
def fake_run(command: list[str], **kwargs: object) -> object:
calls.append(command)
stdout = "gho-keychain\n" if command == expected else ""
return type("CompletedProcess", (), {"returncode": 0 if stdout else 44, "stdout": stdout})()
expected = [
"security",
"find-generic-password",
"-s",
"copilot-cli",
"-a",
"https://github.com:chopratejas",
"-w",
]
monkeypatch.setattr(copilot_macos_keychain.sys, "platform", "darwin")
monkeypatch.setenv("COPILOT_HOME", str(copilot_home))
monkeypatch.delenv("GITHUB_COPILOT_KEYCHAIN_SERVICE", raising=False)
monkeypatch.delenv("GITHUB_COPILOT_KEYCHAIN_ACCOUNT", raising=False)
monkeypatch.setattr(copilot_macos_keychain.subprocess, "run", fake_run)
assert copilot_macos_keychain.read_copilot_oauth_token(host="github.com") == "gho-keychain"
assert expected in calls

View file

@ -0,0 +1,199 @@
"""Cross-platform smoke test for GitHub Copilot subscription routing.
The subscription flow has to behave identically on macOS, Linux, and Windows
(and in headless Docker/CI), but the only OS-specific part reading the
Copilot CLI token from the platform secret store is impossible to exercise
portably. This suite proves the *portable* contract instead:
1. With an explicit token in the environment, resolution + API-URL discovery
succeed on every platform without touching any secret store. This is the
universal escape hatch (``GITHUB_COPILOT_TOKEN`` etc.) that makes the
feature work anywhere, including headless CI.
2. Each OS-specific secret reader is inert on a foreign platform so on any
given OS only that OS's reader can fire, and a missing/foreign secret store
degrades to ``None`` rather than crashing.
3. The proxy injects exactly the token the wrapper validated (the
deterministic-handoff fix), never a different discoverable one.
4. The full wrapperproxy chain carries one consistent token end to end.
Everything here is hermetic: no Keychain, no ``secret-tool``, no Credential
Manager, no network. It runs the same on every OS.
"""
from __future__ import annotations
import asyncio
import pytest
from headroom import copilot_auth, copilot_linux_secret, copilot_macos_keychain
BUSINESS_API = "https://api.business.githubcopilot.com"
def _stub_all_secret_stores(monkeypatch: pytest.MonkeyPatch) -> None:
"""Simulate 'no OS secret store / not logged in' on every platform."""
monkeypatch.setattr(copilot_auth, "_read_windows_copilot_cli_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_macos_keychain_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_linux_secret_oauth_token", lambda: None)
monkeypatch.setattr(copilot_auth, "_read_file_oauth_token_candidates", lambda: [])
monkeypatch.setattr(copilot_auth, "_read_gh_cli_oauth_token", lambda: None)
def _clear_token_env(monkeypatch: pytest.MonkeyPatch) -> None:
for var in (
*copilot_auth._COPILOT_OAUTH_TOKEN_ENV_VARS,
*copilot_auth._GENERIC_GITHUB_TOKEN_ENV_VARS,
*copilot_auth._API_TOKEN_ENV_VARS,
):
monkeypatch.delenv(var, raising=False)
# ---------------------------------------------------------------------------
# 1. The env-var path resolves on any platform with no secret store.
# ---------------------------------------------------------------------------
def test_env_token_resolves_subscription_without_secret_store(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_stub_all_secret_stores(monkeypatch)
_clear_token_env(monkeypatch)
monkeypatch.setenv("GITHUB_COPILOT_TOKEN", "gho-env-universal")
monkeypatch.setattr(
copilot_auth,
"_fetch_copilot_user_info",
lambda token: (
{"endpoints": {"api": BUSINESS_API}} if token == "gho-env-universal" else None
),
)
assert copilot_auth.resolve_subscription_bearer_token() == "gho-env-universal"
assert copilot_auth.resolve_copilot_api_url("gho-env-universal") == BUSINESS_API
def test_api_url_falls_back_to_default_when_user_info_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_clear_token_env(monkeypatch)
monkeypatch.delenv("GITHUB_COPILOT_API_URL", raising=False)
monkeypatch.setattr(copilot_auth, "_fetch_copilot_user_info", lambda token: None)
# No network / no endpoints advertised → safe default, never a crash.
assert copilot_auth.resolve_copilot_api_url("gho-anything") == copilot_auth.DEFAULT_API_URL
def test_subscription_rejects_token_github_does_not_accept(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_stub_all_secret_stores(monkeypatch)
_clear_token_env(monkeypatch)
# A generic GitHub token is present but GitHub's Copilot API rejects it;
# a valid Copilot token is discoverable behind it.
monkeypatch.setattr(
copilot_auth,
"iter_oauth_token_candidates",
lambda: [
copilot_auth.CopilotTokenCandidate(
token="ghp-generic-pat", source="env:GITHUB_TOKEN", confidence="generic-github"
),
copilot_auth.CopilotTokenCandidate(
token="gho-real-copilot",
source="macos-keychain:copilot-cli",
confidence="high",
),
],
)
monkeypatch.setattr(
copilot_auth,
"_fetch_copilot_user_info",
lambda token: {"endpoints": {"api": BUSINESS_API}} if token == "gho-real-copilot" else None,
)
assert copilot_auth.resolve_subscription_bearer_token() == "gho-real-copilot"
# ---------------------------------------------------------------------------
# 2. Each OS reader is inert on a foreign platform.
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("foreign_platform", ["linux", "win32"])
def test_macos_reader_noop_off_darwin(
monkeypatch: pytest.MonkeyPatch, foreign_platform: str
) -> None:
monkeypatch.setattr(copilot_macos_keychain.sys, "platform", foreign_platform)
assert copilot_macos_keychain.read_copilot_oauth_token(host="github.com") is None
@pytest.mark.parametrize("foreign_platform", ["darwin", "win32"])
def test_linux_reader_noop_off_linux(
monkeypatch: pytest.MonkeyPatch, foreign_platform: str
) -> None:
monkeypatch.setattr(copilot_linux_secret.sys, "platform", foreign_platform)
assert copilot_linux_secret.read_copilot_oauth_token(host="github.com") is None
def test_windows_reader_noop_off_windows(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(copilot_auth.os, "name", "posix")
assert copilot_auth._read_windows_copilot_cli_oauth_token() is None
# ---------------------------------------------------------------------------
# 3. The proxy injects exactly the wrapper-validated token (determinism).
# ---------------------------------------------------------------------------
def test_proxy_injects_explicit_token_over_discovered_one(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Reset the cached module-level provider so this test is self-contained.
monkeypatch.setattr(copilot_auth, "_provider", None)
# What `wrap copilot --subscription` exports for the proxy:
monkeypatch.setenv("GITHUB_COPILOT_API_TOKEN", "gho-validated")
monkeypatch.setenv("GITHUB_COPILOT_API_URL", BUSINESS_API)
monkeypatch.setenv("GITHUB_COPILOT_USE_TOKEN_EXCHANGE", "false")
# A *different* token is discoverable — it must be ignored entirely.
monkeypatch.setattr(
copilot_auth, "read_cached_oauth_token", lambda: "gho-WRONG-should-not-be-used"
)
headers = asyncio.run(
copilot_auth.apply_copilot_api_auth(
{"authorization": "Bearer placeholder"},
url=f"{BUSINESS_API}/v1/chat/completions",
)
)
assert headers["Authorization"] == "Bearer gho-validated"
assert "authorization" not in headers
# ---------------------------------------------------------------------------
# 4. Full wrapper→proxy chain carries one consistent token, any account host.
# ---------------------------------------------------------------------------
def test_end_to_end_subscription_chain(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(copilot_auth, "_provider", None)
# (a) wrapper side: resolve + validate the subscription token, then
# discover the account-specific API endpoint.
_stub_all_secret_stores(monkeypatch)
_clear_token_env(monkeypatch)
monkeypatch.setenv("GITHUB_COPILOT_TOKEN", "gho-seat-token")
monkeypatch.setattr(
copilot_auth,
"_fetch_copilot_user_info",
lambda token: {"endpoints": {"api": BUSINESS_API}} if token == "gho-seat-token" else None,
)
resolved_token = copilot_auth.resolve_subscription_bearer_token()
resolved_url = copilot_auth.resolve_copilot_api_url(resolved_token)
assert resolved_token == "gho-seat-token"
assert resolved_url == BUSINESS_API
# (b) hand-off: the wrapper exports exactly these for the proxy.
monkeypatch.setenv("GITHUB_COPILOT_API_TOKEN", resolved_token)
monkeypatch.setenv("GITHUB_COPILOT_API_URL", resolved_url)
# (c) proxy side: build the upstream URL (Copilot has no /v1 prefix) and
# inject the same token onto the outbound request.
upstream = copilot_auth.build_copilot_upstream_url(resolved_url, "/v1/chat/completions")
assert upstream == "https://api.business.githubcopilot.com/chat/completions"
headers = asyncio.run(
copilot_auth.apply_copilot_api_auth({"authorization": "Bearer placeholder"}, url=upstream)
)
assert headers["Authorization"] == f"Bearer {resolved_token}"

View file

@ -466,7 +466,7 @@ class TestDetectDefaultModel:
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("GEMINI_API_KEY", "test")
assert _detect_default_model() == "gemini/gemini-2.0-flash"
assert _detect_default_model() == "gemini/gemini-flash-latest"
def test_anthropic_preferred_over_openai(self, monkeypatch):
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")

View file

@ -1,8 +1,11 @@
from __future__ import annotations
import threading
from types import MethodType, SimpleNamespace
from headroom.proxy.handlers import openai as openai_handler
from headroom.proxy.handlers.openai import OpenAIHandlerMixin
from headroom.transforms.compression_units import UnitCompressionResult
from headroom.transforms.content_router import (
CompressionStrategy,
ContentRouter,
@ -24,6 +27,69 @@ def _handler_with_router(router: ContentRouter) -> OpenAIHandlerMixin:
return handler
def test_openai_responses_unit_parallelism_env_defaults_and_clamps(monkeypatch):
monkeypatch.delenv("HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM", raising=False)
assert openai_handler._openai_responses_unit_parallelism() == 4
monkeypatch.setenv("HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM", "bad")
assert openai_handler._openai_responses_unit_parallelism() == 4
monkeypatch.setenv("HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM", "0")
assert openai_handler._openai_responses_unit_parallelism() == 1
monkeypatch.setenv("HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM", "999")
assert openai_handler._openai_responses_unit_parallelism() == 16
def test_openai_responses_cached_unit_handles_results_without_router_result():
result = UnitCompressionResult(
original="original",
compressed="compressed",
modified=True,
tokens_before=2,
tokens_after=1,
tokens_saved=1,
transforms_applied=[],
strategy="none",
router_result=None,
)
assert openai_handler._openai_responses_result_with_cache_hit(result) is result
def test_openai_responses_unit_cache_evicts_oldest_entry(monkeypatch):
monkeypatch.setattr(openai_handler, "_OPENAI_RESPONSES_UNIT_CACHE_MAX_ENTRIES", 1)
handler = OpenAIHandlerMixin()
first = UnitCompressionResult(
original="first",
compressed="first compressed",
modified=True,
tokens_before=2,
tokens_after=1,
tokens_saved=1,
transforms_applied=[],
strategy="none",
router_result=None,
)
second = UnitCompressionResult(
original="second",
compressed="second compressed",
modified=True,
tokens_before=2,
tokens_after=1,
tokens_saved=1,
transforms_applied=[],
strategy="none",
router_result=None,
)
handler._store_openai_responses_cached_unit("first", first)
handler._store_openai_responses_cached_unit("second", second)
assert handler._get_openai_responses_cached_unit("first") is None
assert handler._get_openai_responses_cached_unit("second") is second
def test_openai_responses_adapter_compresses_only_live_text_slots():
router = ContentRouter()
@ -110,6 +176,162 @@ def test_openai_responses_adapter_compresses_custom_tool_call_output():
assert strategy_chain == []
def test_openai_responses_adapter_reuses_exact_tool_output_cache():
router = ContentRouter()
calls = {"count": 0}
def compress(self, content: str, **_kwargs):
calls["count"] += 1
return RouterCompressionResult(
compressed="cached output summary",
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
router.compress = MethodType(compress, router)
handler = _handler_with_router(router)
long_text = " ".join(f"word{i}" for i in range(180))
payload_one = {
"model": "gpt-5",
"input": [
{"type": "local_shell_call_output", "call_id": "c1", "output": long_text},
],
}
payload_two = {
"model": "gpt-5",
"input": [
{"type": "message", "role": "user", "content": "changed envelope"},
{"type": "local_shell_call_output", "call_id": "c2", "output": long_text},
],
}
new_payload_one, modified_one, saved_one, *_ = (
handler._compress_openai_responses_live_text_units_with_router(
payload_one,
model="gpt-5",
request_id="req_cache_one",
)
)
new_payload_two, modified_two, saved_two, *_ = (
handler._compress_openai_responses_live_text_units_with_router(
payload_two,
model="gpt-5",
request_id="req_cache_two",
)
)
assert calls["count"] == 1
assert modified_one is True
assert modified_two is True
assert saved_one > 0
assert saved_two == saved_one
assert new_payload_one["input"][0]["output"] == "cached output summary"
assert new_payload_two["input"][1]["output"] == "cached output summary"
def test_openai_responses_adapter_reuses_identical_tool_output_in_same_request():
router = ContentRouter()
calls = {"count": 0}
def compress(self, content: str, **_kwargs):
calls["count"] += 1
return RouterCompressionResult(
compressed="same request cached summary",
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
router.compress = MethodType(compress, router)
handler = _handler_with_router(router)
long_text = " ".join(f"word{i}" for i in range(180))
payload = {
"model": "gpt-5",
"input": [
{"type": "function_call_output", "call_id": "c1", "output": long_text},
{"type": "function_call_output", "call_id": "c2", "output": long_text},
],
}
new_payload, modified, saved, *_ = (
handler._compress_openai_responses_live_text_units_with_router(
payload,
model="gpt-5",
request_id="req_same_request_cache",
)
)
assert calls["count"] == 1
assert modified is True
assert saved > 0
assert [item["output"] for item in new_payload["input"]] == [
"same request cached summary",
"same request cached summary",
]
def test_openai_responses_adapter_parallelizes_cache_misses_preserving_order(monkeypatch):
monkeypatch.setenv("HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM", "4")
router = ContentRouter()
lock = threading.Lock()
release = threading.Event()
active = {"count": 0, "max": 0}
def compress(self, content: str, **_kwargs):
with lock:
active["count"] += 1
active["max"] = max(active["max"], active["count"])
if active["count"] >= 2:
release.set()
release.wait(0.05)
try:
marker = content.rsplit(" marker", 1)[1]
return RouterCompressionResult(
compressed=f"summary marker{marker}",
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
finally:
with lock:
active["count"] -= 1
router.compress = MethodType(compress, router)
handler = _handler_with_router(router)
def long_text(index: int) -> str:
return " ".join(f"word{index}_{j}" for j in range(180)) + f" marker{index}"
payload = {
"model": "gpt-5",
"input": [
{
"type": "local_shell_call_output",
"call_id": f"c{i}",
"output": long_text(i),
}
for i in range(4)
],
}
new_payload, modified, saved, *_ = (
handler._compress_openai_responses_live_text_units_with_router(
payload,
model="gpt-5",
request_id="req_parallel",
)
)
assert active["max"] >= 2
assert modified is True
assert saved > 0
assert [item["output"] for item in new_payload["input"]] == [
"summary marker0",
"summary marker1",
"summary marker2",
"summary marker3",
]
def test_openai_responses_adapter_accepts_empty_input_list():
router = ContentRouter()
handler = _handler_with_router(router)

View file

@ -0,0 +1,216 @@
from __future__ import annotations
import json
import threading
from dataclasses import dataclass
from types import MethodType, SimpleNamespace
from headroom.proxy.handlers.openai import OpenAIHandlerMixin
from headroom.transforms.content_router import (
CompressionStrategy,
ContentRouter,
RouterCompressionResult,
)
@dataclass(frozen=True)
class T3FailureCase:
provider_log: str
turn_id: str
request_bytes: int
unit_count: int
# T3 provider logs keep the Headroom 413 metadata, not the raw /v1/responses
# body. These cases recreate the failing byte scale and Responses item shape.
T3_FAILED_CASES = (
T3FailureCase(
provider_log="2b38b84f-b6b0-4d92-8ff0-42f83b59dd70.log",
turn_id="019e8c3f-91d9-73b3-a6f8-4e6ae312f91b",
request_bytes=674_436,
unit_count=8,
),
T3FailureCase(
provider_log="cc084653-feba-4241-a8fd-6655c0dfa799.log",
turn_id="019e8bdd-ffb3-7f31-9182-51b2bdb96f52",
request_bytes=1_288_876,
unit_count=12,
),
)
class TokenCounter:
def count_text(self, text: str) -> int:
return max(1, len(text) // 4)
def _handler_with_router(router: ContentRouter) -> OpenAIHandlerMixin:
handler = OpenAIHandlerMixin()
handler.openai_pipeline = SimpleNamespace(transforms=[router])
handler.openai_provider = SimpleNamespace(
get_token_counter=lambda _model: TokenCounter(),
)
return handler
def _tool_output(case: T3FailureCase, index: int, target_bytes: int) -> str:
line = (
f"{case.turn_id} {case.provider_log} "
f"tool={index} path=/tmp/t3-live-output-{index}.txt status=ok "
"alpha beta gamma delta epsilon zeta eta theta iota kappa\n"
)
return (line * ((target_bytes // len(line)) + 1))[:target_bytes]
def _payload_for_case(case: T3FailureCase) -> dict:
envelope_budget = 2_500
per_unit_bytes = max(2_048, (case.request_bytes - envelope_budget) // case.unit_count)
return {
"model": "gpt-5.4-mini",
"input": [
{
"type": "message",
"role": "user",
"content": "continue after tool output",
},
{
"type": "function_call",
"call_id": "call-shell",
"name": "shell",
"arguments": "{}",
},
*[
{
"type": "function_call_output",
"call_id": f"call-shell-{index}",
"output": _tool_output(case, index, per_unit_bytes),
}
for index in range(case.unit_count)
],
],
}
def _json_bytes(value: object) -> int:
return len(json.dumps(value, separators=(",", ":"), default=str).encode("utf-8"))
def test_t3_failed_size_responses_payload_parallelizes_uncached_tool_outputs(monkeypatch):
monkeypatch.setenv("HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM", "4")
case = T3_FAILED_CASES[0]
router = ContentRouter()
lock = threading.Lock()
release = threading.Event()
active = {"count": 0, "max": 0, "calls": 0}
def compress(self, content: str, **_kwargs):
with lock:
active["count"] += 1
active["calls"] += 1
active["max"] = max(active["max"], active["count"])
if active["count"] >= 2:
release.set()
release.wait(0.05)
try:
marker = content.split(" tool=", 1)[1].split(" ", 1)[0]
return RouterCompressionResult(
compressed=f"summary for tool={marker}",
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
finally:
with lock:
active["count"] -= 1
router.compress = MethodType(compress, router)
handler = _handler_with_router(router)
payload = _payload_for_case(case)
new_payload, modified, saved, transforms, units_by_category, _strategy_chain, attempted = (
handler._compress_openai_responses_live_text_units_with_router(
payload,
model="gpt-5.4-mini",
request_id=f"t3_replay_{case.turn_id}",
)
)
assert _json_bytes(payload) >= case.request_bytes * 0.95
assert attempted > 0
assert modified is True
assert saved > 0
assert active["calls"] == case.unit_count
assert active["max"] >= 2
assert units_by_category == {"applied": case.unit_count}
assert "router:openai:responses:function_call_output:kompress" in transforms
outputs = [
item["output"]
for item in new_payload["input"]
if item.get("type") == "function_call_output"
]
assert outputs == [f"summary for tool={index}" for index in range(case.unit_count)]
def test_t3_failed_size_exact_tool_output_cache_survives_history_changes():
case = T3_FAILED_CASES[1]
router = ContentRouter()
calls = {"count": 0}
def compress(self, content: str, **_kwargs):
calls["count"] += 1
marker = content.split(" tool=", 1)[1].split(" ", 1)[0]
return RouterCompressionResult(
compressed=f"cached summary for tool={marker}",
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
router.compress = MethodType(compress, router)
handler = _handler_with_router(router)
first_payload = _payload_for_case(case)
second_payload = {
"model": "gpt-5.4-mini",
"input": [
# Simulate a harness that changed/trimmed the ancient envelope.
{"type": "message", "role": "user", "content": "history compacted"},
*first_payload["input"][2:],
{
"type": "function_call_output",
"call_id": "call-shell-new",
"output": _tool_output(case, case.unit_count, 32_000),
},
],
}
first_new_payload, first_modified, first_saved, *_ = (
handler._compress_openai_responses_live_text_units_with_router(
first_payload,
model="gpt-5.4-mini",
request_id=f"t3_replay_cache_first_{case.turn_id}",
)
)
second_new_payload, second_modified, second_saved, *_ = (
handler._compress_openai_responses_live_text_units_with_router(
second_payload,
model="gpt-5.4-mini",
request_id=f"t3_replay_cache_second_{case.turn_id}",
)
)
assert first_modified is True
assert second_modified is True
assert first_saved > 0
assert second_saved > 0
assert calls["count"] == case.unit_count + 1
assert [
item["output"]
for item in first_new_payload["input"]
if item.get("type") == "function_call_output"
] == [f"cached summary for tool={index}" for index in range(case.unit_count)]
assert [
item["output"]
for item in second_new_payload["input"]
if item.get("type") == "function_call_output"
] == [
*[f"cached summary for tool={index}" for index in range(case.unit_count)],
f"cached summary for tool={case.unit_count}",
]

View file

@ -0,0 +1,59 @@
"""Regression tests for UTF-8 decoding/encoding of headroom-owned assets.
These guard against ``UnicodeDecodeError`` on systems whose default text
encoding is not UTF-8 (e.g. Windows ``cp949``/``cp1252`` locales). Headroom
ships and writes its own templates, JSON state and config files as UTF-8, so
they must be read and written with an explicit ``encoding="utf-8"`` rather than
relying on the platform default codec. See issue #533.
"""
from __future__ import annotations
from pathlib import Path
from headroom.dashboard import TEMPLATES_DIR, get_dashboard_html
from headroom.memory.sync import _load_sync_state, _save_sync_state
def test_dashboard_template_contains_non_ascii() -> None:
"""The bundled template has non-ASCII bytes, so the bug is reproducible."""
raw = (TEMPLATES_DIR / "dashboard.html").read_bytes()
assert any(byte > 0x7F for byte in raw), "template expected to contain non-ASCII bytes"
def test_get_dashboard_html_reads_as_utf8(monkeypatch) -> None:
"""get_dashboard_html must decode the template as UTF-8, not the OS default.
Before the fix, ``read_text()`` used the platform default codec and raised
``UnicodeDecodeError`` on non-UTF-8 locales. We assert the explicit encoding
is passed so the regression cannot silently return (a utf-8 CI host would
otherwise mask it).
"""
captured: dict[str, object] = {}
original = Path.read_text
def _spy(self: Path, *args: object, **kwargs: object) -> str:
captured["encoding"] = kwargs.get("encoding")
return original(self, *args, **kwargs) # type: ignore[arg-type]
monkeypatch.setattr(Path, "read_text", _spy)
html = get_dashboard_html()
assert captured["encoding"] == "utf-8"
assert html # non-empty
# Content must equal an explicit UTF-8 decode of the raw template.
expected = (TEMPLATES_DIR / "dashboard.html").read_bytes().decode("utf-8")
assert html == expected
def test_sync_state_round_trips_non_ascii(tmp_path) -> None:
"""JSON sync state with non-ASCII values must survive a save/load round-trip."""
state_path = tmp_path / "nested" / "sync_state.json"
state = {"agent": "café", "note": "한국어 메모", "emoji": "🚀"}
_save_sync_state(state_path, state)
# Persisted bytes must be valid UTF-8 regardless of the platform default.
assert state_path.read_bytes().decode("utf-8")
assert _load_sync_state(state_path) == state

View file

@ -34,6 +34,23 @@ class TestTiktokenCounter:
assert counter.model == "gpt-4"
assert counter.encoding_name == "cl100k_base"
def test_unknown_gpt4_snapshot_uses_cl100k(self):
"""Unknown gpt-4 (non-o, non-turbo) snapshots must use cl100k_base.
Regression: the prefix matcher scanned MODEL_TO_ENCODING for the
first key starting with the prefix. For prefix "gpt-4" that matched
the "gpt-4o" entry first and wrongly returned o200k_base for any
gpt-4 snapshot not in the table (e.g. a future dated build).
"""
from headroom.tokenizers.tiktoken_counter import get_encoding_for_model
assert get_encoding_for_model("gpt-4-2025-01-01") == "cl100k_base"
assert get_encoding_for_model("gpt-4-future") == "cl100k_base"
# gpt-4o snapshots still resolve to o200k_base (most-specific first).
assert get_encoding_for_model("gpt-4o-2099-12-31") == "o200k_base"
# gpt-4-turbo snapshots use cl100k_base.
assert get_encoding_for_model("gpt-4-turbo-2099") == "cl100k_base"
def test_count_text_empty(self):
"""Test counting empty text."""
counter = TiktokenCounter()

View file

@ -171,6 +171,66 @@ def test_mixed_content_section_splitting_and_json_extraction() -> None:
assert _extract_json_block(["{", '"a": 1'], 0) == (None, 0)
def test_extract_json_block_ignores_brackets_inside_strings() -> None:
"""Brackets/braces inside JSON string values must not end the block early.
Regression: counting raw ``[``/``]``/``{``/``}`` per line treated the
``]`` inside ``{"path": "a]b"}`` as a closing bracket, so the array was
truncated mid-way and the remaining rows leaked into later sections.
"""
import json as _json
lines = [
"[",
' {"path": "a]b"},',
' {"path": "c"}',
"]",
]
block, end_idx = _extract_json_block(lines, 0)
assert end_idx == 3
assert block is not None
parsed = _json.loads(block)
assert parsed == [{"path": "a]b"}, {"path": "c"}]
# Braces inside a string value must likewise be ignored.
obj_lines = [
"{",
' "msg": "use {curly} and [square]",',
' "n": 1',
"}",
]
obj_block, obj_end = _extract_json_block(obj_lines, 0)
assert obj_end == 3
assert obj_block is not None
assert _json.loads(obj_block) == {"msg": "use {curly} and [square]", "n": 1}
def test_split_into_sections_keeps_json_array_with_bracket_in_string() -> None:
"""A JSON array embedded in prose stays one JSON section, not fragments.
With the bracket-in-string bug, the array below split into a truncated
JSON section plus a stray ``]`` glued onto the trailing prose.
"""
import json as _json
content = "\n".join(
[
"prose line here that is long enough to matter",
"[",
' {"path": "a]b"},',
' {"path": "c"}',
"]",
"trailing prose",
]
)
sections = split_into_sections(content)
json_sections = [s for s in sections if s.content_type == ContentType.JSON_ARRAY]
assert len(json_sections) == 1
parsed = _json.loads(json_sections[0].content)
assert parsed == [{"path": "a]b"}, {"path": "c"}]
def test_content_router_strategy_and_compress_paths(monkeypatch: pytest.MonkeyPatch) -> None:
router = ContentRouter(ContentRouterConfig(prefer_code_aware_for_code=False))

2
uv.lock generated
View file

@ -1200,7 +1200,7 @@ wheels = [
[[package]]
name = "gitpython"
version = "3.1.46"
version = "3.1.47"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "gitdb" },

View file

@ -76,6 +76,7 @@ When configured, Headroom emits OTLP traces for the shared compression pipeline
| `--budget` | None | Daily budget limit in USD |
| `--code-aware` | true | Enable AST-based code compression (env: HEADROOM_CODE_AWARE_ENABLED) |
| `--no-code-aware` | false | Disable code-aware compression |
| `--anthropic-api-url` | `https://api.anthropic.com` | Custom Anthropic API URL endpoint |
| `--openai-api-url` | `https://api.openai.com` | Custom OpenAI API URL endpoint |
### Run Modes
@ -379,7 +380,13 @@ headroom_latency_ms_sum
export HEADROOM_HOST=0.0.0.0
export HEADROOM_PORT=8787
export HEADROOM_BUDGET=100.0
# Route OpenAI passthrough requests to a custom endpoint
export OPENAI_TARGET_API_URL=https://custom.openai.endpoint.com
# Route Anthropic passthrough requests to a custom endpoint
export ANTHROPIC_TARGET_API_URL=https://litellm.company.internal
headroom proxy
```