fix: allow /v1/messages/count_tokens through the proxy allowlist

The exact-match endpoint allowlist in the proxy omitted
`messages/count_tokens`, so every Claude Code / Anthropic SDK request was
404'd with `Path '/v1/messages/count_tokens' not found` before it ever
reached the (fully supported) forwarding path.

Add the endpoint to `_ALLOWED_ENDPOINTS` and a regression test that
pins it as always reachable on POST.

Regression history:
- 933ba105 "add missing messages endpoint" (2026-04-01): added
  messages/count_tokens handling to the forwarding layer.
- 164ed775 "support /message/count_tokens endpoint" (2026-05-09): added
  the local count_tokens handler.
- 96661384 "update not found proxy" (2026-05-14): only GET was gated, so
  POST count_tokens passed implicitly.
- 0217002e "Gate proxy forwarding behind a segment-anchored API path
  allowlist" (2026-08-23): POST gated, but the "v1/" prefix still carried
  count_tokens.
- 5af04364 "Restrict proxy forwarding to an exact method/path allowlist"
  (2026-08-24): switched to an exact table, added "messages" but omitted
  "messages/count_tokens". This is where the endpoint got locked out.
This commit is contained in:
redshift
2026-10-01 23:30:53 +08:00
parent 5d2ba64e6c
commit 60921cef20
2 changed files with 25 additions and 0 deletions
+4
View File
@@ -277,6 +277,10 @@ _ALLOWED_ENDPOINTS: dict[str, frozenset[str]] = {
"completions": frozenset({"POST"}),
"responses": frozenset({"POST"}),
"messages": frozenset({"POST"}),
# Anthropic token-counting subroute; the proxy's allowlist is exact, so the
# "messages" entry above does not carry it. Clients (Claude Code, the
# Anthropic SDKs) call it before every request.
"messages/count_tokens": frozenset({"POST"}),
"embeddings": frozenset({"POST"}),
# TypeSafe System One decision endpoint: POST {state, model, questions}
# -> {answers, usage}. Non-streaming, JSON in/out; billed from the
+21
View File
@@ -51,6 +51,8 @@ def test_ambiguous_paths_are_rejected(path: str) -> None:
"v1/chat/completions",
"chat/completions",
"v1/responses",
"v1/messages",
"v1/messages/count_tokens",
"v1/embeddings",
"models",
"v1/models/gpt-4",
@@ -138,6 +140,7 @@ def test_known_prefix_does_not_carry_an_unknown_endpoint(path: str) -> None:
("completions", "POST"),
("v1/responses", "POST"),
("v1/messages", "POST"),
("v1/messages/count_tokens", "POST"),
("v1/embeddings", "POST"),
("models", "GET"),
("attestation", "GET"),
@@ -163,6 +166,24 @@ def test_method_must_match_the_endpoint(path: str, method: str) -> None:
assert _forwarding_allowed(path, method) is False
@pytest.mark.parametrize(
"path",
[
"messages/count_tokens",
"v1/messages/count_tokens",
"v1/messages/count_tokens/",
],
)
def test_count_tokens_endpoint_stays_allowed(path: str) -> None:
# Regression guard: /v1/messages/count_tokens is supported end-to-end
# (local handler when the upstream lacks native Anthropic support, plain
# forward otherwise), but the exact-match allowlist once omitted it, so
# Claude Code and the Anthropic SDKs were 404'd on every request. It must
# always be reachable, on POST only.
assert _forwarding_allowed(path, "POST") is True
assert _forwarding_allowed(path, "GET") is False
def test_operator_additions_are_parsed_per_endpoint() -> None:
parsed = _parse_extra_allowed_endpoints("POST:v1/rerank, GET:batches ,post:audio/x")
assert parsed == {