Skip to content

Commit 2e83e19

Browse files
authored
fix(anthropic): whitelist count tokens endpoint (#124)
1 parent 9f8f275 commit 2e83e19

File tree

4 files changed

+124
-1
lines changed

4 files changed

+124
-1
lines changed

gateway/src/providers/anthropic.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { DefaultProviderProxy } from './default'
44

55
export class AnthropicProvider extends DefaultProviderProxy {
66
protected isWhitelistedEndpoint(): boolean {
7+
// If there is a query string, drop the query string from the path.
8+
const path = this.restOfPath.split('?')[0]
79
// This endpoint is used by Claude Code.
8-
return this.restOfPath === 'v1/messages/count_tokens'
10+
return path === 'v1/messages/count_tokens'
911
}
1012

1113
protected modelAPI(): ModelAPI {

gateway/test/providers/anthropic.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,19 @@ describe('anthropic', () => {
6464
expect(otelBatch, 'otelBatch length not 1').toHaveLength(1)
6565
expect(deserializeRequest(otelBatch[0]!)).toMatchSnapshot('span')
6666
})
67+
68+
test('should whitelist /v1/messages/count_tokens endpoint', async ({ gateway }) => {
69+
const { fetch, otelBatch } = gateway
70+
71+
const client = new Anthropic({ authToken: 'healthy', baseURL: 'https://example.com/anthropic', fetch })
72+
73+
const result = await client.beta.messages.countTokens({
74+
model: 'claude-sonnet-4-20250514',
75+
messages: [{ role: 'user', content: 'What is the capital of France?' }],
76+
})
77+
78+
expect(result).toMatchSnapshot('count_tokens')
79+
expect(otelBatch, 'otelBatch should be empty for whitelisted endpoint').toHaveLength(1)
80+
expect(deserializeRequest(otelBatch[0]!)).toMatchSnapshot('span')
81+
})
6782
})

gateway/test/providers/anthropic.spec.ts.snap

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,3 +591,54 @@ exports[`anthropic > should call anthropic via gateway with stream > span 1`] =
591591
},
592592
]
593593
`;
594+
595+
exports[`anthropic > should whitelist /v1/messages/count_tokens endpoint > count_tokens 1`] = `
596+
{
597+
"input_tokens": 14,
598+
}
599+
`;
600+
601+
exports[`anthropic > should whitelist /v1/messages/count_tokens endpoint > span 1`] = `
602+
[
603+
{
604+
"attributes": {
605+
"http.request.body.text": "{"model":"claude-sonnet-4-20250514","messages":[{"role":"user","content":"What is the capital of France?"}]}",
606+
"http.request.header.accept": "application/json",
607+
"http.request.header.anthropic-beta": "token-counting-2024-11-01",
608+
"http.request.header.anthropic-version": "2023-06-01",
609+
"http.request.header.authorization": "Bearer healthy",
610+
"http.request.header.content-type": "application/json",
611+
"http.request.header.user-agent": "Anthropic/JS 0.62.0",
612+
"http.request.header.x-stainless-arch": "unknown",
613+
"http.request.header.x-stainless-lang": "js",
614+
"http.request.header.x-stainless-os": "Unknown",
615+
"http.request.header.x-stainless-package-version": "0.62.0",
616+
"http.request.header.x-stainless-retry-count": "0",
617+
"http.request.header.x-stainless-runtime": "unknown",
618+
"http.request.header.x-stainless-runtime-version": "unknown",
619+
"http.request.method": "POST",
620+
"http.response.header.content-length": "19",
621+
"http.response.header.content-type": "application/json",
622+
"http.response.header.server": "uvicorn",
623+
"http.response.status_code": 200,
624+
"logfire.json_schema": "{"type":"object","properties":{"http.request.method":{"type":"string"},"url.full":{"type":"string"},"http.request.header.accept":{"type":"string"},"http.request.header.anthropic-beta":{"type":"string"},"http.request.header.anthropic-version":{"type":"string"},"http.request.header.authorization":{"type":"string"},"http.request.header.content-type":{"type":"string"},"http.request.header.user-agent":{"type":"string"},"http.request.header.x-stainless-arch":{"type":"string"},"http.request.header.x-stainless-lang":{"type":"string"},"http.request.header.x-stainless-os":{"type":"string"},"http.request.header.x-stainless-package-version":{"type":"string"},"http.request.header.x-stainless-retry-count":{"type":"string"},"http.request.header.x-stainless-runtime":{"type":"string"},"http.request.header.x-stainless-runtime-version":{"type":"string"},"http.response.status_code":{"type":"number"},"http.response.header.content-length":{"type":"string"},"http.response.header.content-type":{"type":"string"},"http.response.header.server":{"type":"string"},"http.request.body.text":{"type":"string"}}}",
625+
"logfire.level_num": 9,
626+
"logfire.msg": "POST v1/messages/count_tokens?beta=true",
627+
"url.full": "https://example.com/anthropic/v1/messages/count_tokens?beta=true",
628+
},
629+
"events": [],
630+
"kind": 1,
631+
"links": [],
632+
"name": "POST v1/messages/count_tokens?beta=true",
633+
"parentSpanId": undefined,
634+
"resource": {
635+
"service.name": "PAIG",
636+
"service.version": "test",
637+
},
638+
"scope": "pydantic-ai-gateway",
639+
"status": {
640+
"code": 1,
641+
},
642+
},
643+
]
644+
`;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
interactions:
2+
- request:
3+
body: '{"model":"claude-sonnet-4-20250514","messages":[{"role":"user","content":"What
4+
is the capital of France?"}]}'
5+
headers:
6+
accept:
7+
- '*/*'
8+
accept-encoding:
9+
- gzip, deflate
10+
anthropic-version:
11+
- '2023-06-01'
12+
connection:
13+
- keep-alive
14+
content-length:
15+
- '108'
16+
content-type:
17+
- application/json
18+
host:
19+
- api.anthropic.com
20+
user-agent:
21+
- python-httpx/0.28.1
22+
method: POST
23+
uri: https://api.anthropic.com/v1/messages/count_tokens
24+
response:
25+
body:
26+
string: '{"input_tokens":14}'
27+
headers:
28+
CF-RAY:
29+
- 99a3fb68dde3fb9b-AMS
30+
Connection:
31+
- keep-alive
32+
Content-Length:
33+
- '19'
34+
Content-Type:
35+
- application/json
36+
Date:
37+
- Thu, 06 Nov 2025 10:42:04 GMT
38+
Server:
39+
- cloudflare
40+
X-Robots-Tag:
41+
- none
42+
anthropic-organization-id:
43+
- 44ae676c-958f-4d68-9108-eae9de7b366b
44+
cf-cache-status:
45+
- DYNAMIC
46+
request-id:
47+
- req_011CUrVifYHKXaAjjTXPgXS5
48+
strict-transport-security:
49+
- max-age=31536000; includeSubDomains; preload
50+
x-envoy-upstream-service-time:
51+
- '69'
52+
status:
53+
code: 200
54+
message: OK
55+
version: 1

0 commit comments

Comments
 (0)