feat(server): /v1/messages/count_tokens forwarding on the Python and Rust servers#138
feat(server): /v1/messages/count_tokens forwarding on the Python and Rust servers#138sabhatinas wants to merge 2 commits into
Conversation
…pic target Signed-off-by: Sabhatina Selvam <sabhatinas@nvidia.com>
WalkthroughChangesAnthropic count-tokens flow
Estimated code review effort: 3 (Moderate) | ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/test_anthropic_count_tokens_endpoint.py`:
- Around line 20-38: Replace the _CapturingClient fake and synchronous
TestClient usage with an async ASGI test using httpx.AsyncClient configured with
ASGITransport. Mock the outbound count-tokens request through respx, then assert
the routed request’s URL, JSON payload, and headers to preserve end-to-end
request-shape coverage.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 0f834f53-e4c0-4c21-9e57-1f542dd5a033
📒 Files selected for processing (4)
switchyard/cli/route_bundle.pyswitchyard/lib/endpoints/anthropic_messages_endpoint.pyswitchyard/lib/route_table.pytests/test_anthropic_count_tokens_endpoint.py
| class _CapturingClient: | ||
| """Fake ``httpx.AsyncClient`` that records the forwarded count-tokens call.""" | ||
|
|
||
| captured: dict[str, Any] = {} | ||
|
|
||
| def __init__(self, **_kwargs: Any) -> None: | ||
| pass | ||
|
|
||
| async def __aenter__(self) -> _CapturingClient: | ||
| return self | ||
|
|
||
| async def __aexit__(self, *_exc: object) -> bool: | ||
| return False | ||
|
|
||
| async def post( | ||
| self, url: str, *, json: dict[str, Any], headers: dict[str, str] | ||
| ) -> httpx.Response: | ||
| type(self).captured = {"url": url, "json": json, "headers": headers} | ||
| return httpx.Response(200, json={"input_tokens": 42}) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -t f '(pyproject\.toml|pytest\.ini|setup\.cfg|tox\.ini|conftest\.py)$' . \
-x rg -n 'asyncio_mode|respx|pytest-asyncio' {}Repository: NVIDIA-NeMo/Switchyard
Length of output: 253
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate relevant files and inspect their structure.
git ls-files | rg '^(tests/test_anthropic_count_tokens_endpoint\.py|.*anthropic.*\.py)$'
echo '--- tests/test_anthropic_count_tokens_endpoint.py (outline) ---'
ast-grep outline tests/test_anthropic_count_tokens_endpoint.py --view expanded || true
echo '--- tests/test_anthropic_count_tokens_endpoint.py (selected lines) ---'
nl -ba tests/test_anthropic_count_tokens_endpoint.py | sed -n '1,220p'
echo '--- anthropic-related implementation files ---'
rg -n "count_tokens|AsyncClient|respx|TestClient|ASGITransport" tests src . -g '!**/.git/**'Repository: NVIDIA-NeMo/Switchyard
Length of output: 1314
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- tests/test_anthropic_count_tokens_endpoint.py ---'
sed -n '1,220p' tests/test_anthropic_count_tokens_endpoint.py
echo '--- switchyard/lib/endpoints/anthropic_messages_endpoint.py ---'
sed -n '1,260p' switchyard/lib/endpoints/anthropic_messages_endpoint.py
echo '--- switchyard/lib/backends/anthropic_native_llm_backend.py ---'
sed -n '1,260p' switchyard/lib/backends/anthropic_native_llm_backend.pyRepository: NVIDIA-NeMo/Switchyard
Length of output: 11662
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
paths = [
Path('tests/test_anthropic_count_tokens_endpoint.py'),
Path('switchyard/lib/endpoints/anthropic_messages_endpoint.py'),
Path('switchyard/lib/backends/anthropic_native_llm_backend.py'),
]
for p in paths:
print(f'--- {p} ---')
try:
text = p.read_text()
except Exception as e:
print(f'ERROR: {e}')
continue
lines = text.splitlines()
for i, line in enumerate(lines[:260], 1):
print(f'{i:4d}: {line}')
PYRepository: NVIDIA-NeMo/Switchyard
Length of output: 13450
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for rel in [
'tests/test_anthropic_count_tokens_endpoint.py',
'switchyard/lib/endpoints/anthropic_messages_endpoint.py',
'switchyard/lib/backends/anthropic_native_llm_backend.py',
]:
path = Path(rel)
print(f'--- {rel} ---')
text = path.read_text()
for i, line in enumerate(text.splitlines(), 1):
if i > 260:
break
print(f'{i:4d}: {line}')
PYRepository: NVIDIA-NeMo/Switchyard
Length of output: 13450
Use async httpx.AsyncClient with ASGITransport and respx here.
The current fake AsyncClient + TestClient bypasses the real outbound HTTPX path. An async ASGI test with respx keeps the test aligned with the repo’s async/respx setup and still checks the request shape end to end.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_anthropic_count_tokens_endpoint.py` around lines 20 - 38, Replace
the _CapturingClient fake and synchronous TestClient usage with an async ASGI
test using httpx.AsyncClient configured with ASGITransport. Mock the outbound
count-tokens request through respx, then assert the routed request’s URL, JSON
payload, and headers to preserve end-to-end request-shape coverage.
Sources: Coding guidelines, Learnings
Signed-off-by: Sabhatina Selvam <sabhatinas@nvidia.com>
Adds
POST /v1/messages/count_tokensto both servers (Python FastAPI + Rust libsy). Claude Code calls it to budget context; before this it 503'd.A routing chain can't count tokens, so the handler forwards the request to the route's Anthropic tier and returns its answer. The full target (base_url + key) is gone by request time, so the config capture grabs the first Anthropic-format target at build time and the endpoint forwards to it — swapping the route name for the real model and sending
x-api-key+anthropic-version. No hardcoded keys. No Anthropic tier → clean 400.RouteTable.note_anthropic_target()during route-bundle build; handler inanthropic_messages_endpoint.py.ServerConfigcaptures the target intoServerState; handler inswitchyard-server/src/lib.rs.Tested end to end via
switchyard serve(Python) and theswitchyard-serverbinary (Rust) against inference-api (Opus 4.8): both return{"input_tokens": 22}. Unit + integration tests on both sides.Linear: SWITCH-1048