feat(ai-proxy): strip dynamic cch field from billing header to enable caching (#3518)

This commit is contained in:
澄潭
2026-02-15 23:57:08 +08:00
committed by GitHub
parent a07f5024a9
commit 1c847dd553
2 changed files with 167 additions and 3 deletions

View File

@@ -145,7 +145,8 @@ func (c *ClaudeToOpenAIConverter) ConvertClaudeRequestToOpenAI(body []byte) ([]b
if claudeRequest.System != nil {
systemMsg := chatMessage{Role: roleSystem}
if !claudeRequest.System.IsArray {
systemMsg.Content = claudeRequest.System.StringValue
// Strip dynamic cch field from billing header to enable caching
systemMsg.Content = stripCchFromBillingHeader(claudeRequest.System.StringValue)
} else {
conversionResult := c.convertContentArray(claudeRequest.System.ArrayValue)
systemMsg.Content = conversionResult.openaiContents
@@ -832,10 +833,12 @@ func (c *ClaudeToOpenAIConverter) convertContentArray(claudeContents []claudeCha
switch claudeContent.Type {
case "text":
if claudeContent.Text != "" {
result.textParts = append(result.textParts, claudeContent.Text)
// Strip dynamic cch field from billing header to enable caching
processedText := stripCchFromBillingHeader(claudeContent.Text)
result.textParts = append(result.textParts, processedText)
result.openaiContents = append(result.openaiContents, chatMessageContent{
Type: contentTypeText,
Text: claudeContent.Text,
Text: processedText,
CacheControl: claudeContent.CacheControl,
})
}
@@ -954,3 +957,42 @@ func (c *ClaudeToOpenAIConverter) startToolCall(toolState *toolCallInfo) []*clau
return responses
}
// stripCchFromBillingHeader removes the dynamic cch field from x-anthropic-billing-header text
// to enable caching. The cch value changes on every request, which would break prompt caching.
// Example input: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode; cch=abc123;"
// Example output: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode;"
func stripCchFromBillingHeader(text string) string {
const billingHeaderPrefix = "x-anthropic-billing-header:"
// Check if this is a billing header
if !strings.HasPrefix(text, billingHeaderPrefix) {
return text
}
// Remove cch=xxx pattern (may appear with or without trailing semicolon)
// Pattern: ; cch=<any-non-semicolon-chars> followed by ; or end of string
result := text
// Try to find and remove ; cch=... pattern
// We need to handle both "; cch=xxx;" and "; cch=xxx" (at end)
for {
cchIdx := strings.Index(result, "; cch=")
if cchIdx == -1 {
break
}
// Find the end of cch value (next semicolon or end of string)
start := cchIdx + 2 // skip "; "
end := strings.Index(result[start:], ";")
if end == -1 {
// cch is at the end, remove from "; cch=" to end
result = result[:cchIdx]
} else {
// cch is followed by more content, remove "; cch=xxx" part
result = result[:cchIdx] + result[start+end:]
}
}
return result
}

View File

@@ -859,3 +859,125 @@ func TestClaudeToOpenAIConverter_ConvertReasoningResponseToClaude(t *testing.T)
})
}
}
func TestClaudeToOpenAIConverter_StripCchFromSystemMessage(t *testing.T) {
converter := &ClaudeToOpenAIConverter{}
t.Run("string_system_with_billing_header", func(t *testing.T) {
// Test that cch field is stripped from string format system message
claudeRequest := `{
"model": "claude-sonnet-4",
"max_tokens": 1024,
"system": [
{
"type": "text",
"text": "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode; cch=abc123;"
}
],
"messages": [{
"role": "user",
"content": "Hello"
}]
}`
result, err := converter.ConvertClaudeRequestToOpenAI([]byte(claudeRequest))
require.NoError(t, err)
var openaiRequest chatCompletionRequest
err = json.Unmarshal(result, &openaiRequest)
require.NoError(t, err)
require.Len(t, openaiRequest.Messages, 2)
// First message should be system with cch stripped
systemMsg := openaiRequest.Messages[0]
assert.Equal(t, "system", systemMsg.Role)
// The system content should have cch removed
contentArray, ok := systemMsg.Content.([]interface{})
require.True(t, ok, "System content should be an array")
require.Len(t, contentArray, 1)
contentMap, ok := contentArray[0].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "text", contentMap["type"])
assert.Equal(t, "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode;", contentMap["text"])
assert.NotContains(t, contentMap["text"], "cch=")
})
t.Run("plain_string_system_unchanged", func(t *testing.T) {
// Test that normal system messages are not modified
claudeRequest := `{
"model": "claude-sonnet-4",
"max_tokens": 1024,
"system": "You are a helpful assistant.",
"messages": [{
"role": "user",
"content": "Hello"
}]
}`
result, err := converter.ConvertClaudeRequestToOpenAI([]byte(claudeRequest))
require.NoError(t, err)
var openaiRequest chatCompletionRequest
err = json.Unmarshal(result, &openaiRequest)
require.NoError(t, err)
// First message should be system with original content
systemMsg := openaiRequest.Messages[0]
assert.Equal(t, "system", systemMsg.Role)
assert.Equal(t, "You are a helpful assistant.", systemMsg.Content)
})
}
func TestStripCchFromBillingHeader(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "billing header with cch at end",
input: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode; cch=abc123;",
expected: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode;",
},
{
name: "billing header with cch at end without trailing semicolon",
input: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode; cch=abc123",
expected: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode",
},
{
name: "billing header with cch in middle",
input: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cch=abc123; cc_entrypoint=claude-vscode;",
expected: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode;",
},
{
name: "billing header without cch",
input: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode;",
expected: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode;",
},
{
name: "non-billing header text unchanged",
input: "This is a normal system prompt",
expected: "This is a normal system prompt",
},
{
name: "empty string unchanged",
input: "",
expected: "",
},
{
name: "billing header with multiple cch fields",
input: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cch=first; cc_entrypoint=claude-vscode; cch=second;",
expected: "x-anthropic-billing-header: cc_version=2.1.37.3a3; cc_entrypoint=claude-vscode;",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := stripCchFromBillingHeader(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}