fix(codex): OpenCode Zen telemetry attribution (#1648)

## Description

Fixes #1602.

OpenCode Zen custom-base requests can reach Headroom through the generic
passthrough path, but that route was not supplying endpoint/provider
metadata for Zen chat completions. This made forwarded Zen traffic
invisible in dashboard provider, usage, and token telemetry.

Closes #1602

## 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

- Added a narrow OpenCode Zen custom-base classifier for `POST
/zen/v1/chat/completions` on `opencode.ai` and `www.opencode.ai`.
- Passed `endpoint_name="chat/completions"` and `provider="zen"` into
catch-all passthrough telemetry for matching Zen traffic.
- Attributed normalized OpenCode transport traffic
(`/v1/chat/completions` with `x-headroom-original-path:
/zen/v1/chat/completions`) to `zen` for request outcomes while keeping
the OpenAI parser path unchanged.
- Added coverage for direct catch-all routing, normalized original-path
routing, token usage outcome recording, and false-positive paths like
`/mcp/v1/chat/completions`, `/npm/v1/chat/completions`, and
`/context7/v1/chat/completions`.

## 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
$ rtk pytest tests/test_custom_base_passthrough_telemetry.py -q
Pytest: 4 passed

$ rtk uvx --from ruff==0.15.17 ruff check headroom/proxy/handlers/openai.py headroom/providers/proxy_routes.py tests/test_custom_base_passthrough_telemetry.py tests/test_provider_proxy_routes.py tests/test_proxy/test_openai_transport_path_prefix.py
All checks passed!

$ rtk uvx --from ruff==0.15.17 ruff format --check headroom/proxy/handlers/openai.py headroom/providers/proxy_routes.py tests/test_custom_base_passthrough_telemetry.py tests/test_provider_proxy_routes.py tests/test_proxy/test_openai_transport_path_prefix.py
5 files already formatted

$ rtk /Library/Frameworks/Python.framework/Versions/3.13/bin/python3 -m py_compile headroom/proxy/handlers/openai.py headroom/providers/proxy_routes.py tests/test_custom_base_passthrough_telemetry.py tests/test_provider_proxy_routes.py tests/test_proxy/test_openai_transport_path_prefix.py
# passed

$ rtk git diff --check
# passed
```

GitHub Actions also passed after the final push, including CI, Docker
native/wrap/init E2E, security, lint, and PR governance.

## Real Behavior Proof

- Environment: local worktree on macOS plus GitHub Actions for PR #1648.
- Exact command / steps: ran focused pytest coverage for Zen passthrough
telemetry, Ruff check/format validation on touched files, Python compile
validation, `git diff --check`, and waited for the full GitHub Actions
rollup.
- Observed result: Zen custom-base chat completions now record request
outcomes as provider `zen` with endpoint `chat/completions`;
false-positive OpenCode paths remain unattributed to Zen; GitHub checks
are green.
- Not tested: full local test suite did not collect in this worktree
because the native `headroom._core` extension is not installed. `rtk npm
--prefix plugins/opencode test` is also blocked locally because `vitest`
is not installed in `plugins/opencode/node_modules`.

## 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
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A

## Additional Notes

The documentation and CHANGELOG checklist items are not applicable for
this narrow telemetry bug fix. No new comments were added because the
code path is covered by narrowly named helper/test cases.
This commit is contained in:
Vinay Gupta 2026-07-07 11:35:21 -05:00 committed by GitHub
parent 2ce19c2c55
commit f18c6bd896
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 342 additions and 24 deletions

View file

@ -11,7 +11,10 @@ from urllib.parse import quote
from fastapi import FastAPI, Request, WebSocket from fastapi import FastAPI, Request, WebSocket
from fastapi.responses import Response from fastapi.responses import Response
from headroom.proxy.handlers.openai import _resolve_codex_routing_headers from headroom.proxy.handlers.openai import (
_custom_base_passthrough_telemetry,
_resolve_codex_routing_headers,
)
logger = logging.getLogger("headroom.proxy.routes") logger = logging.getLogger("headroom.proxy.routes")
@ -995,7 +998,18 @@ def register_provider_routes(app: FastAPI, proxy: Any) -> None:
async def passthrough(request: Request, path: str): async def passthrough(request: Request, path: str):
custom_base = request.headers.get("x-headroom-base-url") custom_base = request.headers.get("x-headroom-base-url")
if custom_base: if custom_base:
return await proxy.handle_passthrough(request, custom_base.rstrip("/")) base_url = custom_base.rstrip("/")
endpoint_name, provider_name = _custom_base_passthrough_telemetry(
request.method,
path,
base_url,
)
return await proxy.handle_passthrough(
request,
base_url,
endpoint_name,
provider_name,
)
# Intercept Code Assist authentication and onboarding routes # Intercept Code Assist authentication and onboarding routes
clean_path = path.lstrip("/") clean_path = path.lstrip("/")

View file

@ -81,6 +81,7 @@ _OPENAI_CHAT_COMPLETIONS_PATH = "/chat/completions"
_OPENAI_RESPONSES_PATH = "/responses" _OPENAI_RESPONSES_PATH = "/responses"
_OPENAI_ORIGINAL_PATH_HEADER = "x-headroom-original-path" _OPENAI_ORIGINAL_PATH_HEADER = "x-headroom-original-path"
_OPENAI_BASE_URL_HEADER = "x-headroom-base-url" _OPENAI_BASE_URL_HEADER = "x-headroom-base-url"
_OPENCODE_ZEN_HOSTS = {"opencode.ai", "www.opencode.ai"}
def _header_get(headers: dict[str, str], name: str) -> str | None: def _header_get(headers: dict[str, str], name: str) -> str | None:
@ -92,6 +93,25 @@ def _header_get(headers: dict[str, str], name: str) -> str | None:
return None return None
def _custom_base_passthrough_telemetry(method: str, path: str, base_url: str) -> tuple[str, str]:
"""Return passthrough telemetry metadata for narrow custom-base exceptions."""
# OpenCode Zen sends provider-prefixed OpenAI-compatible traffic through
# custom-base routing. Keep this exact to avoid labeling arbitrary
# custom-base tool traffic as LLM provider telemetry.
if method.upper() != "POST":
return "", ""
try:
host = (urlparse(base_url.strip()).hostname or "").lower()
except ValueError:
return "", ""
if host not in _OPENCODE_ZEN_HOSTS:
return "", ""
normalized_path = path[1:] if path.startswith("/") else path
if normalized_path == "zen/v1/chat/completions":
return "chat/completions", "zen"
return "", ""
def _resolve_openai_handler_path( def _resolve_openai_handler_path(
request_headers: dict[str, str], request_headers: dict[str, str],
*, *,
@ -1962,6 +1982,21 @@ class OpenAIHandlerMixin:
stripped_count=_pre_strip_count_chat, stripped_count=_pre_strip_count_chat,
request_id=request_id, request_id=request_id,
) )
upstream_base_url = _resolve_openai_upstream_base(request.headers)
handler_path = (
_resolve_openai_handler_path(
request.headers,
handler_path=_OPENAI_CHAT_COMPLETIONS_PATH,
)
if upstream_base_url is not None
else "/v1/chat/completions"
)
_, custom_chat_provider = _custom_base_passthrough_telemetry(
request.method,
handler_path,
upstream_base_url or "",
)
openai_chat_outcome_provider = custom_chat_provider or "openai"
# Memory: Get user ID when memory is enabled. Reads `request.headers` # Memory: Get user ID when memory is enabled. Reads `request.headers`
# directly because `headers` was stripped of `x-headroom-*` for the # directly because `headers` was stripped of `x-headroom-*` for the
@ -2012,7 +2047,7 @@ class OpenAIHandlerMixin:
rate_key = headers.get("authorization", "default")[:20] rate_key = headers.get("authorization", "default")[:20]
allowed, wait_seconds = await self.rate_limiter.check_request(rate_key) allowed, wait_seconds = await self.rate_limiter.check_request(rate_key)
if not allowed: if not allowed:
await self.metrics.record_rate_limited(provider="openai") await self.metrics.record_rate_limited(provider=openai_chat_outcome_provider)
raise HTTPException( raise HTTPException(
status_code=429, status_code=429,
detail=f"Rate limited. Retry after {wait_seconds:.1f}s", detail=f"Rate limited. Retry after {wait_seconds:.1f}s",
@ -2069,7 +2104,7 @@ class OpenAIHandlerMixin:
await self._record_request_outcome( await self._record_request_outcome(
RequestOutcome( RequestOutcome(
request_id=request_id, request_id=request_id,
provider="openai", provider=openai_chat_outcome_provider,
model=model, model=model,
original_tokens=0, original_tokens=0,
optimized_tokens=0, optimized_tokens=0,
@ -2798,14 +2833,6 @@ class OpenAIHandlerMixin:
) )
# Direct OpenAI API (no backend configured) # Direct OpenAI API (no backend configured)
upstream_base_url = _resolve_openai_upstream_base(request.headers)
handler_path = (
_resolve_openai_handler_path(
request.headers, handler_path=_OPENAI_CHAT_COMPLETIONS_PATH
)
if upstream_base_url is not None
else "/v1/chat/completions"
)
url = build_copilot_upstream_url( url = build_copilot_upstream_url(
upstream_base_url or self.OPENAI_API_URL, upstream_base_url or self.OPENAI_API_URL,
handler_path, handler_path,
@ -2846,6 +2873,7 @@ class OpenAIHandlerMixin:
optimization_latency, optimization_latency,
pipeline_timing=pipeline_timing, pipeline_timing=pipeline_timing,
prefix_tracker=openai_prefix_tracker, prefix_tracker=openai_prefix_tracker,
outcome_provider=openai_chat_outcome_provider,
) )
else: else:
headers = await apply_copilot_api_auth(headers, url=url) headers = await apply_copilot_api_auth(headers, url=url)
@ -3097,7 +3125,7 @@ class OpenAIHandlerMixin:
await self._record_request_outcome( await self._record_request_outcome(
RequestOutcome( RequestOutcome(
request_id=request_id, request_id=request_id,
provider="openai", provider=openai_chat_outcome_provider,
model=model, model=model,
original_tokens=original_tokens, original_tokens=original_tokens,
optimized_tokens=total_input_tokens, optimized_tokens=total_input_tokens,
@ -3153,7 +3181,7 @@ class OpenAIHandlerMixin:
headers=response_headers, headers=response_headers,
) )
except Exception as e: except Exception as e:
await self.metrics.record_failed(provider="openai") await self.metrics.record_failed(provider=openai_chat_outcome_provider)
# Log full error details internally for debugging # Log full error details internally for debugging
logger.error(f"[{request_id}] OpenAI request failed: {type(e).__name__}: {e}") logger.error(f"[{request_id}] OpenAI request failed: {type(e).__name__}: {e}")
# Return sanitized error message to client (don't expose internal details) # Return sanitized error message to client (don't expose internal details)

View file

@ -0,0 +1,212 @@
from __future__ import annotations
import asyncio
import json
from types import SimpleNamespace
from typing import Any
import httpx
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from headroom.providers.proxy_routes import register_provider_routes
from headroom.proxy.handlers.openai import OpenAIHandlerMixin
class _Runtime:
@staticmethod
def api_target(provider: str) -> str:
return f"https://{provider}.example.test"
@staticmethod
def model_metadata_provider(headers: dict[str, str]) -> str:
return "anthropic"
class _Proxy:
ANTHROPIC_API_URL = "https://anthropic.example.test"
OPENAI_API_URL = "https://openai.example.test"
GEMINI_API_URL = "https://gemini.example.test"
CLOUDCODE_API_URL = "https://cloudcode.example.test"
VERTEX_API_URL = "https://vertex.example.test"
def __init__(self) -> None:
self.config = SimpleNamespace(bedrock_api_url=None)
self.provider_runtime = _Runtime()
self.calls: list[dict[str, Any]] = []
async def handle_passthrough(
self,
request: Any,
base_url: str,
endpoint_name: str = "",
provider: str = "",
) -> JSONResponse:
self.calls.append(
{
"path": request.url.path,
"base_url": base_url,
"endpoint_name": endpoint_name,
"provider": provider,
}
)
return JSONResponse(self.calls[-1])
class _ChatCompletionsRequest:
method = "POST"
headers = {}
url = SimpleNamespace(path="/zen/v1/chat/completions", query="")
async def body(self) -> bytes:
return b'{"model":"zen"}'
class _OpenAIUsageClient:
def __init__(self) -> None:
self.calls: list[dict[str, Any]] = []
async def request(self, **kwargs: Any) -> httpx.Response:
self.calls.append(kwargs)
request = httpx.Request(kwargs["method"], kwargs["url"])
return httpx.Response(
200,
request=request,
headers={"content-type": "application/json"},
json={
"usage": {
"prompt_tokens": 21,
"completion_tokens": 8,
"prompt_tokens_details": {"cached_tokens": 5},
}
},
)
def test_custom_base_provider_prefixed_chat_completions_gets_telemetry() -> None:
app = FastAPI()
proxy = _Proxy()
register_provider_routes(app, proxy)
with TestClient(app) as client:
for base_url, expected_base_url in (
("https://opencode.ai/", "https://opencode.ai"),
("https://www.opencode.ai/", "https://www.opencode.ai"),
):
response = client.post(
"/zen/v1/chat/completions",
headers={"x-headroom-base-url": base_url},
json={"model": "zen"},
)
assert response.status_code == 200
assert response.json() == {
"path": "/zen/v1/chat/completions",
"base_url": expected_base_url,
"endpoint_name": "chat/completions",
"provider": "zen",
}
def test_custom_base_unrelated_passthrough_paths_stay_unclassified() -> None:
app = FastAPI()
proxy = _Proxy()
register_provider_routes(app, proxy)
with TestClient(app) as client:
for path in (
"/mcp",
"/mcp/v1/chat/completions",
"/npm/v1/chat/completions",
"/context7/v1/chat/completions",
):
response = client.post(
path,
headers={"x-headroom-base-url": "https://opencode.ai/"},
json={},
)
assert response.status_code == 200
assert response.json() == {
"path": path,
"base_url": "https://opencode.ai",
"endpoint_name": "",
"provider": "",
}
def test_custom_base_chat_completions_telemetry_is_post_and_opencode_zen_only() -> None:
app = FastAPI()
proxy = _Proxy()
register_provider_routes(app, proxy)
with TestClient(app) as client:
get_response = client.get(
"/zen/v1/chat/completions",
headers={"x-headroom-base-url": "https://opencode.ai/"},
)
other_host_response = client.post(
"/zen/v1/chat/completions",
headers={"x-headroom-base-url": "https://custom.example/"},
json={"model": "zen"},
)
double_slash_response = client.post(
"/zen//v1/chat/completions",
headers={"x-headroom-base-url": "https://opencode.ai/"},
json={"model": "zen"},
)
trailing_slash_response = client.post(
"/zen/v1/chat/completions/",
headers={"x-headroom-base-url": "https://opencode.ai/"},
json={"model": "zen"},
)
for response in (
get_response,
other_host_response,
double_slash_response,
trailing_slash_response,
):
assert response.status_code == 200
assert response.json()["endpoint_name"] == ""
assert response.json()["provider"] == ""
def test_classified_custom_base_passthrough_records_telemetry_usage() -> None:
handler = object.__new__(OpenAIHandlerMixin)
handler.http_client = _OpenAIUsageClient()
outcomes = []
async def next_request_id() -> str:
return "req_zen"
async def record(outcome: Any) -> None:
outcomes.append(outcome)
handler._next_request_id = next_request_id
handler._record_request_outcome = record
response = asyncio.run(
handler.handle_passthrough(
_ChatCompletionsRequest(),
"https://opencode.ai",
"chat/completions",
"zen",
)
)
assert response.status_code == 200
assert json.loads(response.body) == {
"usage": {
"prompt_tokens": 21,
"completion_tokens": 8,
"prompt_tokens_details": {"cached_tokens": 5},
}
}
assert handler.http_client.calls[0]["url"] == ("https://opencode.ai/zen/v1/chat/completions")
assert len(outcomes) == 1
outcome = outcomes[0]
assert outcome.provider == "zen"
assert outcome.model == "passthrough:chat/completions"
assert outcome.optimized_tokens == 21
assert outcome.output_tokens == 8
assert outcome.cache_read_tokens == 5

View file

@ -187,13 +187,55 @@ def test_provider_passthrough_routes_forward_expected_targets(monkeypatch) -> No
assert client.delete("/v1beta/cachedContents/cache-1").json()["sub_path"] == ( assert client.delete("/v1beta/cachedContents/cache-1").json()["sub_path"] == (
"cachedContents" "cachedContents"
) )
assert ( custom_passthrough = client.get(
client.get( "/unhandled/path",
"/unhandled/path", headers={"x-headroom-base-url": "https://custom.example/base/"},
headers={"x-headroom-base-url": "https://custom.example/base/"}, ).json()
).json()["base_url"] assert custom_passthrough["base_url"] == "https://custom.example/base"
== "https://custom.example/base" assert custom_passthrough["sub_path"] == ""
) assert custom_passthrough["provider"] == ""
opencode_zen_passthrough = client.post(
"/zen/v1/chat/completions",
headers={"x-headroom-base-url": "https://opencode.ai/"},
json={"model": "zen"},
).json()
assert opencode_zen_passthrough["base_url"] == "https://opencode.ai"
assert opencode_zen_passthrough["sub_path"] == "chat/completions"
assert opencode_zen_passthrough["provider"] == "zen"
unrelated_custom_passthrough = client.post(
"/mcp",
headers={"x-headroom-base-url": "https://opencode.ai/"},
json={},
).json()
assert unrelated_custom_passthrough["sub_path"] == ""
assert unrelated_custom_passthrough["provider"] == ""
for unrelated_path in (
"/mcp/v1/chat/completions",
"/npm/v1/chat/completions",
"/context7/v1/chat/completions",
):
unrelated_custom_passthrough = client.post(
unrelated_path,
headers={"x-headroom-base-url": "https://opencode.ai/"},
json={},
).json()
assert unrelated_custom_passthrough["sub_path"] == ""
assert unrelated_custom_passthrough["provider"] == ""
get_custom_passthrough = client.get(
"/zen/v1/chat/completions",
headers={"x-headroom-base-url": "https://opencode.ai/"},
).json()
assert get_custom_passthrough["sub_path"] == ""
assert get_custom_passthrough["provider"] == ""
other_host_custom_passthrough = client.post(
"/zen/v1/chat/completions",
headers={"x-headroom-base-url": "https://custom.example/"},
json={"model": "zen"},
).json()
assert other_host_custom_passthrough["sub_path"] == ""
assert other_host_custom_passthrough["provider"] == ""
assert client.get("/another/path", headers={"x-goog-api-key": "test"}).json()[ assert client.get("/another/path", headers={"x-goog-api-key": "test"}).json()[
"base_url" "base_url"
] == ("https://api.gemini.test") ] == ("https://api.gemini.test")

View file

@ -2,8 +2,6 @@
from __future__ import annotations from __future__ import annotations
from unittest.mock import AsyncMock
import httpx import httpx
import pytest import pytest
@ -61,7 +59,11 @@ def _build_openai_client():
) )
proxy._retry_request = _fake_retry proxy._retry_request = _fake_retry
proxy._record_request_outcome = AsyncMock()
async def _record_request_outcome(outcome: object) -> None:
captured["outcome"] = outcome
proxy._record_request_outcome = _record_request_outcome
return TestClient(app), captured return TestClient(app), captured
@ -201,6 +203,26 @@ def test_query_string_is_preserved_for_reconstructed_upstream_paths() -> None:
_assert_path(captured, expected_path) _assert_path(captured, expected_path)
def test_opencode_zen_reconstructed_chat_path_records_zen_provider() -> None:
headers = {
"Authorization": "Bearer sk-test",
"x-headroom-base-url": "https://opencode.ai",
"x-headroom-original-path": "/zen/v1/chat/completions",
}
body = {"model": "zen-model", "messages": [{"role": "user", "content": "hi"}]}
client, captured = _build_openai_client()
response = client.post(_OPENAI_CHAT_PATH, headers=headers, json=body)
assert response.status_code == 200, response.text
_assert_origin(captured, "https://opencode.ai")
_assert_path(captured, "/zen/v1/chat/completions")
outcome = captured.get("outcome")
assert outcome is not None
assert outcome.provider == "zen"
assert outcome.model == "zen-model"
def test_non_http_base_url_falls_back_to_v1() -> None: def test_non_http_base_url_falls_back_to_v1() -> None:
cases = [ cases = [
( (