mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	feat: support OpenRouter reasoning
This commit is contained in:
		@@ -1,6 +1,9 @@
 | 
			
		||||
package conv
 | 
			
		||||
 | 
			
		||||
func AsString(v any) string {
 | 
			
		||||
	str, _ := v.(string)
 | 
			
		||||
	return str
 | 
			
		||||
	if str, ok := v.(string); ok {
 | 
			
		||||
		return str
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import (
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor/geminiv2"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor/minimax"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor/novita"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor/openrouter"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/channeltype"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/meta"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/model"
 | 
			
		||||
@@ -95,6 +96,21 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G
 | 
			
		||||
		return nil, errors.New("request is nil")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	meta := meta.GetByContext(c)
 | 
			
		||||
	switch meta.ChannelType {
 | 
			
		||||
	case channeltype.OpenRouter:
 | 
			
		||||
		includeReasoning := true
 | 
			
		||||
		request.IncludeReasoning = &includeReasoning
 | 
			
		||||
		if request.Provider == nil || request.Provider.Sort == "" {
 | 
			
		||||
			if request.Provider == nil {
 | 
			
		||||
				request.Provider = &openrouter.RequestProvider{}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			request.Provider.Sort = "throughput"
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if request.Stream && !config.EnforceIncludeUsage {
 | 
			
		||||
		logger.Warn(c.Request.Context(),
 | 
			
		||||
			"please set ENFORCE_INCLUDE_USAGE=true to ensure accurate billing in stream mode")
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ const (
 | 
			
		||||
 | 
			
		||||
func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.ErrorWithStatusCode, string, *model.Usage) {
 | 
			
		||||
	responseText := ""
 | 
			
		||||
	reasoningText := ""
 | 
			
		||||
	scanner := bufio.NewScanner(resp.Body)
 | 
			
		||||
	scanner.Split(bufio.ScanLines)
 | 
			
		||||
	var usage *model.Usage
 | 
			
		||||
@@ -62,6 +63,10 @@ func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.E
 | 
			
		||||
			}
 | 
			
		||||
			render.StringData(c, data)
 | 
			
		||||
			for _, choice := range streamResponse.Choices {
 | 
			
		||||
				if choice.Delta.Reasoning != nil {
 | 
			
		||||
					reasoningText += *choice.Delta.Reasoning
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				responseText += conv.AsString(choice.Delta.Content)
 | 
			
		||||
			}
 | 
			
		||||
			if streamResponse.Usage != nil {
 | 
			
		||||
@@ -94,7 +99,7 @@ func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.E
 | 
			
		||||
		return ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, responseText, usage
 | 
			
		||||
	return nil, reasoningText + responseText, usage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handler handles the non-stream response from OpenAI API
 | 
			
		||||
@@ -150,20 +155,26 @@ func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName st
 | 
			
		||||
			CompletionTokens: completionTokens,
 | 
			
		||||
			TotalTokens:      promptTokens + completionTokens,
 | 
			
		||||
		}
 | 
			
		||||
	} else if textResponse.PromptTokensDetails.AudioTokens+textResponse.CompletionTokensDetails.AudioTokens > 0 {
 | 
			
		||||
	} else if (textResponse.PromptTokensDetails != nil && textResponse.PromptTokensDetails.AudioTokens > 0) ||
 | 
			
		||||
		(textResponse.CompletionTokensDetails != nil && textResponse.CompletionTokensDetails.AudioTokens > 0) {
 | 
			
		||||
		// Convert the more expensive audio tokens to uniformly priced text tokens.
 | 
			
		||||
		// Note that when there are no audio tokens in prompt and completion,
 | 
			
		||||
		// OpenAI will return empty PromptTokensDetails and CompletionTokensDetails, which can be misleading.
 | 
			
		||||
		textResponse.Usage.PromptTokens = textResponse.PromptTokensDetails.TextTokens +
 | 
			
		||||
			int(math.Ceil(
 | 
			
		||||
				float64(textResponse.PromptTokensDetails.AudioTokens)*
 | 
			
		||||
					ratio.GetAudioPromptRatio(modelName),
 | 
			
		||||
			))
 | 
			
		||||
		textResponse.Usage.CompletionTokens = textResponse.CompletionTokensDetails.TextTokens +
 | 
			
		||||
			int(math.Ceil(
 | 
			
		||||
				float64(textResponse.CompletionTokensDetails.AudioTokens)*
 | 
			
		||||
					ratio.GetAudioPromptRatio(modelName)*ratio.GetAudioCompletionRatio(modelName),
 | 
			
		||||
			))
 | 
			
		||||
		if textResponse.PromptTokensDetails != nil {
 | 
			
		||||
			textResponse.Usage.PromptTokens = textResponse.PromptTokensDetails.TextTokens +
 | 
			
		||||
				int(math.Ceil(
 | 
			
		||||
					float64(textResponse.PromptTokensDetails.AudioTokens)*
 | 
			
		||||
						ratio.GetAudioPromptRatio(modelName),
 | 
			
		||||
				))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if textResponse.CompletionTokensDetails != nil {
 | 
			
		||||
			textResponse.Usage.CompletionTokens = textResponse.CompletionTokensDetails.TextTokens +
 | 
			
		||||
				int(math.Ceil(
 | 
			
		||||
					float64(textResponse.CompletionTokensDetails.AudioTokens)*
 | 
			
		||||
						ratio.GetAudioPromptRatio(modelName)*ratio.GetAudioCompletionRatio(modelName),
 | 
			
		||||
				))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		textResponse.Usage.TotalTokens = textResponse.Usage.PromptTokens +
 | 
			
		||||
			textResponse.Usage.CompletionTokens
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								relay/adaptor/openrouter/model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								relay/adaptor/openrouter/model.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
package openrouter
 | 
			
		||||
 | 
			
		||||
// RequestProvider customize how your requests are routed using the provider object
 | 
			
		||||
// in the request body for Chat Completions and Completions.
 | 
			
		||||
//
 | 
			
		||||
// https://openrouter.ai/docs/features/provider-routing
 | 
			
		||||
type RequestProvider struct {
 | 
			
		||||
	// Order is list of provider names to try in order (e.g. ["Anthropic", "OpenAI"]). Default: empty
 | 
			
		||||
	Order []string `json:"order,omitempty"`
 | 
			
		||||
	// AllowFallbacks is whether to allow backup providers when the primary is unavailable. Default: true
 | 
			
		||||
	AllowFallbacks bool `json:"allow_fallbacks,omitempty"`
 | 
			
		||||
	// RequireParameters is only use providers that support all parameters in your request. Default: false
 | 
			
		||||
	RequireParameters bool `json:"require_parameters,omitempty"`
 | 
			
		||||
	// DataCollection is control whether to use providers that may store data ("allow" or "deny"). Default: "allow"
 | 
			
		||||
	DataCollection string `json:"data_collection,omitempty" binding:"omitempty,oneof=allow deny"`
 | 
			
		||||
	// Ignore is list of provider names to skip for this request. Default: empty
 | 
			
		||||
	Ignore []string `json:"ignore,omitempty"`
 | 
			
		||||
	// Quantizations is list of quantization levels to filter by (e.g. ["int4", "int8"]). Default: empty
 | 
			
		||||
	Quantizations []string `json:"quantizations,omitempty"`
 | 
			
		||||
	// Sort is sort providers by price or throughput (e.g. "price" or "throughput"). Default: empty
 | 
			
		||||
	Sort string `json:"sort,omitempty" binding:"omitempty,oneof=price throughput latency"`
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import "github.com/songquanpeng/one-api/relay/adaptor/openrouter"
 | 
			
		||||
 | 
			
		||||
type ResponseFormat struct {
 | 
			
		||||
	Type       string      `json:"type,omitempty"`
 | 
			
		||||
	JsonSchema *JSONSchema `json:"json_schema,omitempty"`
 | 
			
		||||
@@ -68,6 +70,9 @@ type GeneralOpenAIRequest struct {
 | 
			
		||||
	// Others
 | 
			
		||||
	Instruction string `json:"instruction,omitempty"`
 | 
			
		||||
	NumCtx      int    `json:"num_ctx,omitempty"`
 | 
			
		||||
	// Openrouter
 | 
			
		||||
	Provider         *openrouter.RequestProvider `json:"provider,omitempty"`
 | 
			
		||||
	IncludeReasoning *bool                       `json:"include_reasoning,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r GeneralOpenAIRequest) ParseInput() []string {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,11 @@ type Message struct {
 | 
			
		||||
	// Prefix Completion feature as the input for the CoT in the last assistant message.
 | 
			
		||||
	// When using this feature, the prefix parameter must be set to true.
 | 
			
		||||
	ReasoningContent *string `json:"reasoning_content,omitempty"`
 | 
			
		||||
	// -------------------------------------
 | 
			
		||||
	// Openrouter
 | 
			
		||||
	// -------------------------------------
 | 
			
		||||
	Reasoning *string `json:"reasoning,omitempty"`
 | 
			
		||||
	Refusal   *bool   `json:"refusal,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type messageAudio struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,11 @@ type Usage struct {
 | 
			
		||||
	CompletionTokens int `json:"completion_tokens"`
 | 
			
		||||
	TotalTokens      int `json:"total_tokens"`
 | 
			
		||||
	// PromptTokensDetails may be empty for some models
 | 
			
		||||
	PromptTokensDetails usagePromptTokensDetails `gorm:"-" json:"prompt_tokens_details,omitempty"`
 | 
			
		||||
	PromptTokensDetails *usagePromptTokensDetails `gorm:"-" json:"prompt_tokens_details,omitempty"`
 | 
			
		||||
	// CompletionTokensDetails may be empty for some models
 | 
			
		||||
	CompletionTokensDetails usageCompletionTokensDetails `gorm:"-" json:"completion_tokens_details,omitempty"`
 | 
			
		||||
	ServiceTier             string                       `gorm:"-" json:"service_tier,omitempty"`
 | 
			
		||||
	SystemFingerprint       string                       `gorm:"-" json:"system_fingerprint,omitempty"`
 | 
			
		||||
	CompletionTokensDetails *usageCompletionTokensDetails `gorm:"-" json:"completion_tokens_details,omitempty"`
 | 
			
		||||
	ServiceTier             string                        `gorm:"-" json:"service_tier,omitempty"`
 | 
			
		||||
	SystemFingerprint       string                        `gorm:"-" json:"system_fingerprint,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Error struct {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user