feat: support openai websearch models

This commit is contained in:
Laisky.Cai 2025-03-13 03:37:38 +00:00
parent d65e1237e4
commit eb5e7efdd7
9 changed files with 144 additions and 18 deletions

View File

@ -105,6 +105,8 @@ func testChannel(ctx context.Context, channel *model.Channel, request *relaymode
if err != nil {
return "", err, nil
}
c.Set(ctxkey.ConvertedRequest, convertedRequest)
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return "", err, nil

View File

@ -3,12 +3,14 @@ package openai
import (
"fmt"
"io"
"math"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"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/relay/adaptor"
"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/novita"
"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/meta"
"github.com/songquanpeng/one-api/relay/model"
@ -161,11 +164,16 @@ func (a *Adaptor) ConvertImageRequest(_ *gin.Context, request *model.ImageReques
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)
}
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 {
var responseText string
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
}

View File

@ -25,4 +25,6 @@ var ModelList = []string{
"o1-mini", "o1-mini-2024-09-12",
"o3-mini", "o3-mini-2025-01-31",
"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",
}

View File

@ -44,8 +44,10 @@ var ModelRatio = map[string]float64{
"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-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-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
// Audio billing will mix text and audio tokens, the unit price is different.
// Here records the cost of text, the cost multiplier of audio

View File

@ -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
quota = 0
}
quotaDelta := quota - preConsumedQuota
quotaDelta := quota - preConsumedQuota + usage.ToolsCost
err := model.PostConsumeTokenQuota(meta.TokenId, quotaDelta)
if err != nil {
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 {
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{
UserId: meta.UserId,
ChannelId: meta.ChannelId,

View File

@ -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())
return nil, err
}
c.Set(ctxkey.ConvertedRequest, convertedRequest)
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
logger.Debugf(c.Request.Context(), "converted request json_marshal_failed: %s\n", err.Error())

View File

@ -45,7 +45,7 @@ type GeneralOpenAIRequest struct {
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
ResponseFormat *ResponseFormat `json:"response_format,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"`
Stream bool `json:"stream,omitempty"`
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
@ -67,6 +67,8 @@ type GeneralOpenAIRequest struct {
Quality *string `json:"quality,omitempty"`
Size string `json:"size,omitempty"`
Style *string `json:"style,omitempty"`
WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
// Others
Instruction string `json:"instruction,omitempty"`
NumCtx int `json:"num_ctx,omitempty"`
@ -81,6 +83,34 @@ type GeneralOpenAIRequest struct {
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
type Thinking struct {
Type string `json:"type"`

View File

@ -36,6 +36,8 @@ type Message struct {
ToolCalls []Tool `json:"tool_calls,omitempty"`
ToolCallId string `json:"tool_call_id,omitempty"`
Audio *messageAudio `json:"audio,omitempty"`
Annotation []AnnotationItem `json:"annotation,omitempty"`
// -------------------------------------
// Deepseek 专有的一些字段
// 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.
// 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"`
// -------------------------------------
// Anthropic
// -------------------------------------
@ -58,6 +62,23 @@ type Message struct {
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
func (m *Message) SetReasoningContent(format string, reasoningContent string) {
switch ReasoningFormat(strings.ToLower(strings.TrimSpace(format))) {

View File

@ -1,15 +1,22 @@
package model
// Usage is the token usage information returned by OpenAI API.
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
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 `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 `json:"completion_tokens_details,omitempty"`
ServiceTier string `json:"service_tier,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 {