mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-05 00:03:42 +08:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			v0.4.10-al
			...
			v0.4.10-al
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2ff15baf66 | ||
| 
						 | 
					4139a7036f | ||
| 
						 | 
					02da0b51f8 | ||
| 
						 | 
					35cfebee12 | ||
| 
						 | 
					0e088f7c3e | ||
| 
						 | 
					f61d326721 | ||
| 
						 | 
					74b06b643a | ||
| 
						 | 
					ccf7709e23 | ||
| 
						 | 
					d592e2c8b8 | 
							
								
								
									
										15
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								README.md
									
									
									
									
									
								
							@@ -60,6 +60,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
 | 
				
			|||||||
## 功能
 | 
					## 功能
 | 
				
			||||||
1. 支持多种 API 访问渠道:
 | 
					1. 支持多种 API 访问渠道:
 | 
				
			||||||
   + [x] OpenAI 官方通道(支持配置镜像)
 | 
					   + [x] OpenAI 官方通道(支持配置镜像)
 | 
				
			||||||
 | 
					   + [x] [Anthropic Claude 系列模型](https://anthropic.com)
 | 
				
			||||||
   + [x] **Azure OpenAI API**
 | 
					   + [x] **Azure OpenAI API**
 | 
				
			||||||
   + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
 | 
					   + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
 | 
				
			||||||
   + [x] [OpenAI-SB](https://openai-sb.com)
 | 
					   + [x] [OpenAI-SB](https://openai-sb.com)
 | 
				
			||||||
@@ -81,17 +82,19 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
 | 
				
			|||||||
12. 支持以美元为单位显示额度。
 | 
					12. 支持以美元为单位显示额度。
 | 
				
			||||||
13. 支持发布公告,设置充值链接,设置新用户初始额度。
 | 
					13. 支持发布公告,设置充值链接,设置新用户初始额度。
 | 
				
			||||||
14. 支持模型映射,重定向用户的请求模型。
 | 
					14. 支持模型映射,重定向用户的请求模型。
 | 
				
			||||||
15. 支持绘图接口。
 | 
					15. 支持失败自动重试。
 | 
				
			||||||
16. 支持丰富的**自定义**设置,
 | 
					16. 支持绘图接口。
 | 
				
			||||||
 | 
					17. 支持丰富的**自定义**设置,
 | 
				
			||||||
    1. 支持自定义系统名称,logo 以及页脚。
 | 
					    1. 支持自定义系统名称,logo 以及页脚。
 | 
				
			||||||
    2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
 | 
					    2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
 | 
				
			||||||
17. 支持通过系统访问令牌访问管理 API。
 | 
					18. 支持通过系统访问令牌访问管理 API。
 | 
				
			||||||
18. 支持 Cloudflare Turnstile 用户校验。
 | 
					19. 支持 Cloudflare Turnstile 用户校验。
 | 
				
			||||||
19. 支持用户管理,支持**多种用户登录注册方式**:
 | 
					20. 支持用户管理,支持**多种用户登录注册方式**:
 | 
				
			||||||
    + 邮箱登录注册以及通过邮箱进行密码重置。
 | 
					    + 邮箱登录注册以及通过邮箱进行密码重置。
 | 
				
			||||||
    + [GitHub 开放授权](https://github.com/settings/applications/new)。
 | 
					    + [GitHub 开放授权](https://github.com/settings/applications/new)。
 | 
				
			||||||
    + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
 | 
					    + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
 | 
				
			||||||
20. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
 | 
					21. 支持 [ChatGLM](https://github.com/THUDM/ChatGLM2-6B)。
 | 
				
			||||||
 | 
					22. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 部署
 | 
					## 部署
 | 
				
			||||||
### 基于 Docker 进行部署
 | 
					### 基于 Docker 进行部署
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,6 +68,7 @@ var AutomaticDisableChannelEnabled = false
 | 
				
			|||||||
var QuotaRemindThreshold = 1000
 | 
					var QuotaRemindThreshold = 1000
 | 
				
			||||||
var PreConsumedQuota = 500
 | 
					var PreConsumedQuota = 500
 | 
				
			||||||
var ApproximateTokenEnabled = false
 | 
					var ApproximateTokenEnabled = false
 | 
				
			||||||
 | 
					var RetryTimes = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var RootUserEmail = ""
 | 
					var RootUserEmail = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -150,6 +151,7 @@ const (
 | 
				
			|||||||
	ChannelTypePaLM      = 11
 | 
						ChannelTypePaLM      = 11
 | 
				
			||||||
	ChannelTypeAPI2GPT   = 12
 | 
						ChannelTypeAPI2GPT   = 12
 | 
				
			||||||
	ChannelTypeAIGC2D    = 13
 | 
						ChannelTypeAIGC2D    = 13
 | 
				
			||||||
 | 
						ChannelTypeAnthropic = 14
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var ChannelBaseURLs = []string{
 | 
					var ChannelBaseURLs = []string{
 | 
				
			||||||
@@ -167,4 +169,5 @@ var ChannelBaseURLs = []string{
 | 
				
			|||||||
	"",                              // 11
 | 
						"",                              // 11
 | 
				
			||||||
	"https://api.api2gpt.com",       // 12
 | 
						"https://api.api2gpt.com",       // 12
 | 
				
			||||||
	"https://api.aigc2d.com",        // 13
 | 
						"https://api.aigc2d.com",        // 13
 | 
				
			||||||
 | 
						"https://api.anthropic.com",     // 14
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,6 +36,8 @@ var ModelRatio = map[string]float64{
 | 
				
			|||||||
	"text-moderation-stable":  0.1,
 | 
						"text-moderation-stable":  0.1,
 | 
				
			||||||
	"text-moderation-latest":  0.1,
 | 
						"text-moderation-latest":  0.1,
 | 
				
			||||||
	"dall-e":                  8,
 | 
						"dall-e":                  8,
 | 
				
			||||||
 | 
						"claude-instant-1":        0.75,
 | 
				
			||||||
 | 
						"claude-2":                30,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ModelRatio2JSONString() string {
 | 
					func ModelRatio2JSONString() string {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,16 +7,19 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetSubscription(c *gin.Context) {
 | 
					func GetSubscription(c *gin.Context) {
 | 
				
			||||||
	var quota int
 | 
						var remainQuota int
 | 
				
			||||||
 | 
						var usedQuota int
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	var token *model.Token
 | 
						var token *model.Token
 | 
				
			||||||
	if common.DisplayTokenStatEnabled {
 | 
						if common.DisplayTokenStatEnabled {
 | 
				
			||||||
		tokenId := c.GetInt("token_id")
 | 
							tokenId := c.GetInt("token_id")
 | 
				
			||||||
		token, err = model.GetTokenById(tokenId)
 | 
							token, err = model.GetTokenById(tokenId)
 | 
				
			||||||
		quota = token.RemainQuota
 | 
							remainQuota = token.RemainQuota
 | 
				
			||||||
 | 
							usedQuota = token.UsedQuota
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		userId := c.GetInt("id")
 | 
							userId := c.GetInt("id")
 | 
				
			||||||
		quota, err = model.GetUserQuota(userId)
 | 
							remainQuota, err = model.GetUserQuota(userId)
 | 
				
			||||||
 | 
							usedQuota, err = model.GetUserUsedQuota(userId)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		openAIError := OpenAIError{
 | 
							openAIError := OpenAIError{
 | 
				
			||||||
@@ -28,6 +31,7 @@ func GetSubscription(c *gin.Context) {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						quota := remainQuota + usedQuota
 | 
				
			||||||
	amount := float64(quota)
 | 
						amount := float64(quota)
 | 
				
			||||||
	if common.DisplayInCurrencyEnabled {
 | 
						if common.DisplayInCurrencyEnabled {
 | 
				
			||||||
		amount /= common.QuotaPerUnit
 | 
							amount /= common.QuotaPerUnit
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -252,6 +252,42 @@ func init() {
 | 
				
			|||||||
			Root:       "code-davinci-edit-001",
 | 
								Root:       "code-davinci-edit-001",
 | 
				
			||||||
			Parent:     nil,
 | 
								Parent:     nil,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								Id:         "ChatGLM",
 | 
				
			||||||
 | 
								Object:     "model",
 | 
				
			||||||
 | 
								Created:    1677649963,
 | 
				
			||||||
 | 
								OwnedBy:    "thudm",
 | 
				
			||||||
 | 
								Permission: permission,
 | 
				
			||||||
 | 
								Root:       "ChatGLM",
 | 
				
			||||||
 | 
								Parent:     nil,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								Id:         "ChatGLM2",
 | 
				
			||||||
 | 
								Object:     "model",
 | 
				
			||||||
 | 
								Created:    1677649963,
 | 
				
			||||||
 | 
								OwnedBy:    "thudm",
 | 
				
			||||||
 | 
								Permission: permission,
 | 
				
			||||||
 | 
								Root:       "ChatGLM2",
 | 
				
			||||||
 | 
								Parent:     nil,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								Id:         "claude-instant-1",
 | 
				
			||||||
 | 
								Object:     "model",
 | 
				
			||||||
 | 
								Created:    1677649963,
 | 
				
			||||||
 | 
								OwnedBy:    "anturopic",
 | 
				
			||||||
 | 
								Permission: permission,
 | 
				
			||||||
 | 
								Root:       "claude-instant-1",
 | 
				
			||||||
 | 
								Parent:     nil,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								Id:         "claude-2",
 | 
				
			||||||
 | 
								Object:     "model",
 | 
				
			||||||
 | 
								Created:    1677649963,
 | 
				
			||||||
 | 
								OwnedBy:    "anturopic",
 | 
				
			||||||
 | 
								Permission: permission,
 | 
				
			||||||
 | 
								Root:       "claude-2",
 | 
				
			||||||
 | 
								Parent:     nil,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	openAIModelsMap = make(map[string]OpenAIModels)
 | 
						openAIModelsMap = make(map[string]OpenAIModels)
 | 
				
			||||||
	for _, model := range openAIModels {
 | 
						for _, model := range openAIModels {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										40
									
								
								controller/relay-claude.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								controller/relay-claude.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					package controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ClaudeMetadata struct {
 | 
				
			||||||
 | 
						UserId string `json:"user_id"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ClaudeRequest struct {
 | 
				
			||||||
 | 
						Model             string   `json:"model"`
 | 
				
			||||||
 | 
						Prompt            string   `json:"prompt"`
 | 
				
			||||||
 | 
						MaxTokensToSample int      `json:"max_tokens_to_sample"`
 | 
				
			||||||
 | 
						StopSequences     []string `json:"stop_sequences,omitempty"`
 | 
				
			||||||
 | 
						Temperature       float64  `json:"temperature,omitempty"`
 | 
				
			||||||
 | 
						TopP              float64  `json:"top_p,omitempty"`
 | 
				
			||||||
 | 
						TopK              int      `json:"top_k,omitempty"`
 | 
				
			||||||
 | 
						//ClaudeMetadata    `json:"metadata,omitempty"`
 | 
				
			||||||
 | 
						Stream bool `json:"stream,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ClaudeError struct {
 | 
				
			||||||
 | 
						Type    string `json:"type"`
 | 
				
			||||||
 | 
						Message string `json:"message"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ClaudeResponse struct {
 | 
				
			||||||
 | 
						Completion string      `json:"completion"`
 | 
				
			||||||
 | 
						StopReason string      `json:"stop_reason"`
 | 
				
			||||||
 | 
						Model      string      `json:"model"`
 | 
				
			||||||
 | 
						Error      ClaudeError `json:"error"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func stopReasonClaude2OpenAI(reason string) string {
 | 
				
			||||||
 | 
						switch reason {
 | 
				
			||||||
 | 
						case "stop_sequence":
 | 
				
			||||||
 | 
							return "stop"
 | 
				
			||||||
 | 
						case "max_tokens":
 | 
				
			||||||
 | 
							return "length"
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return reason
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -15,6 +15,12 @@ import (
 | 
				
			|||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						APITypeOpenAI = iota
 | 
				
			||||||
 | 
						APITypeClaude
 | 
				
			||||||
 | 
						APITypePaLM
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
					func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
				
			||||||
	channelType := c.GetInt("channel")
 | 
						channelType := c.GetInt("channel")
 | 
				
			||||||
	tokenId := c.GetInt("token_id")
 | 
						tokenId := c.GetInt("token_id")
 | 
				
			||||||
@@ -71,12 +77,18 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
				
			|||||||
			isModelMapped = true
 | 
								isModelMapped = true
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						apiType := APITypeOpenAI
 | 
				
			||||||
 | 
						if strings.HasPrefix(textRequest.Model, "claude") {
 | 
				
			||||||
 | 
							apiType = APITypeClaude
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	baseURL := common.ChannelBaseURLs[channelType]
 | 
						baseURL := common.ChannelBaseURLs[channelType]
 | 
				
			||||||
	requestURL := c.Request.URL.String()
 | 
						requestURL := c.Request.URL.String()
 | 
				
			||||||
	if c.GetString("base_url") != "" {
 | 
						if c.GetString("base_url") != "" {
 | 
				
			||||||
		baseURL = c.GetString("base_url")
 | 
							baseURL = c.GetString("base_url")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
 | 
						fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
 | 
				
			||||||
 | 
						switch apiType {
 | 
				
			||||||
 | 
						case APITypeOpenAI:
 | 
				
			||||||
		if channelType == common.ChannelTypeAzure {
 | 
							if channelType == common.ChannelTypeAzure {
 | 
				
			||||||
			// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
 | 
								// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
 | 
				
			||||||
			query := c.Request.URL.Query()
 | 
								query := c.Request.URL.Query()
 | 
				
			||||||
@@ -95,9 +107,12 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
				
			|||||||
			model_ = strings.TrimSuffix(model_, "-0314")
 | 
								model_ = strings.TrimSuffix(model_, "-0314")
 | 
				
			||||||
			model_ = strings.TrimSuffix(model_, "-0613")
 | 
								model_ = strings.TrimSuffix(model_, "-0613")
 | 
				
			||||||
			fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
 | 
								fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
 | 
				
			||||||
	} else if channelType == common.ChannelTypePaLM {
 | 
							}
 | 
				
			||||||
		err := relayPaLM(textRequest, c)
 | 
						case APITypeClaude:
 | 
				
			||||||
		return err
 | 
							fullRequestURL = "https://api.anthropic.com/v1/complete"
 | 
				
			||||||
 | 
							if baseURL != "" {
 | 
				
			||||||
 | 
								fullRequestURL = fmt.Sprintf("%s/v1/complete", baseURL)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var promptTokens int
 | 
						var promptTokens int
 | 
				
			||||||
	var completionTokens int
 | 
						var completionTokens int
 | 
				
			||||||
@@ -142,17 +157,59 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
				
			|||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		requestBody = c.Request.Body
 | 
							requestBody = c.Request.Body
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						switch apiType {
 | 
				
			||||||
 | 
						case APITypeClaude:
 | 
				
			||||||
 | 
							claudeRequest := ClaudeRequest{
 | 
				
			||||||
 | 
								Model:             textRequest.Model,
 | 
				
			||||||
 | 
								Prompt:            "",
 | 
				
			||||||
 | 
								MaxTokensToSample: textRequest.MaxTokens,
 | 
				
			||||||
 | 
								StopSequences:     nil,
 | 
				
			||||||
 | 
								Temperature:       textRequest.Temperature,
 | 
				
			||||||
 | 
								TopP:              textRequest.TopP,
 | 
				
			||||||
 | 
								Stream:            textRequest.Stream,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if claudeRequest.MaxTokensToSample == 0 {
 | 
				
			||||||
 | 
								claudeRequest.MaxTokensToSample = 1000000
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							prompt := ""
 | 
				
			||||||
 | 
							for _, message := range textRequest.Messages {
 | 
				
			||||||
 | 
								if message.Role == "user" {
 | 
				
			||||||
 | 
									prompt += fmt.Sprintf("\n\nHuman: %s", message.Content)
 | 
				
			||||||
 | 
								} else if message.Role == "assistant" {
 | 
				
			||||||
 | 
									prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									// ignore other roles
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								prompt += "\n\nAssistant:"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							claudeRequest.Prompt = prompt
 | 
				
			||||||
 | 
							jsonStr, err := json.Marshal(claudeRequest)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							requestBody = bytes.NewBuffer(jsonStr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
 | 
						req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
 | 
							return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						apiKey := c.Request.Header.Get("Authorization")
 | 
				
			||||||
 | 
						apiKey = strings.TrimPrefix(apiKey, "Bearer ")
 | 
				
			||||||
 | 
						switch apiType {
 | 
				
			||||||
 | 
						case APITypeOpenAI:
 | 
				
			||||||
		if channelType == common.ChannelTypeAzure {
 | 
							if channelType == common.ChannelTypeAzure {
 | 
				
			||||||
		key := c.Request.Header.Get("Authorization")
 | 
								req.Header.Set("api-key", apiKey)
 | 
				
			||||||
		key = strings.TrimPrefix(key, "Bearer ")
 | 
					 | 
				
			||||||
		req.Header.Set("api-key", key)
 | 
					 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
 | 
								req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						case APITypeClaude:
 | 
				
			||||||
 | 
							req.Header.Set("x-api-key", apiKey)
 | 
				
			||||||
 | 
							anthropicVersion := c.Request.Header.Get("anthropic-version")
 | 
				
			||||||
 | 
							if anthropicVersion == "" {
 | 
				
			||||||
 | 
								anthropicVersion = "2023-06-01"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							req.Header.Set("anthropic-version", anthropicVersion)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
 | 
						req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
 | 
				
			||||||
	req.Header.Set("Accept", c.Request.Header.Get("Accept"))
 | 
						req.Header.Set("Accept", c.Request.Header.Get("Accept"))
 | 
				
			||||||
	//req.Header.Set("Connection", c.Request.Header.Get("Connection"))
 | 
						//req.Header.Set("Connection", c.Request.Header.Get("Connection"))
 | 
				
			||||||
@@ -219,22 +276,20 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
						switch apiType {
 | 
				
			||||||
 | 
						case APITypeOpenAI:
 | 
				
			||||||
		if isStream {
 | 
							if isStream {
 | 
				
			||||||
			scanner := bufio.NewScanner(resp.Body)
 | 
								scanner := bufio.NewScanner(resp.Body)
 | 
				
			||||||
			scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
								scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
				
			||||||
				if atEOF && len(data) == 0 {
 | 
									if atEOF && len(data) == 0 {
 | 
				
			||||||
					return 0, nil, nil
 | 
										return 0, nil, nil
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
									if i := strings.Index(string(data), "\n"); i >= 0 {
 | 
				
			||||||
			if i := strings.Index(string(data), "\n\n"); i >= 0 {
 | 
										return i + 1, data[0:i], nil
 | 
				
			||||||
				return i + 2, data[0:i], nil
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					 | 
				
			||||||
				if atEOF {
 | 
									if atEOF {
 | 
				
			||||||
					return len(data), data, nil
 | 
										return len(data), data, nil
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					 | 
				
			||||||
				return 0, nil, nil
 | 
									return 0, nil, nil
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
			dataChan := make(chan string)
 | 
								dataChan := make(chan string)
 | 
				
			||||||
@@ -242,8 +297,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
				
			|||||||
			go func() {
 | 
								go func() {
 | 
				
			||||||
				for scanner.Scan() {
 | 
									for scanner.Scan() {
 | 
				
			||||||
					data := scanner.Text()
 | 
										data := scanner.Text()
 | 
				
			||||||
				if len(data) < 6 { // must be something wrong!
 | 
										if len(data) < 6 { // ignore blank line or wrong format
 | 
				
			||||||
					common.SysError("invalid stream response: " + data)
 | 
					 | 
				
			||||||
						continue
 | 
											continue
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					dataChan <- data
 | 
										dataChan <- data
 | 
				
			||||||
@@ -286,6 +340,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
				
			|||||||
					if strings.HasPrefix(data, "data: [DONE]") {
 | 
										if strings.HasPrefix(data, "data: [DONE]") {
 | 
				
			||||||
						data = data[:12]
 | 
											data = data[:12]
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
										// some implementations may add \r at the end of data
 | 
				
			||||||
 | 
										data = strings.TrimSuffix(data, "\r")
 | 
				
			||||||
					c.Render(-1, common.CustomEvent{Data: data})
 | 
										c.Render(-1, common.CustomEvent{Data: data})
 | 
				
			||||||
					return true
 | 
										return true
 | 
				
			||||||
				case <-stopChan:
 | 
									case <-stopChan:
 | 
				
			||||||
@@ -338,4 +394,136 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			return nil
 | 
								return nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						case APITypeClaude:
 | 
				
			||||||
 | 
							if isStream {
 | 
				
			||||||
 | 
								responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
 | 
				
			||||||
 | 
								createdTime := common.GetTimestamp()
 | 
				
			||||||
 | 
								scanner := bufio.NewScanner(resp.Body)
 | 
				
			||||||
 | 
								scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
				
			||||||
 | 
									if atEOF && len(data) == 0 {
 | 
				
			||||||
 | 
										return 0, nil, nil
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if i := strings.Index(string(data), "\r\n\r\n"); i >= 0 {
 | 
				
			||||||
 | 
										return i + 4, data[0:i], nil
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if atEOF {
 | 
				
			||||||
 | 
										return len(data), data, nil
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return 0, nil, nil
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								dataChan := make(chan string)
 | 
				
			||||||
 | 
								stopChan := make(chan bool)
 | 
				
			||||||
 | 
								go func() {
 | 
				
			||||||
 | 
									for scanner.Scan() {
 | 
				
			||||||
 | 
										data := scanner.Text()
 | 
				
			||||||
 | 
										if !strings.HasPrefix(data, "event: completion") {
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										data = strings.TrimPrefix(data, "event: completion\r\ndata: ")
 | 
				
			||||||
 | 
										dataChan <- data
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									stopChan <- true
 | 
				
			||||||
 | 
								}()
 | 
				
			||||||
 | 
								c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
				
			||||||
 | 
								c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
				
			||||||
 | 
								c.Writer.Header().Set("Connection", "keep-alive")
 | 
				
			||||||
 | 
								c.Writer.Header().Set("Transfer-Encoding", "chunked")
 | 
				
			||||||
 | 
								c.Writer.Header().Set("X-Accel-Buffering", "no")
 | 
				
			||||||
 | 
								c.Stream(func(w io.Writer) bool {
 | 
				
			||||||
 | 
									select {
 | 
				
			||||||
 | 
									case data := <-dataChan:
 | 
				
			||||||
 | 
										// some implementations may add \r at the end of data
 | 
				
			||||||
 | 
										data = strings.TrimSuffix(data, "\r")
 | 
				
			||||||
 | 
										var claudeResponse ClaudeResponse
 | 
				
			||||||
 | 
										err = json.Unmarshal([]byte(data), &claudeResponse)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											common.SysError("error unmarshalling stream response: " + err.Error())
 | 
				
			||||||
 | 
											return true
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										streamResponseText += claudeResponse.Completion
 | 
				
			||||||
 | 
										var choice ChatCompletionsStreamResponseChoice
 | 
				
			||||||
 | 
										choice.Delta.Content = claudeResponse.Completion
 | 
				
			||||||
 | 
										choice.FinishReason = stopReasonClaude2OpenAI(claudeResponse.StopReason)
 | 
				
			||||||
 | 
										var response ChatCompletionsStreamResponse
 | 
				
			||||||
 | 
										response.Id = responseId
 | 
				
			||||||
 | 
										response.Created = createdTime
 | 
				
			||||||
 | 
										response.Object = "chat.completion.chunk"
 | 
				
			||||||
 | 
										response.Model = textRequest.Model
 | 
				
			||||||
 | 
										response.Choices = []ChatCompletionsStreamResponseChoice{choice}
 | 
				
			||||||
 | 
										jsonStr, err := json.Marshal(response)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											common.SysError("error marshalling stream response: " + err.Error())
 | 
				
			||||||
 | 
											return true
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)})
 | 
				
			||||||
 | 
										return true
 | 
				
			||||||
 | 
									case <-stopChan:
 | 
				
			||||||
 | 
										c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
 | 
				
			||||||
 | 
										return false
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								err = resp.Body.Close()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								responseBody, err := io.ReadAll(resp.Body)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								err = resp.Body.Close()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								var claudeResponse ClaudeResponse
 | 
				
			||||||
 | 
								err = json.Unmarshal(responseBody, &claudeResponse)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if claudeResponse.Error.Type != "" {
 | 
				
			||||||
 | 
									return &OpenAIErrorWithStatusCode{
 | 
				
			||||||
 | 
										OpenAIError: OpenAIError{
 | 
				
			||||||
 | 
											Message: claudeResponse.Error.Message,
 | 
				
			||||||
 | 
											Type:    claudeResponse.Error.Type,
 | 
				
			||||||
 | 
											Param:   "",
 | 
				
			||||||
 | 
											Code:    claudeResponse.Error.Type,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										StatusCode: resp.StatusCode,
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								choice := OpenAITextResponseChoice{
 | 
				
			||||||
 | 
									Index: 0,
 | 
				
			||||||
 | 
									Message: Message{
 | 
				
			||||||
 | 
										Role:    "assistant",
 | 
				
			||||||
 | 
										Content: strings.TrimPrefix(claudeResponse.Completion, " "),
 | 
				
			||||||
 | 
										Name:    nil,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								completionTokens := countTokenText(claudeResponse.Completion, textRequest.Model)
 | 
				
			||||||
 | 
								fullTextResponse := OpenAITextResponse{
 | 
				
			||||||
 | 
									Id:      fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
 | 
				
			||||||
 | 
									Object:  "chat.completion",
 | 
				
			||||||
 | 
									Created: common.GetTimestamp(),
 | 
				
			||||||
 | 
									Choices: []OpenAITextResponseChoice{choice},
 | 
				
			||||||
 | 
									Usage: Usage{
 | 
				
			||||||
 | 
										PromptTokens:     promptTokens,
 | 
				
			||||||
 | 
										CompletionTokens: completionTokens,
 | 
				
			||||||
 | 
										TotalTokens:      promptTokens + promptTokens,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								textResponse.Usage = fullTextResponse.Usage
 | 
				
			||||||
 | 
								jsonResponse, err := json.Marshal(fullTextResponse)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								c.Writer.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
 | 
								c.Writer.WriteHeader(resp.StatusCode)
 | 
				
			||||||
 | 
								_, err = c.Writer.Write(jsonResponse)
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"one-api/common"
 | 
						"one-api/common"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
@@ -84,6 +85,20 @@ type TextResponse struct {
 | 
				
			|||||||
	Error OpenAIError `json:"error"`
 | 
						Error OpenAIError `json:"error"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type OpenAITextResponseChoice struct {
 | 
				
			||||||
 | 
						Index        int `json:"index"`
 | 
				
			||||||
 | 
						Message      `json:"message"`
 | 
				
			||||||
 | 
						FinishReason string `json:"finish_reason"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type OpenAITextResponse struct {
 | 
				
			||||||
 | 
						Id      string                     `json:"id"`
 | 
				
			||||||
 | 
						Object  string                     `json:"object"`
 | 
				
			||||||
 | 
						Created int64                      `json:"created"`
 | 
				
			||||||
 | 
						Choices []OpenAITextResponseChoice `json:"choices"`
 | 
				
			||||||
 | 
						Usage   `json:"usage"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ImageResponse struct {
 | 
					type ImageResponse struct {
 | 
				
			||||||
	Created int `json:"created"`
 | 
						Created int `json:"created"`
 | 
				
			||||||
	Data    []struct {
 | 
						Data    []struct {
 | 
				
			||||||
@@ -91,13 +106,19 @@ type ImageResponse struct {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ChatCompletionsStreamResponse struct {
 | 
					type ChatCompletionsStreamResponseChoice struct {
 | 
				
			||||||
	Choices []struct {
 | 
					 | 
				
			||||||
	Delta struct {
 | 
						Delta struct {
 | 
				
			||||||
		Content string `json:"content"`
 | 
							Content string `json:"content"`
 | 
				
			||||||
	} `json:"delta"`
 | 
						} `json:"delta"`
 | 
				
			||||||
		FinishReason string `json:"finish_reason"`
 | 
						FinishReason string `json:"finish_reason,omitempty"`
 | 
				
			||||||
	} `json:"choices"`
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChatCompletionsStreamResponse struct {
 | 
				
			||||||
 | 
						Id      string                                `json:"id"`
 | 
				
			||||||
 | 
						Object  string                                `json:"object"`
 | 
				
			||||||
 | 
						Created int64                                 `json:"created"`
 | 
				
			||||||
 | 
						Model   string                                `json:"model"`
 | 
				
			||||||
 | 
						Choices []ChatCompletionsStreamResponseChoice `json:"choices"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type CompletionsStreamResponse struct {
 | 
					type CompletionsStreamResponse struct {
 | 
				
			||||||
@@ -132,12 +153,21 @@ func Relay(c *gin.Context) {
 | 
				
			|||||||
		err = relayTextHelper(c, relayMode)
 | 
							err = relayTextHelper(c, relayMode)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							retryTimesStr := c.Query("retry")
 | 
				
			||||||
 | 
							retryTimes, _ := strconv.Atoi(retryTimesStr)
 | 
				
			||||||
 | 
							if retryTimesStr == "" {
 | 
				
			||||||
 | 
								retryTimes = common.RetryTimes
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if retryTimes > 0 {
 | 
				
			||||||
 | 
								c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
			if err.StatusCode == http.StatusTooManyRequests {
 | 
								if err.StatusCode == http.StatusTooManyRequests {
 | 
				
			||||||
				err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
 | 
									err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			c.JSON(err.StatusCode, gin.H{
 | 
								c.JSON(err.StatusCode, gin.H{
 | 
				
			||||||
				"error": err.OpenAIError,
 | 
									"error": err.OpenAIError,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		channelId := c.GetInt("channel_id")
 | 
							channelId := c.GetInt("channel_id")
 | 
				
			||||||
		common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
 | 
							common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
 | 
				
			||||||
		// https://platform.openai.com/docs/guides/error-codes/api-errors
 | 
							// https://platform.openai.com/docs/guides/error-codes/api-errors
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,6 +68,7 @@ func InitOptionMap() {
 | 
				
			|||||||
	common.OptionMap["TopUpLink"] = common.TopUpLink
 | 
						common.OptionMap["TopUpLink"] = common.TopUpLink
 | 
				
			||||||
	common.OptionMap["ChatLink"] = common.ChatLink
 | 
						common.OptionMap["ChatLink"] = common.ChatLink
 | 
				
			||||||
	common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
 | 
						common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
 | 
				
			||||||
 | 
						common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
 | 
				
			||||||
	common.OptionMapRWMutex.Unlock()
 | 
						common.OptionMapRWMutex.Unlock()
 | 
				
			||||||
	loadOptionsFromDatabase()
 | 
						loadOptionsFromDatabase()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -196,6 +197,8 @@ func updateOptionMap(key string, value string) (err error) {
 | 
				
			|||||||
		common.QuotaRemindThreshold, _ = strconv.Atoi(value)
 | 
							common.QuotaRemindThreshold, _ = strconv.Atoi(value)
 | 
				
			||||||
	case "PreConsumedQuota":
 | 
						case "PreConsumedQuota":
 | 
				
			||||||
		common.PreConsumedQuota, _ = strconv.Atoi(value)
 | 
							common.PreConsumedQuota, _ = strconv.Atoi(value)
 | 
				
			||||||
 | 
						case "RetryTimes":
 | 
				
			||||||
 | 
							common.RetryTimes, _ = strconv.Atoi(value)
 | 
				
			||||||
	case "ModelRatio":
 | 
						case "ModelRatio":
 | 
				
			||||||
		err = common.UpdateModelRatioByJSONString(value)
 | 
							err = common.UpdateModelRatioByJSONString(value)
 | 
				
			||||||
	case "GroupRatio":
 | 
						case "GroupRatio":
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,11 @@
 | 
				
			|||||||
package router
 | 
					package router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"github.com/gin-contrib/gzip"
 | 
					 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
					 | 
				
			||||||
	"one-api/controller"
 | 
						"one-api/controller"
 | 
				
			||||||
	"one-api/middleware"
 | 
						"one-api/middleware"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/gin-contrib/gzip"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func SetApiRouter(router *gin.Engine) {
 | 
					func SetApiRouter(router *gin.Engine) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ import {
 | 
				
			|||||||
} from 'semantic-ui-react';
 | 
					} from 'semantic-ui-react';
 | 
				
			||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 | 
					import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 | 
				
			||||||
import { UserContext } from '../context/User';
 | 
					import { UserContext } from '../context/User';
 | 
				
			||||||
import { API, getLogo, showError, showSuccess } from '../helpers';
 | 
					import { API, getLogo, showError, showSuccess, showInfo } from '../helpers';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LoginForm = () => {
 | 
					const LoginForm = () => {
 | 
				
			||||||
  const [inputs, setInputs] = useState({
 | 
					  const [inputs, setInputs] = useState({
 | 
				
			||||||
@@ -76,7 +76,7 @@ const LoginForm = () => {
 | 
				
			|||||||
  async function handleSubmit(e) {
 | 
					  async function handleSubmit(e) {
 | 
				
			||||||
    setSubmitted(true);
 | 
					    setSubmitted(true);
 | 
				
			||||||
    if (username && password) {
 | 
					    if (username && password) {
 | 
				
			||||||
      const res = await API.post('/api/user/login', {
 | 
					      const res = await API.post(`/api/user/login`, {
 | 
				
			||||||
        username,
 | 
					        username,
 | 
				
			||||||
        password,
 | 
					        password,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ const OperationSetting = () => {
 | 
				
			|||||||
    DisplayInCurrencyEnabled: '',
 | 
					    DisplayInCurrencyEnabled: '',
 | 
				
			||||||
    DisplayTokenStatEnabled: '',
 | 
					    DisplayTokenStatEnabled: '',
 | 
				
			||||||
    ApproximateTokenEnabled: '',
 | 
					    ApproximateTokenEnabled: '',
 | 
				
			||||||
 | 
					    RetryTimes: 0,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const [originInputs, setOriginInputs] = useState({});
 | 
					  const [originInputs, setOriginInputs] = useState({});
 | 
				
			||||||
  let [loading, setLoading] = useState(false);
 | 
					  let [loading, setLoading] = useState(false);
 | 
				
			||||||
@@ -122,6 +123,9 @@ const OperationSetting = () => {
 | 
				
			|||||||
        if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
 | 
					        if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
 | 
				
			||||||
          await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
 | 
					          await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
 | 
				
			||||||
 | 
					          await updateOption('RetryTimes', inputs.RetryTimes);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@@ -133,7 +137,7 @@ const OperationSetting = () => {
 | 
				
			|||||||
          <Header as='h3'>
 | 
					          <Header as='h3'>
 | 
				
			||||||
            通用设置
 | 
					            通用设置
 | 
				
			||||||
          </Header>
 | 
					          </Header>
 | 
				
			||||||
          <Form.Group widths={3}>
 | 
					          <Form.Group widths={4}>
 | 
				
			||||||
            <Form.Input
 | 
					            <Form.Input
 | 
				
			||||||
              label='充值链接'
 | 
					              label='充值链接'
 | 
				
			||||||
              name='TopUpLink'
 | 
					              name='TopUpLink'
 | 
				
			||||||
@@ -162,6 +166,17 @@ const OperationSetting = () => {
 | 
				
			|||||||
              step='0.01'
 | 
					              step='0.01'
 | 
				
			||||||
              placeholder='一单位货币能兑换的额度'
 | 
					              placeholder='一单位货币能兑换的额度'
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
 | 
					            <Form.Input
 | 
				
			||||||
 | 
					              label='失败重试次数'
 | 
				
			||||||
 | 
					              name='RetryTimes'
 | 
				
			||||||
 | 
					              type={'number'}
 | 
				
			||||||
 | 
					              step='1'
 | 
				
			||||||
 | 
					              min='0'
 | 
				
			||||||
 | 
					              onChange={handleInputChange}
 | 
				
			||||||
 | 
					              autoComplete='new-password'
 | 
				
			||||||
 | 
					              value={inputs.RetryTimes}
 | 
				
			||||||
 | 
					              placeholder='失败重试次数'
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
          </Form.Group>
 | 
					          </Form.Group>
 | 
				
			||||||
          <Form.Group inline>
 | 
					          <Form.Group inline>
 | 
				
			||||||
            <Form.Checkbox
 | 
					            <Form.Checkbox
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
export const CHANNEL_OPTIONS = [
 | 
					export const CHANNEL_OPTIONS = [
 | 
				
			||||||
  { key: 1, text: 'OpenAI', value: 1, color: 'green' },
 | 
					  { key: 1, text: 'OpenAI', value: 1, color: 'green' },
 | 
				
			||||||
 | 
					  { key: 14, text: 'Anthropic', value: 14, color: 'black' },
 | 
				
			||||||
  { key: 8, text: '自定义', value: 8, color: 'pink' },
 | 
					  { key: 8, text: '自定义', value: 8, color: 'pink' },
 | 
				
			||||||
  { key: 3, text: 'Azure', value: 3, color: 'olive' },
 | 
					  { key: 3, text: 'Azure', value: 3, color: 'olive' },
 | 
				
			||||||
  { key: 2, text: 'API2D', value: 2, color: 'blue' },
 | 
					  { key: 2, text: 'API2D', value: 2, color: 'blue' },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import React, { useEffect, useState } from 'react';
 | 
					import React, { useEffect, useState } from 'react';
 | 
				
			||||||
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
 | 
					import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
 | 
				
			||||||
import { useParams } from 'react-router-dom';
 | 
					import { useParams } from 'react-router-dom';
 | 
				
			||||||
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
 | 
					import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
 | 
				
			||||||
import { CHANNEL_OPTIONS } from '../../constants';
 | 
					import { CHANNEL_OPTIONS } from '../../constants';
 | 
				
			||||||
@@ -31,6 +31,7 @@ const EditChannel = () => {
 | 
				
			|||||||
  const [groupOptions, setGroupOptions] = useState([]);
 | 
					  const [groupOptions, setGroupOptions] = useState([]);
 | 
				
			||||||
  const [basicModels, setBasicModels] = useState([]);
 | 
					  const [basicModels, setBasicModels] = useState([]);
 | 
				
			||||||
  const [fullModels, setFullModels] = useState([]);
 | 
					  const [fullModels, setFullModels] = useState([]);
 | 
				
			||||||
 | 
					  const [customModel, setCustomModel] = useState('');
 | 
				
			||||||
  const handleInputChange = (e, { name, value }) => {
 | 
					  const handleInputChange = (e, { name, value }) => {
 | 
				
			||||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
					    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@@ -43,6 +44,19 @@ const EditChannel = () => {
 | 
				
			|||||||
        data.models = [];
 | 
					        data.models = [];
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        data.models = data.models.split(',');
 | 
					        data.models = data.models.split(',');
 | 
				
			||||||
 | 
					        setTimeout(() => {
 | 
				
			||||||
 | 
					          let localModelOptions = [...modelOptions];
 | 
				
			||||||
 | 
					          data.models.forEach((model) => {
 | 
				
			||||||
 | 
					            if (!localModelOptions.find((option) => option.key === model)) {
 | 
				
			||||||
 | 
					              localModelOptions.push({
 | 
				
			||||||
 | 
					                key: model,
 | 
				
			||||||
 | 
					                text: model,
 | 
				
			||||||
 | 
					                value: model
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          setModelOptions(localModelOptions);
 | 
				
			||||||
 | 
					        }, 1000);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (data.group === '') {
 | 
					      if (data.group === '') {
 | 
				
			||||||
        data.groups = [];
 | 
					        data.groups = [];
 | 
				
			||||||
@@ -263,6 +277,27 @@ const EditChannel = () => {
 | 
				
			|||||||
            <Button type={'button'} onClick={() => {
 | 
					            <Button type={'button'} onClick={() => {
 | 
				
			||||||
              handleInputChange(null, { name: 'models', value: [] });
 | 
					              handleInputChange(null, { name: 'models', value: [] });
 | 
				
			||||||
            }}>清除所有模型</Button>
 | 
					            }}>清除所有模型</Button>
 | 
				
			||||||
 | 
					            <Input
 | 
				
			||||||
 | 
					              action={
 | 
				
			||||||
 | 
					                <Button type={'button'} onClick={()=>{
 | 
				
			||||||
 | 
					                  let localModels = [...inputs.models];
 | 
				
			||||||
 | 
					                  localModels.push(customModel);
 | 
				
			||||||
 | 
					                  let localModelOptions = [...modelOptions];
 | 
				
			||||||
 | 
					                  localModelOptions.push({
 | 
				
			||||||
 | 
					                    key: customModel,
 | 
				
			||||||
 | 
					                    text: customModel,
 | 
				
			||||||
 | 
					                    value: customModel,
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                  setModelOptions(localModelOptions);
 | 
				
			||||||
 | 
					                  handleInputChange(null, { name: 'models', value: localModels });
 | 
				
			||||||
 | 
					                }}>填入</Button>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              placeholder='输入自定义模型名称'
 | 
				
			||||||
 | 
					              value={customModel}
 | 
				
			||||||
 | 
					              onChange={(e, { value }) => {
 | 
				
			||||||
 | 
					                setCustomModel(value);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <Form.Field>
 | 
					          <Form.Field>
 | 
				
			||||||
            <Form.TextArea
 | 
					            <Form.TextArea
 | 
				
			||||||
@@ -309,7 +344,7 @@ const EditChannel = () => {
 | 
				
			|||||||
              />
 | 
					              />
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          <Button positive onClick={submit}>提交</Button>
 | 
					          <Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button>
 | 
				
			||||||
        </Form>
 | 
					        </Form>
 | 
				
			||||||
      </Segment>
 | 
					      </Segment>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user