mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	Compare commits
	
		
			17 Commits
		
	
	
		
			v0.4.10-al
			...
			0.4.8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2ae5741214 | ||
| 
						 | 
					28d58849a0 | ||
| 
						 | 
					adc9679d56 | ||
| 
						 | 
					07589ae305 | ||
| 
						 | 
					95bc32c555 | ||
| 
						 | 
					d61dc4a9ca | ||
| 
						 | 
					b29acb0c89 | ||
| 
						 | 
					a8e418275d | ||
| 
						 | 
					d7ab9b0935 | ||
| 
						 | 
					ebd62c3bfc | ||
| 
						 | 
					4e6f9f67b3 | ||
| 
						 | 
					77d295bbf5 | ||
| 
						 | 
					7e3e25fbd9 | ||
| 
						 | 
					9bf98ab53a | ||
| 
						 | 
					3ed9a219c7 | ||
| 
						 | 
					37d7afcedc | ||
| 
						 | 
					2756554f7c | 
							
								
								
									
										2
									
								
								.github/workflows/docker-image-amd64-en.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/docker-image-amd64-en.yml
									
									
									
									
										vendored
									
									
								
							@@ -38,7 +38,7 @@ jobs:
 | 
			
		||||
        uses: docker/metadata-action@v4
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            justsong/one-api-en
 | 
			
		||||
            ckt1031/one-api-en
 | 
			
		||||
 | 
			
		||||
      - name: Build and push Docker images
 | 
			
		||||
        uses: docker/build-push-action@v3
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/docker-image-amd64.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/docker-image-amd64.yml
									
									
									
									
										vendored
									
									
								
							@@ -42,7 +42,7 @@ jobs:
 | 
			
		||||
        uses: docker/metadata-action@v4
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            justsong/one-api
 | 
			
		||||
            ckt1031/one-api
 | 
			
		||||
            ghcr.io/${{ github.repository }}
 | 
			
		||||
 | 
			
		||||
      - name: Build and push Docker images
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/docker-image-arm64.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/docker-image-arm64.yml
									
									
									
									
										vendored
									
									
								
							@@ -49,7 +49,7 @@ jobs:
 | 
			
		||||
        uses: docker/metadata-action@v4
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            justsong/one-api
 | 
			
		||||
            ckt1031/one-api
 | 
			
		||||
            ghcr.io/${{ github.repository }}
 | 
			
		||||
 | 
			
		||||
      - name: Build and push Docker images
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,31 +1,29 @@
 | 
			
		||||
FROM node:16 as builder
 | 
			
		||||
 | 
			
		||||
# Node build stage
 | 
			
		||||
FROM node:18 as builder
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
COPY ./web/package*.json ./
 | 
			
		||||
RUN npm ci
 | 
			
		||||
COPY ./web .
 | 
			
		||||
COPY ./VERSION .
 | 
			
		||||
RUN npm install
 | 
			
		||||
RUN REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
 | 
			
		||||
# Go build stage
 | 
			
		||||
FROM golang AS builder2
 | 
			
		||||
 | 
			
		||||
ENV GO111MODULE=on \
 | 
			
		||||
    CGO_ENABLED=1 \
 | 
			
		||||
    GOOS=linux
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
COPY go.mod .
 | 
			
		||||
COPY go.sum .
 | 
			
		||||
RUN go mod download
 | 
			
		||||
COPY . .
 | 
			
		||||
COPY --from=builder /build/build ./web/build
 | 
			
		||||
RUN go mod download
 | 
			
		||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
 | 
			
		||||
 | 
			
		||||
# Final stage
 | 
			
		||||
FROM alpine
 | 
			
		||||
 | 
			
		||||
RUN apk update \
 | 
			
		||||
    && apk upgrade \
 | 
			
		||||
    && apk add --no-cache ca-certificates tzdata \
 | 
			
		||||
    && update-ca-certificates 2>/dev/null || true
 | 
			
		||||
 | 
			
		||||
RUN apk update && apk upgrade && apk add --no-cache ca-certificates tzdata && update-ca-certificates 2>/dev/null || true
 | 
			
		||||
WORKDIR /data
 | 
			
		||||
COPY --from=builder2 /build/one-api /
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
WORKDIR /data
 | 
			
		||||
