Compare commits

...

17 Commits

Author SHA1 Message Date
CaIon
90d85a6f0a feat: add AzureNoRemoveDotTime constant and update channel handling #1044
- Introduced a new constant `AzureNoRemoveDotTime` in `constant/azure.go` to manage model name formatting for channels created after May 10, 2025.
- Updated `distributor.go` to set `channel_create_time` in the context.
- Modified `adaptor.go` to conditionally remove dots from model names based on the channel creation time.
- Enhanced `relay_info.go` to include `ChannelCreateTime` in the `RelayInfo` struct.
- Updated English localization files to reflect changes in model name handling for new channels.
2025-05-08 22:39:55 +08:00
CaIon
d40429ad93 fix: update OpenAI request handling to include 'o1-preview' model support #1029 2025-05-08 21:34:31 +08:00
Calcium-Ion
30806ef270 Merge pull request #1040 from QuantumNous/responses-quota
fix: tool quota calculate
2025-05-08 01:21:34 +08:00
IcedTangerine
3458476115 Merge pull request #1039 from liusanp/main
Fix grok-2-image request error
2025-05-07 22:06:51 +08:00
IcedTangerine
61c685ad79 Merge pull request #1032 from feitianbubu/upstream
fix: correct error messages for dall-e models size parameters
2025-05-07 20:56:36 +08:00
IcedTangerine
0121795a84 Merge pull request #1037 from LarchLiu/main
fix: gemini response json schema
2025-05-07 20:53:45 +08:00
creamlike1024
ae254f5368 fix: tool quota calculate 2025-05-07 19:33:32 +08:00
liusanp
562448b441 fix: xAi response 2025-05-07 18:59:27 +08:00
liusanp
04f7d89399 fix: xAi requestUrl 2025-05-07 18:32:59 +08:00
Alex Liu
0d456df588 fix: gemini response json schema 2025-05-07 18:08:56 +08:00
CaIon
dc3b453b05 fix: update ResponseChunkData to format data correctly without newline 2025-05-07 17:02:47 +08:00
CaIon
b19e1b8207 feat: add support for BaiduV2 channel in relay info 2025-05-07 16:30:32 +08:00
liusanp
97b5ca8099 fix: quality, size or style are not supported by xAI API 2025-05-07 16:17:22 +08:00
CaIon
4ecf5dde14 Merge remote-tracking branch 'origin/main' 2025-05-07 16:16:19 +08:00
joey
65ccfd0848 feat: support model mapping chain
#1033
2025-05-07 16:00:35 +08:00
skynono
2621b77f9a fix: correct error messages for dall-e models size parameters
(cherry picked from commit 149d06850c10cc6cdb3291164e3e46f99ca59abc)
2025-05-07 11:21:19 +08:00
CaIon
5639f1c2d8 feat: add support for DeepSeek channel in streamSupportedChannels 2025-05-06 18:41:01 +08:00
14 changed files with 130 additions and 59 deletions

5
constant/azure.go Normal file
View File

@@ -0,0 +1,5 @@
package constant
import "time"
var AzureNoRemoveDotTime = time.Date(2025, time.May, 10, 0, 0, 0, 0, time.UTC).Unix()

View File

