瀏覽代碼

feat: support claude cache and thinking for upstream [OpenRouter] (#983)

* feat: support claude cache for upstream [OpenRouter]

* feat: support claude thinking for upstream [OpenRouter]

* feat: reasoning is common params for OpenRouter
neotf 6 月之前
父節點
當前提交
3665ad672e
共有 4 個文件被更改,包括 65 次插入22 次删除
  1. 12 11
      dto/claude.go
  2. 4 1
      dto/openai_request.go
  3. 9 0
      relay/channel/openrouter/dto.go
  4. 40 10
      service/convert.go

+ 12 - 11
dto/claude.go

@@ -7,17 +7,18 @@ type ClaudeMetadata struct {
 }
 
 type ClaudeMediaMessage struct {
-	Type        string               `json:"type,omitempty"`
-	Text        *string              `json:"text,omitempty"`
-	Model       string               `json:"model,omitempty"`
-	Source      *ClaudeMessageSource `json:"source,omitempty"`
-	Usage       *ClaudeUsage         `json:"usage,omitempty"`
-	StopReason  *string              `json:"stop_reason,omitempty"`
-	PartialJson *string              `json:"partial_json,omitempty"`
-	Role        string               `json:"role,omitempty"`
-	Thinking    string               `json:"thinking,omitempty"`
-	Signature   string               `json:"signature,omitempty"`
-	Delta       string               `json:"delta,omitempty"`
+	Type         string               `json:"type,omitempty"`
+	Text         *string              `json:"text,omitempty"`
+	Model        string               `json:"model,omitempty"`
+	Source       *ClaudeMessageSource `json:"source,omitempty"`
+	Usage        *ClaudeUsage         `json:"usage,omitempty"`
+	StopReason   *string              `json:"stop_reason,omitempty"`
+	PartialJson  *string              `json:"partial_json,omitempty"`
+	Role         string               `json:"role,omitempty"`
+	Thinking     string               `json:"thinking,omitempty"`
+	Signature    string               `json:"signature,omitempty"`
+	Delta        string               `json:"delta,omitempty"`
+	CacheControl json.RawMessage      `json:"cache_control,omitempty"`
 	// tool_calls
 	Id        string          `json:"id,omitempty"`
 	Name      string          `json:"name,omitempty"`

+ 4 - 1
dto/openai_request.go

@@ -29,7 +29,6 @@ type GeneralOpenAIRequest struct {
 	MaxTokens           uint           `json:"max_tokens,omitempty"`
 	MaxCompletionTokens uint           `json:"max_completion_tokens,omitempty"`
 	ReasoningEffort     string         `json:"reasoning_effort,omitempty"`
-	//Reasoning           json.RawMessage   `json:"reasoning,omitempty"`
 	Temperature      *float64          `json:"temperature,omitempty"`
 	TopP             float64           `json:"top_p,omitempty"`
 	TopK             int               `json:"top_k,omitempty"`
@@ -56,6 +55,8 @@ type GeneralOpenAIRequest struct {
 	EnableThinking   any               `json:"enable_thinking,omitempty"` // ali
 	ExtraBody        any               `json:"extra_body,omitempty"`
 	WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
+  // OpenRouter Params
+	Reasoning json.RawMessage `json:"reasoning,omitempty"`
 }
 
 func (r *GeneralOpenAIRequest) ToMap() map[string]any {
@@ -125,6 +126,8 @@ type MediaContent struct {
 	InputAudio any    `json:"input_audio,omitempty"`
 	File       any    `json:"file,omitempty"`
 	VideoUrl   any    `json:"video_url,omitempty"`
+	// OpenRouter Params
+	CacheControl json.RawMessage `json:"cache_control,omitempty"`
 }
 
 func (m *MediaContent) GetImageMedia() *MessageImageUrl {

+ 9 - 0
relay/channel/openrouter/dto.go

@@ -0,0 +1,9 @@
+package openrouter
+
+type RequestReasoning struct {
+	// One of the following (not both):
+	Effort    string `json:"effort,omitempty"`     // Can be "high", "medium", or "low" (OpenAI-style)
+	MaxTokens int    `json:"max_tokens,omitempty"` // Specific token limit (Anthropic-style)
+	// Optional: Default is false. All models support this.
+	Exclude bool `json:"exclude,omitempty"` // Set to true to exclude reasoning tokens from response
+}

+ 40 - 10
service/convert.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"one-api/common"
 	"one-api/dto"
+	"one-api/relay/channel/openrouter"
 	relaycommon "one-api/relay/common"
 	"strings"
 )
@@ -18,10 +19,24 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
 		Stream:      claudeRequest.Stream,
 	}
 
+	isOpenRouter := info.ChannelType == common.ChannelTypeOpenRouter
+
 	if claudeRequest.Thinking != nil {
-		if strings.HasSuffix(info.OriginModelName, "-thinking") &&
-			!strings.HasSuffix(claudeRequest.Model, "-thinking") {
-			openAIRequest.Model = openAIRequest.Model + "-thinking"
+		if isOpenRouter {
+			reasoning := openrouter.RequestReasoning{
+				MaxTokens: claudeRequest.Thinking.BudgetTokens,
+			}
+			reasoningJSON, err := json.Marshal(reasoning)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal reasoning: %w", err)
+			}
+			openAIRequest.Reasoning = reasoningJSON
+		} else {
+			thinkingSuffix := "-thinking"
+			if strings.HasSuffix(info.OriginModelName, thinkingSuffix) &&
+				!strings.HasSuffix(openAIRequest.Model, thinkingSuffix) {
+				openAIRequest.Model = openAIRequest.Model + thinkingSuffix
+			}
 		}
 	}
 
@@ -62,16 +77,30 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
 		} else {
 			systems := claudeRequest.ParseSystem()
 			if len(systems) > 0 {
-				systemStr := ""
 				openAIMessage := dto.Message{
 					Role: "system",
 				}
-				for _, system := range systems {
-					if system.Text != nil {
-						systemStr += *system.Text
+				isOpenRouterClaude := isOpenRouter && strings.HasPrefix(info.UpstreamModelName, "anthropic/claude")
+				if isOpenRouterClaude {
+					systemMediaMessages := make([]dto.MediaContent, 0, len(systems))
+					for _, system := range systems {
+						message := dto.MediaContent{
+							Type:         "text",
+							Text:         system.GetText(),
+							CacheControl: system.CacheControl,
+						}
+						systemMediaMessages = append(systemMediaMessages, message)
+					}
+					openAIMessage.SetMediaContent(systemMediaMessages)
+				} else {
+					systemStr := ""
+					for _, system := range systems {
+						if system.Text != nil {
+							systemStr += *system.Text
+						}
 					}
+					openAIMessage.SetStringContent(systemStr)
 				}
-				openAIMessage.SetStringContent(systemStr)
 				openAIMessages = append(openAIMessages, openAIMessage)
 			}
 		}
@@ -97,8 +126,9 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
 				switch mediaMsg.Type {
 				case "text":
 					message := dto.MediaContent{
-						Type: "text",
-						Text: mediaMsg.GetText(),
+						Type:         "text",
+						Text:         mediaMsg.GetText(),
+						CacheControl: mediaMsg.CacheControl,
 					}
 					mediaMessages = append(mediaMessages, message)
 				case "image":