Commit 652506f1 by t0ng7u

Merge branch 'alpha' into refactor/model-pricing

parents 8ffa48ab 1f9134cd
...@@ -65,8 +65,6 @@ func ChannelType2APIType(channelType int) (int, bool) { ...@@ -65,8 +65,6 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeCoze apiType = constant.APITypeCoze
case constant.ChannelTypeJimeng: case constant.ChannelTypeJimeng:
apiType = constant.APITypeJimeng apiType = constant.APITypeJimeng
case constant.ChannelTypeClaudeCode:
apiType = constant.APITypeClaudeCode
} }
if apiType == -1 { if apiType == -1 {
return constant.APITypeOpenAI, false return constant.APITypeOpenAI, false
......
...@@ -31,6 +31,5 @@ const ( ...@@ -31,6 +31,5 @@ const (
APITypeXai APITypeXai
APITypeCoze APITypeCoze
APITypeJimeng APITypeJimeng
APITypeClaudeCode
APITypeDummy // this one is only for count, do not add any channel after this APITypeDummy // this one is only for count, do not add any channel after this
) )
...@@ -50,7 +50,6 @@ const ( ...@@ -50,7 +50,6 @@ const (
ChannelTypeKling = 50 ChannelTypeKling = 50
ChannelTypeJimeng = 51 ChannelTypeJimeng = 51
ChannelTypeVidu = 52 ChannelTypeVidu = 52
ChannelTypeClaudeCode = 53
ChannelTypeDummy // this one is only for count, do not add any channel after this ChannelTypeDummy // this one is only for count, do not add any channel after this
) )
...@@ -109,5 +108,4 @@ var ChannelBaseURLs = []string{ ...@@ -109,5 +108,4 @@ var ChannelBaseURLs = []string{
"https://api.klingai.com", //50 "https://api.klingai.com", //50
"https://visual.volcengineapi.com", //51 "https://visual.volcengineapi.com", //51
"https://api.vidu.cn", //52 "https://api.vidu.cn", //52
"https://api.anthropic.com", //53
} }
...@@ -36,11 +36,30 @@ type OpenAIModel struct { ...@@ -36,11 +36,30 @@ type OpenAIModel struct {
Parent string `json:"parent"` Parent string `json:"parent"`
} }
type GoogleOpenAICompatibleModels []struct {
Name string `json:"name"`
Version string `json:"version"`
DisplayName string `json:"displayName"`
Description string `json:"description,omitempty"`
InputTokenLimit int `json:"inputTokenLimit"`
OutputTokenLimit int `json:"outputTokenLimit"`
SupportedGenerationMethods []string `json:"supportedGenerationMethods"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"topP,omitempty"`
TopK int `json:"topK,omitempty"`
MaxTemperature int `json:"maxTemperature,omitempty"`
}
type OpenAIModelsResponse struct { type OpenAIModelsResponse struct {
Data []OpenAIModel `json:"data"` Data []OpenAIModel `json:"data"`
Success bool `json:"success"` Success bool `json:"success"`
} }
type GoogleOpenAICompatibleResponse struct {
Models []GoogleOpenAICompatibleModels `json:"models"`
NextPageToken string `json:"nextPageToken"`
}
func parseStatusFilter(statusParam string) int { func parseStatusFilter(statusParam string) int {
switch strings.ToLower(statusParam) { switch strings.ToLower(statusParam) {
case "enabled", "1": case "enabled", "1":
...@@ -168,20 +187,52 @@ func FetchUpstreamModels(c *gin.Context) { ...@@ -168,20 +187,52 @@ func FetchUpstreamModels(c *gin.Context) {
if channel.GetBaseURL() != "" { if channel.GetBaseURL() != "" {
baseURL = channel.GetBaseURL() baseURL = channel.GetBaseURL()
} }
url := fmt.Sprintf("%s/v1/models", baseURL)
var url string
switch channel.Type { switch channel.Type {
case constant.ChannelTypeGemini: case constant.ChannelTypeGemini:
url = fmt.Sprintf("%s/v1beta/openai/models", baseURL) // curl https://example.com/v1beta/models?key=$GEMINI_API_KEY
url = fmt.Sprintf("%s/v1beta/openai/models?key=%s", baseURL, channel.Key)
case constant.ChannelTypeAli: case constant.ChannelTypeAli:
url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL) url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
default:
url = fmt.Sprintf("%s/v1/models", baseURL)
}
// 获取响应体 - 根据渠道类型决定是否添加 AuthHeader
var body []byte
if channel.Type == constant.ChannelTypeGemini {
body, err = GetResponseBody("GET", url, channel, nil) // I don't know why, but Gemini requires no AuthHeader
} else {
body, err = GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
} }
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
if err != nil { if err != nil {
common.ApiError(c, err) common.ApiError(c, err)
return return
} }
var result OpenAIModelsResponse var result OpenAIModelsResponse
var parseSuccess bool
// 适配特殊格式
switch channel.Type {
case constant.ChannelTypeGemini:
var googleResult GoogleOpenAICompatibleResponse
if err = json.Unmarshal(body, &googleResult); err == nil {
// 转换Google格式到OpenAI格式
for _, model := range googleResult.Models {
for _, gModel := range model {
result.Data = append(result.Data, OpenAIModel{
ID: gModel.Name,
})
}
}
parseSuccess = true
}
}
// 如果解析失败,尝试OpenAI格式
if !parseSuccess {
if err = json.Unmarshal(body, &result); err != nil { if err = json.Unmarshal(body, &result); err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
...@@ -189,6 +240,7 @@ func FetchUpstreamModels(c *gin.Context) { ...@@ -189,6 +240,7 @@ func FetchUpstreamModels(c *gin.Context) {
}) })
return return
} }
}
var ids []string var ids []string
for _, model := range result.Data { for _, model := range result.Data {
...@@ -669,6 +721,7 @@ func DeleteChannelBatch(c *gin.Context) { ...@@ -669,6 +721,7 @@ func DeleteChannelBatch(c *gin.Context) {
type PatchChannel struct { type PatchChannel struct {
model.Channel model.Channel
MultiKeyMode *string `json:"multi_key_mode"` MultiKeyMode *string `json:"multi_key_mode"`
KeyMode *string `json:"key_mode"` // 多key模式下密钥覆盖或者追加
} }
func UpdateChannel(c *gin.Context) { func UpdateChannel(c *gin.Context) {
...@@ -688,7 +741,7 @@ func UpdateChannel(c *gin.Context) { ...@@ -688,7 +741,7 @@ func UpdateChannel(c *gin.Context) {
return return
} }
// Preserve existing ChannelInfo to ensure multi-key channels keep correct state even if the client does not send ChannelInfo in the request. // Preserve existing ChannelInfo to ensure multi-key channels keep correct state even if the client does not send ChannelInfo in the request.
originChannel, err := model.GetChannelById(channel.Id, false) originChannel, err := model.GetChannelById(channel.Id, true)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
...@@ -704,6 +757,69 @@ func UpdateChannel(c *gin.Context) { ...@@ -704,6 +757,69 @@ func UpdateChannel(c *gin.Context) {
if channel.MultiKeyMode != nil && *channel.MultiKeyMode != "" { if channel.MultiKeyMode != nil && *channel.MultiKeyMode != "" {
channel.ChannelInfo.MultiKeyMode = constant.MultiKeyMode(*channel.MultiKeyMode) channel.ChannelInfo.MultiKeyMode = constant.MultiKeyMode(*channel.MultiKeyMode)
} }
// 处理多key模式下的密钥追加/覆盖逻辑
if channel.KeyMode != nil && channel.ChannelInfo.IsMultiKey {
switch *channel.KeyMode {
case "append":
// 追加模式:将新密钥添加到现有密钥列表
if originChannel.Key != "" {
var newKeys []string
var existingKeys []string
// 解析现有密钥
if strings.HasPrefix(strings.TrimSpace(originChannel.Key), "[") {
// JSON数组格式
var arr []json.RawMessage
if err := json.Unmarshal([]byte(strings.TrimSpace(originChannel.Key)), &arr); err == nil {
existingKeys = make([]string, len(arr))
for i, v := range arr {
existingKeys[i] = string(v)
}
}
} else {
// 换行分隔格式
existingKeys = strings.Split(strings.Trim(originChannel.Key, "\n"), "\n")
}
// 处理 Vertex AI 的特殊情况
if channel.Type == constant.ChannelTypeVertexAi {
// 尝试解析新密钥为JSON数组
if strings.HasPrefix(strings.TrimSpace(channel.Key), "[") {
array, err := getVertexArrayKeys(channel.Key)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "追加密钥解析失败: " + err.Error(),
})
return
}
newKeys = array
} else {
// 单个JSON密钥
newKeys = []string{channel.Key}
}
// 合并密钥
allKeys := append(existingKeys, newKeys...)
channel.Key = strings.Join(allKeys, "\n")
} else {
// 普通渠道的处理
inputKeys := strings.Split(channel.Key, "\n")
for _, key := range inputKeys {
key = strings.TrimSpace(key)
if key != "" {
newKeys = append(newKeys, key)
}
}
// 合并密钥
allKeys := append(existingKeys, newKeys...)
channel.Key = strings.Join(allKeys, "\n")
}
}
case "replace":
// 覆盖模式:直接使用新密钥(默认行为,不需要特殊处理)
}
}
err = channel.Update() err = channel.Update()
if err != nil { if err != nil {
common.ApiError(c, err) common.ApiError(c, err)
......
package controller
import (
"net/http"
"one-api/common"
"one-api/service"
"github.com/gin-gonic/gin"
)
// ExchangeCodeRequest 授权码交换请求
type ExchangeCodeRequest struct {
AuthorizationCode string `json:"authorization_code" binding:"required"`
CodeVerifier string `json:"code_verifier" binding:"required"`
State string `json:"state" binding:"required"`
}
// GenerateClaudeOAuthURL 生成Claude OAuth授权URL
func GenerateClaudeOAuthURL(c *gin.Context) {
params, err := service.GenerateOAuthParams()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "生成OAuth授权URL失败: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "生成OAuth授权URL成功",
"data": params,
})
}
// ExchangeClaudeOAuthCode 交换Claude OAuth授权码
func ExchangeClaudeOAuthCode(c *gin.Context) {
var req ExchangeCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "请求参数错误: " + err.Error(),
})
return
}
// 解析授权码
cleanedCode, err := service.ParseAuthorizationCode(req.AuthorizationCode)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": err.Error(),
})
return
}
// 交换token
tokenResult, err := service.ExchangeCode(cleanedCode, req.CodeVerifier, req.State, nil)
if err != nil {
common.SysError("Claude OAuth token exchange failed: " + err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "授权码交换失败: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "授权码交换成功",
"data": tokenResult,
})
}
...@@ -47,7 +47,7 @@ func relayHandler(c *gin.Context, relayMode int) *types.NewAPIError { ...@@ -47,7 +47,7 @@ func relayHandler(c *gin.Context, relayMode int) *types.NewAPIError {
err = relay.TextHelper(c) err = relay.TextHelper(c)
} }
if constant2.ErrorLogEnabled && err != nil { if constant2.ErrorLogEnabled && err != nil && types.IsRecordErrorLog(err) {
// 保存错误日志到mysql中 // 保存错误日志到mysql中
userId := c.GetInt("id") userId := c.GetInt("id")
tokenName := c.GetString("token_name") tokenName := c.GetString("token_name")
...@@ -62,6 +62,14 @@ func relayHandler(c *gin.Context, relayMode int) *types.NewAPIError { ...@@ -62,6 +62,14 @@ func relayHandler(c *gin.Context, relayMode int) *types.NewAPIError {
other["channel_id"] = channelId other["channel_id"] = channelId
other["channel_name"] = c.GetString("channel_name") other["channel_name"] = c.GetString("channel_name")
other["channel_type"] = c.GetInt("channel_type") other["channel_type"] = c.GetInt("channel_type")
adminInfo := make(map[string]interface{})
adminInfo["use_channel"] = c.GetStringSlice("use_channel")
isMultiKey := common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey)
if isMultiKey {
adminInfo["is_multi_key"] = true
adminInfo["multi_key_index"] = common.GetContextKeyInt(c, constant.ContextKeyChannelMultiKeyIndex)
}
other["admin_info"] = adminInfo
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveError(), tokenId, 0, false, userGroup, other) model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveError(), tokenId, 0, false, userGroup, other)
} }
......
package gemini package dto
import ( import (
"encoding/json" "encoding/json"
...@@ -56,7 +56,7 @@ type FunctionCall struct { ...@@ -56,7 +56,7 @@ type FunctionCall struct {
Arguments any `json:"args"` Arguments any `json:"args"`
} }
type FunctionResponse struct { type GeminiFunctionResponse struct {
Name string `json:"name"` Name string `json:"name"`
Response map[string]interface{} `json:"response"` Response map[string]interface{} `json:"response"`
} }
...@@ -81,7 +81,7 @@ type GeminiPart struct { ...@@ -81,7 +81,7 @@ type GeminiPart struct {
Thought bool `json:"thought,omitempty"` Thought bool `json:"thought,omitempty"`
InlineData *GeminiInlineData `json:"inlineData,omitempty"` InlineData *GeminiInlineData `json:"inlineData,omitempty"`
FunctionCall *FunctionCall `json:"functionCall,omitempty"` FunctionCall *FunctionCall `json:"functionCall,omitempty"`
FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"` FunctionResponse *GeminiFunctionResponse `json:"functionResponse,omitempty"`
FileData *GeminiFileData `json:"fileData,omitempty"` FileData *GeminiFileData `json:"fileData,omitempty"`
ExecutableCode *GeminiPartExecutableCode `json:"executableCode,omitempty"` ExecutableCode *GeminiPartExecutableCode `json:"executableCode,omitempty"`
CodeExecutionResult *GeminiPartCodeExecutionResult `json:"codeExecutionResult,omitempty"` CodeExecutionResult *GeminiPartCodeExecutionResult `json:"codeExecutionResult,omitempty"`
......
...@@ -87,7 +87,6 @@ require ( ...@@ -87,7 +87,6 @@ require (
github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.12.0 // indirect golang.org/x/arch v0.12.0 // indirect
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect
......
...@@ -231,8 +231,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v ...@@ -231,8 +231,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 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/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
......
...@@ -86,9 +86,6 @@ func main() { ...@@ -86,9 +86,6 @@ func main() {
// 数据看板 // 数据看板
go model.UpdateQuotaData() go model.UpdateQuotaData()
// Start Claude Code token refresh scheduler
service.StartClaudeTokenRefreshScheduler()
if os.Getenv("CHANNEL_UPDATE_FREQUENCY") != "" { if os.Getenv("CHANNEL_UPDATE_FREQUENCY") != "" {
frequency, err := strconv.Atoi(os.Getenv("CHANNEL_UPDATE_FREQUENCY")) frequency, err := strconv.Atoi(os.Getenv("CHANNEL_UPDATE_FREQUENCY"))
if err != nil { if err != nil {
......
...@@ -269,6 +269,9 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode ...@@ -269,6 +269,9 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
if channel.ChannelInfo.IsMultiKey { if channel.ChannelInfo.IsMultiKey {
common.SetContextKey(c, constant.ContextKeyChannelIsMultiKey, true) common.SetContextKey(c, constant.ContextKeyChannelIsMultiKey, true)
common.SetContextKey(c, constant.ContextKeyChannelMultiKeyIndex, index) common.SetContextKey(c, constant.ContextKeyChannelMultiKeyIndex, index)
} else {
// 必须设置为 false,否则在重试到单个 key 的时候会导致日志显示错误
common.SetContextKey(c, constant.ContextKeyChannelIsMultiKey, false)
} }
// c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key)) // c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", key))
common.SetContextKey(c, constant.ContextKeyChannelKey, key) common.SetContextKey(c, constant.ContextKeyChannelKey, key)
......
...@@ -284,6 +284,21 @@ func FixAbility() (int, int, error) { ...@@ -284,6 +284,21 @@ func FixAbility() (int, int, error) {
return 0, 0, errors.New("已经有一个修复任务在运行中,请稍后再试") return 0, 0, errors.New("已经有一个修复任务在运行中,请稍后再试")
} }
defer fixLock.Unlock() defer fixLock.Unlock()
// truncate abilities table
if common.UsingSQLite {
err := DB.Exec("DELETE FROM abilities").Error
if err != nil {
common.SysError(fmt.Sprintf("Delete abilities failed: %s", err.Error()))
return 0, 0, err
}
} else {
err := DB.Exec("TRUNCATE TABLE abilities").Error
if err != nil {
common.SysError(fmt.Sprintf("Truncate abilities failed: %s", err.Error()))
return 0, 0, err
}
}
var channels []*Channel var channels []*Channel
// Find all channels // Find all channels
err := DB.Model(&Channel{}).Find(&channels).Error err := DB.Model(&Channel{}).Find(&channels).Error
......
...@@ -46,6 +46,9 @@ type Channel struct { ...@@ -46,6 +46,9 @@ type Channel struct {
ParamOverride *string `json:"param_override" gorm:"type:text"` ParamOverride *string `json:"param_override" gorm:"type:text"`
// add after v0.8.5 // add after v0.8.5
ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"` ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"`
// cache info
Keys []string `json:"-" gorm:"-"`
} }
type ChannelInfo struct { type ChannelInfo struct {
...@@ -71,6 +74,9 @@ func (channel *Channel) getKeys() []string { ...@@ -71,6 +74,9 @@ func (channel *Channel) getKeys() []string {
if channel.Key == "" { if channel.Key == "" {
return []string{} return []string{}
} }
if len(channel.Keys) > 0 {
return channel.Keys
}
trimmed := strings.TrimSpace(channel.Key) trimmed := strings.TrimSpace(channel.Key)
// If the key starts with '[', try to parse it as a JSON array (e.g., for Vertex AI scenarios) // If the key starts with '[', try to parse it as a JSON array (e.g., for Vertex AI scenarios)
if strings.HasPrefix(trimmed, "[") { if strings.HasPrefix(trimmed, "[") {
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"one-api/common" "one-api/common"
"one-api/constant"
"one-api/setting" "one-api/setting"
"sort" "sort"
"strings" "strings"
...@@ -66,6 +67,20 @@ func InitChannelCache() { ...@@ -66,6 +67,20 @@ func InitChannelCache() {
channelSyncLock.Lock() channelSyncLock.Lock()
group2model2channels = newGroup2model2channels group2model2channels = newGroup2model2channels
//channelsIDM = newChannelId2channel
for i, channel := range newChannelId2channel {
if channel.ChannelInfo.IsMultiKey {
channel.Keys = channel.getKeys()
if channel.ChannelInfo.MultiKeyMode == constant.MultiKeyModePolling {
if oldChannel, ok := channelsIDM[i]; ok {
// 存在旧的渠道,如果是多key且轮询,保留轮询索引信息
if oldChannel.ChannelInfo.IsMultiKey && oldChannel.ChannelInfo.MultiKeyMode == constant.MultiKeyModePolling {
channel.ChannelInfo.MultiKeyPollingIndex = oldChannel.ChannelInfo.MultiKeyPollingIndex
}
}
}
}
}
channelsIDM = newChannelId2channel channelsIDM = newChannelId2channel
channelSyncLock.Unlock() channelSyncLock.Unlock()
common.SysLog("channels synced from database") common.SysLog("channels synced from database")
...@@ -203,9 +218,6 @@ func CacheGetChannel(id int) (*Channel, error) { ...@@ -203,9 +218,6 @@ func CacheGetChannel(id int) (*Channel, error) {
if !ok { if !ok {
return nil, fmt.Errorf("渠道# %d,已不存在", id) return nil, fmt.Errorf("渠道# %d,已不存在", id)
} }
if c.Status != common.ChannelStatusEnabled {
return nil, fmt.Errorf("渠道# %d,已被禁用", id)
}
return c, nil return c, nil
} }
...@@ -224,9 +236,6 @@ func CacheGetChannelInfo(id int) (*ChannelInfo, error) { ...@@ -224,9 +236,6 @@ func CacheGetChannelInfo(id int) (*ChannelInfo, error) {
if !ok { if !ok {
return nil, fmt.Errorf("渠道# %d,已不存在", id) return nil, fmt.Errorf("渠道# %d,已不存在", id)
} }
if c.Status != common.ChannelStatusEnabled {
return nil, fmt.Errorf("渠道# %d,已被禁用", id)
}
return &c.ChannelInfo, nil return &c.ChannelInfo, nil
} }
......
...@@ -26,6 +26,7 @@ type Adaptor interface { ...@@ -26,6 +26,7 @@ type Adaptor interface {
GetModelList() []string GetModelList() []string
GetChannelName() string GetChannelName() string
ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error)
ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error)
} }
type TaskAdaptor interface { type TaskAdaptor interface {
......
...@@ -18,6 +18,11 @@ import ( ...@@ -18,6 +18,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -22,6 +22,11 @@ type Adaptor struct { ...@@ -22,6 +22,11 @@ type Adaptor struct {
RequestMode int RequestMode int
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
c.Set("request_model", request.Model) c.Set("request_model", request.Model)
c.Set("converted_request", request) c.Set("converted_request", request)
......
...@@ -18,6 +18,11 @@ import ( ...@@ -18,6 +18,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -18,6 +18,11 @@ import ( ...@@ -18,6 +18,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -24,6 +24,11 @@ type Adaptor struct { ...@@ -24,6 +24,11 @@ type Adaptor struct {
RequestMode int RequestMode int
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
return request, nil return request, nil
} }
......
package claude_code
import (
"errors"
"fmt"
"io"
"net/http"
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/channel/claude"
relaycommon "one-api/relay/common"
"one-api/types"
"strings"
"github.com/gin-gonic/gin"
)
const (
RequestModeCompletion = 1
RequestModeMessage = 2
DefaultSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude."
)
type Adaptor struct {
RequestMode int
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
// Use configured system prompt if available, otherwise use default
if info.ChannelSetting.SystemPrompt != "" {
request.System = info.ChannelSetting.SystemPrompt
} else {
request.System = DefaultSystemPrompt
}
return request, nil
}
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
return nil, errors.New("not implemented")
}
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
if strings.HasPrefix(info.UpstreamModelName, "claude-2") || strings.HasPrefix(info.UpstreamModelName, "claude-instant") {
a.RequestMode = RequestModeCompletion
} else {
a.RequestMode = RequestModeMessage
}
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if a.RequestMode == RequestModeMessage {
return fmt.Sprintf("%s/v1/messages", info.BaseUrl), nil
} else {
return fmt.Sprintf("%s/v1/complete", info.BaseUrl), nil
}
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
channel.SetupApiRequestHeader(info, c, req)
// Parse accesstoken|refreshtoken format and use only the access token
accessToken := info.ApiKey
if strings.Contains(info.ApiKey, "|") {
parts := strings.Split(info.ApiKey, "|")
if len(parts) >= 1 {
accessToken = parts[0]
}
}
// Claude Code specific headers - force override
req.Set("Authorization", "Bearer "+accessToken)
// 只有在没有设置的情况下才设置 anthropic-version
if req.Get("anthropic-version") == "" {
req.Set("anthropic-version", "2023-06-01")
}
req.Set("content-type", "application/json")
// 只有在 user-agent 不包含 claude-cli 时才设置
userAgent := req.Get("user-agent")
if userAgent == "" || !strings.Contains(strings.ToLower(userAgent), "claude-cli") {
req.Set("user-agent", "claude-cli/1.0.61 (external, cli)")
}
// 只有在 anthropic-beta 不包含 claude-code 时才设置
anthropicBeta := req.Get("anthropic-beta")
if anthropicBeta == "" || !strings.Contains(strings.ToLower(anthropicBeta), "claude-code") {
req.Set("anthropic-beta", "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14")
}
// if Anthropic-Dangerous-Direct-Browser-Access
anthropicDangerousDirectBrowserAccess := req.Get("anthropic-dangerous-direct-browser-access")
if anthropicDangerousDirectBrowserAccess == "" {
req.Set("anthropic-dangerous-direct-browser-access", "true")
}
return nil
}
func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
if request == nil {
return nil, errors.New("request is nil")
}
if a.RequestMode == RequestModeCompletion {
return claude.RequestOpenAI2ClaudeComplete(*request), nil
} else {
claudeRequest, err := claude.RequestOpenAI2ClaudeMessage(*request)
if err != nil {
return nil, err
}
// Use configured system prompt if available, otherwise use default
if info.ChannelSetting.SystemPrompt != "" {
claudeRequest.System = info.ChannelSetting.SystemPrompt
} else {
claudeRequest.System = DefaultSystemPrompt
}
return claudeRequest, nil
}
}
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
return nil, nil
}
func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
return nil, errors.New("not implemented")
}
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
return channel.DoApiRequest(a, c, info, requestBody)
}
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.IsStream {
err, usage = claude.ClaudeStreamHandler(c, resp, info, a.RequestMode)
} else {
err, usage = claude.ClaudeHandler(c, resp, a.RequestMode, info)
}
return
}
func (a *Adaptor) GetModelList() []string {
return ModelList
}
func (a *Adaptor) GetChannelName() string {
return ChannelName
}
package claude_code
var ModelList = []string{
"claude-3-5-haiku-20241022",
"claude-3-5-sonnet-20241022",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-20250219-thinking",
"claude-sonnet-4-20250514",
"claude-sonnet-4-20250514-thinking",
"claude-opus-4-20250514",
"claude-opus-4-20250514-thinking",
}
var ChannelName = "claude_code"
package claude_code
// Claude Code uses the same DTO structures as Claude since it's based on the same API
// This file is kept for consistency with the channel structure pattern
\ No newline at end of file
...@@ -18,6 +18,11 @@ import ( ...@@ -18,6 +18,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -17,6 +17,11 @@ import ( ...@@ -17,6 +17,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -18,6 +18,11 @@ import ( ...@@ -18,6 +18,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *common.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
// ConvertAudioRequest implements channel.Adaptor. // ConvertAudioRequest implements channel.Adaptor.
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *common.RelayInfo, request dto.AudioRequest) (io.Reader, error) { func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *common.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
......
...@@ -19,6 +19,11 @@ import ( ...@@ -19,6 +19,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -24,6 +24,11 @@ type Adaptor struct { ...@@ -24,6 +24,11 @@ type Adaptor struct {
BotType int BotType int
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -20,6 +20,26 @@ import ( ...@@ -20,6 +20,26 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) {
if len(request.Contents) > 0 {
for i, content := range request.Contents {
if i == 0 {
if request.Contents[0].Role == "" {
request.Contents[0].Role = "user"
}
}
for _, part := range content.Parts {
if part.FileData != nil {
if part.FileData.MimeType == "" && strings.Contains(part.FileData.FileUri, "www.youtube.com") {
part.FileData.MimeType = "video/webm"
}
}
}
}
}
return request, nil
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) {
adaptor := openai.Adaptor{} adaptor := openai.Adaptor{}
oaiReq, err := adaptor.ConvertClaudeRequest(c, info, req) oaiReq, err := adaptor.ConvertClaudeRequest(c, info, req)
...@@ -51,13 +71,13 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf ...@@ -51,13 +71,13 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
} }
// build gemini imagen request // build gemini imagen request
geminiRequest := GeminiImageRequest{ geminiRequest := dto.GeminiImageRequest{
Instances: []GeminiImageInstance{ Instances: []dto.GeminiImageInstance{
{ {
Prompt: request.Prompt, Prompt: request.Prompt,
}, },
}, },
Parameters: GeminiImageParameters{ Parameters: dto.GeminiImageParameters{
SampleCount: request.N, SampleCount: request.N,
AspectRatio: aspectRatio, AspectRatio: aspectRatio,
PersonGeneration: "allow_adult", // default allow adult PersonGeneration: "allow_adult", // default allow adult
...@@ -138,9 +158,9 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela ...@@ -138,9 +158,9 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
} }
// only process the first input // only process the first input
geminiRequest := GeminiEmbeddingRequest{ geminiRequest := dto.GeminiEmbeddingRequest{
Content: GeminiChatContent{ Content: dto.GeminiChatContent{
Parts: []GeminiPart{ Parts: []dto.GeminiPart{
{ {
Text: inputs[0], Text: inputs[0],
}, },
......
package gemini package gemini
import ( import (
"github.com/pkg/errors"
"io" "io"
"net/http" "net/http"
"one-api/common" "one-api/common"
...@@ -28,7 +29,7 @@ func GeminiTextGenerationHandler(c *gin.Context, info *relaycommon.RelayInfo, re ...@@ -28,7 +29,7 @@ func GeminiTextGenerationHandler(c *gin.Context, info *relaycommon.RelayInfo, re
} }
// 解析为 Gemini 原生响应格式 // 解析为 Gemini 原生响应格式
var geminiResponse GeminiChatResponse var geminiResponse dto.GeminiChatResponse
err = common.Unmarshal(responseBody, &geminiResponse) err = common.Unmarshal(responseBody, &geminiResponse)
if err != nil { if err != nil {
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
...@@ -71,7 +72,7 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn ...@@ -71,7 +72,7 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn
responseText := strings.Builder{} responseText := strings.Builder{}
helper.StreamScannerHandler(c, resp, info, func(data string) bool { helper.StreamScannerHandler(c, resp, info, func(data string) bool {
var geminiResponse GeminiChatResponse var geminiResponse dto.GeminiChatResponse
err := common.UnmarshalJsonStr(data, &geminiResponse) err := common.UnmarshalJsonStr(data, &geminiResponse)
if err != nil { if err != nil {
common.LogError(c, "error unmarshalling stream response: "+err.Error()) common.LogError(c, "error unmarshalling stream response: "+err.Error())
...@@ -110,10 +111,14 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn ...@@ -110,10 +111,14 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn
if err != nil { if err != nil {
common.LogError(c, err.Error()) common.LogError(c, err.Error())
} }
info.SendResponseCount++
return true return true
}) })
if info.SendResponseCount == 0 {
return nil, types.NewOpenAIError(errors.New("no response received from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError)
}
if imageCount != 0 { if imageCount != 0 {
if usage.CompletionTokens == 0 { if usage.CompletionTokens == 0 {
usage.CompletionTokens = imageCount * 258 usage.CompletionTokens = imageCount * 258
......
...@@ -4,7 +4,6 @@ import ( ...@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/gin-gonic/gin"
"io" "io"
"net/http" "net/http"
"one-api/dto" "one-api/dto"
...@@ -13,11 +12,18 @@ import ( ...@@ -13,11 +12,18 @@ import (
relaycommon "one-api/relay/common" relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant" relayconstant "one-api/relay/constant"
"one-api/types" "one-api/types"
"github.com/gin-gonic/gin"
) )
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
} }
......
...@@ -19,6 +19,11 @@ import ( ...@@ -19,6 +19,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -16,6 +16,11 @@ import ( ...@@ -16,6 +16,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -18,6 +18,11 @@ import ( ...@@ -18,6 +18,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -17,6 +17,11 @@ import ( ...@@ -17,6 +17,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
openaiAdaptor := openai.Adaptor{} openaiAdaptor := openai.Adaptor{}
openaiRequest, err := openaiAdaptor.ConvertClaudeRequest(c, info, request) openaiRequest, err := openaiAdaptor.ConvertClaudeRequest(c, info, request)
......
...@@ -34,6 +34,15 @@ type Adaptor struct { ...@@ -34,6 +34,15 @@ type Adaptor struct {
ResponseFormat string ResponseFormat string
} }
func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) {
// 使用 service.GeminiToOpenAIRequest 转换请求格式
openaiRequest, err := service.GeminiToOpenAIRequest(request, info)
if err != nil {
return nil, err
}
return a.ConvertOpenAIRequest(c, info, openaiRequest)
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
//if !strings.Contains(request.Model, "claude") { //if !strings.Contains(request.Model, "claude") {
// return nil, fmt.Errorf("you are using openai channel type with path /v1/messages, only claude model supported convert, but got %s", request.Model) // return nil, fmt.Errorf("you are using openai channel type with path /v1/messages, only claude model supported convert, but got %s", request.Model)
...@@ -64,7 +73,7 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { ...@@ -64,7 +73,7 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
} }
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if info.RelayFormat == relaycommon.RelayFormatClaude { if info.RelayFormat == relaycommon.RelayFormatClaude || info.RelayFormat == relaycommon.RelayFormatGemini {
return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil
} }
if info.RelayMode == relayconstant.RelayModeRealtime { if info.RelayMode == relayconstant.RelayModeRealtime {
......
...@@ -2,6 +2,8 @@ package openai ...@@ -2,6 +2,8 @@ package openai
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http"
"one-api/common" "one-api/common"
"one-api/dto" "one-api/dto"
relaycommon "one-api/relay/common" relaycommon "one-api/relay/common"
...@@ -16,11 +18,14 @@ import ( ...@@ -16,11 +18,14 @@ import (
// 辅助函数 // 辅助函数
func HandleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error { func HandleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error {
info.SendResponseCount++ info.SendResponseCount++
switch info.RelayFormat { switch info.RelayFormat {
case relaycommon.RelayFormatOpenAI: case relaycommon.RelayFormatOpenAI:
return sendStreamData(c, info, data, forceFormat, thinkToContent) return sendStreamData(c, info, data, forceFormat, thinkToContent)
case relaycommon.RelayFormatClaude: case relaycommon.RelayFormatClaude:
return handleClaudeFormat(c, data, info) return handleClaudeFormat(c, data, info)
case relaycommon.RelayFormatGemini:
return handleGeminiFormat(c, data, info)
} }
return nil return nil
} }
...@@ -41,6 +46,36 @@ func handleClaudeFormat(c *gin.Context, data string, info *relaycommon.RelayInfo ...@@ -41,6 +46,36 @@ func handleClaudeFormat(c *gin.Context, data string, info *relaycommon.RelayInfo
return nil return nil
} }
func handleGeminiFormat(c *gin.Context, data string, info *relaycommon.RelayInfo) error {
var streamResponse dto.ChatCompletionsStreamResponse
if err := common.Unmarshal(common.StringToByteSlice(data), &streamResponse); err != nil {
common.LogError(c, "failed to unmarshal stream response: "+err.Error())
return err
}
geminiResponse := service.StreamResponseOpenAI2Gemini(&streamResponse, info)
// 如果返回 nil,表示没有实际内容,跳过发送
if geminiResponse == nil {
return nil
}
geminiResponseStr, err := common.Marshal(geminiResponse)
if err != nil {
common.LogError(c, "failed to marshal gemini response: "+err.Error())
return err
}
// send gemini format response
c.Render(-1, common.CustomEvent{Data: "data: " + string(geminiResponseStr)})
if flusher, ok := c.Writer.(http.Flusher); ok {
flusher.Flush()
} else {
return errors.New("streaming error: flusher not found")
}
return nil
}
func ProcessStreamResponse(streamResponse dto.ChatCompletionsStreamResponse, responseTextBuilder *strings.Builder, toolCount *int) error { func ProcessStreamResponse(streamResponse dto.ChatCompletionsStreamResponse, responseTextBuilder *strings.Builder, toolCount *int) error {
for _, choice := range streamResponse.Choices { for _, choice := range streamResponse.Choices {
responseTextBuilder.WriteString(choice.Delta.GetContentString()) responseTextBuilder.WriteString(choice.Delta.GetContentString())
...@@ -185,6 +220,37 @@ func HandleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStream ...@@ -185,6 +220,37 @@ func HandleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStream
for _, resp := range claudeResponses { for _, resp := range claudeResponses {
_ = helper.ClaudeData(c, *resp) _ = helper.ClaudeData(c, *resp)
} }
case relaycommon.RelayFormatGemini:
var streamResponse dto.ChatCompletionsStreamResponse
if err := common.Unmarshal(common.StringToByteSlice(lastStreamData), &streamResponse); err != nil {
common.SysError("error unmarshalling stream response: " + err.Error())
return
}
// 这里处理的是 openai 最后一个流响应,其 delta 为空,有 finish_reason 字段
// 因此相比较于 google 官方的流响应,由 openai 转换而来会多一个 parts 为空,finishReason 为 STOP 的响应
// 而包含最后一段文本输出的响应(倒数第二个)的 finishReason 为 null
// 暂不知是否有程序会不兼容。
geminiResponse := service.StreamResponseOpenAI2Gemini(&streamResponse, info)
// openai 流响应开头的空数据
if geminiResponse == nil {
return
}
geminiResponseStr, err := common.Marshal(geminiResponse)
if err != nil {
common.SysError("error marshalling gemini response: " + err.Error())
return
}
// 发送最终的 Gemini 响应
c.Render(-1, common.CustomEvent{Data: "data: " + string(geminiResponseStr)})
if flusher, ok := c.Writer.(http.Flusher); ok {
flusher.Flush()
}
} }
} }
......
...@@ -223,6 +223,13 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo ...@@ -223,6 +223,13 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
return nil, types.NewError(err, types.ErrorCodeBadResponseBody) return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
} }
responseBody = claudeRespStr responseBody = claudeRespStr
case relaycommon.RelayFormatGemini:
geminiResp := service.ResponseOpenAI2Gemini(&simpleResponse, info)
geminiRespStr, err := common.Marshal(geminiResp)
if err != nil {
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
responseBody = geminiRespStr
} }
common.IOCopyBytesGracefully(c, resp, responseBody) common.IOCopyBytesGracefully(c, resp, responseBody)
......
...@@ -17,6 +17,11 @@ import ( ...@@ -17,6 +17,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -17,6 +17,11 @@ import ( ...@@ -17,6 +17,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -18,6 +18,11 @@ import ( ...@@ -18,6 +18,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) {
adaptor := openai.Adaptor{} adaptor := openai.Adaptor{}
return adaptor.ConvertClaudeRequest(c, info, req) return adaptor.ConvertClaudeRequest(c, info, req)
......
...@@ -25,6 +25,11 @@ type Adaptor struct { ...@@ -25,6 +25,11 @@ type Adaptor struct {
Timestamp int64 Timestamp int64
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -44,6 +44,11 @@ type Adaptor struct { ...@@ -44,6 +44,11 @@ type Adaptor struct {
AccountCredentials Credentials AccountCredentials Credentials
} }
func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) {
geminiAdaptor := gemini.Adaptor{}
return geminiAdaptor.ConvertGeminiRequest(c, info, request)
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
if v, ok := claudeModelMap[info.UpstreamModelName]; ok { if v, ok := claudeModelMap[info.UpstreamModelName]; ok {
c.Set("request_model", v) c.Set("request_model", v)
......
...@@ -36,7 +36,12 @@ var Cache = asynccache.NewAsyncCache(asynccache.Options{ ...@@ -36,7 +36,12 @@ var Cache = asynccache.NewAsyncCache(asynccache.Options{
}) })
func getAccessToken(a *Adaptor, info *relaycommon.RelayInfo) (string, error) { func getAccessToken(a *Adaptor, info *relaycommon.RelayInfo) (string, error) {
cacheKey := fmt.Sprintf("access-token-%d", info.ChannelId) var cacheKey string
if info.ChannelIsMultiKey {
cacheKey = fmt.Sprintf("access-token-%d-%d", info.ChannelId, info.ChannelMultiKeyIndex)
} else {
cacheKey = fmt.Sprintf("access-token-%d", info.ChannelId)
}
val, err := Cache.Get(cacheKey) val, err := Cache.Get(cacheKey)
if err == nil { if err == nil {
return val.(string), nil return val.(string), nil
......
...@@ -23,6 +23,11 @@ import ( ...@@ -23,6 +23,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -19,6 +19,11 @@ import ( ...@@ -19,6 +19,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
//panic("implement me") //panic("implement me")
......
...@@ -17,6 +17,11 @@ type Adaptor struct { ...@@ -17,6 +17,11 @@ type Adaptor struct {
request *dto.GeneralOpenAIRequest request *dto.GeneralOpenAIRequest
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -16,6 +16,11 @@ import ( ...@@ -16,6 +16,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -18,6 +18,11 @@ import ( ...@@ -18,6 +18,11 @@ import (
type Adaptor struct { type Adaptor struct {
} }
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me //TODO implement me
panic("implement me") panic("implement me")
......
...@@ -62,6 +62,8 @@ type ResponsesUsageInfo struct { ...@@ -62,6 +62,8 @@ type ResponsesUsageInfo struct {
type RelayInfo struct { type RelayInfo struct {
ChannelType int ChannelType int
ChannelId int ChannelId int
ChannelIsMultiKey bool // 是否多密钥
ChannelMultiKeyIndex int // 多密钥索引
TokenId int TokenId int
TokenKey string TokenKey string
UserId int UserId int
...@@ -260,6 +262,9 @@ func GenRelayInfo(c *gin.Context) *RelayInfo { ...@@ -260,6 +262,9 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
IsFirstThinkingContent: true, IsFirstThinkingContent: true,
SendLastThinkingContent: false, SendLastThinkingContent: false,
}, },
ChannelIsMultiKey: common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey),
ChannelMultiKeyIndex: common.GetContextKeyInt(c, constant.ContextKeyChannelMultiKeyIndex),
} }
if strings.HasPrefix(c.Request.URL.Path, "/pg") { if strings.HasPrefix(c.Request.URL.Path, "/pg") {
info.IsPlayground = true info.IsPlayground = true
......
...@@ -20,8 +20,8 @@ import ( ...@@ -20,8 +20,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func getAndValidateGeminiRequest(c *gin.Context) (*gemini.GeminiChatRequest, error) { func getAndValidateGeminiRequest(c *gin.Context) (*dto.GeminiChatRequest, error) {
request := &gemini.GeminiChatRequest{} request := &dto.GeminiChatRequest{}
err := common.UnmarshalBodyReusable(c, request) err := common.UnmarshalBodyReusable(c, request)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -44,7 +44,7 @@ func checkGeminiStreamMode(c *gin.Context, relayInfo *relaycommon.RelayInfo) { ...@@ -44,7 +44,7 @@ func checkGeminiStreamMode(c *gin.Context, relayInfo *relaycommon.RelayInfo) {
// } // }
} }
func checkGeminiInputSensitive(textRequest *gemini.GeminiChatRequest) ([]string, error) { func checkGeminiInputSensitive(textRequest *dto.GeminiChatRequest) ([]string, error) {
var inputTexts []string var inputTexts []string
for _, content := range textRequest.Contents { for _, content := range textRequest.Contents {
for _, part := range content.Parts { for _, part := range content.Parts {
...@@ -61,7 +61,7 @@ func checkGeminiInputSensitive(textRequest *gemini.GeminiChatRequest) ([]string, ...@@ -61,7 +61,7 @@ func checkGeminiInputSensitive(textRequest *gemini.GeminiChatRequest) ([]string,
return sensitiveWords, err return sensitiveWords, err
} }
func getGeminiInputTokens(req *gemini.GeminiChatRequest, info *relaycommon.RelayInfo) int { func getGeminiInputTokens(req *dto.GeminiChatRequest, info *relaycommon.RelayInfo) int {
// 计算输入 token 数量 // 计算输入 token 数量
var inputTexts []string var inputTexts []string
for _, content := range req.Contents { for _, content := range req.Contents {
...@@ -78,9 +78,13 @@ func getGeminiInputTokens(req *gemini.GeminiChatRequest, info *relaycommon.Relay ...@@ -78,9 +78,13 @@ func getGeminiInputTokens(req *gemini.GeminiChatRequest, info *relaycommon.Relay
return inputTokens return inputTokens
} }
func isNoThinkingRequest(req *gemini.GeminiChatRequest) bool { func isNoThinkingRequest(req *dto.GeminiChatRequest) bool {
if req.GenerationConfig.ThinkingConfig != nil && req.GenerationConfig.ThinkingConfig.ThinkingBudget != nil { if req.GenerationConfig.ThinkingConfig != nil && req.GenerationConfig.ThinkingConfig.ThinkingBudget != nil {
return *req.GenerationConfig.ThinkingConfig.ThinkingBudget == 0 configBudget := req.GenerationConfig.ThinkingConfig.ThinkingBudget
if configBudget != nil && *configBudget == 0 {
// 如果思考预算为 0,则认为是非思考请求
return true
}
} }
return false return false
} }
...@@ -202,7 +206,12 @@ func GeminiHelper(c *gin.Context) (newAPIError *types.NewAPIError) { ...@@ -202,7 +206,12 @@ func GeminiHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
} }
requestBody = bytes.NewReader(body) requestBody = bytes.NewReader(body)
} else { } else {
jsonData, err := common.Marshal(req) // 使用 ConvertGeminiRequest 转换请求格式
convertedRequest, err := adaptor.ConvertGeminiRequest(c, relayInfo, req)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
}
jsonData, err := common.Marshal(convertedRequest)
if err != nil { if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
} }
......
...@@ -305,10 +305,10 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo ...@@ -305,10 +305,10 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
return 0, 0, types.NewError(err, types.ErrorCodeQueryDataError, types.ErrOptionWithSkipRetry()) return 0, 0, types.NewError(err, types.ErrorCodeQueryDataError, types.ErrOptionWithSkipRetry())
} }
if userQuota <= 0 { if userQuota <= 0 {
return 0, 0, types.NewErrorWithStatusCode(errors.New("user quota is not enough"), types.ErrorCodeInsufficientUserQuota, http.StatusForbidden, types.ErrOptionWithSkipRetry()) return 0, 0, types.NewErrorWithStatusCode(errors.New("user quota is not enough"), types.ErrorCodeInsufficientUserQuota, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
} }
if userQuota-preConsumedQuota < 0 { if userQuota-preConsumedQuota < 0 {
return 0, 0, types.NewErrorWithStatusCode(fmt.Errorf("pre-consume quota failed, user quota: %s, need quota: %s", common.FormatQuota(userQuota), common.FormatQuota(preConsumedQuota)), types.ErrorCodeInsufficientUserQuota, http.StatusForbidden, types.ErrOptionWithSkipRetry()) return 0, 0, types.NewErrorWithStatusCode(fmt.Errorf("pre-consume quota failed, user quota: %s, need quota: %s", common.FormatQuota(userQuota), common.FormatQuota(preConsumedQuota)), types.ErrorCodeInsufficientUserQuota, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
} }
relayInfo.UserQuota = userQuota relayInfo.UserQuota = userQuota
if userQuota > 100*preConsumedQuota { if userQuota > 100*preConsumedQuota {
...@@ -332,7 +332,7 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo ...@@ -332,7 +332,7 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
if preConsumedQuota > 0 { if preConsumedQuota > 0 {
err := service.PreConsumeTokenQuota(relayInfo, preConsumedQuota) err := service.PreConsumeTokenQuota(relayInfo, preConsumedQuota)
if err != nil { if err != nil {
return 0, 0, types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry()) return 0, 0, types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
} }
err = model.DecreaseUserQuota(relayInfo.UserId, preConsumedQuota) err = model.DecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
if err != nil { if err != nil {
......
...@@ -9,7 +9,6 @@ import ( ...@@ -9,7 +9,6 @@ import (
"one-api/relay/channel/baidu" "one-api/relay/channel/baidu"
"one-api/relay/channel/baidu_v2" "one-api/relay/channel/baidu_v2"
"one-api/relay/channel/claude" "one-api/relay/channel/claude"
"one-api/relay/channel/claude_code"
"one-api/relay/channel/cloudflare" "one-api/relay/channel/cloudflare"
"one-api/relay/channel/cohere" "one-api/relay/channel/cohere"
"one-api/relay/channel/coze" "one-api/relay/channel/coze"
...@@ -99,8 +98,6 @@ func GetAdaptor(apiType int) channel.Adaptor { ...@@ -99,8 +98,6 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &coze.Adaptor{} return &coze.Adaptor{}
case constant.APITypeJimeng: case constant.APITypeJimeng:
return &jimeng.Adaptor{} return &jimeng.Adaptor{}
case constant.APITypeClaudeCode:
return &claude_code.Adaptor{}
} }
return nil return nil
} }
......
...@@ -120,9 +120,6 @@ func SetApiRouter(router *gin.Engine) { ...@@ -120,9 +120,6 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag) channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
channelRoute.GET("/tag/models", controller.GetTagModels) channelRoute.GET("/tag/models", controller.GetTagModels)
channelRoute.POST("/copy/:id", controller.CopyChannel) channelRoute.POST("/copy/:id", controller.CopyChannel)
// Claude OAuth路由
channelRoute.GET("/claude/oauth/url", controller.GenerateClaudeOAuthURL)
channelRoute.POST("/claude/oauth/exchange", controller.ExchangeClaudeOAuthCode)
} }
tokenRoute := apiRouter.Group("/token") tokenRoute := apiRouter.Group("/token")
tokenRoute.Use(middleware.UserAuth()) tokenRoute.Use(middleware.UserAuth())
......
package service
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"time"
"golang.org/x/oauth2"
)
const (
// Default OAuth configuration values
DefaultAuthorizeURL = "https://claude.ai/oauth/authorize"
DefaultTokenURL = "https://console.anthropic.com/v1/oauth/token"
DefaultClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
DefaultRedirectURI = "https://console.anthropic.com/oauth/code/callback"
DefaultScopes = "user:inference"
)
// getOAuthValues returns OAuth configuration values from environment variables or defaults
func getOAuthValues() (authorizeURL, tokenURL, clientID, redirectURI, scopes string) {
authorizeURL = os.Getenv("CLAUDE_AUTHORIZE_URL")
if authorizeURL == "" {
authorizeURL = DefaultAuthorizeURL
}
tokenURL = os.Getenv("CLAUDE_TOKEN_URL")
if tokenURL == "" {
tokenURL = DefaultTokenURL
}
clientID = os.Getenv("CLAUDE_CLIENT_ID")
if clientID == "" {
clientID = DefaultClientID
}
redirectURI = os.Getenv("CLAUDE_REDIRECT_URI")
if redirectURI == "" {
redirectURI = DefaultRedirectURI
}
scopes = os.Getenv("CLAUDE_SCOPES")
if scopes == "" {
scopes = DefaultScopes
}
return
}
type OAuth2Credentials struct {
AuthURL string `json:"auth_url"`
CodeVerifier string `json:"code_verifier"`
State string `json:"state"`
CodeChallenge string `json:"code_challenge"`
}
// GetClaudeOAuthConfig returns the Claude OAuth2 configuration
func GetClaudeOAuthConfig() *oauth2.Config {
authorizeURL, tokenURL, clientID, redirectURI, scopes := getOAuthValues()
return &oauth2.Config{
ClientID: clientID,
RedirectURL: redirectURI,
Scopes: strings.Split(scopes, " "),
Endpoint: oauth2.Endpoint{
AuthURL: authorizeURL,
TokenURL: tokenURL,
},
}
}
// getOAuthConfig is kept for backward compatibility
func getOAuthConfig() *oauth2.Config {
return GetClaudeOAuthConfig()
}
// GenerateOAuthParams generates OAuth authorization URL and related parameters
func GenerateOAuthParams() (*OAuth2Credentials, error) {
config := getOAuthConfig()
// Generate PKCE parameters
codeVerifier := oauth2.GenerateVerifier()
state := oauth2.GenerateVerifier() // Reuse generator as state
// Generate authorization URL
authURL := config.AuthCodeURL(state,
oauth2.S256ChallengeOption(codeVerifier),
oauth2.SetAuthURLParam("code", "true"), // Claude-specific parameter
)
return &OAuth2Credentials{
AuthURL: authURL,
CodeVerifier: codeVerifier,
State: state,
CodeChallenge: oauth2.S256ChallengeFromVerifier(codeVerifier),
}, nil
}
// ExchangeCode
func ExchangeCode(authorizationCode, codeVerifier, state string, client *http.Client) (*oauth2.Token, error) {
config := getOAuthConfig()
if strings.Contains(authorizationCode, "#") {
parts := strings.Split(authorizationCode, "#")
if len(parts) > 0 {
authorizationCode = parts[0]
}
}
ctx := context.Background()
if client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
}
token, err := config.Exchange(ctx, authorizationCode,
oauth2.VerifierOption(codeVerifier),
oauth2.SetAuthURLParam("state", state),
)
if err != nil {
return nil, fmt.Errorf("token exchange failed: %w", err)
}
return token, nil
}
func ParseAuthorizationCode(input string) (string, error) {
if input == "" {
return "", fmt.Errorf("please provide a valid authorization code")
}
// URLs are not allowed
if strings.Contains(input, "http") || strings.Contains(input, "https") {
return "", fmt.Errorf("authorization code cannot contain URLs")
}
return input, nil
}
// GetClaudeHTTPClient returns a configured HTTP client for Claude OAuth operations
func GetClaudeHTTPClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
}
}
// RefreshClaudeToken refreshes a Claude OAuth token using the refresh token
func RefreshClaudeToken(accessToken, refreshToken string) (*oauth2.Token, error) {
config := GetClaudeOAuthConfig()
// Create token from current values
currentToken := &oauth2.Token{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
}
ctx := context.Background()
if client := GetClaudeHTTPClient(); client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
}
// Refresh the token
newToken, err := config.TokenSource(ctx, currentToken).Token()
if err != nil {
return nil, fmt.Errorf("failed to refresh Claude token: %w", err)
}
return newToken, nil
}
package service
import (
"fmt"
"one-api/common"
"one-api/constant"
"one-api/model"
"strings"
"time"
"github.com/bytedance/gopkg/util/gopool"
)
// StartClaudeTokenRefreshScheduler starts the scheduled token refresh for Claude Code channels
func StartClaudeTokenRefreshScheduler() {
ticker := time.NewTicker(5 * time.Minute)
gopool.Go(func() {
defer ticker.Stop()
for range ticker.C {
RefreshClaudeCodeTokens()
}
})
common.SysLog("Claude Code token refresh scheduler started (5 minute interval)")
}
// RefreshClaudeCodeTokens refreshes tokens for all active Claude Code channels
func RefreshClaudeCodeTokens() {
var channels []model.Channel
// Get all active Claude Code channels
err := model.DB.Where("type = ? AND status = ?", constant.ChannelTypeClaudeCode, common.ChannelStatusEnabled).Find(&channels).Error
if err != nil {
common.SysError("Failed to get Claude Code channels: " + err.Error())
return
}
refreshCount := 0
for _, channel := range channels {
if refreshTokenForChannel(&channel) {
refreshCount++
}
}
if refreshCount > 0 {
common.SysLog(fmt.Sprintf("Successfully refreshed %d Claude Code channel tokens", refreshCount))
}
}
// refreshTokenForChannel attempts to refresh token for a single channel
func refreshTokenForChannel(channel *model.Channel) bool {
// Parse key in format: accesstoken|refreshtoken
if channel.Key == "" || !strings.Contains(channel.Key, "|") {
common.SysError(fmt.Sprintf("Channel %d has invalid key format, expected accesstoken|refreshtoken", channel.Id))
return false
}
parts := strings.Split(channel.Key, "|")
if len(parts) < 2 {
common.SysError(fmt.Sprintf("Channel %d has invalid key format, expected accesstoken|refreshtoken", channel.Id))
return false
}
accessToken := parts[0]
refreshToken := parts[1]
if refreshToken == "" {
common.SysError(fmt.Sprintf("Channel %d has empty refresh token", channel.Id))
return false
}
// Check if token needs refresh (refresh 30 minutes before expiry)
// if !shouldRefreshToken(accessToken) {
// return false
// }
// Use shared refresh function
newToken, err := RefreshClaudeToken(accessToken, refreshToken)
if err != nil {
common.SysError(fmt.Sprintf("Failed to refresh token for channel %d: %s", channel.Id, err.Error()))
return false
}
// Update channel with new tokens
newKey := fmt.Sprintf("%s|%s", newToken.AccessToken, newToken.RefreshToken)
err = model.DB.Model(channel).Update("key", newKey).Error
if err != nil {
common.SysError(fmt.Sprintf("Failed to update channel %d with new token: %s", channel.Id, err.Error()))
return false
}
common.SysLog(fmt.Sprintf("Successfully refreshed token for Claude Code channel %d (%s)", channel.Id, channel.Name))
return true
}
...@@ -13,9 +13,6 @@ var AutomaticDisableKeywords = []string{ ...@@ -13,9 +13,6 @@ var AutomaticDisableKeywords = []string{
"The security token included in the request is invalid", "The security token included in the request is invalid",
"Operation not allowed", "Operation not allowed",
"Your account is not authorized", "Your account is not authorized",
// Claude Code
"Invalid bearer token",
"OAuth authentication is currently not allowed for this endpoint",
} }
func AutomaticDisableKeywordsToString() string { func AutomaticDisableKeywordsToString() string {
......
...@@ -79,6 +79,7 @@ type NewAPIError struct { ...@@ -79,6 +79,7 @@ type NewAPIError struct {
Err error Err error
RelayError any RelayError any
skipRetry bool skipRetry bool
recordErrorLog *bool
errorType ErrorType errorType ErrorType
errorCode ErrorCode errorCode ErrorCode
StatusCode int StatusCode int
...@@ -278,3 +279,20 @@ func ErrOptionWithSkipRetry() NewAPIErrorOptions { ...@@ -278,3 +279,20 @@ func ErrOptionWithSkipRetry() NewAPIErrorOptions {
e.skipRetry = true e.skipRetry = true
} }
} }
func ErrOptionWithNoRecordErrorLog() NewAPIErrorOptions {
return func(e *NewAPIError) {
e.recordErrorLog = common.GetPointer(false)
}
}
func IsRecordErrorLog(e *NewAPIError) bool {
if e == nil {
return false
}
if e.recordErrorLog == nil {
// default to true if not set
return true
}
return *e.recordErrorLog
}
...@@ -65,7 +65,8 @@ const JSONEditor = ({ ...@@ -65,7 +65,8 @@ const JSONEditor = ({
const keyCount = Object.keys(parsed).length; const keyCount = Object.keys(parsed).length;
return keyCount > 10 ? 'manual' : 'visual'; return keyCount > 10 ? 'manual' : 'visual';
} catch (error) { } catch (error) {
return 'visual'; // JSON无效时默认显示手动编辑模式
return 'manual';
} }
} }
return 'visual'; return 'visual';
...@@ -201,6 +202,18 @@ const JSONEditor = ({ ...@@ -201,6 +202,18 @@ const JSONEditor = ({
// 渲染键值对编辑器 // 渲染键值对编辑器
const renderKeyValueEditor = () => { const renderKeyValueEditor = () => {
if (typeof jsonData !== 'object' || jsonData === null) {
return (
<div className="text-center py-6 px-4">
<div className="text-gray-400 mb-2">
<IconCode size={32} />
</div>
<Text type="tertiary" className="text-gray-500 text-sm">
{t('无效的JSON数据,请检查格式')}
</Text>
</div>
);
}
const entries = Object.entries(jsonData); const entries = Object.entries(jsonData);
return ( return (
......
...@@ -159,14 +159,6 @@ export const CHANNEL_OPTIONS = [ ...@@ -159,14 +159,6 @@ export const CHANNEL_OPTIONS = [
color: 'purple', color: 'purple',
label: 'Vidu', label: 'Vidu',
}, },
{
value: 53,
color: 'indigo',
label: 'Claude Code',
},
]; ];
export const MODEL_TABLE_PAGE_SIZE = 10; export const MODEL_TABLE_PAGE_SIZE = 10;
// Claude Code 相关常量
export const CLAUDE_CODE_DEFAULT_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude.";
...@@ -353,7 +353,6 @@ export function getChannelIcon(channelType) { ...@@ -353,7 +353,6 @@ export function getChannelIcon(channelType) {
return <Ollama size={iconSize} />; return <Ollama size={iconSize} />;
case 14: // Anthropic Claude case 14: // Anthropic Claude
case 33: // AWS Claude case 33: // AWS Claude
case 53: // Claude Code
return <Claude.Color size={iconSize} />; return <Claude.Color size={iconSize} />;
case 41: // Vertex AI case 41: // Vertex AI
return <Gemini.Color size={iconSize} />; return <Gemini.Color size={iconSize} />;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment