diff --git a/dto/claude.go b/dto/claude.go index 48a5000..32e3171 100644 --- a/dto/claude.go +++ b/dto/claude.go @@ -215,8 +215,7 @@ type ClaudeRequest struct { Metadata json.RawMessage `json:"metadata,omitempty"` // ServiceTier specifies upstream service level and may affect billing. // This field is filtered by default and can be enabled via channel setting allow_service_tier. - ServiceTier string `json:"service_tier,omitempty"` - CacheControl json.RawMessage `json:"cache_control,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` } // createClaudeFileSource 根据数据内容创建正确类型的 FileSource diff --git a/dto/openai_request.go b/dto/openai_request.go index 897f75a..ef6dbed 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -515,12 +515,6 @@ func (m *Message) ParseContent() []MediaContent { // 尝试解析为数组 //var arrayContent []map[string]interface{} - // 直接是 []MediaContent 的情况(如通过 SetMediaContent 设置) - if mediaContent, ok := m.Content.([]MediaContent); ok { - m.parsedContent = mediaContent - return mediaContent - } - arrayContent, ok := m.Content.([]any) if !ok { return contentList diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go index df36aec..594afd4 100644 --- a/relay/channel/claude/relay-claude.go +++ b/relay/channel/claude/relay-claude.go @@ -274,9 +274,8 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe for _, ctx := range message.ParseContent() { if ctx.Type == "text" { systemMessages = append(systemMessages, dto.ClaudeMediaMessage{ - Type: "text", - Text: common.GetPointer[string](ctx.Text), - CacheControl: ctx.CacheControl, + Type: "text", + Text: common.GetPointer[string](ctx.Text), }) } // 未来可以在这里扩展对图片等其他类型的支持 @@ -336,8 +335,7 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe claudeMediaMessages := make([]dto.ClaudeMediaMessage, 0) for _, mediaMessage := range message.ParseContent() { claudeMediaMessage := dto.ClaudeMediaMessage{ - Type: mediaMessage.Type, - CacheControl: mediaMessage.CacheControl, + Type: mediaMessage.Type, } if mediaMessage.Type == "text" { claudeMediaMessage.Text = common.GetPointer[string](mediaMessage.Text) @@ -391,7 +389,6 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe claudeRequest.Prompt = "" claudeRequest.Messages = claudeMessages - claudeRequest.CacheControl = json.RawMessage(`{"type":"ephemeral"}`) return &claudeRequest, nil } diff --git a/relay/channel/claude/relay_claude_test.go b/relay/channel/claude/relay_claude_test.go index 126b0b0..e34c861 100644 --- a/relay/channel/claude/relay_claude_test.go +++ b/relay/channel/claude/relay_claude_test.go @@ -1,12 +1,10 @@ package claude import ( - "encoding/json" "strings" "testing" "github.com/QuantumNous/new-api/dto" - "github.com/stretchr/testify/require" ) func TestFormatClaudeResponseInfo_MessageStart(t *testing.T) { @@ -175,159 +173,3 @@ func TestFormatClaudeResponseInfo_ContentBlockDelta(t *testing.T) { t.Errorf("ResponseText = %q, want %q", claudeInfo.ResponseText.String(), "hello") } } - -func TestRequestOpenAI2ClaudeMessage_AutoCacheControl(t *testing.T) { - req := dto.GeneralOpenAIRequest{ - Model: "claude-sonnet-4-20250514", - Messages: []dto.Message{ - {Role: "user", Content: "Hello"}, - }, - MaxTokens: 1024, - } - - claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req) - require.NoError(t, err) - require.NotNil(t, claudeReq) - require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl)) -} - -func TestRequestOpenAI2ClaudeMessage_SystemStringWithAutoCache(t *testing.T) { - req := dto.GeneralOpenAIRequest{ - Model: "claude-sonnet-4-20250514", - Messages: []dto.Message{ - {Role: "system", Content: "You are a helpful assistant."}, - {Role: "user", Content: "Hello"}, - }, - MaxTokens: 1024, - } - - claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req) - require.NoError(t, err) - require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl)) - - systemMedia, ok := claudeReq.System.([]dto.ClaudeMediaMessage) - require.True(t, ok, "system should be []ClaudeMediaMessage") - require.Len(t, systemMedia, 1) - require.Equal(t, "You are a helpful assistant.", *systemMedia[0].Text) -} - -func TestRequestOpenAI2ClaudeMessage_SystemComplexWithCacheControl(t *testing.T) { - // 通过 JSON 反序列化构造请求,模拟真实场景 - jsonReq := `{ - "model": "claude-sonnet-4-20250514", - "messages": [ - {"role": "system", "content": [{"type": "text", "text": "System instruction", "cache_control": {"type": "ephemeral"}}]}, - {"role": "user", "content": "Hello"} - ], - "max_tokens": 1024 - }` - var req dto.GeneralOpenAIRequest - require.NoError(t, json.Unmarshal([]byte(jsonReq), &req)) - - claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req) - require.NoError(t, err) - - // system 复合内容从 JSON 反序列化后 Content 是 []any - // ParseContent 会解析 map[string]any,但不会提取 cache_control(已知限制) - // 所以这里验证 system 被正确传递,cache_control 在当前实现中不会透传 - require.NotNil(t, claudeReq.System) - // 顶层 cache_control 仍然注入 - require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl)) -} - -func TestRequestOpenAI2ClaudeMessage_UserMessageCacheControlPassthrough(t *testing.T) { - // 通过 SetMediaContent 直接设置(如 OpenRouter 等非标准路径) - msg := dto.Message{Role: "user"} - msg.SetMediaContent([]dto.MediaContent{ - {Type: "text", Text: "Hello", CacheControl: json.RawMessage(`{"type":"ephemeral"}`)}, - }) - - req := dto.GeneralOpenAIRequest{ - Model: "claude-sonnet-4-20250514", - Messages: []dto.Message{msg}, - MaxTokens: 1024, - } - - claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req) - require.NoError(t, err) - require.Len(t, claudeReq.Messages, 1) - - // content block CacheControl 透传(仅在 Content 为 []MediaContent 时有效) - content, ok := claudeReq.Messages[0].Content.([]dto.ClaudeMediaMessage) - require.True(t, ok) - require.Len(t, content, 1) - require.Equal(t, `{"type":"ephemeral"}`, string(content[0].CacheControl)) -} - -func TestRequestOpenAI2ClaudeMessage_ToolUseWithAutoCache(t *testing.T) { - req := dto.GeneralOpenAIRequest{ - Model: "claude-sonnet-4-20250514", - Messages: []dto.Message{ - {Role: "user", Content: "What's the weather?"}, - { - Role: "assistant", - Content: "", - ToolCalls: json.RawMessage(`[{"id":"call_1","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"NYC\"}"}}]`), - }, - { - Role: "tool", - Content: `{"temp":72}`, - ToolCallId: "call_1", - }, - }, - MaxTokens: 1024, - } - - claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req) - require.NoError(t, err) - require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl)) - // user + assistant(with tool_use) + user(tool_result) = 3 messages - require.Len(t, claudeReq.Messages, 3) -} - -func TestRequestOpenAI2ClaudeMessage_MinimalRequestNoPanic(t *testing.T) { - req := dto.GeneralOpenAIRequest{ - Model: "claude-sonnet-4-20250514", - Messages: []dto.Message{}, - } - - claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req) - require.NoError(t, err) - require.NotNil(t, claudeReq) - require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl)) -} - -func TestRequestOpenAI2ClaudeMessage_FullSerialization(t *testing.T) { - req := dto.GeneralOpenAIRequest{ - Model: "claude-sonnet-4-20250514", - Messages: []dto.Message{ - {Role: "system", Content: "You are helpful."}, - {Role: "user", Content: "Hello"}, - }, - MaxTokens: 1024, - } - - claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req) - require.NoError(t, err) - - jsonBytes, err := json.Marshal(claudeReq) - require.NoError(t, err) - jsonStr := string(jsonBytes) - - require.Contains(t, jsonStr, `"cache_control":{"type":"ephemeral"}`) - require.Contains(t, jsonStr, `"model":"claude-sonnet-4-20250514"`) - require.Contains(t, jsonStr, `"max_tokens":1024`) -} - -func TestClaudeRequest_EmptyCacheControl_Omitted(t *testing.T) { - req := dto.ClaudeRequest{ - Model: "claude-sonnet-4-20250514", - MaxTokens: 1024, - } - - jsonBytes, err := json.Marshal(req) - require.NoError(t, err) - jsonStr := string(jsonBytes) - - require.NotContains(t, jsonStr, "cache_control") -}