Commit graph

1 commit

Author SHA1 Message Date
Serge ARADJ
d7a8cdbee1
feat(proxy): label GitHub Copilot traffic as "copilot" in the outcome… (#2377)
## Description

Requests routed to the GitHub Copilot API travel on the OpenAI or
Anthropic
wire, so the proxy handlers stamp the *wire* provider (`openai` /
`anthropic`)
on the outcome. As a result, Copilot traffic is attributed to
OpenAI/Claude in
the dashboard's per-request provider stats, hiding the real upstream.
(This is
distinct from the existing **Copilot Quota** panel, which is separate
from
per-request provider attribution.)

This labels Copilot traffic as `copilot` in the single outcome funnel.
`build_copilot_upstream_url()` is already the one routing chokepoint
every
Copilot surface goes through (OpenAI `/chat/completions` + `/responses`
and the
Anthropic `/v1/messages` route all build their upstream URL there), so
it flags
the request via a task-local `ContextVar`; `emit_request_outcome()`
reads the
flag and relabels the provider. The relabel runs before the `>= 500`
failed
guard, so a failed Copilot request is attributed to `copilot` too.
Non-Copilot
traffic never sets the flag and is untouched.

## Type of Change

- [x] New feature (non-breaking change that adds functionality)

## Changes Made

- `headroom/copilot_auth.py`: add a task-local
`_request_routed_to_copilot`
`ContextVar` with `mark_request_routed_to_copilot()` /
`request_routed_to_copilot()`
helpers; set the flag in `build_copilot_upstream_url()` whenever the
base is a
  Copilot API URL (the existing `is_copilot_api_url` check). `/v1` path
  normalization is unchanged.
- `headroom/proxy/outcome.py`: in `emit_request_outcome()`, when the
request was
routed to Copilot and the wire provider is `openai`/`anthropic`, relabel
the
  outcome provider to `copilot` (before the 5xx guard).
- `tests/test_copilot_provider_label.py`: new tests for the chokepoint
marking
  and the outcome relabel.

## Testing

- [x] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`) — ran on the changed files only
(clean)
- [ ] Type checking passes (`mypy headroom`) — ran on the changed files
only (clean)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
$ python -m pytest tests/test_copilot_provider_label.py tests/test_outcome_records_5xx_as_failed.py -q
tests/test_copilot_provider_label.py .....                               [ 71%]
tests/test_outcome_records_5xx_as_failed.py ..                           [100%]
7 passed

$ python -m pytest tests/test_copilot_auth.py -k "build_copilot_upstream_url or copilot_api_url" -q
8 passed, 58 deselected      # existing /v1-stripping behavior preserved

$ python -m ruff check headroom/copilot_auth.py headroom/proxy/outcome.py tests/test_copilot_provider_label.py
All checks passed!

$ python -m mypy headroom/copilot_auth.py headroom/proxy/outcome.py
Success: no issues found in 2 source files
```

## Real Behavior Proof

- Environment: Python 3.11, headroom installed with the `proxy` extra.
- Exact command / steps: the unit tests above drive
`build_copilot_upstream_url()`
followed by `emit_request_outcome()` in an isolated context and assert
the
  recorded provider.
- Observed result: an `anthropic`/`openai` outcome for a request routed
to
`https://api.githubcopilot.com` is recorded as provider `copilot`; a
request
  not routed to Copilot is recorded under its wire provider unchanged.
- Not tested: end-to-end against a live Copilot subscription (no live
seat in the
  test environment).

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Additional Notes

- The flag is a `ContextVar` (task-local), so it cannot bleed across
concurrent
requests; each request that is not routed to Copilot simply reads the
`False`
  default.
- No `CHANGELOG.md` edits (release-please generates it from the
Conventional
  Commit PR title).

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-18 09:54:06 -07:00