Merge pull request #4913 from jeshica/fix/anthropic-count-tokens-max-tokens

fix: strip max_tokens from Anthropic count tokens
This commit is contained in:
Wesley Liddick authored and GitHub committed 2026-07-31 11:58:28 +08:00
commit 2be08f3f39
3 files changed
+37 -2

No files matched your search

@@ -523,7 +523,8 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_ModelMappingPreservesOtherFie
require.Equal(t, "hello world", gjson.GetBytes(sentBody, "messages.0.content.0.text").String(), "messages 字段不应被修改")
require.Equal(t, "enabled", gjson.GetBytes(sentBody, "thinking.type").String(), "thinking 字段不应被修改")
require.Equal(t, int64(5000), gjson.GetBytes(sentBody, "thinking.budget_tokens").Int(), "thinking.budget_tokens 不应被修改")
require.Equal(t, int64(1024), gjson.GetBytes(sentBody, "max_tokens").Int(), "max_tokens 不应被修改")
require.False(t, gjson.GetBytes(sentBody, "max_tokens").Exists(),
"max_tokens 作为生成参数应被 count_tokens 过滤剥离")
}
func TestGatewayService_AnthropicAPIKeyPassthrough_CountTokensFiltersGenerationFields(t *testing.T) {
@@ -582,7 +583,8 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_CountTokensFiltersGenerationF
require.Equal(t, "sys", gjson.GetBytes(sentBody, "system.0.text").String())
require.Equal(t, "hello", gjson.GetBytes(sentBody, "messages.0.content").String())
require.Equal(t, "tool", gjson.GetBytes(sentBody, "tools.0.name").String())
require.Equal(t, int64(1024), gjson.GetBytes(sentBody, "max_tokens").Int())
require.False(t, gjson.GetBytes(sentBody, "max_tokens").Exists(),
"count_tokens 请求不得携带生成参数 max_tokens")
require.Equal(t, "enabled", gjson.GetBytes(sentBody, "thinking.type").String())
}
@@ -596,6 +596,35 @@ func TestBuildCountTokensRequest_OAuthMimicHaiku_PreservesContextManagementEndTo
"count_tokens 路径必须含 token-counting beta")
}
func TestBuildCountTokensRequest_OAuthMimic_DropsInjectedMaxTokens(t *testing.T) {
// OAuth mimicry injects max_tokens=128000 for normal messages requests. It is
// invalid for Anthropic's count_tokens endpoint and must be stripped on wire.
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages/count_tokens", nil)
account := &Account{ID: 413, Platform: PlatformAnthropic, Type: AccountTypeOAuth,
Credentials: map[string]any{"access_token": "oauth-tok"},
Status: StatusActive, Schedulable: true,
}
normalized, _ := normalizeClaudeOAuthRequestBody(
[]byte(`{"model":"claude-sonnet-4-5","messages":[]}`),
"claude-sonnet-4-5", claudeOAuthNormalizeOptions{},
)
require.Equal(t, int64(128000), gjson.GetBytes(normalized, "max_tokens").Int(),
"precondition: OAuth mimicry injects the Claude Code default")
svc := &GatewayService{cfg: &config.Config{}}
req, _, err := svc.buildCountTokensRequest(
context.Background(), c, account, normalized,
"oauth-tok", "oauth", "claude-sonnet-4-5", true,
)
require.NoError(t, err)
require.False(t, gjson.GetBytes(readUpstreamBodyForTest(t, req), "max_tokens").Exists(),
"count_tokens wire body must not contain max_tokens")
}
func TestBuildCountTokensRequest_APIKeyHaiku_StripsContextManagementEndToEnd(t *testing.T) {
// API-key + haiku + 客户端 header 不带 context-management beta → final beta 不含 → strip
gin.SetMode(gin.TestMode)
@@ -584,6 +584,10 @@ func sanitizeCountTokensRequestBody(body []byte) []byte {
"stream",
"stop_sequences",
"stop",
// Anthropic's /v1/messages/count_tokens accepts request-input fields only.
// max_tokens is a generation parameter; OAuth mimicry may inject it to
// resemble Claude Code messages requests, so it must never reach this endpoint.
"max_tokens",
} {
if gjson.GetBytes(out, path).Exists() {
if next, ok := deleteJSONPathBytes(out, path); ok {