Skip to content

feat(server): /v1/messages/count_tokens forwarding on the Python and Rust servers#138

Open
sabhatinas wants to merge 2 commits into
mainfrom
sabhatinas/switch-1048-count-tokens
Open

feat(server): /v1/messages/count_tokens forwarding on the Python and Rust servers#138
sabhatinas wants to merge 2 commits into
mainfrom
sabhatinas/switch-1048-count-tokens

Conversation

@sabhatinas

@sabhatinas sabhatinas commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Adds POST /v1/messages/count_tokens to 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.

  • Python: RouteTable.note_anthropic_target() during route-bundle build; handler in anthropic_messages_endpoint.py.
  • Rust: ServerConfig captures the target into ServerState; handler in switchyard-server/src/lib.rs.

Tested end to end via switchyard serve (Python) and the switchyard-server binary (Rust) against inference-api (Opus 4.8): both return {"input_tokens": 22}. Unit + integration tests on both sides.

Linear: SWITCH-1048

…pic target

Signed-off-by: Sabhatina Selvam <sabhatinas@nvidia.com>
@sabhatinas
sabhatinas requested a review from a team as a code owner July 24, 2026 21:36
@sabhatinas sabhatinas self-assigned this Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Anthropic count-tokens flow

Layer / File(s) Summary
Anthropic target capture
switchyard/lib/route_table.py, switchyard/cli/route_bundle.py
RouteTable stores the first Anthropic target discovered while expanding supported route types.
Count-tokens upstream forwarding
switchyard/lib/endpoints/anthropic_messages_endpoint.py
Adds POST /v1/messages/count_tokens, rewrites the model, forwards the request upstream, and returns JSON or error responses.
Count-tokens endpoint validation
tests/test_anthropic_count_tokens_endpoint.py
Tests successful forwarding and the missing-Anthropic-target response.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Poem

I’m a rabbit with routes in my pack,
Sending token-count requests upstream and back.
Anthropic targets now hop into view,
Headers and models follow them too.
Tests twitch their noses: the flow works true!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title captures the main change, adding count_tokens forwarding, though it incorrectly mentions Rust servers.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5b7f4ab and bf4ae62.

📒 Files selected for processing (4)
  • switchyard/cli/route_bundle.py
  • switchyard/lib/endpoints/anthropic_messages_endpoint.py
  • switchyard/lib/route_table.py
  • tests/test_anthropic_count_tokens_endpoint.py

Comment on lines +20 to +38
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})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.py

Repository: 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}')
PY

Repository: 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}')
PY

Repository: 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>
@sabhatinas sabhatinas changed the title feat(server): forward /v1/messages/count_tokens to the route's Anthropic target feat(server): /v1/messages/count_tokens forwarding on the Python and Rust servers Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant