headroom/tests/test_provider_display_classification.py
Manmit Singh 996c1174a8
feat(proxy): show real upstream provider on dashboard for OpenAI-compatible endpoints (#1594)
## Description

When the proxy runs against a custom OpenAI-compatible endpoint via
`--openai-api-url` (OpenRouter, Groq, Together, Azure OpenAI, …), the
dashboard always showed the provider as **OpenAI**, because the OpenAI
handler records every request with `provider="openai"`.

This detects well-known upstreams from the `--openai-api-url` host and
adds a `--provider-name` override that takes precedence (the issue's
option 3). The label is resolved only where the dashboard/stats payload
is built — the internal provider key stays `openai`, so pricing and
request formatting are unaffected.

| Upstream URL | Provider shown |
|--------------|----------------|
| `https://api.openai.com/v1` | OpenAI |
| `https://openrouter.ai/api/v1` | OpenRouter |
| `https://api.groq.com/openai/v1` | Groq |
| `https://api.together.xyz/v1` | Together AI |
| `https://<resource>.openai.azure.com/` | Azure OpenAI |

Unknown hosts keep the `openai` label unless `--provider-name` is set.

Closes #1533

## Type of Change

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

## Changes Made

- `helpers.py`: `classify_openai_upstream()` (host → display name) +
`resolve_display_provider()` (precedence: `--provider-name` > host
detection > raw provider; only relabels `openai`).
- `models.py`: `ProxyConfig.provider_name`.
- `cli/proxy.py`: `--provider-name` flag, threaded into `ProxyConfig`.
- `server.py`: relabel at the four dashboard/stats display sites (recent
requests, transformations feed, `requests.by_provider`, agent-usage
breakdown) via the resolver / `_remap_provider_counts`. Stored logs and
metrics keys are untouched.
- `docs/content/docs/proxy.mdx`: document `--provider-name`.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] New tests added

### Test Output

```text
$ pytest tests/test_provider_display_classification.py tests/test_dashboard_agent_usage.py -q
16 passed
13 passed

$ ruff check headroom/proxy/helpers.py headroom/proxy/server.py headroom/proxy/models.py headroom/cli/proxy.py
All checks passed!
```

## Real Behavior Proof

- Environment: repo branch `feat/1533-upstream-provider-classify` @
HEAD, local `.venv` (Python 3)
- Exact command / steps: ran the helpers directly from the venv —
`python -c "from headroom.proxy.helpers import classify_openai_upstream,
resolve_display_provider;
print(classify_openai_upstream('https://openrouter.ai/api/v1'));
print(resolve_display_provider('openai',
openai_api_url='https://openrouter.ai/api/v1'));
print(resolve_display_provider('openai',
openai_api_url='https://openrouter.ai/api/v1', provider_name='Groq'));
print(resolve_display_provider('anthropic'))"`
- Observed result: host detection relabels `openai` → `OpenRouter`,
`--provider-name` overrides detection (`Groq`), and the `anthropic`
label (plus the `openai` pricing key) is unchanged. Full output below:
  ```text
  classify openrouter           -> OpenRouter
  resolve openai+openrouter url -> OpenRouter
  override provider-name        -> Groq
  anthropic untouched           -> anthropic
  ```
- Not tested: live dashboard render against a real OpenRouter key (the
payload-builder logic 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
- [x] 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

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:09:56 +00:00

85 lines
2.8 KiB
Python

"""Upstream-provider display classification for OpenAI-compatible endpoints.
Covers issue #1533: when ``--openai-api-url`` points at a non-OpenAI upstream
(OpenRouter, Groq, …), the dashboard should show that provider instead of
always "openai". The internal provider key stays ``openai`` so pricing and
request formatting are unaffected — only the display label changes.
"""
from __future__ import annotations
import pytest
from headroom.proxy.helpers import (
classify_openai_upstream,
resolve_display_provider,
)
from headroom.proxy.models import ProxyConfig
from headroom.proxy.server import _remap_provider_counts
@pytest.mark.parametrize(
"url,expected",
[
("https://api.openai.com/v1", "OpenAI"),
("https://openrouter.ai/api/v1", "OpenRouter"),
("https://api.groq.com/openai/v1", "Groq"),
("https://api.together.xyz/v1", "Together AI"),
("https://my-resource.openai.azure.com/", "Azure OpenAI"),
("https://api.deepseek.com/v1", "DeepSeek"),
],
)
def test_classify_known_hosts(url: str, expected: str) -> None:
assert classify_openai_upstream(url) == expected
@pytest.mark.parametrize("url", [None, "", "not-a-url", "https://api.mycorp.internal/v1"])
def test_classify_unknown_or_missing_returns_none(url: str | None) -> None:
assert classify_openai_upstream(url) is None
def test_resolve_detects_from_url() -> None:
assert (
resolve_display_provider("openai", openai_api_url="https://openrouter.ai/api/v1")
== "OpenRouter"
)
def test_resolve_explicit_name_wins_over_detection() -> None:
assert (
resolve_display_provider(
"openai",
openai_api_url="https://openrouter.ai/api/v1",
provider_name="Internal Gateway",
)
== "Internal Gateway"
)
def test_resolve_non_openai_provider_untouched() -> None:
# Anthropic/Bedrock/etc. keep their own label regardless of openai_api_url.
assert (
resolve_display_provider("anthropic", openai_api_url="https://openrouter.ai/api/v1")
== "anthropic"
)
def test_resolve_plain_openai_unchanged() -> None:
assert resolve_display_provider("openai") == "openai"
# Unknown custom host with no override falls back to the raw label.
assert (
resolve_display_provider("openai", openai_api_url="https://api.mycorp.internal/v1")
== "openai"
)
def test_remap_provider_counts_relabels_only_openai() -> None:
config = ProxyConfig(openai_api_url="https://openrouter.ai/api/v1")
counts = {"openai": 7, "anthropic": 3}
assert _remap_provider_counts(counts, config) == {"OpenRouter": 7, "anthropic": 3}
def test_remap_provider_counts_noop_without_custom_upstream() -> None:
config = ProxyConfig()
counts = {"openai": 5}
assert _remap_provider_counts(counts, config) == {"openai": 5}