diff --git a/common/copy.go b/common/copy.go
index 8573d6e0..3edb2fa2 100644
--- a/common/copy.go
+++ b/common/copy.go
@@ -2,7 +2,8 @@ package common
import (
"fmt"
- "github.com/antlabs/pcopy"
+
+ "github.com/jinzhu/copier"
)
func DeepCopy[T any](src *T) (*T, error) {
@@ -10,12 +11,9 @@ func DeepCopy[T any](src *T) (*T, error) {
return nil, fmt.Errorf("copy source cannot be nil")
}
var dst T
- err := pcopy.Copy(&dst, src)
+ err := copier.CopyWithOption(&dst, src, copier.Option{DeepCopy: true, IgnoreEmpty: true})
if err != nil {
return nil, err
}
- if &dst == nil {
- return nil, fmt.Errorf("copy result cannot be nil")
- }
return &dst, nil
}
diff --git a/common/json.go b/common/json.go
index 69aa952e..13e23a46 100644
--- a/common/json.go
+++ b/common/json.go
@@ -20,3 +20,25 @@ func DecodeJson(reader *bytes.Reader, v any) error {
func Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
+
+func GetJsonType(data json.RawMessage) string {
+ data = bytes.TrimSpace(data)
+ if len(data) == 0 {
+ return "unknown"
+ }
+ firstChar := bytes.TrimSpace(data)[0]
+ switch firstChar {
+ case '{':
+ return "object"
+ case '[':
+ return "array"
+ case '"':
+ return "string"
+ case 't', 'f':
+ return "boolean"
+ case 'n':
+ return "null"
+ default:
+ return "number"
+ }
+}
diff --git a/controller/channel.go b/controller/channel.go
index 020a3327..70be91d4 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -380,6 +380,85 @@ func GetChannel(c *gin.Context) {
return
}
+// GetChannelKey 验证2FA后获取渠道密钥
+func GetChannelKey(c *gin.Context) {
+ type GetChannelKeyRequest struct {
+ Code string `json:"code" binding:"required"`
+ }
+
+ var req GetChannelKeyRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ common.ApiError(c, fmt.Errorf("参数错误: %v", err))
+ return
+ }
+
+ userId := c.GetInt("id")
+ channelId, err := strconv.Atoi(c.Param("id"))
+ if err != nil {
+ common.ApiError(c, fmt.Errorf("渠道ID格式错误: %v", err))
+ return
+ }
+
+ // 获取2FA记录并验证
+ twoFA, err := model.GetTwoFAByUserId(userId)
+ if err != nil {
+ common.ApiError(c, fmt.Errorf("获取2FA信息失败: %v", err))
+ return
+ }
+
+ if twoFA == nil || !twoFA.IsEnabled {
+ common.ApiError(c, fmt.Errorf("用户未启用2FA,无法查看密钥"))
+ return
+ }
+
+ // 统一的2FA验证逻辑
+ if !validateTwoFactorAuth(twoFA, req.Code) {
+ common.ApiError(c, fmt.Errorf("验证码或备用码错误,请重试"))
+ return
+ }
+
+ // 获取渠道信息(包含密钥)
+ channel, err := model.GetChannelById(channelId, true)
+ if err != nil {
+ common.ApiError(c, fmt.Errorf("获取渠道信息失败: %v", err))
+ return
+ }
+
+ if channel == nil {
+ common.ApiError(c, fmt.Errorf("渠道不存在"))
+ return
+ }
+
+ // 记录操作日志
+ model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("查看渠道密钥信息 (渠道ID: %d)", channelId))
+
+ // 统一的成功响应格式
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "验证成功",
+ "data": map[string]interface{}{
+ "key": channel.Key,
+ },
+ })
+}
+
+// validateTwoFactorAuth 统一的2FA验证函数
+func validateTwoFactorAuth(twoFA *model.TwoFA, code string) bool {
+ // 尝试验证TOTP
+ if cleanCode, err := common.ValidateNumericCode(code); err == nil {
+ if isValid, _ := twoFA.ValidateTOTPAndUpdateUsage(cleanCode); isValid {
+ return true
+ }
+ }
+
+ // 尝试验证备用码
+ if isValid, err := twoFA.ValidateBackupCodeAndUpdateUsage(code); err == nil && isValid {
+ return true
+ }
+
+ return false
+}
+
// validateChannel 通用的渠道校验函数
func validateChannel(channel *model.Channel, isAdd bool) error {
// 校验 channel settings
diff --git a/dto/claude.go b/dto/claude.go
index 5c4396f2..963e588b 100644
--- a/dto/claude.go
+++ b/dto/claude.go
@@ -488,14 +488,14 @@ func (c *ClaudeResponse) GetClaudeError() *types.ClaudeError {
case string:
// 处理简单字符串错误
return &types.ClaudeError{
- Type: "error",
+ Type: "upstream_error",
Message: err,
}
default:
// 未知类型,尝试转换为字符串
return &types.ClaudeError{
- Type: "unknown_error",
- Message: fmt.Sprintf("%v", err),
+ Type: "unknown_upstream_error",
+ Message: fmt.Sprintf("unknown_error: %v", err),
}
}
}
diff --git a/dto/openai_image.go b/dto/openai_image.go
index 8833e774..9e838688 100644
--- a/dto/openai_image.go
+++ b/dto/openai_image.go
@@ -2,7 +2,9 @@ package dto
import (
"encoding/json"
+ "one-api/common"
"one-api/types"
+ "reflect"
"strings"
"github.com/gin-gonic/gin"
@@ -29,6 +31,68 @@ type ImageRequest struct {
Extra map[string]json.RawMessage `json:"-"`
}
+func (i *ImageRequest) UnmarshalJSON(data []byte) error {
+ // 先解析成 map[string]interface{}
+ var rawMap map[string]json.RawMessage
+ if err := common.Unmarshal(data, &rawMap); err != nil {
+ return err
+ }
+
+ // 用 struct tag 获取所有已定义字段名
+ knownFields := GetJSONFieldNames(reflect.TypeOf(*i))
+
+ // 再正常解析已定义字段
+ type Alias ImageRequest
+ var known Alias
+ if err := common.Unmarshal(data, &known); err != nil {
+ return err
+ }
+ *i = ImageRequest(known)
+
+ // 提取多余字段
+ i.Extra = make(map[string]json.RawMessage)
+ for k, v := range rawMap {
+ if _, ok := knownFields[k]; !ok {
+ i.Extra[k] = v
+ }
+ }
+ return nil
+}
+
+func GetJSONFieldNames(t reflect.Type) map[string]struct{} {
+ fields := make(map[string]struct{})
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+
+ // 跳过匿名字段(例如 ExtraFields)
+ if field.Anonymous {
+ continue
+ }
+
+ tag := field.Tag.Get("json")
+ if tag == "-" || tag == "" {
+ continue
+ }
+
+ // 取逗号前字段名(排除 omitempty 等)
+ name := tag
+ if commaIdx := indexComma(tag); commaIdx != -1 {
+ name = tag[:commaIdx]
+ }
+ fields[name] = struct{}{}
+ }
+ return fields
+}
+
+func indexComma(s string) int {
+ for i := 0; i < len(s); i++ {
+ if s[i] == ',' {
+ return i
+ }
+ }
+ return -1
+}
+
func (i *ImageRequest) GetTokenCountMeta() *types.TokenCountMeta {
var sizeRatio = 1.0
var qualityRatio = 1.0
diff --git a/dto/openai_request.go b/dto/openai_request.go
index 02f969a7..cd05a63c 100644
--- a/dto/openai_request.go
+++ b/dto/openai_request.go
@@ -57,18 +57,24 @@ type GeneralOpenAIRequest struct {
Dimensions int `json:"dimensions,omitempty"`
Modalities json.RawMessage `json:"modalities,omitempty"`
Audio json.RawMessage `json:"audio,omitempty"`
- EnableThinking any `json:"enable_thinking,omitempty"` // ali
- THINKING json.RawMessage `json:"thinking,omitempty"` // doubao,zhipu_v4
- ExtraBody json.RawMessage `json:"extra_body,omitempty"`
- SearchParameters any `json:"search_parameters,omitempty"` //xai
- WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
+ // gemini
+ ExtraBody json.RawMessage `json:"extra_body,omitempty"`
+ //xai
+ SearchParameters json.RawMessage `json:"search_parameters,omitempty"`
+ // claude
+ WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
// OpenRouter Params
Usage json.RawMessage `json:"usage,omitempty"`
Reasoning json.RawMessage `json:"reasoning,omitempty"`
// Ali Qwen Params
VlHighResolutionImages json.RawMessage `json:"vl_high_resolution_images,omitempty"`
- // 用匿名参数接收额外参数,例如ollama的think参数在此接收
- Extra map[string]json.RawMessage `json:"-"`
+ EnableThinking any `json:"enable_thinking,omitempty"`
+ // ollama Params
+ Think json.RawMessage `json:"think,omitempty"`
+ // baidu v2
+ WebSearch json.RawMessage `json:"web_search,omitempty"`
+ // doubao,zhipu_v4
+ THINKING json.RawMessage `json:"thinking,omitempty"`
}
func (r *GeneralOpenAIRequest) GetTokenCountMeta() *types.TokenCountMeta {
@@ -760,27 +766,27 @@ type WebSearchOptions struct {
// https://platform.openai.com/docs/api-reference/responses/create
type OpenAIResponsesRequest struct {
- Model string `json:"model"`
- Input any `json:"input,omitempty"`
- Include json.RawMessage `json:"include,omitempty"`
- Instructions json.RawMessage `json:"instructions,omitempty"`
- MaxOutputTokens uint `json:"max_output_tokens,omitempty"`
- Metadata json.RawMessage `json:"metadata,omitempty"`
- ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
- PreviousResponseID string `json:"previous_response_id,omitempty"`
- Reasoning *Reasoning `json:"reasoning,omitempty"`
- ServiceTier string `json:"service_tier,omitempty"`
- Store bool `json:"store,omitempty"`
- Stream bool `json:"stream,omitempty"`
- Temperature float64 `json:"temperature,omitempty"`
- Text json.RawMessage `json:"text,omitempty"`
- ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
- Tools []map[string]any `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
- TopP float64 `json:"top_p,omitempty"`
- Truncation string `json:"truncation,omitempty"`
- User string `json:"user,omitempty"`
- MaxToolCalls uint `json:"max_tool_calls,omitempty"`
- Prompt json.RawMessage `json:"prompt,omitempty"`
+ Model string `json:"model"`
+ Input json.RawMessage `json:"input,omitempty"`
+ Include json.RawMessage `json:"include,omitempty"`
+ Instructions json.RawMessage `json:"instructions,omitempty"`
+ MaxOutputTokens uint `json:"max_output_tokens,omitempty"`
+ Metadata json.RawMessage `json:"metadata,omitempty"`
+ ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
+ PreviousResponseID string `json:"previous_response_id,omitempty"`
+ Reasoning *Reasoning `json:"reasoning,omitempty"`
+ ServiceTier string `json:"service_tier,omitempty"`
+ Store bool `json:"store,omitempty"`
+ Stream bool `json:"stream,omitempty"`
+ Temperature float64 `json:"temperature,omitempty"`
+ Text json.RawMessage `json:"text,omitempty"`
+ ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
+ Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
+ TopP float64 `json:"top_p,omitempty"`
+ Truncation string `json:"truncation,omitempty"`
+ User string `json:"user,omitempty"`
+ MaxToolCalls uint `json:"max_tool_calls,omitempty"`
+ Prompt json.RawMessage `json:"prompt,omitempty"`
}
func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {
@@ -832,8 +838,7 @@ func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {
}
if len(r.Tools) > 0 {
- toolStr, _ := common.Marshal(r.Tools)
- texts = append(texts, string(toolStr))
+ texts = append(texts, string(r.Tools))
}
return &types.TokenCountMeta{
@@ -853,6 +858,14 @@ func (r *OpenAIResponsesRequest) SetModelName(modelName string) {
}
}
+func (r *OpenAIResponsesRequest) GetToolsMap() []map[string]any {
+ var toolsMap []map[string]any
+ if len(r.Tools) > 0 {
+ _ = common.Unmarshal(r.Tools, &toolsMap)
+ }
+ return toolsMap
+}
+
type Reasoning struct {
Effort string `json:"effort,omitempty"`
Summary string `json:"summary,omitempty"`
@@ -879,13 +892,21 @@ func (r *OpenAIResponsesRequest) ParseInput() []MediaInput {
var inputs []MediaInput
// Try string first
- if str, ok := r.Input.(string); ok {
+ // if str, ok := common.GetJsonType(r.Input); ok {
+ // inputs = append(inputs, MediaInput{Type: "input_text", Text: str})
+ // return inputs
+ // }
+ if common.GetJsonType(r.Input) == "string" {
+ var str string
+ _ = common.Unmarshal(r.Input, &str)
inputs = append(inputs, MediaInput{Type: "input_text", Text: str})
return inputs
}
// Try array of parts
- if array, ok := r.Input.([]any); ok {
+ if common.GetJsonType(r.Input) == "array" {
+ var array []any
+ _ = common.Unmarshal(r.Input, &array)
for _, itemAny := range array {
// Already parsed MediaInput
if media, ok := itemAny.(MediaInput); ok {
diff --git a/go.mod b/go.mod
index 1a92947e..501d966d 100644
--- a/go.mod
+++ b/go.mod
@@ -23,6 +23,7 @@ require (
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
+ github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.5.0
@@ -44,11 +45,7 @@ require (
)
require (
- github.com/Masterminds/goutils v1.1.1 // indirect
- github.com/Masterminds/semver/v3 v3.2.0 // indirect
- github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 // indirect
- github.com/antlabs/pcopy v0.1.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 // indirect
@@ -73,8 +70,6 @@ require (
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
- github.com/huandu/xstrings v1.3.3 // indirect
- github.com/imdario/mergo v0.3.11 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
@@ -85,14 +80,11 @@ require (
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
- github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
- github.com/spf13/cast v1.3.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
diff --git a/go.sum b/go.sum
index 7b8104b9..189d09de 100644
--- a/go.sum
+++ b/go.sum
@@ -1,19 +1,11 @@
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
-github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
-github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
-github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
-github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
-github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
-github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8=
-github.com/antlabs/pcopy v0.1.5 h1:5Fa1ExY9T6ar3ysAi4rzB5jiYg72Innm+/ESEIOSHvQ=
-github.com/antlabs/pcopy v0.1.5/go.mod h1:2FvdkPD3cFiM1CjGuXFCDQZqhKVcLI7IzeSJ2xUIOOI=
github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg=
@@ -110,7 +102,6 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
@@ -121,10 +112,6 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
-github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
-github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -133,6 +120,8 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
+github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@@ -163,12 +152,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
-github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
-github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -201,19 +186,14 @@ github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
-github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
-github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -251,36 +231,25 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -289,29 +258,18 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
@@ -326,7 +284,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/middleware/disable-cache.go b/middleware/disable-cache.go
new file mode 100644
index 00000000..3076e90a
--- /dev/null
+++ b/middleware/disable-cache.go
@@ -0,0 +1,12 @@
+package middleware
+
+import "github.com/gin-gonic/gin"
+
+func DisableCache() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Header("Cache-Control", "no-store, no-cache, must-revalidate, private, max-age=0")
+ c.Header("Pragma", "no-cache")
+ c.Header("Expires", "0")
+ c.Next()
+ }
+}
diff --git a/middleware/distributor.go b/middleware/distributor.go
index a80ed3c6..1e6df872 100644
--- a/middleware/distributor.go
+++ b/middleware/distributor.go
@@ -185,7 +185,7 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
modelRequest.Model = modelName
}
c.Set("relay_mode", relayMode)
- } else if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") && !strings.HasPrefix(c.Request.URL.Path, "/v1/images/edits") {
+ } else if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") && !strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
err = common.UnmarshalBodyReusable(c, &modelRequest)
}
if err != nil {
@@ -208,7 +208,10 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "dall-e")
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/edits") {
- modelRequest.Model = common.GetStringIfEmpty(c.PostForm("model"), "gpt-image-1")
+ //modelRequest.Model = common.GetStringIfEmpty(c.PostForm("model"), "gpt-image-1")
+ if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
+ modelRequest.Model = c.PostForm("model")
+ }
}
if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") {
relayMode := relayconstant.RelayModeAudioSpeech
diff --git a/relay/channel/ali/adaptor.go b/relay/channel/ali/adaptor.go
index c676badc..3ce9e22d 100644
--- a/relay/channel/ali/adaptor.go
+++ b/relay/channel/ali/adaptor.go
@@ -3,7 +3,6 @@ package ali
import (
"errors"
"fmt"
- "github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/dto"
@@ -14,6 +13,8 @@ import (
"one-api/relay/constant"
"one-api/types"
"strings"
+
+ "github.com/gin-gonic/gin"
)
type Adaptor struct {
@@ -44,6 +45,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
fullRequestURL = fmt.Sprintf("%s/api/v1/services/rerank/text-rerank/text-rerank", info.ChannelBaseUrl)
case constant.RelayModeImagesGenerations:
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/text2image/image-synthesis", info.ChannelBaseUrl)
+ case constant.RelayModeImagesEdits:
+ fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", info.ChannelBaseUrl)
case constant.RelayModeCompletions:
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/completions", info.ChannelBaseUrl)
default:
@@ -66,6 +69,9 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
if info.RelayMode == constant.RelayModeImagesGenerations {
req.Set("X-DashScope-Async", "enable")
}
+ if info.RelayMode == constant.RelayModeImagesEdits {
+ req.Set("Content-Type", "application/json")
+ }
return nil
}
@@ -93,11 +99,30 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
}
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
- aliRequest, err := oaiImage2Ali(request)
- if err != nil {
- return nil, fmt.Errorf("convert image request failed: %w", err)
+ if info.RelayMode == constant.RelayModeImagesGenerations {
+ aliRequest, err := oaiImage2Ali(request)
+ if err != nil {
+ return nil, fmt.Errorf("convert image request failed: %w", err)
+ }
+ return aliRequest, nil
+ } else if info.RelayMode == constant.RelayModeImagesEdits {
+ // ali image edit https://bailian.console.aliyun.com/?tab=api#/api/?type=model&url=2976416
+ // 如果用户使用表单,则需要解析表单数据
+ if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
+ aliRequest, err := oaiFormEdit2AliImageEdit(c, info, request)
+ if err != nil {
+ return nil, fmt.Errorf("convert image edit form request failed: %w", err)
+ }
+ return aliRequest, nil
+ } else {
+ aliRequest, err := oaiImage2Ali(request)
+ if err != nil {
+ return nil, fmt.Errorf("convert image request failed: %w", err)
+ }
+ return aliRequest, nil
+ }
}
- return aliRequest, nil
+ return nil, fmt.Errorf("unsupported image relay mode: %d", info.RelayMode)
}
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
@@ -134,6 +159,8 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
switch info.RelayMode {
case constant.RelayModeImagesGenerations:
err, usage = aliImageHandler(c, resp, info)
+ case constant.RelayModeImagesEdits:
+ err, usage = aliImageEditHandler(c, resp, info)
case constant.RelayModeRerank:
err, usage = RerankHandler(c, resp, info)
default:
diff --git a/relay/channel/ali/dto.go b/relay/channel/ali/dto.go
index d40e077d..0873c99f 100644
--- a/relay/channel/ali/dto.go
+++ b/relay/channel/ali/dto.go
@@ -3,10 +3,15 @@ package ali
import "one-api/dto"
type AliMessage struct {
- Content string `json:"content"`
+ Content any `json:"content"`
Role string `json:"role"`
}
+type AliMediaContent struct {
+ Image string `json:"image,omitempty"`
+ Text string `json:"text,omitempty"`
+}
+
type AliInput struct {
Prompt string `json:"prompt,omitempty"`
//History []AliMessage `json:"history,omitempty"`
@@ -70,13 +75,14 @@ type TaskResult struct {
}
type AliOutput struct {
- TaskId string `json:"task_id,omitempty"`
- TaskStatus string `json:"task_status,omitempty"`
- Text string `json:"text"`
- FinishReason string `json:"finish_reason"`
- Message string `json:"message,omitempty"`
- Code string `json:"code,omitempty"`
- Results []TaskResult `json:"results,omitempty"`
+ TaskId string `json:"task_id,omitempty"`
+ TaskStatus string `json:"task_status,omitempty"`
+ Text string `json:"text"`
+ FinishReason string `json:"finish_reason"`
+ Message string `json:"message,omitempty"`
+ Code string `json:"code,omitempty"`
+ Results []TaskResult `json:"results,omitempty"`
+ Choices []map[string]any `json:"choices,omitempty"`
}
type AliResponse struct {
@@ -101,8 +107,9 @@ type AliImageParameters struct {
}
type AliImageInput struct {
- Prompt string `json:"prompt"`
- NegativePrompt string `json:"negative_prompt,omitempty"`
+ Prompt string `json:"prompt,omitempty"`
+ NegativePrompt string `json:"negative_prompt,omitempty"`
+ Messages []AliMessage `json:"messages,omitempty"`
}
type AliRerankParameters struct {
diff --git a/relay/channel/ali/image.go b/relay/channel/ali/image.go
index 78b0c334..490c9d0a 100644
--- a/relay/channel/ali/image.go
+++ b/relay/channel/ali/image.go
@@ -1,9 +1,12 @@
package ali
import (
+ "context"
+ "encoding/base64"
"errors"
"fmt"
"io"
+ "mime/multipart"
"net/http"
"one-api/common"
"one-api/dto"
@@ -21,7 +24,7 @@ func oaiImage2Ali(request dto.ImageRequest) (*AliImageRequest, error) {
var imageRequest AliImageRequest
imageRequest.Model = request.Model
imageRequest.ResponseFormat = request.ResponseFormat
-
+ logger.LogJson(context.Background(), "oaiImage2Ali request extra", request.Extra)
if request.Extra != nil {
if val, ok := request.Extra["parameters"]; ok {
err := common.Unmarshal(val, &imageRequest.Parameters)
@@ -54,6 +57,100 @@ func oaiImage2Ali(request dto.ImageRequest) (*AliImageRequest, error) {
return &imageRequest, nil
}
+func oaiFormEdit2AliImageEdit(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (*AliImageRequest, error) {
+ var imageRequest AliImageRequest
+ imageRequest.Model = request.Model
+ imageRequest.ResponseFormat = request.ResponseFormat
+
+ mf := c.Request.MultipartForm
+ if mf == nil {
+ if _, err := c.MultipartForm(); err != nil {
+ return nil, fmt.Errorf("failed to parse image edit form request: %w", err)
+ }
+ mf = c.Request.MultipartForm
+ }
+
+ var imageFiles []*multipart.FileHeader
+ var exists bool
+
+ // First check for standard "image" field
+ if imageFiles, exists = mf.File["image"]; !exists || len(imageFiles) == 0 {
+ // If not found, check for "image[]" field
+ if imageFiles, exists = mf.File["image[]"]; !exists || len(imageFiles) == 0 {
+ // If still not found, iterate through all fields to find any that start with "image["
+ foundArrayImages := false
+ for fieldName, files := range mf.File {
+ if strings.HasPrefix(fieldName, "image[") && len(files) > 0 {
+ foundArrayImages = true
+ imageFiles = append(imageFiles, files...)
+ }
+ }
+
+ // If no image fields found at all
+ if !foundArrayImages && (len(imageFiles) == 0) {
+ return nil, errors.New("image is required")
+ }
+ }
+ }
+
+ if len(imageFiles) == 0 {
+ return nil, errors.New("image is required")
+ }
+
+ if len(imageFiles) > 1 {
+ return nil, errors.New("only one image is supported for qwen edit")
+ }
+
+ // 获取base64编码的图片
+ var imageBase64s []string
+ for _, file := range imageFiles {
+ image, err := file.Open()
+ if err != nil {
+ return nil, errors.New("failed to open image file")
+ }
+
+ // 读取文件内容
+ imageData, err := io.ReadAll(image)
+ if err != nil {
+ return nil, errors.New("failed to read image file")
+ }
+
+ // 获取MIME类型
+ mimeType := http.DetectContentType(imageData)
+
+ // 编码为base64
+ base64Data := base64.StdEncoding.EncodeToString(imageData)
+
+ // 构造data URL格式
+ dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)
+ imageBase64s = append(imageBase64s, dataURL)
+ image.Close()
+ }
+
+ //dto.MediaContent{}
+ mediaContents := make([]AliMediaContent, len(imageBase64s))
+ for i, b64 := range imageBase64s {
+ mediaContents[i] = AliMediaContent{
+ Image: b64,
+ }
+ }
+ mediaContents = append(mediaContents, AliMediaContent{
+ Text: request.Prompt,
+ })
+ imageRequest.Input = AliImageInput{
+ Messages: []AliMessage{
+ {
+ Role: "user",
+ Content: mediaContents,
+ },
+ },
+ }
+ imageRequest.Parameters = AliImageParameters{
+ Watermark: request.Watermark,
+ }
+ return &imageRequest, nil
+}
+
func updateTask(info *relaycommon.RelayInfo, taskID string) (*AliResponse, error, []byte) {
url := fmt.Sprintf("%s/api/v1/tasks/%s", info.ChannelBaseUrl, taskID)
@@ -196,8 +293,47 @@ func aliImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rela
if err != nil {
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
}
- c.Writer.Header().Set("Content-Type", "application/json")
- c.Writer.WriteHeader(resp.StatusCode)
- c.Writer.Write(jsonResponse)
+ service.IOCopyBytesGracefully(c, resp, jsonResponse)
+ return nil, &dto.Usage{}
+}
+
+func aliImageEditHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*types.NewAPIError, *dto.Usage) {
+ var aliResponse AliResponse
+ responseBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError), nil
+ }
+
+ service.CloseResponseBodyGracefully(resp)
+ err = common.Unmarshal(responseBody, &aliResponse)
+ if err != nil {
+ return types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError), nil
+ }
+
+ if aliResponse.Message != "" {
+ logger.LogError(c, "ali_task_failed: "+aliResponse.Message)
+ return types.NewError(errors.New(aliResponse.Message), types.ErrorCodeBadResponse), nil
+ }
+ var fullTextResponse dto.ImageResponse
+ if len(aliResponse.Output.Choices) > 0 {
+ fullTextResponse = dto.ImageResponse{
+ Created: info.StartTime.Unix(),
+ Data: []dto.ImageData{
+ {
+ Url: aliResponse.Output.Choices[0]["message"].(map[string]any)["content"].([]any)[0].(map[string]any)["image"].(string),
+ B64Json: "",
+ },
+ },
+ }
+ }
+
+ var mapResponse map[string]any
+ _ = common.Unmarshal(responseBody, &mapResponse)
+ fullTextResponse.Extra = mapResponse
+ jsonResponse, err := common.Marshal(fullTextResponse)
+ if err != nil {
+ return types.NewError(err, types.ErrorCodeBadResponseBody), nil
+ }
+ service.IOCopyBytesGracefully(c, resp, jsonResponse)
return nil, &dto.Usage{}
}
diff --git a/relay/channel/baidu_v2/adaptor.go b/relay/channel/baidu_v2/adaptor.go
index 6744f8ba..0577ebcb 100644
--- a/relay/channel/baidu_v2/adaptor.go
+++ b/relay/channel/baidu_v2/adaptor.go
@@ -81,20 +81,23 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
if strings.HasSuffix(info.UpstreamModelName, "-search") {
info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-search")
request.Model = info.UpstreamModelName
- toMap := request.ToMap()
- toMap["web_search"] = map[string]any{
- "enable": true,
- "enable_citation": true,
- "enable_trace": true,
- "enable_status": false,
+ if len(request.WebSearch) == 0 {
+ toMap := request.ToMap()
+ toMap["web_search"] = map[string]any{
+ "enable": true,
+ "enable_citation": true,
+ "enable_trace": true,
+ "enable_status": false,
+ }
+ return toMap, nil
}
- return toMap, nil
+ return request, nil
}
return request, nil
}
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
- return nil, nil
+ return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
diff --git a/relay/channel/ollama/relay-ollama.go b/relay/channel/ollama/relay-ollama.go
index be2029f5..27c67b4e 100644
--- a/relay/channel/ollama/relay-ollama.go
+++ b/relay/channel/ollama/relay-ollama.go
@@ -68,9 +68,7 @@ func requestOpenAI2Ollama(c *gin.Context, request *dto.GeneralOpenAIRequest) (*O
StreamOptions: request.StreamOptions,
Suffix: request.Suffix,
}
- if think, ok := request.Extra["think"]; ok {
- ollamaRequest.Think = think
- }
+ ollamaRequest.Think = request.Think
return ollamaRequest, nil
}
diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go
index 939c0223..1d8286a4 100644
--- a/relay/channel/openai/adaptor.go
+++ b/relay/channel/openai/adaptor.go
@@ -538,7 +538,13 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
// 转换模型推理力度后缀
effort, originModel := parseReasoningEffortFromModelSuffix(request.Model)
if effort != "" {
- request.Reasoning.Effort = effort
+ if request.Reasoning == nil {
+ request.Reasoning = &dto.Reasoning{
+ Effort: effort,
+ }
+ } else {
+ request.Reasoning.Effort = effort
+ }
request.Model = originModel
}
return request, nil
diff --git a/relay/channel/openai/relay_responses.go b/relay/channel/openai/relay_responses.go
index ab2aa8a4..e77080e6 100644
--- a/relay/channel/openai/relay_responses.go
+++ b/relay/channel/openai/relay_responses.go
@@ -92,6 +92,8 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
}
}
}
+ } else {
+ logger.LogError(c, "failed to unmarshal stream response: "+err.Error())
}
return true
})
@@ -107,7 +109,7 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
}
if usage.PromptTokens == 0 && usage.CompletionTokens != 0 {
- usage.PromptTokens = usage.CompletionTokens
+ usage.PromptTokens = info.PromptTokens
} else {
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
}
diff --git a/relay/channel/volcengine/adaptor.go b/relay/channel/volcengine/adaptor.go
index b46cb952..0af019da 100644
--- a/relay/channel/volcengine/adaptor.go
+++ b/relay/channel/volcengine/adaptor.go
@@ -2,6 +2,7 @@ package volcengine
import (
"bytes"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -214,6 +215,12 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
if request == nil {
return nil, errors.New("request is nil")
}
+ // 适配 方舟deepseek混合模型 的 thinking 后缀
+ if strings.HasSuffix(info.UpstreamModelName, "-thinking") && strings.HasPrefix(info.UpstreamModelName, "deepseek") {
+ info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-thinking")
+ request.Model = info.UpstreamModelName
+ request.THINKING = json.RawMessage(`{"type": "enabled"}`)
+ }
return request, nil
}
diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go
index caf8b452..83314bd5 100644
--- a/relay/common/relay_info.go
+++ b/relay/common/relay_info.go
@@ -313,7 +313,7 @@ func GenRelayInfoResponses(c *gin.Context, request *dto.OpenAIResponsesRequest)
BuiltInTools: make(map[string]*BuildInToolInfo),
}
if len(request.Tools) > 0 {
- for _, tool := range request.Tools {
+ for _, tool := range request.GetToolsMap() {
toolType := common.Interface2String(tool["type"])
info.ResponsesUsageInfo.BuiltInTools[toolType] = &BuildInToolInfo{
ToolName: toolType,
diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go
index 56d65a3f..1f6c525b 100644
--- a/relay/compatible_handler.go
+++ b/relay/compatible_handler.go
@@ -130,7 +130,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
jsonData, err := common.Marshal(convertedRequest)
if err != nil {
- return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
+ return types.NewError(err, types.ErrorCodeJsonMarshalFailed, types.ErrOptionWithSkipRetry())
}
// apply param override
diff --git a/relay/helper/valid_request.go b/relay/helper/valid_request.go
index 285f26aa..4d1c1f9b 100644
--- a/relay/helper/valid_request.go
+++ b/relay/helper/valid_request.go
@@ -132,30 +132,34 @@ func GetAndValidOpenAIImageRequest(c *gin.Context, relayMode int) (*dto.ImageReq
switch relayMode {
case relayconstant.RelayModeImagesEdits:
- _, err := c.MultipartForm()
- if err != nil {
- return nil, fmt.Errorf("failed to parse image edit form request: %w", err)
- }
- formData := c.Request.PostForm
- imageRequest.Prompt = formData.Get("prompt")
- imageRequest.Model = formData.Get("model")
- imageRequest.N = uint(common.String2Int(formData.Get("n")))
- imageRequest.Quality = formData.Get("quality")
- imageRequest.Size = formData.Get("size")
-
- if imageRequest.Model == "gpt-image-1" {
- if imageRequest.Quality == "" {
- imageRequest.Quality = "standard"
+ if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
+ _, err := c.MultipartForm()
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse image edit form request: %w", err)
}
- }
- if imageRequest.N == 0 {
- imageRequest.N = 1
- }
+ formData := c.Request.PostForm
+ imageRequest.Prompt = formData.Get("prompt")
+ imageRequest.Model = formData.Get("model")
+ imageRequest.N = uint(common.String2Int(formData.Get("n")))
+ imageRequest.Quality = formData.Get("quality")
+ imageRequest.Size = formData.Get("size")
- watermark := formData.Has("watermark")
- if watermark {
- imageRequest.Watermark = &watermark
+ if imageRequest.Model == "gpt-image-1" {
+ if imageRequest.Quality == "" {
+ imageRequest.Quality = "standard"
+ }
+ }
+ if imageRequest.N == 0 {
+ imageRequest.N = 1
+ }
+
+ watermark := formData.Has("watermark")
+ if watermark {
+ imageRequest.Watermark = &watermark
+ }
+ break
}
+ fallthrough
default:
err := common.UnmarshalBodyReusable(c, imageRequest)
if err != nil {
@@ -163,7 +167,8 @@ func GetAndValidOpenAIImageRequest(c *gin.Context, relayMode int) (*dto.ImageReq
}
if imageRequest.Model == "" {
- imageRequest.Model = "dall-e-3"
+ //imageRequest.Model = "dall-e-3"
+ return nil, errors.New("model is required")
}
if strings.Contains(imageRequest.Size, "×") {
@@ -194,9 +199,9 @@ func GetAndValidOpenAIImageRequest(c *gin.Context, relayMode int) (*dto.ImageReq
}
}
- if imageRequest.Prompt == "" {
- return nil, errors.New("prompt is required")
- }
+ //if imageRequest.Prompt == "" {
+ // return nil, errors.New("prompt is required")
+ //}
if imageRequest.N == 0 {
imageRequest.N = 1
diff --git a/relay/image_handler.go b/relay/image_handler.go
index c700424f..14a7103c 100644
--- a/relay/image_handler.go
+++ b/relay/image_handler.go
@@ -2,14 +2,13 @@ package relay
import (
"bytes"
- "encoding/json"
"fmt"
"io"
"net/http"
"one-api/common"
"one-api/dto"
+ "one-api/logger"
relaycommon "one-api/relay/common"
- relayconstant "one-api/relay/constant"
"one-api/relay/helper"
"one-api/service"
"one-api/setting/model_setting"
@@ -56,10 +55,12 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
- if info.RelayMode == relayconstant.RelayModeImagesEdits {
+
+ switch convertedRequest.(type) {
+ case *bytes.Buffer:
requestBody = convertedRequest.(io.Reader)
- } else {
- jsonData, err := json.Marshal(convertedRequest)
+ default:
+ jsonData, err := common.Marshal(convertedRequest)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
}
@@ -73,7 +74,7 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
}
if common.DebugEnabled {
- println(fmt.Sprintf("image request body: %s", string(jsonData)))
+ logger.LogDebug(c, fmt.Sprintf("image request body: %s", string(jsonData)))
}
requestBody = bytes.NewBuffer(jsonData)
}
diff --git a/router/api-router.go b/router/api-router.go
index be721b05..311bb0a4 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -114,6 +114,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.GET("/models", controller.ChannelListModels)
channelRoute.GET("/models_enabled", controller.EnabledListModels)
channelRoute.GET("/:id", controller.GetChannel)
+ channelRoute.POST("/:id/key", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.GetChannelKey)
channelRoute.GET("/test", controller.TestAllChannels)
channelRoute.GET("/test/:id", controller.TestChannel)
channelRoute.GET("/update_balance", controller.UpdateAllChannelsBalance)
diff --git a/service/convert.go b/service/convert.go
index ea219c4f..b232ca39 100644
--- a/service/convert.go
+++ b/service/convert.go
@@ -248,9 +248,10 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
},
})
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
- Type: "content_block_delta",
+ Index: &info.ClaudeConvertInfo.Index,
+ Type: "content_block_delta",
Delta: &dto.ClaudeMediaMessage{
- Type: "text",
+ Type: "text_delta",
Text: common.GetPointer[string](openAIResponse.Choices[0].Delta.GetContentString()),
},
})
diff --git a/setting/operation_setting/tools.go b/setting/operation_setting/tools.go
index 9f19ee84..549a1862 100644
--- a/setting/operation_setting/tools.go
+++ b/setting/operation_setting/tools.go
@@ -32,11 +32,12 @@ func GetWebSearchPricePerThousand(modelName string, contextSize string) float64
// 确定模型类型
// https://platform.openai.com/docs/pricing Web search 价格按模型类型收费
// 新版计费规则不再关联 search context size,故在const区域将各size的价格设为一致。
- // gpt-4o and gpt-4.1 models (including mini models) 等模型更贵,o3, o4-mini, o3-pro, and deep research models 等模型更便宜
+ // gpt-5, gpt-5-mini, gpt-5-nano 和 o 系列模型价格为 10.00 美元/千次调用,产生额外 token 计入 input_tokens
+ // gpt-4o, gpt-4.1, gpt-4o-mini 和 gpt-4.1-mini 价格为 25.00 美元/千次调用,不产生额外 token
isNormalPriceModel :=
strings.HasPrefix(modelName, "o3") ||
strings.HasPrefix(modelName, "o4") ||
- strings.Contains(modelName, "deep-research")
+ strings.HasPrefix(modelName, "gpt-5")
var priceWebSearchPerThousandCalls float64
if isNormalPriceModel {
priceWebSearchPerThousandCalls = WebSearchPrice
diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go
index d61c7546..f06cd71e 100644
--- a/setting/ratio_setting/model_ratio.go
+++ b/setting/ratio_setting/model_ratio.go
@@ -52,52 +52,52 @@ var defaultModelRatio = map[string]float64{
"gpt-4o-realtime-preview-2024-12-17": 2.5,
"gpt-4o-mini-realtime-preview": 0.3,
"gpt-4o-mini-realtime-preview-2024-12-17": 0.3,
- "gpt-4.1": 1.0, // $2 / 1M tokens
- "gpt-4.1-2025-04-14": 1.0, // $2 / 1M tokens
- "gpt-4.1-mini": 0.2, // $0.4 / 1M tokens
- "gpt-4.1-mini-2025-04-14": 0.2, // $0.4 / 1M tokens
- "gpt-4.1-nano": 0.05, // $0.1 / 1M tokens
- "gpt-4.1-nano-2025-04-14": 0.05, // $0.1 / 1M tokens
- "gpt-image-1": 2.5, // $5 / 1M tokens
- "o1": 7.5, // $15 / 1M tokens
- "o1-2024-12-17": 7.5, // $15 / 1M tokens
- "o1-preview": 7.5, // $15 / 1M tokens
- "o1-preview-2024-09-12": 7.5, // $15 / 1M tokens
- "o1-mini": 0.55, // $1.1 / 1M tokens
- "o1-mini-2024-09-12": 0.55, // $1.1 / 1M tokens
- "o1-pro": 75.0, // $150 / 1M tokens
- "o1-pro-2025-03-19": 75.0, // $150 / 1M tokens
- "o3-mini": 0.55,
- "o3-mini-2025-01-31": 0.55,
- "o3-mini-high": 0.55,
- "o3-mini-2025-01-31-high": 0.55,
- "o3-mini-low": 0.55,
- "o3-mini-2025-01-31-low": 0.55,
- "o3-mini-medium": 0.55,
- "o3-mini-2025-01-31-medium": 0.55,
- "o3": 1.0, // $2 / 1M tokens
- "o3-2025-04-16": 1.0, // $2 / 1M tokens
- "o3-pro": 10.0, // $20 / 1M tokens
- "o3-pro-2025-06-10": 10.0, // $20 / 1M tokens
- "o3-deep-research": 5.0, // $10 / 1M tokens
- "o3-deep-research-2025-06-26": 5.0, // $10 / 1M tokens
- "o4-mini": 0.55, // $1.1 / 1M tokens
- "o4-mini-2025-04-16": 0.55, // $1.1 / 1M tokens
- "o4-mini-deep-research": 1.0, // $2 / 1M tokens
- "o4-mini-deep-research-2025-06-26": 1.0, // $2 / 1M tokens
- "gpt-4o-mini": 0.075,
- "gpt-4o-mini-2024-07-18": 0.075,
- "gpt-4-turbo": 5, // $0.01 / 1K tokens
- "gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens
- "gpt-4.5-preview": 37.5,
- "gpt-4.5-preview-2025-02-27": 37.5,
- "gpt-5": 0.625,
- "gpt-5-2025-08-07": 0.625,
- "gpt-5-chat-latest": 0.625,
- "gpt-5-mini": 0.125,
- "gpt-5-mini-2025-08-07": 0.125,
- "gpt-5-nano": 0.025,
- "gpt-5-nano-2025-08-07": 0.025,
+ "gpt-4.1": 1.0, // $2 / 1M tokens
+ "gpt-4.1-2025-04-14": 1.0, // $2 / 1M tokens
+ "gpt-4.1-mini": 0.2, // $0.4 / 1M tokens
+ "gpt-4.1-mini-2025-04-14": 0.2, // $0.4 / 1M tokens
+ "gpt-4.1-nano": 0.05, // $0.1 / 1M tokens
+ "gpt-4.1-nano-2025-04-14": 0.05, // $0.1 / 1M tokens
+ "gpt-image-1": 2.5, // $5 / 1M tokens
+ "o1": 7.5, // $15 / 1M tokens
+ "o1-2024-12-17": 7.5, // $15 / 1M tokens
+ "o1-preview": 7.5, // $15 / 1M tokens
+ "o1-preview-2024-09-12": 7.5, // $15 / 1M tokens
+ "o1-mini": 0.55, // $1.1 / 1M tokens
+ "o1-mini-2024-09-12": 0.55, // $1.1 / 1M tokens
+ "o1-pro": 75.0, // $150 / 1M tokens
+ "o1-pro-2025-03-19": 75.0, // $150 / 1M tokens
+ "o3-mini": 0.55,
+ "o3-mini-2025-01-31": 0.55,
+ "o3-mini-high": 0.55,
+ "o3-mini-2025-01-31-high": 0.55,
+ "o3-mini-low": 0.55,
+ "o3-mini-2025-01-31-low": 0.55,
+ "o3-mini-medium": 0.55,
+ "o3-mini-2025-01-31-medium": 0.55,
+ "o3": 1.0, // $2 / 1M tokens
+ "o3-2025-04-16": 1.0, // $2 / 1M tokens
+ "o3-pro": 10.0, // $20 / 1M tokens
+ "o3-pro-2025-06-10": 10.0, // $20 / 1M tokens
+ "o3-deep-research": 5.0, // $10 / 1M tokens
+ "o3-deep-research-2025-06-26": 5.0, // $10 / 1M tokens
+ "o4-mini": 0.55, // $1.1 / 1M tokens
+ "o4-mini-2025-04-16": 0.55, // $1.1 / 1M tokens
+ "o4-mini-deep-research": 1.0, // $2 / 1M tokens
+ "o4-mini-deep-research-2025-06-26": 1.0, // $2 / 1M tokens
+ "gpt-4o-mini": 0.075,
+ "gpt-4o-mini-2024-07-18": 0.075,
+ "gpt-4-turbo": 5, // $0.01 / 1K tokens
+ "gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens
+ "gpt-4.5-preview": 37.5,
+ "gpt-4.5-preview-2025-02-27": 37.5,
+ "gpt-5": 0.625,
+ "gpt-5-2025-08-07": 0.625,
+ "gpt-5-chat-latest": 0.625,
+ "gpt-5-mini": 0.125,
+ "gpt-5-mini-2025-08-07": 0.125,
+ "gpt-5-nano": 0.025,
+ "gpt-5-nano-2025-08-07": 0.025,
//"gpt-3.5-turbo-0301": 0.75, //deprecated
"gpt-3.5-turbo": 0.25,
"gpt-3.5-turbo-0613": 0.75,
@@ -468,7 +468,13 @@ func GetCompletionRatio(name string) float64 {
func getHardcodedCompletionModelRatio(name string) (float64, bool) {
lowercaseName := strings.ToLower(name)
- if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
+
+ isReservedModel := strings.HasSuffix(name, "-all") || strings.HasSuffix(name, "-gizmo-*")
+ if isReservedModel {
+ return 2, false
+ }
+
+ if strings.HasPrefix(name, "gpt-") {
if strings.HasPrefix(name, "gpt-4o") {
if name == "gpt-4o-2024-05-13" {
return 3, true
@@ -535,7 +541,7 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
if strings.HasPrefix(name, "gemini-2.5-flash-lite") {
return 4, false
}
- return 2.5 / 0.3, true
+ return 2.5 / 0.3, false
}
return 4, false
}
diff --git a/web/src/components/common/modals/TwoFactorAuthModal.jsx b/web/src/components/common/modals/TwoFactorAuthModal.jsx
new file mode 100644
index 00000000..b2271968
--- /dev/null
+++ b/web/src/components/common/modals/TwoFactorAuthModal.jsx
@@ -0,0 +1,129 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see