ENTRYPOINT ["/one-api"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							@@ -60,7 +60,6 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
 | 
			
		||||
## 功能
 | 
			
		||||
1. 支持多种 API 访问渠道:
 | 
			
		||||
   + [x] OpenAI 官方通道(支持配置镜像)
 | 
			
		||||
   + [x] [Anthropic Claude 系列模型](https://anthropic.com)
 | 
			
		||||
   + [x] **Azure OpenAI API**
 | 
			
		||||
   + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj)
 | 
			
		||||
   + [x] [OpenAI-SB](https://openai-sb.com)
 | 
			
		||||
@@ -82,19 +81,16 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
 | 
			
		||||
12. 支持以美元为单位显示额度。
 | 
			
		||||
13. 支持发布公告,设置充值链接,设置新用户初始额度。
 | 
			
		||||
14. 支持模型映射,重定向用户的请求模型。
 | 
			
		||||
15. 支持失败自动重试。
 | 
			
		||||
16. 支持绘图接口。
 | 
			
		||||
17. 支持丰富的**自定义**设置,
 | 
			
		||||
15. 支持丰富的**自定义**设置,
 | 
			
		||||
    1. 支持自定义系统名称,logo 以及页脚。
 | 
			
		||||
    2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
 | 
			
		||||
18. 支持通过系统访问令牌访问管理 API。
 | 
			
		||||
19. 支持 Cloudflare Turnstile 用户校验。
 | 
			
		||||
20. 支持用户管理,支持**多种用户登录注册方式**:
 | 
			
		||||
16. 支持通过系统访问令牌访问管理 API。
 | 
			
		||||
17. 支持 Cloudflare Turnstile 用户校验。
 | 
			
		||||
18. 支持用户管理,支持**多种用户登录注册方式**:
 | 
			
		||||
    + 邮箱登录注册以及通过邮箱进行密码重置。
 | 
			
		||||
    + [GitHub 开放授权](https://github.com/settings/applications/new)。
 | 
			
		||||
    + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
 | 
			
		||||
21. 支持 [ChatGLM](https://github.com/THUDM/ChatGLM2-6B)。
 | 
			
		||||
22. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
 | 
			
		||||
19. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
 | 
			
		||||
 | 
			
		||||
## 部署
 | 
			
		||||
### 基于 Docker 进行部署
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,6 @@ var AutomaticDisableChannelEnabled = false
 | 
			
		||||
var QuotaRemindThreshold = 1000
 | 
			
		||||
var PreConsumedQuota = 500
 | 
			
		||||
var ApproximateTokenEnabled = false
 | 
			
		||||
var RetryTimes = 0
 | 
			
		||||
 | 
			
		||||
var RootUserEmail = ""
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +150,6 @@ const (
 | 
			
		||||
	ChannelTypePaLM      = 11
 | 
			
		||||
	ChannelTypeAPI2GPT   = 12
 | 
			
		||||
	ChannelTypeAIGC2D    = 13
 | 
			
		||||
	ChannelTypeAnthropic = 14
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ChannelBaseURLs = []string{
 | 
			
		||||
@@ -169,5 +167,4 @@ var ChannelBaseURLs = []string{
 | 
			
		||||
	"",                              // 11
 | 
			
		||||
	"https://api.api2gpt.com",       // 12
 | 
			
		||||
	"https://api.aigc2d.com",        // 13
 | 
			
		||||
	"https://api.anthropic.com",     // 14
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,8 +36,6 @@ var ModelRatio = map[string]float64{
 | 
			
		||||
	"text-moderation-stable":  0.1,
 | 
			
		||||
	"text-moderation-latest":  0.1,
 | 
			
		||||
	"dall-e":                  8,
 | 
			
		||||
	"claude-instant-1":        0.75,
 | 
			
		||||
	"claude-2":                30,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ModelRatio2JSONString() string {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,19 +7,16 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetSubscription(c *gin.Context) {
 | 
			
		||||
	var remainQuota int
 | 
			
		||||
	var usedQuota int
 | 
			
		||||
	var quota int
 | 
			
		||||
	var err error
 | 
			
		||||
	var token *model.Token
 | 
			
		||||
	if common.DisplayTokenStatEnabled {
 | 
			
		||||
		tokenId := c.GetInt("token_id")
 | 
			
		||||
		token, err = model.GetTokenById(tokenId)
 | 
			
		||||
		remainQuota = token.RemainQuota
 | 
			
		||||
		usedQuota = token.UsedQuota
 | 
			
		||||
		quota = token.RemainQuota
 | 
			
		||||
	} else {
 | 
			
		||||
		userId := c.GetInt("id")
 | 
			
		||||
		remainQuota, err = model.GetUserQuota(userId)
 | 
			
		||||
		usedQuota, err = model.GetUserUsedQuota(userId)
 | 
			
		||||
		quota, err = model.GetUserQuota(userId)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		openAIError := OpenAIError{
 | 
			
		||||
@@ -31,7 +28,6 @@ func GetSubscription(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	quota := remainQuota + usedQuota
 | 
			
		||||
	amount := float64(quota)
 | 
			
		||||
	if common.DisplayInCurrencyEnabled {
 | 
			
		||||
		amount /= common.QuotaPerUnit
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,21 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func testChannel(channel *model.Channel, request ChatRequest) error {
 | 
			
		||||
@@ -50,15 +54,69 @@ func testChannel(channel *model.Channel, request ChatRequest) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	var response TextResponse
 | 
			
		||||
	err = json.NewDecoder(resp.Body).Decode(&response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if response.Usage.CompletionTokens == 0 {
 | 
			
		||||
		return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message))
 | 
			
		||||
	isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
 | 
			
		||||
	var streamResponseText string
 | 
			
		||||
 | 
			
		||||
	if isStream {
 | 
			
		||||
		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), "\n\n"); i >= 0 {
 | 
			
		||||
				return i + 2, data[0:i], nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if atEOF {
 | 
			
		||||
				return len(data), data, nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return 0, nil, nil
 | 
			
		||||
		})
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			data := scanner.Text()
 | 
			
		||||
			if len(data) < 6 { // must be something wrong!
 | 
			
		||||
				common.SysError("invalid stream response: " + data)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			data = data[6:]
 | 
			
		||||
			if !strings.HasPrefix(data, "[DONE]") {
 | 
			
		||||
				var streamResponse ChatCompletionsStreamResponse
 | 
			
		||||
				err = json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				for _, choice := range streamResponse.Choices {
 | 
			
		||||
					streamResponseText += choice.Delta.Content
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if streamResponseText == "" {
 | 
			
		||||
			return errors.New("empty stream response")
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		body, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		err = json.Unmarshal(body, &response)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// channel.BaseURL starts with https://api.openai.com
 | 
			
		||||
		if response.Usage.CompletionTokens == 0 && strings.HasPrefix(channel.BaseURL, "https://api.openai.com") {
 | 
			
		||||
			return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -252,42 +252,6 @@ func init() {
 | 
			
		||||
			Root:       "code-davinci-edit-001",
 | 
			
		||||
			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)
 | 
			
		||||
	for _, model := range openAIModels {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -22,26 +22,26 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
	consumeQuota := c.GetBool("consume_quota")
 | 
			
		||||
	group := c.GetString("group")
 | 
			
		||||
 | 
			
		||||
	var imageRequest ImageRequest
 | 
			
		||||
	var textRequest GeneralOpenAIRequest
 | 
			
		||||
	if consumeQuota {
 | 
			
		||||
		err := common.UnmarshalBodyReusable(c, &imageRequest)
 | 
			
		||||
		err := common.UnmarshalBodyReusable(c, &textRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prompt validation
 | 
			
		||||
	if imageRequest.Prompt == "" {
 | 
			
		||||
	if textRequest.Prompt == "" {
 | 
			
		||||
		return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Not "256x256", "512x512", or "1024x1024"
 | 
			
		||||
	if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" {
 | 
			
		||||
	if textRequest.Size != "" && textRequest.Size != "256x256" && textRequest.Size != "512x512" && textRequest.Size != "1024x1024" {
 | 
			
		||||
		return errorWrapper(errors.New("size must be one of 256x256, 512x512, or 1024x1024"), "invalid_field_value", http.StatusBadRequest)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// N should between 1 and 10
 | 
			
		||||
	if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) {
 | 
			
		||||
	// N should between 1 to 10
 | 
			
		||||
	if textRequest.N != 0 && (textRequest.N < 1 || textRequest.N > 10) {
 | 
			
		||||
		return errorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -71,7 +71,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
 | 
			
		||||
	var requestBody io.Reader
 | 
			
		||||
	if isModelMapped {
 | 
			
		||||
		jsonStr, err := json.Marshal(imageRequest)
 | 
			
		||||
		jsonStr, err := json.Marshal(textRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
@@ -87,14 +87,14 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
 | 
			
		||||
	sizeRatio := 1.0
 | 
			
		||||
	// Size
 | 
			
		||||
	if imageRequest.Size == "256x256" {
 | 
			
		||||
	if textRequest.Size == "256x256" {
 | 
			
		||||
		sizeRatio = 1
 | 
			
		||||
	} else if imageRequest.Size == "512x512" {
 | 
			
		||||
	} else if textRequest.Size == "512x512" {
 | 
			
		||||
		sizeRatio = 1.125
 | 
			
		||||
	} else if imageRequest.Size == "1024x1024" {
 | 
			
		||||
	} else if textRequest.Size == "1024x1024" {
 | 
			
		||||
		sizeRatio = 1.25
 | 
			
		||||
	}
 | 
			
		||||
	quota := int(ratio*sizeRatio*1000) * imageRequest.N
 | 
			
		||||
	quota := int(ratio * sizeRatio * 1000)
 | 
			
		||||
 | 
			
		||||
	if consumeQuota && userQuota-quota < 0 {
 | 
			
		||||
		return errorWrapper(err, "insufficient_user_quota", http.StatusForbidden)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,19 +6,12 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	APITypeOpenAI = iota
 | 
			
		||||
	APITypeClaude
 | 
			
		||||
	APITypePaLM
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
@@ -37,9 +30,6 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	if relayMode == RelayModeModerations && textRequest.Model == "" {
 | 
			
		||||
		textRequest.Model = "text-moderation-latest"
 | 
			
		||||
	}
 | 
			
		||||
	if relayMode == RelayModeEmbeddings && textRequest.Model == "" {
 | 
			
		||||
		textRequest.Model = c.Param("model")
 | 
			
		||||
	}
 | 
			
		||||
	// request validation
 | 
			
		||||
	if textRequest.Model == "" {
 | 
			
		||||
		return errorWrapper(errors.New("model is required"), "required_field_missing", http.StatusBadRequest)
 | 
			
		||||
@@ -77,42 +67,33 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			isModelMapped = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	apiType := APITypeOpenAI
 | 
			
		||||
	if strings.HasPrefix(textRequest.Model, "claude") {
 | 
			
		||||
		apiType = APITypeClaude
 | 
			
		||||
	}
 | 
			
		||||
	baseURL := common.ChannelBaseURLs[channelType]
 | 
			
		||||
	requestURL := c.Request.URL.String()
 | 
			
		||||
	if c.GetString("base_url") != "" {
 | 
			
		||||
		baseURL = c.GetString("base_url")
 | 
			
		||||
	}
 | 
			
		||||
	fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
 | 
			
		||||
	switch apiType {
 | 
			
		||||
	case APITypeOpenAI:
 | 
			
		||||
		if channelType == common.ChannelTypeAzure {
 | 
			
		||||
			// 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()
 | 
			
		||||
			apiVersion := query.Get("api-version")
 | 
			
		||||
			if apiVersion == "" {
 | 
			
		||||
				apiVersion = c.GetString("api_version")
 | 
			
		||||
			}
 | 
			
		||||
			requestURL := strings.Split(requestURL, "?")[0]
 | 
			
		||||
			requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
 | 
			
		||||
			baseURL = c.GetString("base_url")
 | 
			
		||||
			task := strings.TrimPrefix(requestURL, "/v1/")
 | 
			
		||||
			model_ := textRequest.Model
 | 
			
		||||
			model_ = strings.Replace(model_, ".", "", -1)
 | 
			
		||||
			// https://github.com/songquanpeng/one-api/issues/67
 | 
			
		||||
			model_ = strings.TrimSuffix(model_, "-0301")
 | 
			
		||||
			model_ = strings.TrimSuffix(model_, "-0314")
 | 
			
		||||
			model_ = strings.TrimSuffix(model_, "-0613")
 | 
			
		||||
			fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeClaude:
 | 
			
		||||
		fullRequestURL = "https://api.anthropic.com/v1/complete"
 | 
			
		||||
		if baseURL != "" {
 | 
			
		||||
			fullRequestURL = fmt.Sprintf("%s/v1/complete", baseURL)
 | 
			
		||||
	if channelType == common.ChannelTypeAzure {
 | 
			
		||||
		// 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()
 | 
			
		||||
		apiVersion := query.Get("api-version")
 | 
			
		||||
		if apiVersion == "" {
 | 
			
		||||
			apiVersion = c.GetString("api_version")
 | 
			
		||||
		}
 | 
			
		||||
		requestURL := strings.Split(requestURL, "?")[0]
 | 
			
		||||
		requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
 | 
			
		||||
		baseURL = c.GetString("base_url")
 | 
			
		||||
		task := strings.TrimPrefix(requestURL, "/v1/")
 | 
			
		||||
		model_ := textRequest.Model
 | 
			
		||||
		model_ = strings.Replace(model_, ".", "", -1)
 | 
			
		||||
		// https://github.com/songquanpeng/one-api/issues/67
 | 
			
		||||
		model_ = strings.TrimSuffix(model_, "-0301")
 | 
			
		||||
		model_ = strings.TrimSuffix(model_, "-0314")
 | 
			
		||||
		model_ = strings.TrimSuffix(model_, "-0613")
 | 
			
		||||
		fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
 | 
			
		||||
	} else if channelType == common.ChannelTypePaLM {
 | 
			
		||||
		err := relayPaLM(textRequest, c)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	var promptTokens int
 | 
			
		||||
	var completionTokens int
 | 
			
		||||
@@ -157,58 +138,16 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	} else {
 | 
			
		||||
		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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		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 {
 | 
			
		||||
			req.Header.Set("api-key", apiKey)
 | 
			
		||||
		} else {
 | 
			
		||||
			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)
 | 
			
		||||
	if channelType == common.ChannelTypeAzure {
 | 
			
		||||
		key := c.Request.Header.Get("Authorization")
 | 
			
		||||
		key = strings.TrimPrefix(key, "Bearer ")
 | 
			
		||||
		req.Header.Set("api-key", key)
 | 
			
		||||
	} else {
 | 
			
		||||
		req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
 | 
			
		||||
	req.Header.Set("Accept", c.Request.Header.Get("Accept"))
 | 
			
		||||
@@ -276,198 +215,86 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	switch apiType {
 | 
			
		||||
	case APITypeOpenAI:
 | 
			
		||||
		if isStream {
 | 
			
		||||
			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), "\n"); i >= 0 {
 | 
			
		||||
					return i + 1, data[0:i], nil
 | 
			
		||||
				}
 | 
			
		||||
				if atEOF {
 | 
			
		||||
					return len(data), data, nil
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
	if isStream {
 | 
			
		||||
		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
 | 
			
		||||
			})
 | 
			
		||||
			dataChan := make(chan string)
 | 
			
		||||
			stopChan := make(chan bool)
 | 
			
		||||
			go func() {
 | 
			
		||||
				for scanner.Scan() {
 | 
			
		||||
					data := scanner.Text()
 | 
			
		||||
					if len(data) < 6 { // ignore blank line or wrong format
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					dataChan <- data
 | 
			
		||||
					data = data[6:]
 | 
			
		||||
					if !strings.HasPrefix(data, "[DONE]") {
 | 
			
		||||
						switch relayMode {
 | 
			
		||||
						case RelayModeChatCompletions:
 | 
			
		||||
							var streamResponse ChatCompletionsStreamResponse
 | 
			
		||||
							err = json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
							if err != nil {
 | 
			
		||||
								common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
								return
 | 
			
		||||
							}
 | 
			
		||||
							for _, choice := range streamResponse.Choices {
 | 
			
		||||
								streamResponseText += choice.Delta.Content
 | 
			
		||||
							}
 | 
			
		||||
						case RelayModeCompletions:
 | 
			
		||||
							var streamResponse CompletionsStreamResponse
 | 
			
		||||
							err = json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
							if err != nil {
 | 
			
		||||
								common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
								return
 | 
			
		||||
							}
 | 
			
		||||
							for _, choice := range streamResponse.Choices {
 | 
			
		||||
								streamResponseText += choice.Text
 | 
			
		||||
							}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if i := strings.Index(string(data), "\n\n"); i >= 0 {
 | 
			
		||||
				return i + 2, 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 len(data) < 6 { // must be something wrong!
 | 
			
		||||
					common.SysError("invalid stream response: " + data)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				dataChan <- data
 | 
			
		||||
				data = data[6:]
 | 
			
		||||
				if !strings.HasPrefix(data, "[DONE]") {
 | 
			
		||||
					switch relayMode {
 | 
			
		||||
					case RelayModeChatCompletions:
 | 
			
		||||
						var streamResponse ChatCompletionsStreamResponse
 | 
			
		||||
						err = json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
							return
 | 
			
		||||
						}
 | 
			
		||||
						for _, choice := range streamResponse.Choices {
 | 
			
		||||
							streamResponseText += choice.Delta.Content
 | 
			
		||||
						}
 | 
			
		||||
					case RelayModeCompletions:
 | 
			
		||||
						var streamResponse CompletionsStreamResponse
 | 
			
		||||
						err = json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
							return
 | 
			
		||||
						}
 | 
			
		||||
						for _, choice := range streamResponse.Choices {
 | 
			
		||||
							streamResponseText += choice.Text
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				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:
 | 
			
		||||
					if strings.HasPrefix(data, "data: [DONE]") {
 | 
			
		||||
						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})
 | 
			
		||||
					return true
 | 
			
		||||
				case <-stopChan:
 | 
			
		||||
					return false
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			err = resp.Body.Close()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			if consumeQuota {
 | 
			
		||||
				responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			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:
 | 
			
		||||
				if strings.HasPrefix(data, "data: [DONE]") {
 | 
			
		||||
					data = data[:12]
 | 
			
		||||
				}
 | 
			
		||||
				err = resp.Body.Close()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
				}
 | 
			
		||||
				err = json.Unmarshal(responseBody, &textResponse)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
				}
 | 
			
		||||
				if textResponse.Error.Type != "" {
 | 
			
		||||
					return &OpenAIErrorWithStatusCode{
 | 
			
		||||
						OpenAIError: textResponse.Error,
 | 
			
		||||
						StatusCode:  resp.StatusCode,
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				// Reset response body
 | 
			
		||||
				resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
 | 
			
		||||
				c.Render(-1, common.CustomEvent{Data: data})
 | 
			
		||||
				return true
 | 
			
		||||
			case <-stopChan:
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
			// We shouldn't set the header before we parse the response body, because the parse part may fail.
 | 
			
		||||
			// And then we will have to send an error response, but in this case, the header has already been set.
 | 
			
		||||
			// So the client will be confused by the response.
 | 
			
		||||
			// For example, Postman will report error, and we cannot check the response at all.
 | 
			
		||||
			for k, v := range resp.Header {
 | 
			
		||||
				c.Writer.Header().Set(k, v[0])
 | 
			
		||||
			}
 | 
			
		||||
			c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
			_, err = io.Copy(c.Writer, resp.Body)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			}
 | 
			
		||||
			err = resp.Body.Close()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		})
 | 
			
		||||
		err = resp.Body.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
	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 {
 | 
			
		||||
		return nil
 | 
			
		||||
	} else {
 | 
			
		||||
		if consumeQuota {
 | 
			
		||||
			responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
@@ -476,54 +303,35 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			}
 | 
			
		||||
			var claudeResponse ClaudeResponse
 | 
			
		||||
			err = json.Unmarshal(responseBody, &claudeResponse)
 | 
			
		||||
			err = json.Unmarshal(responseBody, &textResponse)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
			}
 | 
			
		||||
			if claudeResponse.Error.Type != "" {
 | 
			
		||||
			if textResponse.Error.Type != "" {
 | 
			
		||||
				return &OpenAIErrorWithStatusCode{
 | 
			
		||||
					OpenAIError: OpenAIError{
 | 
			
		||||
						Message: claudeResponse.Error.Message,
 | 
			
		||||
						Type:    claudeResponse.Error.Type,
 | 
			
		||||
						Param:   "",
 | 
			
		||||
						Code:    claudeResponse.Error.Type,
 | 
			
		||||
					},
 | 
			
		||||
					StatusCode: resp.StatusCode,
 | 
			
		||||
					OpenAIError: textResponse.Error,
 | 
			
		||||
					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
 | 
			
		||||
			// Reset response body
 | 
			
		||||
			resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError)
 | 
			
		||||
		// We shouldn't set the header before we parse the response body, because the parse part may fail.
 | 
			
		||||
		// And then we will have to send an error response, but in this case, the header has already been set.
 | 
			
		||||
		// So the client will be confused by the response.
 | 
			
		||||
		// For example, Postman will report error, and we cannot check the response at all.
 | 
			
		||||
		for k, v := range resp.Header {
 | 
			
		||||
			c.Writer.Header().Set(k, v[0])
 | 
			
		||||
		}
 | 
			
		||||
		c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
		_, err = io.Copy(c.Writer, resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		err = resp.Body.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
@@ -56,12 +55,6 @@ type TextRequest struct {
 | 
			
		||||
	//Stream   bool      `json:"stream"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ImageRequest struct {
 | 
			
		||||
	Prompt string `json:"prompt"`
 | 
			
		||||
	N      int    `json:"n"`
 | 
			
		||||
	Size   string `json:"size"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Usage struct {
 | 
			
		||||
	PromptTokens     int `json:"prompt_tokens"`
 | 
			
		||||
	CompletionTokens int `json:"completion_tokens"`
 | 
			
		||||
@@ -85,20 +78,6 @@ type TextResponse struct {
 | 
			
		||||
	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 {
 | 
			
		||||
	Created int `json:"created"`
 | 
			
		||||
	Data    []struct {
 | 
			
		||||
@@ -106,19 +85,13 @@ type ImageResponse struct {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatCompletionsStreamResponseChoice struct {
 | 
			
		||||
	Delta struct {
 | 
			
		||||
		Content string `json:"content"`
 | 
			
		||||
	} `json:"delta"`
 | 
			
		||||
	FinishReason string `json:"finish_reason,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatCompletionsStreamResponse struct {
 | 
			
		||||
	Id      string                                `json:"id"`
 | 
			
		||||
	Object  string                                `json:"object"`
 | 
			
		||||
	Created int64                                 `json:"created"`
 | 
			
		||||
	Model   string                                `json:"model"`
 | 
			
		||||
	Choices []ChatCompletionsStreamResponseChoice `json:"choices"`
 | 
			
		||||
	Choices []struct {
 | 
			
		||||
		Delta struct {
 | 
			
		||||
			Content string `json:"content"`
 | 
			
		||||
		} `json:"delta"`
 | 
			
		||||
		FinishReason string `json:"finish_reason"`
 | 
			
		||||
	} `json:"choices"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CompletionsStreamResponse struct {
 | 
			
		||||
@@ -136,8 +109,6 @@ func Relay(c *gin.Context) {
 | 
			
		||||
		relayMode = RelayModeCompletions
 | 
			
		||||
	} else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") {
 | 
			
		||||
		relayMode = RelayModeEmbeddings
 | 
			
		||||
	} else if strings.HasSuffix(c.Request.URL.Path, "embeddings") {
 | 
			
		||||
		relayMode = RelayModeEmbeddings
 | 
			
		||||
	} else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
 | 
			
		||||
		relayMode = RelayModeModerations
 | 
			
		||||
	} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
 | 
			
		||||
@@ -153,25 +124,16 @@ func Relay(c *gin.Context) {
 | 
			
		||||
		err = relayTextHelper(c, relayMode)
 | 
			
		||||
	}
 | 
			
		||||
	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 {
 | 
			
		||||
				err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
 | 
			
		||||
			}
 | 
			
		||||
			c.JSON(err.StatusCode, gin.H{
 | 
			
		||||
				"error": err.OpenAIError,
 | 
			
		||||
			})
 | 
			
		||||
		if err.StatusCode == http.StatusTooManyRequests {
 | 
			
		||||
			err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
 | 
			
		||||
		}
 | 
			
		||||
		c.JSON(err.StatusCode, gin.H{
 | 
			
		||||
			"error": err.OpenAIError,
 | 
			
		||||
		})
 | 
			
		||||
		channelId := c.GetInt("channel_id")
 | 
			
		||||
		common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
 | 
			
		||||
		// https://platform.openai.com/docs/guides/error-codes/api-errors
 | 
			
		||||
		if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key" || err.Code == "account_deactivated") {
 | 
			
		||||
		if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key") {
 | 
			
		||||
			channelId := c.GetInt("channel_id")
 | 
			
		||||
			channelName := c.GetString("channel_name")
 | 
			
		||||
			disableChannel(channelId, channelName, err.Message)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								go.mod
									
									
									
									
									
								
							@@ -9,28 +9,28 @@ require (
 | 
			
		||||
	github.com/gin-contrib/sessions v0.0.5
 | 
			
		||||
	github.com/gin-contrib/static v0.0.1
 | 
			
		||||
	github.com/gin-gonic/gin v1.9.1
 | 
			
		||||
	github.com/go-playground/validator/v10 v10.14.0
 | 
			
		||||
	github.com/go-playground/validator/v10 v10.14.1
 | 
			
		||||
	github.com/go-redis/redis/v8 v8.11.5
 | 
			
		||||
	github.com/google/uuid v1.3.0
 | 
			
		||||
	github.com/pkoukk/tiktoken-go v0.1.1
 | 
			
		||||
	golang.org/x/crypto v0.9.0
 | 
			
		||||
	gorm.io/driver/mysql v1.4.3
 | 
			
		||||
	gorm.io/driver/sqlite v1.4.3
 | 
			
		||||
	gorm.io/gorm v1.24.0
 | 
			
		||||
	github.com/pkoukk/tiktoken-go v0.1.4
 | 
			
		||||
	golang.org/x/crypto v0.11.0
 | 
			
		||||
	gorm.io/driver/mysql v1.5.1
 | 
			
		||||
	gorm.io/driver/sqlite v1.5.2
 | 
			
		||||
	gorm.io/gorm v1.25.2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
 | 
			
		||||
	github.com/bytedance/sonic v1.9.1 // indirect
 | 
			
		||||
	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 | 
			
		||||
	github.com/bytedance/sonic v1.9.2 // indirect
 | 
			
		||||
	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 | 
			
		||||
	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
 | 
			
		||||
	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 | 
			
		||||
	github.com/dlclark/regexp2 v1.8.1 // indirect
 | 
			
		||||
	github.com/dlclark/regexp2 v1.10.0 // indirect
 | 
			
		||||
	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
 | 
			
		||||
	github.com/gin-contrib/sse v0.1.0 // indirect
 | 
			
		||||
	github.com/go-playground/locales v0.14.1 // indirect
 | 
			
		||||
	github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
			
		||||
	github.com/go-sql-driver/mysql v1.6.0 // indirect
 | 
			
		||||
	github.com/go-sql-driver/mysql v1.7.1 // indirect
 | 
			
		||||
	github.com/goccy/go-json v0.10.2 // indirect
 | 
			
		||||
	github.com/gomodule/redigo v2.0.0+incompatible // indirect
 | 
			
		||||
	github.com/gorilla/context v1.1.1 // indirect
 | 
			
		||||
@@ -39,7 +39,7 @@ require (
 | 
			
		||||
	github.com/jinzhu/inflection v1.0.0 // indirect
 | 
			
		||||
	github.com/jinzhu/now v1.1.5 // indirect
 | 
			
		||||
	github.com/json-iterator/go v1.1.12 // indirect
 | 
			
		||||
	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
 | 
			
		||||
	github.com/klauspost/cpuid/v2 v2.2.5 // indirect
 | 
			
		||||
	github.com/leodido/go-urn v1.2.4 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.19 // indirect
 | 
			
		||||
	github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
 | 
			
		||||
@@ -48,10 +48,10 @@ require (
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
 | 
			
		||||
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 | 
			
		||||
	github.com/ugorji/go/codec v1.2.11 // indirect
 | 
			
		||||
	golang.org/x/arch v0.3.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.10.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.8.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.9.0 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.30.0 // indirect
 | 
			
		||||
	golang.org/x/arch v0.4.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.12.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.10.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.11.0 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.31.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								go.sum
									
									
									
									
									
								
							@@ -3,8 +3,12 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
 | 
			
		||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 | 
			
		||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
 | 
			
		||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 | 
			
		||||
github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=
 | 
			
		||||
github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
			
		||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
 | 
			
		||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 | 
			
		||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
 | 
			
		||||
@@ -16,6 +20,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
 | 
			
		||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 | 
			
		||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
 | 
			
		||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 | 
			
		||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
 | 
			
		||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 | 
			
		||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 | 
			
		||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
 | 
			
		||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
 | 
			
		||||
@@ -47,10 +53,15 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
 | 
			
		||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 | 
			
		||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
 | 
			
		||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 | 
			
		||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 | 
			
		||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 | 
			
		||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 | 
			
		||||
@@ -81,6 +92,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
 | 
			
		||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 | 
			
		||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 | 
			
		||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 | 
			
		||||
@@ -115,6 +128,8 @@ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNc
 | 
			
		||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.4 h1:bniMzWdUvNO6YkRbASo2x5qJf2LAG/TIJojqz+Igm8E=
 | 
			
		||||
github.com/pkoukk/tiktoken-go v0.1.4/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 | 
			
		||||
@@ -144,27 +159,38 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
 | 
			
		||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 | 
			
		||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
 | 
			
		||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 | 
			
		||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
 | 
			
		||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
			
		||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
 | 
			
		||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 | 
			
		||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
 | 
			
		||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
 | 
			
		||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
 | 
			
		||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 | 
			
		||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
 | 
			
		||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 | 
			
		||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
 | 
			
		||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 | 
			
		||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 | 
			
		||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
 | 
			
		||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
@@ -172,6 +198,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
 | 
			
		||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
			
		||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
 | 
			
		||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
			
		||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
 | 
			
		||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 | 
			
		||||
@@ -188,9 +216,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
 | 
			
		||||
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
 | 
			
		||||
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
 | 
			
		||||
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
 | 
			
		||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
 | 
			
		||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
 | 
			
		||||
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
 | 
			
		||||
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
 | 
			
		||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 | 
			
		||||
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
 | 
			
		||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
 | 
			
		||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 | 
			
		||||
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
 | 
			
		||||
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 | 
			
		||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 | 
			
		||||
 
 | 
			
		||||
@@ -74,11 +74,6 @@ func Distribute() func(c *gin.Context) {
 | 
			
		||||
					modelRequest.Model = "text-moderation-stable"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if strings.HasSuffix(c.Request.URL.Path, "embeddings") {
 | 
			
		||||
				if modelRequest.Model == "" {
 | 
			
		||||
					modelRequest.Model = c.Param("model")
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
 | 
			
		||||
				if modelRequest.Model == "" {
 | 
			
		||||
					modelRequest.Model = "dall-e"
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,6 @@ func InitOptionMap() {
 | 
			
		||||
	common.OptionMap["TopUpLink"] = common.TopUpLink
 | 
			
		||||
	common.OptionMap["ChatLink"] = common.ChatLink
 | 
			
		||||
	common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
 | 
			
		||||
	common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
 | 
			
		||||
	common.OptionMapRWMutex.Unlock()
 | 
			
		||||
	loadOptionsFromDatabase()
 | 
			
		||||
}
 | 
			
		||||
@@ -197,8 +196,6 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
		common.QuotaRemindThreshold, _ = strconv.Atoi(value)
 | 
			
		||||
	case "PreConsumedQuota":
 | 
			
		||||
		common.PreConsumedQuota, _ = strconv.Atoi(value)
 | 
			
		||||
	case "RetryTimes":
 | 
			
		||||
		common.RetryTimes, _ = strconv.Atoi(value)
 | 
			
		||||
	case "ModelRatio":
 | 
			
		||||
		err = common.UpdateModelRatioByJSONString(value)
 | 
			
		||||
	case "GroupRatio":
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ func SetApiRouter(router *gin.Engine) {
 | 
			
		||||
		userRoute := apiRouter.Group("/user")
 | 
			
		||||
		{
 | 
			
		||||
			userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
 | 
			
		||||
			userRoute.POST("/login", middleware.CriticalRateLimit(), controller.Login)
 | 
			
		||||
			userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
 | 
			
		||||
			userRoute.GET("/logout", controller.Logout)
 | 
			
		||||
 | 
			
		||||
			selfRoute := userRoute.Group("/")
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,6 @@ func SetRelayRouter(router *gin.Engine) {
 | 
			
		||||
		relayV1Router.POST("/images/edits", controller.RelayNotImplemented)
 | 
			
		||||
		relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
 | 
			
		||||
		relayV1Router.POST("/embeddings", controller.Relay)
 | 
			
		||||
		relayV1Router.POST("/engines/:model/embeddings", controller.Relay)
 | 
			
		||||
		relayV1Router.POST("/audio/transcriptions", controller.RelayNotImplemented)
 | 
			
		||||
		relayV1Router.POST("/audio/translations", controller.RelayNotImplemented)
 | 
			
		||||
		relayV1Router.GET("/files", controller.RelayNotImplemented)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -22,5 +22,4 @@ npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
.idea
 | 
			
		||||
package-lock.json
 | 
			
		||||
yarn.lock
 | 
			
		||||
yarn.lock
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18281
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										18281
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -3,18 +3,17 @@
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "axios": "^0.27.2",
 | 
			
		||||
    "axios": "^1.4.0",
 | 
			
		||||
    "history": "^5.3.0",
 | 
			
		||||
    "marked": "^4.1.1",
 | 
			
		||||
    "marked": "^5.1.1",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-dropzone": "^14.2.3",
 | 
			
		||||
    "react-router-dom": "^6.3.0",
 | 
			
		||||
    "react-scripts": "5.0.1",
 | 
			
		||||
    "react-toastify": "^9.0.8",
 | 
			
		||||
    "react-turnstile": "^1.0.5",
 | 
			
		||||
    "react-router-dom": "^6.14.1",
 | 
			
		||||
    "react-toastify": "^9.1.3",
 | 
			
		||||
    "react-turnstile": "^1.1.1",
 | 
			
		||||
    "semantic-ui-css": "^2.5.0",
 | 
			
		||||
    "semantic-ui-react": "^2.1.3"
 | 
			
		||||
    "semantic-ui-react": "^2.1.4"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "react-scripts start",
 | 
			
		||||
@@ -41,7 +40,8 @@
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "prettier": "^2.7.1"
 | 
			
		||||
    "prettier": "^2.7.1",
 | 
			
		||||
    "react-scripts": "^5.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "prettier": {
 | 
			
		||||
    "singleQuote": true,
 | 
			
		||||
 
 | 
			
		||||
@@ -55,15 +55,6 @@ function App() {
 | 
			
		||||
      } else {
 | 
			
		||||
        localStorage.removeItem('chat_link');
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        data.version !== process.env.REACT_APP_VERSION &&
 | 
			
		||||
        data.version !== 'v0.0.0' &&
 | 
			
		||||
        process.env.REACT_APP_VERSION !== ''
 | 
			
		||||
      ) {
 | 
			
		||||
        showNotice(
 | 
			
		||||
          `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      showError('无法正常连接至服务器!');
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import {
 | 
			
		||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
import { API, getLogo, showError, showSuccess, showInfo } from '../helpers';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
 | 
			
		||||
const LoginForm = () => {
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
@@ -24,6 +25,9 @@ const LoginForm = () => {
 | 
			
		||||
  const [submitted, setSubmitted] = useState(false);
 | 
			
		||||
  const { username, password } = inputs;
 | 
			
		||||
  const [userState, userDispatch] = useContext(UserContext);
 | 
			
		||||
  const [turnstileEnabled, setTurnstileEnabled] = useState(false);
 | 
			
		||||
  const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
 | 
			
		||||
  const [turnstileToken, setTurnstileToken] = useState('');
 | 
			
		||||
  let navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const [status, setStatus] = useState({});
 | 
			
		||||
@@ -37,6 +41,11 @@ const LoginForm = () => {
 | 
			
		||||
    if (status) {
 | 
			
		||||
      status = JSON.parse(status);
 | 
			
		||||
      setStatus(status);
 | 
			
		||||
 | 
			
		||||
      if (status.turnstile_check) {
 | 
			
		||||
        setTurnstileEnabled(true);
 | 
			
		||||
        setTurnstileSiteKey(status.turnstile_site_key);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
@@ -76,7 +85,12 @@ const LoginForm = () => {
 | 
			
		||||
  async function handleSubmit(e) {
 | 
			
		||||
    setSubmitted(true);
 | 
			
		||||
    if (username && password) {
 | 
			
		||||
      const res = await API.post(`/api/user/login`, {
 | 
			
		||||
      if (turnstileEnabled && turnstileToken === '') {
 | 
			
		||||
        showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const res = await API.post(`/api/user/login?turnstile=${turnstileToken}`, {
 | 
			
		||||
        username,
 | 
			
		||||
        password,
 | 
			
		||||
      });
 | 
			
		||||
@@ -119,6 +133,16 @@ const LoginForm = () => {
 | 
			
		||||
              value={password}
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
            />
 | 
			
		||||
            {turnstileEnabled ? (
 | 
			
		||||
              <Turnstile
 | 
			
		||||
                sitekey={turnstileSiteKey}
 | 
			
		||||
                onVerify={(token) => {
 | 
			
		||||
                  setTurnstileToken(token);
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button color="" fluid size="large" onClick={handleSubmit}>
 | 
			
		||||
              登录
 | 
			
		||||
            </Button>
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,6 @@ const OperationSetting = () => {
 | 
			
		||||
    DisplayInCurrencyEnabled: '',
 | 
			
		||||
    DisplayTokenStatEnabled: '',
 | 
			
		||||
    ApproximateTokenEnabled: '',
 | 
			
		||||
    RetryTimes: 0,
 | 
			
		||||
  });
 | 
			
		||||
  const [originInputs, setOriginInputs] = useState({});
 | 
			
		||||
  let [loading, setLoading] = useState(false);
 | 
			
		||||
@@ -123,9 +122,6 @@ const OperationSetting = () => {
 | 
			
		||||
        if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
 | 
			
		||||
          await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
 | 
			
		||||
        }
 | 
			
		||||
        if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
 | 
			
		||||
          await updateOption('RetryTimes', inputs.RetryTimes);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
@@ -137,7 +133,7 @@ const OperationSetting = () => {
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            通用设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={4}>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='充值链接'
 | 
			
		||||
              name='TopUpLink'
 | 
			
		||||
@@ -166,17 +162,6 @@ const OperationSetting = () => {
 | 
			
		||||
              step='0.01'
 | 
			
		||||
              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 inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
 
 | 
			
		||||
@@ -226,7 +226,6 @@ const UsersTable = () => {
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content={user.email ? user.email : '未绑定邮箱地址'}
 | 
			
		||||
                      key={user.username}
 | 
			
		||||
                      header={user.display_name ? user.display_name : user.username}
 | 
			
		||||
                      trigger={<span>{renderText(user.username, 10)}</span>}
 | 
			
		||||
                      hoverable
 | 
			
		||||
                    />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
export const CHANNEL_OPTIONS = [
 | 
			
		||||
  { key: 1, text: 'OpenAI', value: 1, color: 'green' },
 | 
			
		||||
  { key: 14, text: 'Anthropic', value: 14, color: 'black' },
 | 
			
		||||
  { key: 8, text: '自定义', value: 8, color: 'pink' },
 | 
			
		||||
  { key: 3, text: 'Azure', value: 3, color: 'olive' },
 | 
			
		||||
  { key: 2, text: 'API2D', value: 2, color: 'blue' },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { useParams } from 'react-router-dom';
 | 
			
		||||
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
 | 
			
		||||
import { CHANNEL_OPTIONS } from '../../constants';
 | 
			
		||||
@@ -31,7 +31,6 @@ const EditChannel = () => {
 | 
			
		||||
  const [groupOptions, setGroupOptions] = useState([]);
 | 
			
		||||
  const [basicModels, setBasicModels] = useState([]);
 | 
			
		||||
  const [fullModels, setFullModels] = useState([]);
 | 
			
		||||
  const [customModel, setCustomModel] = useState('');
 | 
			
		||||
  const handleInputChange = (e, { name, value }) => {
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  };
 | 
			
		||||
@@ -44,19 +43,6 @@ const EditChannel = () => {
 | 
			
		||||
        data.models = [];
 | 
			
		||||
      } else {
 | 
			
		||||
        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 === '') {
 | 
			
		||||
        data.groups = [];
 | 
			
		||||
@@ -277,27 +263,6 @@ const EditChannel = () => {
 | 
			
		||||
            <Button type={'button'} onClick={() => {
 | 
			
		||||
              handleInputChange(null, { name: 'models', value: [] });
 | 
			
		||||
            }}>清除所有模型</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>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
@@ -344,7 +309,7 @@ const EditChannel = () => {
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          <Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button>
 | 
			
		||||
          <Button positive onClick={submit}>提交</Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Segment>
 | 
			
		||||
    </>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user