diff --git a/dto/claude.go b/dto/claude.go index 8068feb8..36dfc02e 100644 --- a/dto/claude.go +++ b/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"` diff --git a/dto/openai_request.go b/dto/openai_request.go index 16cdf3a2..a7325fe8 100644 --- a/dto/openai_request.go +++ b/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 { diff --git a/relay/channel/openrouter/dto.go b/relay/channel/openrouter/dto.go new file mode 100644 index 00000000..607f495b --- /dev/null +++ b/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 +} diff --git a/service/convert.go b/service/convert.go index cc462b40..cb964a46 100644 --- a/service/convert.go +++ b/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":