mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
8 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1db6d88ab4
|
fix(wrap): honor Copilot OAuth wire-api override and model default (#2387)
## Description `headroom wrap copilot -- --model gpt-5.4` fails with `400 The requested model is not available for integrator "copilot-language-server"`, even though the same GitHub account uses GPT-5.4 fine in the native `copilot` CLI. On the hosted Copilot OAuth path (authenticated via `headroom copilot-auth`, no `--subscription`), `headroom/cli/wrap.py` forced the `completions` wire API for every non-`--subscription` launch and ignored a caller-supplied `COPILOT_PROVIDER_WIRE_API`. GPT-5.x / o-series reasoning models need the `responses` wire API. The OAuth path now honors a valid inherited `COPILOT_PROVIDER_WIRE_API` (`completions` or `responses`) and otherwise uses the model-aware default via `_copilot_default_wire_api_for_model(selected_model)`, so GPT-5.x routes to `responses` while GPT-4.1 stays on `completions`. The `--subscription` path is unchanged. This supersedes the earlier closed #2243, which mixed the fix with unrelated proxy/CI changes and hit merge conflicts. This PR ships only the `headroom/cli/wrap.py` wire-api selection plus regression tests, rebased clean on `main`. Closes #2222 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Hosted Copilot OAuth launch resolves the wire API from an explicit `COPILOT_PROVIDER_WIRE_API` env value when it is `completions`/`responses`, else from `_copilot_default_wire_api_for_model(selected_model)` instead of a hardcoded `completions`. The `subscription`-only gating on the model-aware default is removed so OAuth and subscription paths pick the same model-correct wire API. ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ .venv/bin/python -m pytest tests/test_cli/test_wrap_copilot.py tests/test_provider_copilot_wrap.py -q 70 passed in 0.28s ``` New tests cover: the OAuth path honoring an inherited `COPILOT_PROVIDER_WIRE_API`, the OAuth path resolving GPT-5.4 to `responses`, and `default_wire_api_for_model("gpt-5.4")` returning `responses`. ## Real Behavior Proof - Environment: local checkout, Python 3.14, target tests only. - Exact command / steps: `.venv/bin/python -m pytest tests/test_cli/test_wrap_copilot.py tests/test_provider_copilot_wrap.py -q` - Observed result: 70 passed; the OAuth-path tests assert `env["COPILOT_PROVIDER_WIRE_API"] == "responses"` for GPT-5.x and honor an inherited override. - Not tested: the end-to-end live Copilot `400` reproduction, which needs a real Copilot OAuth session plus a GPT-5.x request. The wire-api selection that caused the `400` is covered by the unit tests above. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` — it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this) ## Screenshots (if applicable) N/A — CLI behavior change covered by the unit tests above. ## Additional Notes The change is scoped to the wire-api selection line; the `--subscription` path and the existing explicit `--wire-api` CLI flag are untouched. AI was used for assistance. --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> |
||
|
|
24cf256e50
|
fix: respect COPILOT_PROVIDER_TYPE env var when provider_type is auto (#549)
## Description Fixes #297 by respecting `COPILOT_PROVIDER_TYPE` when Copilot provider type resolution is set to `auto`, while keeping explicit `--provider-type` values authoritative. Invalid environment values now fall back to the backend-based default instead of silently selecting a surprising provider. ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Documentation - [ ] Refactor - [ ] Tests only ## Changes Made - Added guarded `COPILOT_PROVIDER_TYPE` handling for `anthropic` and `openai` in `resolve_provider_type()`. - Preserved explicit provider type precedence over environment configuration. - Added focused tests for explicit precedence, environment precedence, invalid env fallback, and backend defaults. ## Testing - [x] Unit tests - [x] Lint/static checks - [ ] Integration tests - [ ] Manual testing ### Test Output ```text UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --with pytest python -m pytest tests/test_provider_copilot_wrap.py -q 8 passed, 1 warning in 0.62s UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --with ruff ruff check headroom/providers/copilot/wrap.py tests/test_provider_copilot_wrap.py All checks passed! ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13.3, focused local worktree for PR #549. - Exact command / steps: Ran the focused Copilot provider wrap test module and ruff against the changed production/test files. - Observed result: Provider selection tests pass, and ruff reports no issues. - Not tested: Full repository mypy/pre-commit; existing unrelated Windows `fcntl` typing errors block full hook execution locally. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review <!-- headroom-maintainer-template-completion:start --> ## Description This PR prepares `fix: respect COPILOT_PROVIDER_TYPE env var when provider_type is auto` for review by documenting the intended change, validation evidence, and remaining merge-readiness context. Linked issues: #297 ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Documentation - [ ] Refactor - [ ] Tests only ## Changes Made - Commit: fix: add encoding='utf-8' to read_text() for UnicodeDecodeError on no… - Commit: fix: respect COPILOT_PROVIDER_TYPE env var in resolve_provider_type - Commit: Merge remote-tracking branch 'origin/main' into fix-copilot-provider-… - Commit: test(copilot): cover provider type env precedence - Touches `headroom/cache/dynamic_detector.py` - Touches `headroom/providers/copilot/wrap.py` - Touches `tests/test_provider_copilot_wrap.py` ## Testing - [x] GitHub checks reviewed - [x] Metadata/template validation - [ ] Local functional testing ### Test Output ```text gh pr view 549 --repo chopratejas/headroom --json statusCheckRollup - PR Governance / template: FAILURE - PR Governance / label: SUCCESS - external / GitGuardian Security Checks: SUCCESS ``` ## Real Behavior Proof - Environment: GitHub PR metadata and checks for `chopratejas/headroom` PR #549. - Exact command / steps: Reviewed PR title, commits, changed files, linked issues, labels, and check rollup; appended this maintainer template completion block without replacing the author's original description. - Observed result: PR body now contains all required governance sections, checked readiness fields, and a non-placeholder validation evidence block. - Not tested: This pass updated PR metadata only; code validation remains represented by the linked GitHub checks and any author-provided evidence above. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review <!-- headroom-maintainer-template-completion:end --> --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
e67ee2af65
|
feat: add Copilot BYOK provider wrapper utilities and CLI support (#1041)
## Description Fix `--model auto` causing `400 The requested model is not supported` errors when using Copilot BYOK mode. `auto` is a Copilot-internal virtual routing token that external providers (Anthropic, OpenAI) do not recognise as a valid model name. In subscription/OAuth mode the wrapper now strips `--model auto` before launching Copilot so its own native auto-selection takes effect. In BYOK mode `auto` is treated as unconfigured and a clear, actionable error message is shown. Closes #972 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/providers/copilot/wrap.py`: added `is_auto_model()` and `strip_auto_model_args()` helpers; updated `model_configured()` to treat `auto` as unconfigured for BYOK - `headroom/providers/copilot/__init__.py`: exported both new helpers via `__all__` - `headroom/cli/wrap.py`: strips `--model auto` in subscription mode before launch; shows specific actionable error in BYOK mode - `tests/test_provider_copilot_wrap.py`: 17 new parametrized test cases for `is_auto_model`, `strip_auto_model_args`, and updated `model_configured` ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uv run pytest tests/test_provider_copilot_wrap.py -v platform win32 -- Python 3.14.6, pytest-9.0.3, pluggy-1.6.0 collected 34 items tests/test_provider_copilot_wrap.py::test_is_auto_model[auto-True] PASSED tests/test_provider_copilot_wrap.py::test_is_auto_model[Auto-True] PASSED tests/test_provider_copilot_wrap.py::test_is_auto_model[AUTO-True] PASSED tests/test_provider_copilot_wrap.py::test_strip_auto_model_args[args0-expected0] PASSED tests/test_provider_copilot_wrap.py::test_strip_auto_model_args[args1-expected1] PASSED tests/test_provider_copilot_wrap.py::test_strip_auto_model_args[args2-expected2] PASSED tests/test_provider_copilot_wrap.py::test_model_configured_detects_env_and_cli_variants PASSED ============================= 34 passed in 0.46s ============================== $ ruff check headroom/providers/copilot/wrap.py headroom/cli/wrap.py All checks passed! ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.14.6, headroom-ai 0.25.0 editable install from branch fix-automode-issue - Exact command / steps: ran uv run pytest tests/test_provider_copilot_wrap.py -v and ruff check on all four changed files; reviewed CLI code path for both subscription and BYOK modes - Observed result: 34 passed, ruff All checks passed; --model auto is stripped silently in subscription mode and rejected with a specific actionable error in BYOK mode - Not tested: live end-to-end Copilot CLI session, macOS/Linux keychain auth, Docker/CI token-injection paths ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes mypy is not installed in the local venv so type checking was skipped; the code uses standard type hints and passes ruff checks cleanly. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
914a60a2b0
|
feat(proxy): per-project savings breakdown on the dashboard (claude, codex, aider, copilot, cursor) (#803)
## Summary Adds a **per-project savings breakdown** to the proxy dashboard, covering **all wrap-supported agents: Claude Code, Codex, aider, Copilot, and Cursor**. Spec and rationale in #802 (feature-request issue — happy to adjust scope per maintainer feedback). How it works — two attribution channels, by client capability: **Header channel** (clients that can send custom headers): - `headroom wrap claude` appends `X-Headroom-Project: <basename(cwd)>` to `ANTHROPIC_CUSTOM_HEADERS` (a user-supplied x-headroom-project header always wins; other user headers preserved). - `headroom wrap codex` extends the injected `[model_providers.headroom]` block with `env_http_headers = { "X-Headroom-Project" = "HEADROOM_PROJECT" }` and sets `HEADROOM_PROJECT` per launch — Codex only sends the header when the env var is set, so the static config stays inert outside wrap. **Base-URL prefix channel** (clients that cannot send custom headers): - The proxy accepts `/p/<url-encoded-name>/...`; middleware strips the prefix before routing (before Starlette caches the URL) and binds the project. The explicit header wins over the prefix. - `headroom wrap aider` points `OPENAI_API_BASE` / `ANTHROPIC_BASE_URL` at the prefixed URL. - `headroom wrap copilot` points `COPILOT_PROVIDER_BASE_URL` at the prefixed URL (BYOK anthropic/openai provider types and the GitHub-subscription path). - `headroom wrap cursor` prints the prefixed Override Base URL in its setup instructions, with a note explaining the attribution. **Shared plumbing:** - New `headroom/proxy/project_context.py`: header classification, `/p/` prefix split/strip, URL-prefix builder, and a contextvar bound per request (HTTP middleware + WS accept for the Codex responses bridge); the outcome funnel resolves it (explicit `RequestOutcome.project` wins), stamps `RequestLog.tags["project"]`, and forwards it through `PrometheusMetrics.record_request` into the `SavingsTracker`. - `SavingsTracker`: persisted state gains a `projects` map (requests, tokens saved, savings USD, input tokens/cost, last activity). Schema v2 → v3 with transparent forward migration (v2 files load cleanly, `projects` starts empty). Names sanitized (printable-only, 128-char cap); map capped at 50 projects, evicting the smallest bucket. - `/stats` exposes `savings.per_project` (and `projects`/`projects_limit` inside `persistent_savings`); `/stats-history` exposes `projects`. - Dashboard gains a "Per-Project Savings" table mirroring the per-model table (Alpine `x-text` only — names are user-supplied, no HTML injection). **Behavior changes:** none for unattributed traffic — no header and no prefix means no bucket, aggregate totals exactly as before (regression-tested: legacy `/stats` and `/stats-history` shapes are pinned by tests). ## Real behavior proof **Setup tested on:** macOS (Darwin 25.5.0), Python 3.11.9, `pip install -e ".[dev]"`, proxy on spare ports with isolated `HEADROOM_SAVINGS_PATH`; real Claude Code CLI (subscription auth) and real Codex CLI (ChatGPT auth) as clients. **Header channel — exact steps:** ``` HEADROOM_SAVINGS_PATH=/tmp/headroom-proof-savings.json .venv/bin/python -m headroom.cli proxy --port 9123 # background cd /tmp/proof-alpha && headroom wrap claude --port 9123 --no-rtk --no-mcp --no-serena -- -p "Reply with exactly: OK" cd /tmp/proof-beta && headroom wrap claude --port 9123 --no-rtk --no-mcp --no-serena -- -p "Reply with exactly: OK" cd /tmp/proof-beta && headroom wrap codex --port 9123 --no-rtk --no-mcp --no-serena -- exec --skip-git-repo-check "Reply with exactly: OK" ``` **Observed** (copied live `/stats` output; all runs replied `OK` through the proxy): ```json { "proof-beta": { "requests": 3, "tokens_saved": 140, "compression_savings_usd": 0.0007, "total_input_tokens": 38581, "total_input_cost_usd": 0.360433, "last_activity_at": "2026-06-10T09:19:17Z", "savings_percent": 0.36 }, "proof-alpha": { "requests": 1, "tokens_saved": 0, "compression_savings_usd": 0.0, "total_input_tokens": 9050, "total_input_cost_usd": 0.32245, "last_activity_at": "2026-06-10T09:18:09Z", "savings_percent": 0.0 } } ``` `proof-beta` aggregates one Claude Code turn + one Codex `exec` run (Codex attribution flows through `env_http_headers`). **Prefix channel — exact steps** (the same mechanism the aider/copilot/cursor wraps emit, driven by a real client): ``` .venv/bin/python -m headroom.cli proxy --port 9124 # background, isolated savings path ANTHROPIC_BASE_URL="http://127.0.0.1:9124/p/aider-style-project" claude -p "Reply with exactly: OK" ``` **Observed:** ```json { "aider-style-project": { "requests": 1, "tokens_saved": 0, "compression_savings_usd": 0.0, "total_input_tokens": 8571, "total_input_cost_usd": 1.018575, "last_activity_at": "2026-06-10T10:10:13Z", "savings_percent": 0.0 } } ``` `/stats-history` returned `schema_version: 3` with the same `projects` keys; `GET /dashboard` HTML contains the new "Per-Project Savings" table; persisted state survived a tracker reload; a hand-written v2 savings file loaded cleanly with an empty `projects` map. **What I did NOT test live:** the actual aider/Copilot/Cursor binaries end-to-end (their wraps emit exactly the prefixed URLs exercised above — unit tests pin the emitted env/URLs); Codex subscription-mode routing via the built-in `openai` provider (no provider headers there — such traffic simply stays unattributed); multi-worker uvicorn; Windows. ## Tests - `tests/test_proxy_project_savings.py` (17 tests): sanitization, header classification, `/p/` prefix split + URL-builder round-trip, tracker aggregation/persistence/migration/cardinality-cap/state-sanitization, funnel→`/stats` end-to-end, middleware binding for header + prefix + precedence, plus regression tests pinning legacy `/stats`/`/stats-history` shape and unattributed-traffic totals. - Wrap/provider tests extended: `test_cli/test_wrap_helpers.py`, `test_cli/test_wrap_codex.py`, `test_provider_aider.py`, `test_provider_cursor.py`, `test_provider_copilot_wrap.py` (prefixed env/URLs, user-override wins, no duplicate header, TOML block contents, block strip, setup-line note). - Full `pytest` suite run locally; `ruff check` + `ruff format` clean on all touched files. - `CHANGELOG.md` updated. ## Dependencies None added or bumped. Closes #802 (pending maintainer 👍 per CONTRIBUTING — raised there first with the full spec). --------- Co-authored-by: Ash Rhodes <ashley.rhodes@king.com> |
||
|
|
84ac332d14
|
fix(copilot): use responses API for subscription reasoning models (#647)
Fixes #644 ## Summary - default `headroom wrap copilot --subscription` to the responses wire API when the selected Copilot model is GPT-5/o1/o3-family - normalize `--subscription` to the OpenAI-compatible provider mode before validating `--wire-api responses` - add provider and CLI regressions for model-derived defaults and explicit `--wire-api responses` ## Tests - `UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev python -m pytest tests/test_provider_copilot_wrap.py tests/test_cli/test_wrap_copilot.py -q` - `UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev python -m ruff check headroom/providers/copilot/wrap.py headroom/providers/copilot/__init__.py headroom/cli/wrap.py tests/test_provider_copilot_wrap.py tests/test_cli/test_wrap_copilot.py` - `UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev python -m compileall -q headroom/providers/copilot/wrap.py headroom/providers/copilot/__init__.py headroom/cli/wrap.py tests/test_provider_copilot_wrap.py tests/test_cli/test_wrap_copilot.py` --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
efd2ac1ca4 |
chore: renormalize line endings to LF
`.gitattributes` declares `*.py text eol=lf` and `*.sh text eol=lf`, but 74 files (73 .py, 1 .sh) are stored in the index with CRLF line endings, violating that contract. Every macOS/Linux clone reports these files as "modified" on fresh checkout because git's diff engine sees the stored bytes don't match the attribute contract, even though the working tree and index match byte-for-byte. Running `git add --renormalize .` rewrites each affected blob so the stored form matches the attribute declaration. No semantic changes — every affected file's diff is "N insertions, N deletions" with inserts and deletes being the same lines modulo line endings. Follow-up commit adds `.git-blame-ignore-revs` so `git blame` / GitHub blame skip this mechanical commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7831620eca |
test: expand provider slice coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
ce8a2f3cf8 |
test: expand provider pipeline coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |