mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-09-18 01:26:37 +08:00
feat: support openai websearch models
This commit is contained in:
parent
d65e1237e4
commit
eb5e7efdd7
@ -105,6 +105,8 @@ func testChannel(ctx context.Context, channel *model.Channel, request *relaymode
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err, nil
|
return "", err, nil
|
||||||
}
|
}
|
||||||
|
c.Set(ctxkey.ConvertedRequest, convertedRequest)
|
||||||
|
|
||||||
jsonData, err := json.Marshal(convertedRequest)
|
jsonData, err := json.Marshal(convertedRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err, nil
|
return "", err, nil
|
||||||
|
@ -3,12 +3,14 @@ package openai
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/songquanpeng/one-api/common/config"
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
|
"github.com/songquanpeng/one-api/common/ctxkey"
|
||||||
"github.com/songquanpeng/one-api/common/logger"
|
"github.com/songquanpeng/one-api/common/logger"
|
||||||
"github.com/songquanpeng/one-api/relay/adaptor"
|
"github.com/songquanpeng/one-api/relay/adaptor"
|
||||||
"github.com/songquanpeng/one-api/relay/adaptor/alibailian"
|
"github.com/songquanpeng/one-api/relay/adaptor/alibailian"
|
||||||
@ -18,6 +20,7 @@ import (
|
|||||||
"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/adaptor/openrouter"
|
||||||
|
"github.com/songquanpeng/one-api/relay/billing/ratio"
|
||||||
"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"
|
||||||
@ -161,11 +164,16 @@ func (a *Adaptor) ConvertImageRequest(_ *gin.Context, request *model.ImageReques
|
|||||||
return request, nil
|
return request, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) {
|
func (a *Adaptor) DoRequest(c *gin.Context,
|
||||||
|
meta *meta.Meta,
|
||||||
|
requestBody io.Reader) (*http.Response, error) {
|
||||||
return adaptor.DoRequestHelper(a, c, meta, requestBody)
|
return adaptor.DoRequestHelper(a, c, meta, requestBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) {
|
func (a *Adaptor) DoResponse(c *gin.Context,
|
||||||
|
resp *http.Response,
|
||||||
|
meta *meta.Meta) (usage *model.Usage,
|
||||||
|
err *model.ErrorWithStatusCode) {
|
||||||
if meta.IsStream {
|
if meta.IsStream {
|
||||||
var responseText string
|
var responseText string
|
||||||
err, responseText, usage = StreamHandler(c, resp, meta.Mode)
|
err, responseText, usage = StreamHandler(c, resp, meta.Mode)
|
||||||
@ -187,6 +195,52 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Met
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------
|
||||||
|
// calculate web-search tool cost
|
||||||
|
// -------------------------------------
|
||||||
|
searchContextSize := "medium"
|
||||||
|
var req *model.GeneralOpenAIRequest
|
||||||
|
if vi, ok := c.Get(ctxkey.ConvertedRequest); ok {
|
||||||
|
if req, ok = vi.(*model.GeneralOpenAIRequest); ok {
|
||||||
|
if req != nil &&
|
||||||
|
req.WebSearchOptions != nil &&
|
||||||
|
req.WebSearchOptions.SearchContextSize != nil {
|
||||||
|
searchContextSize = *req.WebSearchOptions.SearchContextSize
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(meta.ActualModelName, "gpt-4o-search"):
|
||||||
|
switch searchContextSize {
|
||||||
|
case "low":
|
||||||
|
usage.ToolsCost += int64(math.Ceil(30 / 1000 * ratio.USD))
|
||||||
|
case "medium":
|
||||||
|
usage.ToolsCost += int64(math.Ceil(35 / 1000 * ratio.USD))
|
||||||
|
case "high":
|
||||||
|
usage.ToolsCost += int64(math.Ceil(40 / 1000 * ratio.USD))
|
||||||
|
default:
|
||||||
|
return nil, ErrorWrapper(
|
||||||
|
errors.Errorf("invalid search context size %q", searchContextSize),
|
||||||
|
"invalid search context size: "+searchContextSize,
|
||||||
|
http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(meta.ActualModelName, "gpt-4o-mini-search"):
|
||||||
|
switch searchContextSize {
|
||||||
|
case "low":
|
||||||
|
usage.ToolsCost += int64(math.Ceil(25 / 1000 * ratio.USD))
|
||||||
|
case "medium":
|
||||||
|
usage.ToolsCost += int64(math.Ceil(27.5 / 1000 * ratio.USD))
|
||||||
|
case "high":
|
||||||
|
usage.ToolsCost += int64(math.Ceil(30 / 1000 * ratio.USD))
|
||||||
|
default:
|
||||||
|
return nil, ErrorWrapper(
|
||||||
|
errors.Errorf("invalid search context size %q", searchContextSize),
|
||||||
|
"invalid search context size: "+searchContextSize,
|
||||||
|
http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,4 +25,6 @@ var ModelList = []string{
|
|||||||
"o1-mini", "o1-mini-2024-09-12",
|
"o1-mini", "o1-mini-2024-09-12",
|
||||||
"o3-mini", "o3-mini-2025-01-31",
|
"o3-mini", "o3-mini-2025-01-31",
|
||||||
"gpt-4.5-preview", "gpt-4.5-preview-2025-02-27",
|
"gpt-4.5-preview", "gpt-4.5-preview-2025-02-27",
|
||||||
|
// https://platform.openai.com/docs/guides/tools-web-search?api-mode=chat
|
||||||
|
"gpt-4o-search-preview", "gpt-4o-mini-search-preview",
|
||||||
}
|
}
|
||||||
|
@ -44,8 +44,10 @@ var ModelRatio = map[string]float64{
|
|||||||
"gpt-4o-2024-05-13": 2.5, // $0.005 / 1K tokens
|
"gpt-4o-2024-05-13": 2.5, // $0.005 / 1K tokens
|
||||||
"gpt-4o-2024-08-06": 1.25, // $0.0025 / 1K tokens
|
"gpt-4o-2024-08-06": 1.25, // $0.0025 / 1K tokens
|
||||||
"gpt-4o-2024-11-20": 1.25, // $0.0025 / 1K tokens
|
"gpt-4o-2024-11-20": 1.25, // $0.0025 / 1K tokens
|
||||||
|
"gpt-4o-search-preview": 2.5, // $0.005 / 1K tokens
|
||||||
"gpt-4o-mini": 0.075, // $0.00015 / 1K tokens
|
"gpt-4o-mini": 0.075, // $0.00015 / 1K tokens
|
||||||
"gpt-4o-mini-2024-07-18": 0.075, // $0.00015 / 1K tokens
|
"gpt-4o-mini-2024-07-18": 0.075, // $0.00015 / 1K tokens
|
||||||
|
"gpt-4o-mini-search-preview": 0.075, // $0.00015 / 1K tokens
|
||||||
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
|
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
|
||||||
// Audio billing will mix text and audio tokens, the unit price is different.
|
// Audio billing will mix text and audio tokens, the unit price is different.
|
||||||
// Here records the cost of text, the cost multiplier of audio
|
// Here records the cost of text, the cost multiplier of audio
|
||||||
|
@ -118,7 +118,7 @@ func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *meta.M
|
|||||||
// we cannot just return, because we may have to return the pre-consumed quota
|
// we cannot just return, because we may have to return the pre-consumed quota
|
||||||
quota = 0
|
quota = 0
|
||||||
}
|
}
|
||||||
quotaDelta := quota - preConsumedQuota
|
quotaDelta := quota - preConsumedQuota + usage.ToolsCost
|
||||||
err := model.PostConsumeTokenQuota(meta.TokenId, quotaDelta)
|
err := model.PostConsumeTokenQuota(meta.TokenId, quotaDelta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "error consuming token remain quota: "+err.Error())
|
logger.Error(ctx, "error consuming token remain quota: "+err.Error())
|
||||||
@ -127,7 +127,13 @@ func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *meta.M
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "error update user quota cache: "+err.Error())
|
logger.Error(ctx, "error update user quota cache: "+err.Error())
|
||||||
}
|
}
|
||||||
logContent := fmt.Sprintf("model rate %.2f, group rate %.2f, completion rate %.2f", modelRatio, groupRatio, completionRatio)
|
|
||||||
|
var logContent string
|
||||||
|
if usage.ToolsCost == 0 {
|
||||||
|
logContent = fmt.Sprintf("model rate %.2f, group rate %.2f, completion rate %.2f", modelRatio, groupRatio, completionRatio)
|
||||||
|
} else {
|
||||||
|
logContent = fmt.Sprintf("model rate %.2f, group rate %.2f, completion rate %.2f, tools cost %d", modelRatio, groupRatio, completionRatio, usage.ToolsCost)
|
||||||
|
}
|
||||||
model.RecordConsumeLog(ctx, &model.Log{
|
model.RecordConsumeLog(ctx, &model.Log{
|
||||||
UserId: meta.UserId,
|
UserId: meta.UserId,
|
||||||
ChannelId: meta.ChannelId,
|
ChannelId: meta.ChannelId,
|
||||||
|
@ -138,6 +138,8 @@ func getRequestBody(c *gin.Context, meta *metalib.Meta, textRequest *relaymodel.
|
|||||||
logger.Debugf(c.Request.Context(), "converted request failed: %s\n", err.Error())
|
logger.Debugf(c.Request.Context(), "converted request failed: %s\n", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
c.Set(ctxkey.ConvertedRequest, convertedRequest)
|
||||||
|
|
||||||
jsonData, err := json.Marshal(convertedRequest)
|
jsonData, err := json.Marshal(convertedRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debugf(c.Request.Context(), "converted request json_marshal_failed: %s\n", err.Error())
|
logger.Debugf(c.Request.Context(), "converted request json_marshal_failed: %s\n", err.Error())
|
||||||
|
@ -45,7 +45,7 @@ type GeneralOpenAIRequest struct {
|
|||||||
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
|
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
|
||||||
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
|
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
|
||||||
Seed float64 `json:"seed,omitempty"`
|
Seed float64 `json:"seed,omitempty"`
|
||||||
ServiceTier *string `json:"service_tier,omitempty"`
|
ServiceTier *string `json:"service_tier,omitempty" binding:"omitempty,oneof=default auto"`
|
||||||
Stop any `json:"stop,omitempty"`
|
Stop any `json:"stop,omitempty"`
|
||||||
Stream bool `json:"stream,omitempty"`
|
Stream bool `json:"stream,omitempty"`
|
||||||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||||||
@ -67,6 +67,8 @@ type GeneralOpenAIRequest struct {
|
|||||||
Quality *string `json:"quality,omitempty"`
|
Quality *string `json:"quality,omitempty"`
|
||||||
Size string `json:"size,omitempty"`
|
Size string `json:"size,omitempty"`
|
||||||
Style *string `json:"style,omitempty"`
|
Style *string `json:"style,omitempty"`
|
||||||
|
WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
|
||||||
|
|
||||||
// Others
|
// Others
|
||||||
Instruction string `json:"instruction,omitempty"`
|
Instruction string `json:"instruction,omitempty"`
|
||||||
NumCtx int `json:"num_ctx,omitempty"`
|
NumCtx int `json:"num_ctx,omitempty"`
|
||||||
@ -81,6 +83,34 @@ type GeneralOpenAIRequest struct {
|
|||||||
Thinking *Thinking `json:"thinking,omitempty"`
|
Thinking *Thinking `json:"thinking,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebSearchOptions is the tool searches the web for relevant results to use in a response.
|
||||||
|
type WebSearchOptions struct {
|
||||||
|
// SearchContextSize is the high level guidance for the amount of context window space to use for the search,
|
||||||
|
// default is "medium".
|
||||||
|
SearchContextSize *string `json:"search_context_size,omitempty" binding:"omitempty,oneof=low medium high"`
|
||||||
|
UserLocation *UserLocation `json:"user_location,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserLocation is a struct that contains the location of the user.
|
||||||
|
type UserLocation struct {
|
||||||
|
// Approximate is the approximate location parameters for the search.
|
||||||
|
Approximate UserLocationApproximate `json:"approximate" binding:"required"`
|
||||||
|
// Type is the type of location approximation.
|
||||||
|
Type string `json:"type" binding:"required,oneof=approximate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserLocationApproximate is a struct that contains the approximate location of the user.
|
||||||
|
type UserLocationApproximate struct {
|
||||||
|
// City is the city of the user, e.g. San Francisco.
|
||||||
|
City *string `json:"city,omitempty"`
|
||||||
|
// Country is the country of the user, e.g. US.
|
||||||
|
Country *string `json:"country,omitempty"`
|
||||||
|
// Region is the region of the user, e.g. California.
|
||||||
|
Region *string `json:"region,omitempty"`
|
||||||
|
// Timezone is the IANA timezone of the user, e.g. America/Los_Angeles.
|
||||||
|
Timezone *string `json:"timezone,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#implementing-extended-thinking
|
// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#implementing-extended-thinking
|
||||||
type Thinking struct {
|
type Thinking struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
@ -36,6 +36,8 @@ type Message struct {
|
|||||||
ToolCalls []Tool `json:"tool_calls,omitempty"`
|
ToolCalls []Tool `json:"tool_calls,omitempty"`
|
||||||
ToolCallId string `json:"tool_call_id,omitempty"`
|
ToolCallId string `json:"tool_call_id,omitempty"`
|
||||||
Audio *messageAudio `json:"audio,omitempty"`
|
Audio *messageAudio `json:"audio,omitempty"`
|
||||||
|
Annotation []AnnotationItem `json:"annotation,omitempty"`
|
||||||
|
|
||||||
// -------------------------------------
|
// -------------------------------------
|
||||||
// Deepseek 专有的一些字段
|
// Deepseek 专有的一些字段
|
||||||
// https://api-docs.deepseek.com/api/create-chat-completion
|
// https://api-docs.deepseek.com/api/create-chat-completion
|
||||||
@ -46,11 +48,13 @@ 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
|
// Openrouter
|
||||||
// -------------------------------------
|
// -------------------------------------
|
||||||
Reasoning *string `json:"reasoning,omitempty"`
|
Reasoning *string `json:"reasoning,omitempty"`
|
||||||
Refusal *bool `json:"refusal,omitempty"`
|
Refusal *bool `json:"refusal,omitempty"`
|
||||||
|
|
||||||
// -------------------------------------
|
// -------------------------------------
|
||||||
// Anthropic
|
// Anthropic
|
||||||
// -------------------------------------
|
// -------------------------------------
|
||||||
@ -58,6 +62,23 @@ type Message struct {
|
|||||||
Signature *string `json:"signature,omitempty"`
|
Signature *string `json:"signature,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AnnotationItem struct {
|
||||||
|
Type string `json:"type" binding:"oneof=url_citation"`
|
||||||
|
UrlCitation UrlCitation `json:"url_citation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UrlCitation is a URL citation when using web search.
|
||||||
|
type UrlCitation struct {
|
||||||
|
// Endpoint is the index of the last character of the URL citation in the message.
|
||||||
|
EndIndex int `json:"end_index"`
|
||||||
|
// StartIndex is the index of the first character of the URL citation in the message.
|
||||||
|
StartIndex int `json:"start_index"`
|
||||||
|
// Title is the title of the web resource.
|
||||||
|
Title string `json:"title"`
|
||||||
|
// Url is the URL of the web resource.
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
// SetReasoningContent sets the reasoning content based on the format
|
// SetReasoningContent sets the reasoning content based on the format
|
||||||
func (m *Message) SetReasoningContent(format string, reasoningContent string) {
|
func (m *Message) SetReasoningContent(format string, reasoningContent string) {
|
||||||
switch ReasoningFormat(strings.ToLower(strings.TrimSpace(format))) {
|
switch ReasoningFormat(strings.ToLower(strings.TrimSpace(format))) {
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
// Usage is the token usage information returned by OpenAI API.
|
||||||
type Usage struct {
|
type Usage struct {
|
||||||
PromptTokens int `json:"prompt_tokens"`
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
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 `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 `json:"completion_tokens_details,omitempty"`
|
||||||
ServiceTier string `gorm:"-" json:"service_tier,omitempty"`
|
ServiceTier string `json:"service_tier,omitempty"`
|
||||||
SystemFingerprint string `gorm:"-" json:"system_fingerprint,omitempty"`
|
SystemFingerprint string `json:"system_fingerprint,omitempty"`
|
||||||
|
|
||||||
|
// -------------------------------------
|
||||||
|
// Custom fields
|
||||||
|
// -------------------------------------
|
||||||
|
// ToolsCost is the cost of using tools, in quota.
|
||||||
|
ToolsCost int64 `json:"tools_cost,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user