@@ -213,6 +213,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
c.Set("channel_id", channel.Id)
c.Set("channel_name", channel.Name)
c.Set("channel_type", channel.Type)
c.Set("channel_create_time", channel.CreatedTime)
c.Set("channel_setting", channel.GetSetting())
c.Set("param_override", channel.GetParamOverride())
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {

View File

@@ -391,6 +391,7 @@ func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interfac
}
// 删除所有的title字段
delete(v, "title")
delete(v, "$schema")
// 如果type不为object和array则直接返回
if typeVal, exists := v["type"]; !exists || (typeVal != "object" && typeVal != "array") {
return schema

View File

@@ -8,6 +8,7 @@ import (
"io"
"mime/multipart"
"net/http"
"net/textproto"
"one-api/common"
constant2 "one-api/constant"
"one-api/dto"
@@ -25,8 +26,6 @@ import (
"path/filepath"
"strings"
"net/textproto"
"github.com/gin-gonic/gin"
)
@@ -93,7 +92,10 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
task := strings.TrimPrefix(requestURL, "/v1/")
model_ := info.UpstreamModelName
model_ = strings.Replace(model_, ".", "", -1)
// 2025年5月10日后创建的渠道不移除.
if info.ChannelCreateTime < constant2.AzureNoRemoveDotTime {
model_ = strings.Replace(model_, ".", "", -1)
}
// https://github.com/songquanpeng/one-api/issues/67
requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model_, task)
if info.RelayMode == constant.RelayModeRealtime {
@@ -173,7 +175,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
info.UpstreamModelName = request.Model
// o系列模型developer适配o1-mini除外
if !strings.HasPrefix(request.Model, "o1-mini") {
if !strings.HasPrefix(request.Model, "o1-mini") && !strings.HasPrefix(request.Model, "o1-preview") {
//修改第一个Message的内容将system改为developer
if len(request.Messages) > 0 && request.Messages[0].Role == "system" {
request.Messages[0].Role = "developer"

View File

@@ -2,14 +2,16 @@ package xai
import (
"errors"
"fmt"
"io"
"net/http"
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"strings"
"one-api/relay/constant"
"github.com/gin-gonic/gin"
)
@@ -28,15 +30,20 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
}
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
request.Size = ""
return request, nil
xaiRequest := ImageRequest{
Model: request.Model,
Prompt: request.Prompt,
N: request.N,
ResponseFormat: request.ResponseFormat,
}
return xaiRequest, nil
}
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil
return relaycommon.GetFullRequestURL(info.BaseUrl, info.RequestURLPath, info.ChannelType), nil
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
@@ -89,15 +96,16 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
}
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
if info.IsStream {
err, usage = xAIStreamHandler(c, resp, info)
} else {
err, usage = xAIHandler(c, resp, info)
switch info.RelayMode {
case constant.RelayModeImagesGenerations, constant.RelayModeImagesEdits:
err, usage = openai.OpenaiHandlerWithUsage(c, resp, info)
default:
if info.IsStream {
err, usage = xAIStreamHandler(c, resp, info)
} else {
err, usage = xAIHandler(c, resp, info)
}
}
//if _, ok := usage.(*dto.Usage); ok && usage != nil {
// usage.(*dto.Usage).CompletionTokens = usage.(*dto.Usage).TotalTokens - usage.(*dto.Usage).PromptTokens
//}
return
}

View File

@@ -12,3 +12,16 @@ type ChatCompletionResponse struct {
Usage *dto.Usage `json:"usage"`
SystemFingerprint string `json:"system_fingerprint"`
}
// quality, size or style are not supported by xAI API at the moment.
type ImageRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt" binding:"required"`
N int `json:"n,omitempty"`
// Size string `json:"size,omitempty"`
// Quality string `json:"quality,omitempty"`
ResponseFormat string `json:"response_format,omitempty"`
// Style string `json:"style,omitempty"`
// User string `json:"user,omitempty"`
// ExtraFields json.RawMessage `json:"extra_fields,omitempty"`
}

View File

@@ -98,6 +98,7 @@ type RelayInfo struct {
UserQuota int
RelayFormat string
SendResponseCount int
ChannelCreateTime int64
ThinkingContentInfo
*ClaudeConvertInfo
*RerankerInfo
@@ -115,6 +116,8 @@ var streamSupportedChannels = map[int]bool{
common.ChannelTypeVolcEngine: true,
common.ChannelTypeOllama: true,
common.ChannelTypeXai: true,
common.ChannelTypeDeepSeek: true,
common.ChannelTypeBaiduV2: true,
}
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
@@ -207,14 +210,15 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
OriginModelName: c.GetString("original_model"),
UpstreamModelName: c.GetString("original_model"),
//RecodeModelName: c.GetString("original_model"),
IsModelMapped: false,
ApiType: apiType,
ApiVersion: c.GetString("api_version"),
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
Organization: c.GetString("channel_organization"),
ChannelSetting: channelSetting,
ParamOverride: paramOverride,
RelayFormat: RelayFormatOpenAI,
IsModelMapped: false,
ApiType: apiType,
ApiVersion: c.GetString("api_version"),
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
Organization: c.GetString("channel_organization"),
ChannelSetting: channelSetting,
ChannelCreateTime: c.GetInt64("channel_create_time"),
ParamOverride: paramOverride,
RelayFormat: RelayFormatOpenAI,
ThinkingContentInfo: ThinkingContentInfo{
IsFirstThinkingContent: true,
SendLastThinkingContent: false,

View File

@@ -37,7 +37,7 @@ func ClaudeData(c *gin.Context, resp dto.ClaudeResponse) error {
func ClaudeChunkData(c *gin.Context, resp dto.ClaudeResponse, data string) {
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("event: %s\n", resp.Type)})
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("data: %s", data)})
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("data: %s\n", data)})
if flusher, ok := c.Writer.(http.Flusher); ok {
flusher.Flush()
}
@@ -45,7 +45,7 @@ func ClaudeChunkData(c *gin.Context, resp dto.ClaudeResponse, data string) {
func ResponseChunkData(c *gin.Context, resp dto.ResponsesStreamResponse, data string) {
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("event: %s\n", resp.Type)})
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("data: %s\n", data)})
c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("data: %s", data)})
if flusher, ok := c.Writer.(http.Flusher); ok {
flusher.Flush()
}

