Merge pull request #3438 from madwiki/fix/strip-claude-code-attribution
fix: strip Claude Code attribution from non-Anthropic translations
This commit is contained in:
@@ -101,7 +101,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
systemTypePromptResult := systemPromptResult.Get("type")
|
systemTypePromptResult := systemPromptResult.Get("type")
|
||||||
if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
|
if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
|
||||||
systemPrompt := systemPromptResult.Get("text").String()
|
systemPrompt := systemPromptResult.Get("text").String()
|
||||||
if strings.HasPrefix(systemPrompt, "x-anthropic-billing-header:") {
|
if util.IsClaudeCodeAttributionSystemText(systemPrompt) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
partJSON := []byte(`{}`)
|
partJSON := []byte(`{}`)
|
||||||
@@ -112,7 +112,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
hasSystemInstruction = true
|
hasSystemInstruction = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if systemResult.Type == gjson.String {
|
} else if systemResult.Type == gjson.String && !util.IsClaudeCodeAttributionSystemText(systemResult.String()) {
|
||||||
systemInstructionJSON = []byte(`{"role":"user","parts":[{"text":""}]}`)
|
systemInstructionJSON = []byte(`{"role":"user","parts":[{"text":""}]}`)
|
||||||
systemInstructionJSON, _ = sjson.SetBytes(systemInstructionJSON, "parts.0.text", systemResult.String())
|
systemInstructionJSON, _ = sjson.SetBytes(systemInstructionJSON, "parts.0.text", systemResult.String())
|
||||||
hasSystemInstruction = true
|
hasSystemInstruction = true
|
||||||
|
|||||||
@@ -70,6 +70,28 @@ func uint64Ptr(v uint64) *uint64 {
|
|||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConvertClaudeRequestToAntigravity_StripsClaudeCodeAttribution(t *testing.T) {
|
||||||
|
inputJSON := []byte(`{
|
||||||
|
"model": "claude-sonnet-4-5",
|
||||||
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}],
|
||||||
|
"system": [
|
||||||
|
{"type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.63.abc; cc_entrypoint=cli; cch=12345;"},
|
||||||
|
{"type": "text", "text": "Antigravity system prompt"}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false)
|
||||||
|
outputStr := string(output)
|
||||||
|
|
||||||
|
parts := gjson.Get(outputStr, "request.systemInstruction.parts").Array()
|
||||||
|
if len(parts) != 1 {
|
||||||
|
t.Fatalf("Expected 1 system part after attribution strip, got %d: %s", len(parts), gjson.Get(outputStr, "request.systemInstruction.parts").Raw)
|
||||||
|
}
|
||||||
|
if got := parts[0].Get("text").String(); got != "Antigravity system prompt" {
|
||||||
|
t.Fatalf("Unexpected system part: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testNonAnthropicRawSignature(t *testing.T) string {
|
func testNonAnthropicRawSignature(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -50,7 +51,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
contentIndex := 0
|
contentIndex := 0
|
||||||
|
|
||||||
appendSystemText := func(text string) {
|
appendSystemText := func(text string) {
|
||||||
if text == "" || strings.HasPrefix(text, "x-anthropic-billing-header: ") {
|
if text == "" || util.IsClaudeCodeAttributionSystemText(text) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
if systemPromptResult.Get("type").String() == "text" {
|
if systemPromptResult.Get("type").String() == "text" {
|
||||||
textResult := systemPromptResult.Get("text")
|
textResult := systemPromptResult.Get("text")
|
||||||
if textResult.Type == gjson.String {
|
if textResult.Type == gjson.String {
|
||||||
|
if util.IsClaudeCodeAttributionSystemText(textResult.String()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
part := []byte(`{"text":""}`)
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.SetBytes(part, "text", textResult.String())
|
part, _ = sjson.SetBytes(part, "text", textResult.String())
|
||||||
systemInstruction, _ = sjson.SetRawBytes(systemInstruction, "parts.-1", part)
|
systemInstruction, _ = sjson.SetRawBytes(systemInstruction, "parts.-1", part)
|
||||||
@@ -60,7 +63,7 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
if hasSystemParts {
|
if hasSystemParts {
|
||||||
out, _ = sjson.SetRawBytes(out, "request.systemInstruction", systemInstruction)
|
out, _ = sjson.SetRawBytes(out, "request.systemInstruction", systemInstruction)
|
||||||
}
|
}
|
||||||
} else if systemResult.Type == gjson.String {
|
} else if systemResult.Type == gjson.String && !util.IsClaudeCodeAttributionSystemText(systemResult.String()) {
|
||||||
out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.-1.text", systemResult.String())
|
out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.-1.text", systemResult.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,3 +40,24 @@ func TestConvertClaudeRequestToCLI_ToolChoice_SpecificTool(t *testing.T) {
|
|||||||
t.Fatalf("Expected allowedFunctionNames ['json'], got %s", gjson.GetBytes(output, "request.toolConfig.functionCallingConfig.allowedFunctionNames").Raw)
|
t.Fatalf("Expected allowedFunctionNames ['json'], got %s", gjson.GetBytes(output, "request.toolConfig.functionCallingConfig.allowedFunctionNames").Raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConvertClaudeRequestToCLI_StripsClaudeCodeAttribution(t *testing.T) {
|
||||||
|
inputJSON := []byte(`{
|
||||||
|
"model": "claude-sonnet-4-5",
|
||||||
|
"system": [
|
||||||
|
{"type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.63.abc; cc_entrypoint=cli; cch=12345;"},
|
||||||
|
{"type": "text", "text": "User system prompt"}
|
||||||
|
],
|
||||||
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
output := ConvertClaudeRequestToCLI("gemini-3-flash-preview", inputJSON, false)
|
||||||
|
|
||||||
|
parts := gjson.GetBytes(output, "request.systemInstruction.parts").Array()
|
||||||
|
if len(parts) != 1 {
|
||||||
|
t.Fatalf("Expected 1 system part after attribution strip, got %d: %s", len(parts), gjson.GetBytes(output, "request.systemInstruction.parts").Raw)
|
||||||
|
}
|
||||||
|
if got := parts[0].Get("text").String(); got != "User system prompt" {
|
||||||
|
t.Fatalf("Unexpected system part: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
if systemPromptResult.Get("type").String() == "text" {
|
if systemPromptResult.Get("type").String() == "text" {
|
||||||
textResult := systemPromptResult.Get("text")
|
textResult := systemPromptResult.Get("text")
|
||||||
if textResult.Type == gjson.String {
|
if textResult.Type == gjson.String {
|
||||||
|
if util.IsClaudeCodeAttributionSystemText(textResult.String()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
part := []byte(`{"text":""}`)
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.SetBytes(part, "text", textResult.String())
|
part, _ = sjson.SetBytes(part, "text", textResult.String())
|
||||||
systemInstruction, _ = sjson.SetRawBytes(systemInstruction, "parts.-1", part)
|
systemInstruction, _ = sjson.SetRawBytes(systemInstruction, "parts.-1", part)
|
||||||
@@ -54,7 +57,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
if hasSystemParts {
|
if hasSystemParts {
|
||||||
out, _ = sjson.SetRawBytes(out, "system_instruction", systemInstruction)
|
out, _ = sjson.SetRawBytes(out, "system_instruction", systemInstruction)
|
||||||
}
|
}
|
||||||
} else if systemResult.Type == gjson.String {
|
} else if systemResult.Type == gjson.String && !util.IsClaudeCodeAttributionSystemText(systemResult.String()) {
|
||||||
out, _ = sjson.SetBytes(out, "system_instruction.parts.-1.text", systemResult.String())
|
out, _ = sjson.SetBytes(out, "system_instruction.parts.-1.text", systemResult.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,3 +78,31 @@ func TestConvertClaudeRequestToGemini_ImageContent(t *testing.T) {
|
|||||||
t.Fatalf("Expected image data 'aGVsbG8=', got '%s'", got)
|
t.Fatalf("Expected image data 'aGVsbG8=', got '%s'", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConvertClaudeRequestToGemini_StripsClaudeCodeAttribution(t *testing.T) {
|
||||||
|
inputJSON := []byte(`{
|
||||||
|
"model": "claude-sonnet-4-5",
|
||||||
|
"system": [
|
||||||
|
{"type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.63.abc; cc_entrypoint=cli; cch=12345;"},
|
||||||
|
{"type": "text", "text": "You are a Claude agent, built on Anthropic's Claude Agent SDK."},
|
||||||
|
{"type": "text", "text": "User system prompt"}
|
||||||
|
],
|
||||||
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
output := ConvertClaudeRequestToGemini("gemini-3-flash-preview", inputJSON, false)
|
||||||
|
|
||||||
|
parts := gjson.GetBytes(output, "system_instruction.parts").Array()
|
||||||
|
if len(parts) != 2 {
|
||||||
|
t.Fatalf("Expected 2 system parts after attribution strip, got %d: %s", len(parts), gjson.GetBytes(output, "system_instruction.parts").Raw)
|
||||||
|
}
|
||||||
|
if got := parts[0].Get("text").String(); got != "You are a Claude agent, built on Anthropic's Claude Agent SDK." {
|
||||||
|
t.Fatalf("Unexpected first system part: %q", got)
|
||||||
|
}
|
||||||
|
if got := parts[1].Get("text").String(); got != "User system prompt" {
|
||||||
|
t.Fatalf("Unexpected second system part: %q", got)
|
||||||
|
}
|
||||||
|
if gjson.GetBytes(output, `system_instruction.parts.#(text%"x-anthropic-billing-header:*")`).Exists() {
|
||||||
|
t.Fatalf("Claude Code attribution block was forwarded: %s", gjson.GetBytes(output, "system_instruction.parts").Raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -103,7 +104,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
hasSystemContent := false
|
hasSystemContent := false
|
||||||
if system := root.Get("system"); system.Exists() {
|
if system := root.Get("system"); system.Exists() {
|
||||||
if system.Type == gjson.String {
|
if system.Type == gjson.String {
|
||||||
if system.String() != "" {
|
if system.String() != "" && !util.IsClaudeCodeAttributionSystemText(system.String()) {
|
||||||
oldSystem := []byte(`{"type":"text","text":""}`)
|
oldSystem := []byte(`{"type":"text","text":""}`)
|
||||||
oldSystem, _ = sjson.SetBytes(oldSystem, "text", system.String())
|
oldSystem, _ = sjson.SetBytes(oldSystem, "text", system.String())
|
||||||
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", oldSystem)
|
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", oldSystem)
|
||||||
@@ -334,7 +335,7 @@ func convertClaudeContentPart(part gjson.Result) (string, bool) {
|
|||||||
switch partType {
|
switch partType {
|
||||||
case "text":
|
case "text":
|
||||||
text := part.Get("text").String()
|
text := part.Get("text").String()
|
||||||
if strings.TrimSpace(text) == "" {
|
if strings.TrimSpace(text) == "" || util.IsClaudeCodeAttributionSystemText(text) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
textContent := []byte(`{"type":"text","text":""}`)
|
textContent := []byte(`{"type":"text","text":""}`)
|
||||||
|
|||||||
@@ -696,3 +696,28 @@ func TestConvertClaudeRequestToOpenAI_AssistantThinkingToolUseThinkingSplit(t *t
|
|||||||
t.Fatalf("Expected reasoning_content %q, got %q", "t1\n\nt2", got)
|
t.Fatalf("Expected reasoning_content %q, got %q", "t1\n\nt2", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConvertClaudeRequestToOpenAI_StripsClaudeCodeAttribution(t *testing.T) {
|
||||||
|
inputJSON := []byte(`{
|
||||||
|
"model": "claude-sonnet-4-5",
|
||||||
|
"system": [
|
||||||
|
{"type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.63.abc; cc_entrypoint=cli; cch=12345;"},
|
||||||
|
{"type": "text", "text": "User system prompt"}
|
||||||
|
],
|
||||||
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
output := ConvertClaudeRequestToOpenAI("gpt-5", inputJSON, false)
|
||||||
|
messages := gjson.GetBytes(output, "messages").Array()
|
||||||
|
if len(messages) == 0 || messages[0].Get("role").String() != "system" {
|
||||||
|
t.Fatalf("Expected first message to be system, got: %s", gjson.GetBytes(output, "messages").Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := messages[0].Get("content").Array()
|
||||||
|
if len(content) != 1 {
|
||||||
|
t.Fatalf("Expected 1 system content item after attribution strip, got %d: %s", len(content), messages[0].Get("content").Raw)
|
||||||
|
}
|
||||||
|
if got := content[0].Get("text").String(); got != "User system prompt" {
|
||||||
|
t.Fatalf("Unexpected system content: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
const claudeCodeAttributionSystemPrefix = "x-anthropic-billing-header:"
|
||||||
|
|
||||||
|
// IsClaudeCodeAttributionSystemText reports whether text is the Claude Code
|
||||||
|
// attribution block that carries per-request billing and prompt fingerprint data.
|
||||||
|
func IsClaudeCodeAttributionSystemText(text string) bool {
|
||||||
|
text = strings.TrimLeftFunc(text, unicode.IsSpace)
|
||||||
|
return strings.HasPrefix(text, claudeCodeAttributionSystemPrefix)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestIsClaudeCodeAttributionSystemText(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
text string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Claude Code attribution block",
|
||||||
|
text: "x-anthropic-billing-header: cc_version=2.1.63.abc; cc_entrypoint=cli; cch=12345;",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leading whitespace",
|
||||||
|
text: "\n\t x-anthropic-billing-header: cc_version=2.1.63.abc; cch=12345;",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regular system prompt",
|
||||||
|
text: "You are helpful.",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty text",
|
||||||
|
text: "",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := IsClaudeCodeAttributionSystemText(tt.text); got != tt.want {
|
||||||
|
t.Fatalf("IsClaudeCodeAttributionSystemText(%q) = %v, want %v", tt.text, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user