mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-09-18 01:26:37 +08:00
feat: support OpenRouter reasoning
This commit is contained in:
parent
2a5908586d
commit
a5f5e85c44
@ -1,6 +1,9 @@
|
|||||||
package conv
|
package conv
|
||||||
|
|
||||||
func AsString(v any) string {
|
func AsString(v any) string {
|
||||||
str, _ := v.(string)
|
if str, ok := v.(string); ok {
|
||||||
return str
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/songquanpeng/one-api/relay/adaptor/geminiv2"
|
"github.com/songquanpeng/one-api/relay/adaptor/geminiv2"
|
||||||
"github.com/songquanpeng/one-api/relay/adaptor/minimax"
|
"github.com/songquanpeng/one-api/relay/adaptor/minimax"
|
||||||
"github.com/songquanpeng/one-api/relay/adaptor/novita"
|
"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/channeltype"
|
||||||
"github.com/songquanpeng/one-api/relay/meta"
|
"github.com/songquanpeng/one-api/relay/meta"
|
||||||
"github.com/songquanpeng/one-api/relay/model"
|
"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")
|
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 {
|
if request.Stream && !config.EnforceIncludeUsage {
|
||||||
logger.Warn(c.Request.Context(),
|
logger.Warn(c.Request.Context(),
|
||||||
"please set ENFORCE_INCLUDE_USAGE=true to ensure accurate billing in stream mode")
|
"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) {
|
func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.ErrorWithStatusCode, string, *model.Usage) {
|
||||||
responseText := ""
|
responseText := ""
|
||||||
|
reasoningText := ""
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
scanner.Split(bufio.ScanLines)
|
scanner.Split(bufio.ScanLines)
|
||||||
var usage *model.Usage
|
var usage *model.Usage
|
||||||
@ -62,6 +63,10 @@ func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.E
|
|||||||
}
|
}
|
||||||
render.StringData(c, data)
|
render.StringData(c, data)
|
||||||
for _, choice := range streamResponse.Choices {
|
for _, choice := range streamResponse.Choices {
|
||||||
|
if choice.Delta.Reasoning != nil {
|
||||||
|
reasoningText += *choice.Delta.Reasoning
|
||||||
|
}
|
||||||
|
|
||||||
responseText += conv.AsString(choice.Delta.Content)
|
responseText += conv.AsString(choice.Delta.Content)
|
||||||
}
|
}
|
||||||
if streamResponse.Usage != nil {
|
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 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
|
// 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,
|
CompletionTokens: completionTokens,
|
||||||
TotalTokens: promptTokens + 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.
|
// Convert the more expensive audio tokens to uniformly priced text tokens.
|
||||||
// Note that when there are no audio tokens in prompt and completion,
|
// Note that when there are no audio tokens in prompt and completion,
|
||||||
// OpenAI will return empty PromptTokensDetails and CompletionTokensDetails, which can be misleading.
|
// OpenAI will return empty PromptTokensDetails and CompletionTokensDetails, which can be misleading.
|
||||||
textResponse.Usage.PromptTokens = textResponse.PromptTokensDetails.TextTokens +
|
if textResponse.PromptTokensDetails != nil {
|
||||||
int(math.Ceil(
|
textResponse.Usage.PromptTokens = textResponse.PromptTokensDetails.TextTokens +
|
||||||
float64(textResponse.PromptTokensDetails.AudioTokens)*
|
int(math.Ceil(
|
||||||
ratio.GetAudioPromptRatio(modelName),
|
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.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.TotalTokens = textResponse.Usage.PromptTokens +
|
||||||
textResponse.Usage.CompletionTokens
|
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
|
package model
|
||||||
|
|
||||||
|
import "github.com/songquanpeng/one-api/relay/adaptor/openrouter"
|
||||||
|
|
||||||
type ResponseFormat struct {
|
type ResponseFormat struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
JsonSchema *JSONSchema `json:"json_schema,omitempty"`
|
JsonSchema *JSONSchema `json:"json_schema,omitempty"`
|
||||||
@ -68,6 +70,9 @@ type GeneralOpenAIRequest struct {
|
|||||||
// Others
|
// Others
|
||||||
Instruction string `json:"instruction,omitempty"`
|
Instruction string `json:"instruction,omitempty"`
|
||||||
NumCtx int `json:"num_ctx,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 {
|
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.
|
// 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.
|
// When using this feature, the prefix parameter must be set to true.
|
||||||
ReasoningContent *string `json:"reasoning_content,omitempty"`
|
ReasoningContent *string `json:"reasoning_content,omitempty"`
|
||||||
|
// -------------------------------------
|
||||||
|
// Openrouter
|
||||||
|
// -------------------------------------
|
||||||
|
Reasoning *string `json:"reasoning,omitempty"`
|
||||||
|
Refusal *bool `json:"refusal,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type messageAudio struct {
|
type messageAudio struct {
|
||||||
|
@ -5,11 +5,11 @@ type Usage struct {
|
|||||||
CompletionTokens int `json:"completion_tokens"`
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
TotalTokens int `json:"total_tokens"`
|
TotalTokens int `json:"total_tokens"`
|
||||||
// PromptTokensDetails may be empty for some models
|
// 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 may be empty for some models
|
||||||
CompletionTokensDetails usageCompletionTokensDetails `gorm:"-" json:"completion_tokens_details,omitempty"`
|
CompletionTokensDetails *usageCompletionTokensDetails `gorm:"-" json:"completion_tokens_details,omitempty"`
|
||||||
ServiceTier string `gorm:"-" json:"service_tier,omitempty"`
|
ServiceTier string `gorm:"-" json:"service_tier,omitempty"`
|
||||||
SystemFingerprint string `gorm:"-" json:"system_fingerprint,omitempty"`
|
SystemFingerprint string `gorm:"-" json:"system_fingerprint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user