mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Refactors provider-specific proxy routing into provider-owned helper modules so `headroom/providers/proxy_routes.py` primarily registers routes and delegates behavior. This keeps Codex, OpenAI Responses/images, model metadata, Vertex, Cloud Code, passthrough target selection, and request path normalization logic testable outside the route table. Closes # ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Performance improvement - [x] Code refactoring (no functional changes) ## Changes Made - Extracted Codex routing helpers for headers, endpoint URLs, image forwarding, response subpaths, and model metadata. - Moved provider target selection, route specs, OpenAI Responses/images helpers, Vertex runtime helpers, Cloud Code path normalization, passthrough telemetry, and request scope normalization into focused modules. - Kept `proxy_routes.py` as route registration/delegation and preserved current-main `/v1/messages` custom-base behavior. - Added focused provider/proxy tests for the extracted modules and route delegation behavior. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text python -m pytest tests/test_package_init_lazy.py::test_codex_package_import_stays_runtime_only tests/test_provider_cloudcode_runtime.py tests/test_provider_codex_endpoints.py tests/test_provider_codex_headers.py tests/test_provider_codex_images.py tests/test_provider_codex_model_metadata.py tests/test_provider_codex_responses.py tests/test_provider_model_metadata.py tests/test_provider_openai_images.py tests/test_provider_openai_responses.py tests/test_provider_proxy_targets.py tests/test_provider_route_specs.py tests/test_provider_vertex_runtime.py tests/test_proxy_request_scope.py tests/test_provider_proxy_routes.py::test_provider_passthrough_routes_forward_expected_targets tests/test_provider_proxy_routes.py::test_proxy_route_helpers_prefer_legacy_targets_and_gemini_passthrough tests/test_provider_proxy_routes.py::test_provider_specific_routes_delegate_to_expected_proxy_handlers tests/test_provider_proxy_routes.py::test_openai_response_websocket_aliases_delegate_to_openai_ws_handler tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_returns_502_on_http_failure tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_uses_openai_target tests/test_provider_proxy_routes.py::test_openai_response_subpath_aliases_and_chatgpt_auth_use_expected_targets tests/test_provider_proxy_routes.py::test_openai_image_routes_use_codex_backend_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_openai_image_codex_response_strips_stale_compression_headers tests/test_provider_proxy_routes.py::test_openai_image_edits_api_key_auth_falls_through_to_openai_passthrough tests/test_provider_proxy_routes.py::test_openai_image_edits_preserves_multipart_body_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_gemini_batch_embed_contents_passthrough_uses_gemini_target tests/test_provider_proxy_routes.py::test_v1_models_fetches_codex_registry_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_falls_back_to_synthetic_list_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_get_single_dynamic_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_still_forwards_under_non_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_routes_claude_code_gateway_discovery_to_anthropic tests/test_provider_proxy_routes.py::test_anthropic_model_metadata_strips_ansi_model_ids tests/test_custom_base_passthrough_telemetry.py tests/test_proxy_passthrough.py tests/test_proxy_google_cloudcode_route_aliases.py tests/test_proxy_project_savings.py::test_with_project_prefix_round_trips_through_split tests/test_vertex_claude_compression.py ============================ 102 passed in 34.83s ============================= python -m ruff check headroom/providers/cloudcode headroom/providers/codex headroom/providers/vertex headroom/providers/model_metadata.py headroom/providers/openai_images.py headroom/providers/openai_responses.py headroom/providers/proxy_targets.py headroom/providers/route_specs.py headroom/providers/proxy_routes.py headroom/proxy/handlers/openai.py headroom/proxy/passthrough.py headroom/proxy/request_scope.py headroom/proxy/project_context.py tests/test_package_init_lazy.py tests/test_provider_cloudcode_runtime.py tests/test_provider_codex_endpoints.py tests/test_provider_codex_headers.py tests/test_provider_codex_images.py tests/test_provider_codex_model_metadata.py tests/test_provider_codex_responses.py tests/test_provider_model_metadata.py tests/test_provider_openai_images.py tests/test_provider_openai_responses.py tests/test_provider_proxy_targets.py tests/test_provider_route_specs.py tests/test_provider_vertex_runtime.py tests/test_proxy_request_scope.py tests/test_provider_proxy_routes.py tests/test_custom_base_passthrough_telemetry.py tests/test_proxy_passthrough.py tests/test_proxy_google_cloudcode_route_aliases.py tests/test_proxy_project_savings.py tests/test_vertex_claude_compression.py All checks passed! python -m compileall -q headroom\providers\cloudcode headroom\providers\codex headroom\providers\vertex headroom\providers\model_metadata.py headroom\providers\openai_images.py headroom\providers\openai_responses.py headroom\providers\proxy_targets.py headroom\providers\route_specs.py headroom\providers\proxy_routes.py headroom\proxy\handlers\openai.py headroom\proxy\passthrough.py headroom\proxy\request_scope.py headroom\proxy\project_context.py # no output; exited 0 git commit -m "refactor(providers): split proxy route adapters" Sync plugin versions.....................................................Passed check for merge conflicts................................................Passed ruff.....................................................................Passed ruff-format..............................................................Passed mypy.....................................................................Passed ``` ## Real Behavior Proof - Environment: Windows PowerShell, Python 3.13.13, branch `jd/provider-route-slices` based on `headroomlabs/main`. - Exact command / steps: Ran the focused provider/proxy pytest suite, focused ruff command, compileall over changed Python modules, and commit hooks. - Observed result: Provider/proxy route behavior tests passed; lint, formatting, and mypy passed. - Not tested: Full pytest suite, live upstream provider calls, and manual end-to-end proxy traffic. ## 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 ## Screenshots (if applicable) N/A ## Additional Notes Documentation and CHANGELOG updates are N/A for this internal refactor. The full pytest suite was not run; coverage here is focused on provider/proxy routing behavior touched by this slice. --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
162 lines
5.1 KiB
Python
162 lines
5.1 KiB
Python
import base64
|
|
import json
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from headroom.providers.codex import (
|
|
CodexRoutingDecision as ExportedCodexRoutingDecision,
|
|
)
|
|
from headroom.providers.codex import (
|
|
resolve_codex_routing as exported_resolve_codex_routing,
|
|
)
|
|
from headroom.providers.codex.model_metadata import (
|
|
codex_model_registry_entry,
|
|
fetch_chatgpt_codex_model_ids,
|
|
normalize_codex_registry_headers,
|
|
synthetic_model_get_response,
|
|
synthetic_models_list_response,
|
|
)
|
|
from headroom.providers.codex.runtime import (
|
|
decode_openai_bearer_payload,
|
|
resolve_codex_routing,
|
|
resolve_codex_routing_headers,
|
|
)
|
|
|
|
|
|
def _jwt(payload: dict) -> str:
|
|
header = {"alg": "none", "typ": "JWT"}
|
|
|
|
def encode(part: dict) -> str:
|
|
raw = json.dumps(part, separators=(",", ":")).encode("utf-8")
|
|
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
|
|
|
return f"{encode(header)}.{encode(payload)}."
|
|
|
|
|
|
def test_codex_runtime_resolves_chatgpt_account_from_openai_oauth_jwt() -> None:
|
|
token = _jwt(
|
|
{
|
|
"https://api.openai.com/auth": {
|
|
"chatgpt_account_id": "acct-from-jwt",
|
|
}
|
|
}
|
|
)
|
|
|
|
payload = decode_openai_bearer_payload({"authorization": f"Bearer {token}"})
|
|
headers, is_chatgpt_auth = resolve_codex_routing_headers({"authorization": f"Bearer {token}"})
|
|
|
|
assert payload == {
|
|
"https://api.openai.com/auth": {
|
|
"chatgpt_account_id": "acct-from-jwt",
|
|
}
|
|
}
|
|
assert is_chatgpt_auth is True
|
|
assert headers["ChatGPT-Account-ID"] == "acct-from-jwt"
|
|
|
|
|
|
def test_codex_runtime_exposes_named_routing_decision() -> None:
|
|
decision = resolve_codex_routing({"ChatGPT-Account-ID": "acct-explicit"})
|
|
|
|
assert decision.is_chatgpt_auth is True
|
|
assert decision.headers == {"ChatGPT-Account-ID": "acct-explicit"}
|
|
|
|
|
|
def test_codex_package_exports_routing_decision() -> None:
|
|
decision = exported_resolve_codex_routing({"ChatGPT-Account-ID": "acct-root"})
|
|
|
|
assert isinstance(decision, ExportedCodexRoutingDecision)
|
|
assert decision.is_chatgpt_auth is True
|
|
assert decision.headers == {"ChatGPT-Account-ID": "acct-root"}
|
|
|
|
|
|
def test_codex_runtime_ignores_malformed_oauth_jwt() -> None:
|
|
assert decode_openai_bearer_payload({"authorization": "Bearer header.***.sig"}) is None
|
|
|
|
headers, is_chatgpt_auth = resolve_codex_routing_headers(
|
|
{"authorization": "Bearer header.***.sig"}
|
|
)
|
|
|
|
assert is_chatgpt_auth is False
|
|
assert headers == {"authorization": "Bearer header.***.sig"}
|
|
|
|
|
|
def test_codex_registry_headers_normalize_account_and_accept_headers() -> None:
|
|
headers = normalize_codex_registry_headers(
|
|
{
|
|
"Host": "localhost:8787",
|
|
"authorization": "Bearer token",
|
|
"ChatGPT-Account-ID": "acct",
|
|
"Accept": "text/event-stream",
|
|
}
|
|
)
|
|
|
|
assert headers == {
|
|
"authorization": "Bearer token",
|
|
"chatgpt-account-id": "acct",
|
|
"accept": "application/json",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_codex_model_registry_fetch_returns_slugs() -> None:
|
|
class FakeClient:
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, dict[str, str]]] = []
|
|
|
|
async def get(self, url, **kwargs): # type: ignore[no-untyped-def]
|
|
self.calls.append((url, dict(kwargs.get("headers", {}))))
|
|
return httpx.Response(
|
|
200,
|
|
json={"models": [{"slug": "gpt-5.5"}, {"slug": ""}, {"slug": "gpt-5.4"}]},
|
|
)
|
|
|
|
client = FakeClient()
|
|
|
|
model_ids = await fetch_chatgpt_codex_model_ids(
|
|
client,
|
|
{"authorization": "Bearer token", "chatgpt-account-id": "acct"},
|
|
"0.135.0",
|
|
)
|
|
|
|
assert model_ids == ("gpt-5.5", "gpt-5.4")
|
|
assert client.calls == [
|
|
(
|
|
"https://chatgpt.com/backend-api/codex/models?client_version=0.135.0",
|
|
{
|
|
"authorization": "Bearer token",
|
|
"chatgpt-account-id": "acct",
|
|
"accept": "application/json",
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
def test_codex_synthetic_model_metadata_responses() -> None:
|
|
list_payload = json.loads(synthetic_models_list_response().body)
|
|
known_payload = json.loads(synthetic_model_get_response("gpt-5.5").body)
|
|
unknown = synthetic_model_get_response("gpt-99-future")
|
|
|
|
assert list_payload["object"] == "list"
|
|
assert "gpt-5.5" in {entry["id"] for entry in list_payload["data"]}
|
|
assert known_payload == {
|
|
"id": "gpt-5.5",
|
|
"object": "model",
|
|
"created": 0,
|
|
"owned_by": "openai",
|
|
}
|
|
assert unknown.status_code == 404
|
|
|
|
|
|
def test_codex_model_registry_entry_preserves_upstream_fields_and_defaults() -> None:
|
|
entry = codex_model_registry_entry(
|
|
"gpt-5.3-codex-spark",
|
|
{"display_name": "Spark", "context_window": 12345},
|
|
)
|
|
|
|
assert entry["slug"] == "gpt-5.3-codex-spark"
|
|
assert entry["display_name"] == "Spark"
|
|
assert entry["context_window"] == 12345
|
|
assert entry["default_reasoning_level"] == "medium"
|
|
assert entry["supports_parallel_tool_calls"] is True
|
|
assert entry["supported_in_api"] is True
|