View File

@@ -2,9 +2,11 @@ package helper
import (
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"one-api/relay/common"
"github.com/gin-gonic/gin"
)
func ModelMappedHelper(c *gin.Context, info *common.RelayInfo) error {
@@ -16,9 +18,36 @@ func ModelMappedHelper(c *gin.Context, info *common.RelayInfo) error {
if err != nil {
return fmt.Errorf("unmarshal_model_mapping_failed")
}
if modelMap[info.OriginModelName] != "" {
info.UpstreamModelName = modelMap[info.OriginModelName]
info.IsModelMapped = true
// 支持链式模型重定向,最终使用链尾的模型
currentModel := info.OriginModelName
visitedModels := map[string]bool{
currentModel: true,
}
for {
if mappedModel, exists := modelMap[currentModel]; exists && mappedModel != "" {
// 模型重定向循环检测,避免无限循环
if visitedModels[mappedModel] {
if mappedModel == currentModel {
if currentModel == info.OriginModelName {
info.IsModelMapped = false
return nil
} else {
info.IsModelMapped = true
break
}
}
return errors.New("model_mapping_contains_cycle")
}
visitedModels[mappedModel] = true
currentModel = mappedModel
info.IsModelMapped = true
} else {
break
}
}
if info.IsModelMapped {
info.UpstreamModelName = currentModel
}
}
return nil

View File

@@ -49,11 +49,11 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
// Not "256x256", "512x512", or "1024x1024"
if imageRequest.Model == "dall-e-2" || imageRequest.Model == "dall-e" {
if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" {
return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024")
return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024 for dall-e-2 or dall-e")
}
} else if imageRequest.Model == "dall-e-3" {
if imageRequest.Size != "" && imageRequest.Size != "1024x1024" && imageRequest.Size != "1024x1792" && imageRequest.Size != "1792x1024" {
return nil, errors.New("size must be one of 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024")
return nil, errors.New("size must be one of 1024x1024, 1024x1792 or 1792x1024 for dall-e-3")
}
if imageRequest.Quality == "" {
imageRequest.Quality = "standard"

View File

@@ -364,11 +364,11 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
var webSearchPrice float64
if relayInfo.ResponsesUsageInfo != nil {
if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists && webSearchTool.CallCount > 0 {
// 计算 web search 调用的配额 (配额 = 价格 * 调用次数 / 1000)
// 计算 web search 调用的配额 (配额 = 价格 * 调用次数 / 1000 * 分组倍率)
webSearchPrice = operation_setting.GetWebSearchPricePerThousand(modelName, webSearchTool.SearchContextSize)
dWebSearchQuota = decimal.NewFromFloat(webSearchPrice).
Mul(decimal.NewFromInt(int64(webSearchTool.CallCount))).
Div(decimal.NewFromInt(1000))
Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
extraContent += fmt.Sprintf("Web Search 调用 %d 次,上下文大小 %s调用花费 $%s",
webSearchTool.CallCount, webSearchTool.SearchContextSize, dWebSearchQuota.String())
}
@@ -381,7 +381,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
fileSearchPrice = operation_setting.GetFileSearchPricePerThousand()
dFileSearchQuota = decimal.NewFromFloat(fileSearchPrice).
Mul(decimal.NewFromInt(int64(fileSearchTool.CallCount))).
Div(decimal.NewFromInt(1000))
Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
extraContent += fmt.Sprintf("File Search 调用 %d 次,调用花费 $%s",
fileSearchTool.CallCount, dFileSearchQuota.String())
}

View File

@@ -354,8 +354,8 @@ export function renderModelPrice(
let price =
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
(completionTokens / 1000000) * completionRatioPrice * groupRatio +
(webSearchCallCount / 1000) * webSearchPrice +
(fileSearchCallCount / 1000) * fileSearchPrice;
(webSearchCallCount / 1000) * webSearchPrice * groupRatio +
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
return (
<>
@@ -446,7 +446,7 @@ export function renderModelPrice(
)
: webSearch && webSearchCallCount > 0 && !image && !fileSearch
? i18next.t(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} = ${{total}}',
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}} = ${{total}}',
{
input: inputTokens,
price: inputRatioPrice,
@@ -458,9 +458,12 @@ export function renderModelPrice(
total: price.toFixed(6),
},
)
: fileSearch && fileSearchCallCount > 0 && !image && !webSearch
: fileSearch &&
fileSearchCallCount > 0 &&
!image &&
!webSearch
? i18next.t(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} = ${{total}}',
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
{
input: inputTokens,
price: inputRatioPrice,
@@ -472,9 +475,13 @@ export function renderModelPrice(
total: price.toFixed(6),
},
)
: webSearch && webSearchCallCount > 0 && fileSearch && fileSearchCallCount > 0 && !image
: webSearch &&
webSearchCallCount > 0 &&
fileSearch &&
fileSearchCallCount > 0 &&
!image
? i18next.t(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} = ${{total}}',
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}}+ 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
{
input: inputTokens,
price: inputRatioPrice,

View File

@@ -493,6 +493,7 @@
"默认": "default",
"图片演示": "Image demo",
"注意系统请求的时模型名称中的点会被剔除例如gpt-4.1会请求为gpt-41所以在Azure部署的时候部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41",
"2025年5月10日后添加的渠道不需要再在部署的时候移除模型名称中的\".\"": "After May 10, 2025, channels added do not need to remove the dot in the model name during deployment",
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
"取消无限额度": "Cancel unlimited quota",
"取消": "Cancel",

View File

@@ -477,24 +477,24 @@ const EditChannel = (props) => {
type={'warning'}
description={
<>
{t('注意系统请求的时模型名称中的点会被剔除例如gpt-4.1会请求为gpt-41所以在Azure部署的时候,部署模型名称需要手动改为gpt-41')}
<br />
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer',
}}
onClick={() => {
setModalImageUrl(
'/azure_model_name.png',
);
setIsModalOpenurl(true)
{t('2025年5月10日后添加的渠道不需要再在部署的时候移除模型名称中的"."')}
{/*<br />*/}
{/*<Typography.Text*/}
{/* style={{*/}
{/* color: 'rgba(var(--semi-blue-5), 1)',*/}
{/* userSelect: 'none',*/}
{/* cursor: 'pointer',*/}
{/* }}*/}
{/* onClick={() => {*/}
{/* setModalImageUrl(*/}
{/* '/azure_model_name.png',*/}
{/* );*/}
{/* setIsModalOpenurl(true)*/}
}}
>
{t('查看示例')}
</Typography.Text>
{/* }}*/}
{/*>*/}
{/* {t('查看示例')}*/}
{/*</Typography.Text>*/}
</>
}
></Banner>