feat(gateway): align body shape with real Claude Code CLI defaults
Three field-level alignments in normalizeClaudeOAuthRequestBody to
match real Claude Code CLI traffic byte-for-byte:
1. temperature: previously deleted unconditionally; now passes
through client value, defaults to 1 when absent (real CLI
always sends temperature, default 1).
2. max_tokens: defaults to 128000 when absent (real CLI default).
3. context_management: when thinking.type is enabled/adaptive
and the client did not provide context_management, inject
{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]}
to mirror real CLI behavior.
tool_choice removal is unchanged (Claude Code OAuth credentials
do not allow client-supplied tool_choice).
Tests updated:
- gateway_body_order_test.go: temperature/max_tokens are now
expected in output; tool_choice still removed.
- gateway_prompt_test.go: system array is now 2 blocks
(billing + cc prompt), assertions adjusted.
- gateway_anthropic_apikey_passthrough_test.go: same 2-block
assertion.
This commit is contained in:
@@ -762,8 +762,14 @@ func TestGatewayService_AnthropicOAuth_ForwardPreservesBillingHeaderSystemBlock(
|
|||||||
system := gjson.GetBytes(upstream.lastBody, "system")
|
system := gjson.GetBytes(upstream.lastBody, "system")
|
||||||
require.True(t, system.Exists())
|
require.True(t, system.Exists())
|
||||||
require.True(t, system.IsArray(), "system should be an array")
|
require.True(t, system.IsArray(), "system should be an array")
|
||||||
require.Equal(t, claudeCodeSystemPrompt, system.Array()[0].Get("text").String())
|
arr := system.Array()
|
||||||
require.Equal(t, "ephemeral", system.Array()[0].Get("cache_control.type").String())
|
require.Len(t, arr, 2, "system array should have billing block + cc prompt block")
|
||||||
|
|
||||||
|
require.Contains(t, arr[0].Get("text").String(), "x-anthropic-billing-header:")
|
||||||
|
require.Contains(t, arr[0].Get("text").String(), "cc_version=")
|
||||||
|
|
||||||
|
require.Equal(t, claudeCodeSystemPrompt, arr[1].Get("text").String())
|
||||||
|
require.Equal(t, "ephemeral", arr[1].Get("cache_control.type").String())
|
||||||
|
|
||||||
// 原始 system prompt 应迁移至 messages 中
|
// 原始 system prompt 应迁移至 messages 中
|
||||||
messages := gjson.GetBytes(upstream.lastBody, "messages")
|
messages := gjson.GetBytes(upstream.lastBody, "messages")
|
||||||
|
|||||||
@@ -41,12 +41,13 @@ func TestNormalizeClaudeOAuthRequestBody_PreservesTopLevelFieldOrder(t *testing.
|
|||||||
resultStr := string(result)
|
resultStr := string(result)
|
||||||
|
|
||||||
require.Equal(t, claude.NormalizeModelID("claude-3-5-sonnet-latest"), modelID)
|
require.Equal(t, claude.NormalizeModelID("claude-3-5-sonnet-latest"), modelID)
|
||||||
assertJSONTokenOrder(t, resultStr, `"alpha"`, `"model"`, `"system"`, `"messages"`, `"omega"`, `"tools"`, `"metadata"`)
|
assertJSONTokenOrder(t, resultStr, `"alpha"`, `"model"`, `"temperature"`, `"system"`, `"messages"`, `"omega"`, `"tools"`, `"metadata"`, `"max_tokens"`)
|
||||||
require.NotContains(t, resultStr, `"temperature"`)
|
require.Contains(t, resultStr, `"temperature":0.2`)
|
||||||
require.NotContains(t, resultStr, `"tool_choice"`)
|
require.NotContains(t, resultStr, `"tool_choice"`)
|
||||||
require.Contains(t, resultStr, `"system":"`+claudeCodeSystemPrompt+`"`)
|
require.Contains(t, resultStr, `"system":"`+claudeCodeSystemPrompt+`"`)
|
||||||
require.Contains(t, resultStr, `"tools":[]`)
|
require.Contains(t, resultStr, `"tools":[]`)
|
||||||
require.Contains(t, resultStr, `"metadata":{"user_id":"user-1"}`)
|
require.Contains(t, resultStr, `"metadata":{"user_id":"user-1"}`)
|
||||||
|
require.Contains(t, resultStr, `"max_tokens":128000`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInjectClaudeCodePrompt_PreservesFieldOrder(t *testing.T) {
|
func TestInjectClaudeCodePrompt_PreservesFieldOrder(t *testing.T) {
|
||||||
|
|||||||
@@ -378,16 +378,27 @@ func TestRewriteSystemForNonClaudeCode(t *testing.T) {
|
|||||||
err := json.Unmarshal(result, &parsed)
|
err := json.Unmarshal(result, &parsed)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// system 应为 array 格式: [{type: "text", text: "...", cache_control: {type: "ephemeral"}}]
|
// system 应为 array 格式,对齐真实 Claude Code CLI 的 2-block 形态:
|
||||||
|
// [0] billing attribution block (x-anthropic-billing-header: cc_version=...;)
|
||||||
|
// [1] Claude Code prompt block (带 cache_control)
|
||||||
systemArr, ok := parsed["system"].([]any)
|
systemArr, ok := parsed["system"].([]any)
|
||||||
require.True(t, ok, "system should be an array, got %T", parsed["system"])
|
require.True(t, ok, "system should be an array, got %T", parsed["system"])
|
||||||
require.Len(t, systemArr, 1, "system array should have exactly 1 block")
|
require.Len(t, systemArr, 2, "system array should have exactly 2 blocks (billing + cc prompt)")
|
||||||
systemBlock, ok := systemArr[0].(map[string]any)
|
|
||||||
|
billingBlock, ok := systemArr[0].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "text", billingBlock["type"])
|
||||||
|
require.Contains(t, billingBlock["text"], "x-anthropic-billing-header:")
|
||||||
|
require.Contains(t, billingBlock["text"], "cc_version=")
|
||||||
|
require.Contains(t, billingBlock["text"], "cc_entrypoint=cli")
|
||||||
|
require.Contains(t, billingBlock["text"], "cch=00000")
|
||||||
|
|
||||||
|
systemBlock, ok := systemArr[1].(map[string]any)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
require.Equal(t, "text", systemBlock["type"])
|
require.Equal(t, "text", systemBlock["type"])
|
||||||
require.Equal(t, tt.wantSystemText, systemBlock["text"])
|
require.Equal(t, tt.wantSystemText, systemBlock["text"])
|
||||||
cc, ok := systemBlock["cache_control"].(map[string]any)
|
cc, ok := systemBlock["cache_control"].(map[string]any)
|
||||||
require.True(t, ok, "system block should have cache_control")
|
require.True(t, ok, "cc prompt block should have cache_control")
|
||||||
require.Equal(t, "ephemeral", cc["type"])
|
require.Equal(t, "ephemeral", cc["type"])
|
||||||
|
|
||||||
// 检查 messages
|
// 检查 messages
|
||||||
|
|||||||
@@ -1078,12 +1078,38 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if gjson.GetBytes(out, "temperature").Exists() {
|
// temperature:真实 Claude Code CLI 总是发送 temperature(默认 1,客户端可覆盖)。
|
||||||
if next, ok := deleteJSONPathBytes(out, "temperature"); ok {
|
// 之前的实现直接 delete 会导致 payload 缺字段,与真实 CLI 字节级不一致。
|
||||||
|
// 策略:客户端传了什么就透传;没传则补默认 1。
|
||||||
|
if !gjson.GetBytes(out, "temperature").Exists() {
|
||||||
|
if next, ok := setJSONValueBytes(out, "temperature", 1); ok {
|
||||||
out = next
|
out = next
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// max_tokens:真实 CLI 的默认值是 128000。缺失时补齐以对齐指纹。
|
||||||
|
if !gjson.GetBytes(out, "max_tokens").Exists() {
|
||||||
|
if next, ok := setJSONValueBytes(out, "max_tokens", 128000); ok {
|
||||||
|
out = next
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// context_management:thinking.type 为 enabled/adaptive 时,真实 CLI 会自动
|
||||||
|
// 附带 {"edits":[{"type":"clear_thinking_20251015","keep":"all"}]}。
|
||||||
|
// 客户端显式传了就透传;否则按 CLI 行为补齐。
|
||||||
|
if !gjson.GetBytes(out, "context_management").Exists() {
|
||||||
|
thinkingType := gjson.GetBytes(out, "thinking.type").String()
|
||||||
|
if thinkingType == "enabled" || thinkingType == "adaptive" {
|
||||||
|
const cmDefault = `{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]}`
|
||||||
|
if next, ok := setJSONRawBytes(out, "context_management", []byte(cmDefault)); ok {
|
||||||
|
out = next
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if gjson.GetBytes(out, "tool_choice").Exists() {
|
if gjson.GetBytes(out, "tool_choice").Exists() {
|
||||||
if next, ok := deleteJSONPathBytes(out, "tool_choice"); ok {
|
if next, ok := deleteJSONPathBytes(out, "tool_choice"); ok {
|
||||||
out = next
|
out = next
|
||||||
|
|||||||
Reference in New Issue
Block a user