Fix Claude count tokens routing and model discovery

This commit is contained in:
Cita
2026-03-21 18:42:28 +08:00
parent 6a19d809f0
commit dc93c58582
5 changed files with 201 additions and 46 deletions
@@ -156,4 +156,44 @@ describe('claude count_tokens proxy route', () => {
},
]);
});
it('supports /v1/messages/count_tokens for openai-platform gateways that expose Claude messages endpoints', async () => {
selectChannelMock.mockReturnValue({
channel: { id: 12, routeId: 23 },
site: { name: 'gateway-site', url: 'https://gateway.example.com', platform: 'openai' },
account: { id: 34, username: 'gateway-user@example.com' },
tokenName: 'default',
tokenValue: 'sk-gateway',
actualModel: 'claude-sonnet-4-5-20250929',
});
fetchMock.mockResolvedValue(new Response(JSON.stringify({
input_tokens: 9,
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'POST',
url: '/v1/messages/count_tokens',
payload: {
model: 'claude-sonnet-4-5-20250929',
messages: [
{
role: 'user',
content: [{ type: 'text', text: 'count through a compatible gateway' }],
},
],
},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ input_tokens: 9 });
expect(fetchMock).toHaveBeenCalledTimes(1);
const [targetUrl, options] = fetchMock.mock.calls[0] as [string, any];
expect(targetUrl).toBe('https://gateway.example.com/v1/messages/count_tokens?beta=true');
expect(options.headers['x-api-key']).toBe('sk-gateway');
expect(options.headers['anthropic-version']).toBe('2023-06-01');
});
});
+46 -18
View File
@@ -1521,32 +1521,60 @@ export function buildClaudeCountTokensUpstreamRequest(input: {
delete sanitizedBody.maxTokens;
delete sanitizedBody.stream;
const providerProfile = resolveProviderProfile(sitePlatform);
if (providerProfile?.id !== 'claude') {
throw new Error(`missing claude provider profile for platform: ${sitePlatform || 'unknown'}`);
const effectiveClaudeHeaders = {
...claudeHeaders,
...(betas.length > 0 ? { 'anthropic-beta': betas.join(',') } : {}),
};
if (providerProfile?.id === 'claude') {
const prepared = providerProfile.prepareRequest({
endpoint: 'messages',
modelName: input.modelName,
stream: false,
tokenValue: input.tokenValue,
oauthProvider: input.oauthProvider,
sitePlatform,
baseHeaders: {
'Content-Type': 'application/json',
},
claudeHeaders: effectiveClaudeHeaders,
body: sanitizedBody,
action: 'countTokens',
});
return {
path: prepared.path,
headers: prepared.headers,
body: prepared.body,
runtime: {
executor: 'claude',
modelName: input.modelName,
stream: false,
action: 'countTokens',
},
};
}
const prepared = providerProfile.prepareRequest({
endpoint: 'messages',
modelName: input.modelName,
stream: false,
tokenValue: input.tokenValue,
oauthProvider: input.oauthProvider,
sitePlatform,
const anthropicVersion = (
effectiveClaudeHeaders['anthropic-version']
|| '2023-06-01'
);
const isClaudeOauthUpstream = sitePlatform === 'claude' && input.oauthProvider === 'claude';
const headers = buildClaudeRuntimeHeaders({
baseHeaders: {
'Content-Type': 'application/json',
},
claudeHeaders: {
...claudeHeaders,
...(betas.length > 0 ? { 'anthropic-beta': betas.join(',') } : {}),
},
body: sanitizedBody,
action: 'countTokens',
claudeHeaders: effectiveClaudeHeaders,
anthropicVersion,
stream: false,
isClaudeOauthUpstream,
tokenValue: input.tokenValue,
});
return {
path: prepared.path,
headers: prepared.headers,
body: prepared.body,
path: '/v1/messages/count_tokens?beta=true',
headers,
body: sanitizedBody,
runtime: {
executor: 'claude',
modelName: input.modelName,