diff --git a/headroom/copilot_auth.py b/headroom/copilot_auth.py index d8c779e44..a1aed76a8 100644 --- a/headroom/copilot_auth.py +++ b/headroom/copilot_auth.py @@ -1007,7 +1007,12 @@ def build_copilot_upstream_url(base_url: str, path: str) -> str: # chat/responses and Anthropic messages all build their upstream URL # here), so mark the request for provider relabeling downstream. mark_request_routed_to_copilot() - if normalized_path.startswith("/v1/"): + # Copilot serves its OpenAI-compatible surface WITHOUT a ``/v1`` prefix + # (``/chat/completions``, ``/responses``, ...), so strip it there. But its + # Anthropic surface for Claude models IS ``/v1/messages`` (with the + # ``/v1``); stripping it forwarded ``/messages`` and Copilot returned 404 + # for claude-* models (#2409). Keep ``/v1`` for the messages endpoint. + if normalized_path.startswith("/v1/") and not normalized_path.startswith("/v1/messages"): normalized_path = normalized_path[3:] else: reset_request_routed_to_copilot() diff --git a/tests/test_copilot_auth.py b/tests/test_copilot_auth.py index 68f7fea97..e1af9a11b 100644 --- a/tests/test_copilot_auth.py +++ b/tests/test_copilot_auth.py @@ -612,6 +612,34 @@ def test_build_copilot_upstream_url_strips_v1_only_for_copilot_hosts() -> None: ) +def test_build_copilot_upstream_url_preserves_v1_messages_for_copilot() -> None: + # Copilot's Anthropic surface for Claude models is /v1/messages (with the + # /v1); stripping it forwarded /messages and Copilot 404'd (#2409). + assert ( + copilot_auth.build_copilot_upstream_url( + "https://api.githubcopilot.com", + "/v1/messages", + ) + == "https://api.githubcopilot.com/v1/messages" + ) + # Batches under the messages endpoint keep /v1 too. + assert ( + copilot_auth.build_copilot_upstream_url( + "https://api.githubcopilot.com", + "/v1/messages/batches", + ) + == "https://api.githubcopilot.com/v1/messages/batches" + ) + # A GHE Copilot host keeps /v1/messages as well. + assert ( + copilot_auth.build_copilot_upstream_url( + "https://copilot-api.acme.ghe.com", + "/v1/messages", + ) + == "https://copilot-api.acme.ghe.com/v1/messages" + ) + + def test_build_copilot_upstream_url_strips_v1_for_ghe_copilot_hosts() -> None: assert ( copilot_auth.build_copilot_upstream_url(