mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
			v0.5.8-alp
			...
			v0.5.9-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					a3f80a3392 | ||
| 
						 | 
					8f5b83562b | ||
| 
						 | 
					b7570d5c77 | ||
| 
						 | 
					0e73418cdf | ||
| 
						 | 
					9889377f0e | ||
| 
						 | 
					b273464e77 | ||
| 
						 | 
					b4e43d97fd | ||
| 
						 | 
					3347a44023 | ||
| 
						 | 
					923e24534b | ||
| 
						 | 
					b4d67ca614 | ||
| 
						 | 
					d85e356b6e | 
@@ -51,15 +51,15 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 
 | 
			
		||||
  <a href="https://iamazing.cn/page/reward">赞赏支持</a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
> **Note**
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
> 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
 | 
			
		||||
> 
 | 
			
		||||
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
 | 
			
		||||
 | 
			
		||||
> **Warning**
 | 
			
		||||
> [!WARNING]
 | 
			
		||||
> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
 | 
			
		||||
 | 
			
		||||
> **Warning**
 | 
			
		||||
> [!WARNING]
 | 
			
		||||
> 使用 root 用户初次登录系统后,务必修改默认密码 `123456`!
 | 
			
		||||
 | 
			
		||||
## 功能
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,7 @@ var QuotaForInviter = 0
 | 
			
		||||
var QuotaForInvitee = 0
 | 
			
		||||
var ChannelDisableThreshold = 5.0
 | 
			
		||||
var AutomaticDisableChannelEnabled = false
 | 
			
		||||
var AutomaticEnableChannelEnabled = false
 | 
			
		||||
var QuotaRemindThreshold = 1000
 | 
			
		||||
var PreConsumedQuota = 500
 | 
			
		||||
var ApproximateTokenEnabled = false
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,13 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/smtp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func SendEmail(subject string, receiver string, content string) error {
 | 
			
		||||
@@ -13,15 +15,32 @@ func SendEmail(subject string, receiver string, content string) error {
 | 
			
		||||
		SMTPFrom = SMTPAccount
 | 
			
		||||
	}
 | 
			
		||||
	encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
 | 
			
		||||
 | 
			
		||||
	// Extract domain from SMTPFrom
 | 
			
		||||
	parts := strings.Split(SMTPFrom, "@")
 | 
			
		||||
	var domain string
 | 
			
		||||
	if len(parts) > 1 {
 | 
			
		||||
		domain = parts[1]
 | 
			
		||||
	}
 | 
			
		||||
	// Generate a unique Message-ID
 | 
			
		||||
	buf := make([]byte, 16)
 | 
			
		||||
	_, err := rand.Read(buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	messageId := fmt.Sprintf("<%x@%s>", buf, domain)
 | 
			
		||||
 | 
			
		||||
	mail := []byte(fmt.Sprintf("To: %s\r\n"+
 | 
			
		||||
		"From: %s<%s>\r\n"+
 | 
			
		||||
		"Subject: %s\r\n"+
 | 
			
		||||
		"Message-ID: %s\r\n"+ // add Message-ID header to avoid being treated as spam, RFC 5322
 | 
			
		||||
		"Date: %s\r\n"+
 | 
			
		||||
		"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
 | 
			
		||||
		receiver, SystemName, SMTPFrom, encodedSubject, content))
 | 
			
		||||
		receiver, SystemName, SMTPFrom, encodedSubject, messageId, time.Now().Format(time.RFC1123Z), content))
 | 
			
		||||
	auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
 | 
			
		||||
	addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
 | 
			
		||||
	to := strings.Split(receiver, ";")
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	if SMTPPort == 465 {
 | 
			
		||||
		tlsConfig := &tls.Config{
 | 
			
		||||
			InsecureSkipVerify: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -76,6 +76,8 @@ var ModelRatio = map[string]float64{
 | 
			
		||||
	"dall-e-3":                  20,     // $0.040 - $0.120 / image
 | 
			
		||||
	"claude-instant-1":          0.815,  // $1.63 / 1M tokens
 | 
			
		||||
	"claude-2":                  5.51,   // $11.02 / 1M tokens
 | 
			
		||||
	"claude-2.0":                5.51,   // $11.02 / 1M tokens
 | 
			
		||||
	"claude-2.1":                5.51,   // $11.02 / 1M tokens
 | 
			
		||||
	"ERNIE-Bot":                 0.8572, // ¥0.012 / 1k tokens
 | 
			
		||||
	"ERNIE-Bot-turbo":           0.5715, // ¥0.008 / 1k tokens
 | 
			
		||||
	"ERNIE-Bot-4":               8.572,  // ¥0.12 / 1k tokens
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,9 @@ func testChannel(channel *model.Channel, request ChatRequest) (err error, openai
 | 
			
		||||
		return fmt.Errorf("Error: %s\nResp body: %s", err, body), nil
 | 
			
		||||
	}
 | 
			
		||||
	if response.Usage.CompletionTokens == 0 {
 | 
			
		||||
		if response.Error.Message == "" {
 | 
			
		||||
			response.Error.Message = "补全 tokens 非预期返回 0"
 | 
			
		||||
		}
 | 
			
		||||
		return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message)), &response.Error
 | 
			
		||||
	}
 | 
			
		||||
	return nil, nil
 | 
			
		||||
@@ -142,20 +145,32 @@ func TestChannel(c *gin.Context) {
 | 
			
		||||
var testAllChannelsLock sync.Mutex
 | 
			
		||||
var testAllChannelsRunning bool = false
 | 
			
		||||
 | 
			
		||||
// disable & notify
 | 
			
		||||
func disableChannel(channelId int, channelName string, reason string) {
 | 
			
		||||
func notifyRootUser(subject string, content string) {
 | 
			
		||||
	if common.RootUserEmail == "" {
 | 
			
		||||
		common.RootUserEmail = model.GetRootUserEmail()
 | 
			
		||||
	}
 | 
			
		||||
	model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled)
 | 
			
		||||
	subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
 | 
			
		||||
	content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
 | 
			
		||||
	err := common.SendEmail(subject, common.RootUserEmail, content)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// disable & notify
 | 
			
		||||
func disableChannel(channelId int, channelName string, reason string) {
 | 
			
		||||
	model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled)
 | 
			
		||||
	subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
 | 
			
		||||
	content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
 | 
			
		||||
	notifyRootUser(subject, content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// enable & notify
 | 
			
		||||
func enableChannel(channelId int, channelName string) {
 | 
			
		||||
	model.UpdateChannelStatusById(channelId, common.ChannelStatusEnabled)
 | 
			
		||||
	subject := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId)
 | 
			
		||||
	content := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId)
 | 
			
		||||
	notifyRootUser(subject, content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testAllChannels(notify bool) error {
 | 
			
		||||
	if common.RootUserEmail == "" {
 | 
			
		||||
		common.RootUserEmail = model.GetRootUserEmail()
 | 
			
		||||
@@ -178,20 +193,21 @@ func testAllChannels(notify bool) error {
 | 
			
		||||
	}
 | 
			
		||||
	go func() {
 | 
			
		||||
		for _, channel := range channels {
 | 
			
		||||
			if channel.Status != common.ChannelStatusEnabled {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			isChannelEnabled := channel.Status == common.ChannelStatusEnabled
 | 
			
		||||
			tik := time.Now()
 | 
			
		||||
			err, openaiErr := testChannel(channel, *testRequest)
 | 
			
		||||
			tok := time.Now()
 | 
			
		||||
			milliseconds := tok.Sub(tik).Milliseconds()
 | 
			
		||||
			if milliseconds > disableThreshold {
 | 
			
		||||
			if isChannelEnabled && milliseconds > disableThreshold {
 | 
			
		||||
				err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
 | 
			
		||||
				disableChannel(channel.Id, channel.Name, err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			if shouldDisableChannel(openaiErr, -1) {
 | 
			
		||||
			if isChannelEnabled && shouldDisableChannel(openaiErr, -1) {
 | 
			
		||||
				disableChannel(channel.Id, channel.Name, err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			if !isChannelEnabled && shouldEnableChannel(err, openaiErr) {
 | 
			
		||||
				enableChannel(channel.Id, channel.Name)
 | 
			
		||||
			}
 | 
			
		||||
			channel.UpdateResponseTime(milliseconds)
 | 
			
		||||
			time.Sleep(common.RequestInterval)
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -360,6 +360,24 @@ func init() {
 | 
			
		||||
			Root:       "claude-2",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "claude-2.1",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "anthropic",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "claude-2.1",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "claude-2.0",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "anthropic",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "claude-2.0",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "ERNIE-Bot",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,13 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
@@ -37,24 +39,24 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	preConsumedTokens := common.PreConsumedQuota
 | 
			
		||||
	modelRatio := common.GetModelRatio(audioModel)
 | 
			
		||||
	groupRatio := common.GetGroupRatio(group)
 | 
			
		||||
	ratio := modelRatio * groupRatio
 | 
			
		||||
	preConsumedQuota := int(float64(preConsumedTokens) * ratio)
 | 
			
		||||
	var quota int
 | 
			
		||||
	var preConsumedQuota int
 | 
			
		||||
	switch relayMode {
 | 
			
		||||
	case RelayModeAudioSpeech:
 | 
			
		||||
		preConsumedQuota = int(float64(len(ttsRequest.Input)) * ratio)
 | 
			
		||||
		quota = preConsumedQuota
 | 
			
		||||
	default:
 | 
			
		||||
		preConsumedQuota = int(float64(common.PreConsumedQuota) * ratio)
 | 
			
		||||
	}
 | 
			
		||||
	userQuota, err := model.CacheGetUserQuota(userId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	quota := 0
 | 
			
		||||
	// Check if user quota is enough
 | 
			
		||||
	if relayMode == RelayModeAudioSpeech {
 | 
			
		||||
		quota = int(float64(len(ttsRequest.Input)) * modelRatio * groupRatio)
 | 
			
		||||
		if quota > userQuota {
 | 
			
		||||
			return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
	if userQuota-preConsumedQuota < 0 {
 | 
			
		||||
		return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
 | 
			
		||||
	}
 | 
			
		||||
@@ -73,7 +75,6 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
			return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// map model name
 | 
			
		||||
	modelMapping := c.GetString("model_mapping")
 | 
			
		||||
@@ -95,13 +96,28 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fullRequestURL := getFullRequestURL(baseURL, requestURL, channelType)
 | 
			
		||||
	if relayMode == RelayModeAudioTranscription && channelType == common.ChannelTypeAzure {
 | 
			
		||||
		// https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api
 | 
			
		||||
		apiVersion := GetAPIVersion(c)
 | 
			
		||||
		fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/audio/transcriptions?api-version=%s", baseURL, audioModel, apiVersion)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestBody := c.Request.Body
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if relayMode == RelayModeAudioTranscription && channelType == common.ChannelTypeAzure {
 | 
			
		||||
		// https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api
 | 
			
		||||
		apiKey := c.Request.Header.Get("Authorization")
 | 
			
		||||
		apiKey = strings.TrimPrefix(apiKey, "Bearer ")
 | 
			
		||||
		req.Header.Set("api-key", apiKey)
 | 
			
		||||
		req.ContentLength = c.Request.ContentLength
 | 
			
		||||
	} 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"))
 | 
			
		||||
 | 
			
		||||
@@ -119,11 +135,7 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if relayMode == RelayModeAudioSpeech {
 | 
			
		||||
		defer func(ctx context.Context) {
 | 
			
		||||
			go postConsumeQuota(ctx, tokenId, quota, userId, channelId, modelRatio, groupRatio, audioModel, tokenName)
 | 
			
		||||
		}(c.Request.Context())
 | 
			
		||||
	} else {
 | 
			
		||||
	if relayMode != RelayModeAudioSpeech {
 | 
			
		||||
		responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
@@ -137,13 +149,29 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		defer func(ctx context.Context) {
 | 
			
		||||
			quota := countTokenText(whisperResponse.Text, audioModel)
 | 
			
		||||
			quotaDelta := quota - preConsumedQuota
 | 
			
		||||
			go postConsumeQuota(ctx, tokenId, quotaDelta, userId, channelId, modelRatio, groupRatio, audioModel, tokenName)
 | 
			
		||||
		}(c.Request.Context())
 | 
			
		||||
		quota = countTokenText(whisperResponse.Text, audioModel)
 | 
			
		||||
		resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
 | 
			
		||||
	}
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		if preConsumedQuota > 0 {
 | 
			
		||||
			// we need to roll back the pre-consumed quota
 | 
			
		||||
			defer func(ctx context.Context) {
 | 
			
		||||
				go func() {
 | 
			
		||||
					// negative means add quota back for token & user
 | 
			
		||||
					err := model.PostConsumeTokenQuota(tokenId, -preConsumedQuota)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						common.LogError(ctx, fmt.Sprintf("error rollback pre-consumed quota: %s", err.Error()))
 | 
			
		||||
					}
 | 
			
		||||
				}()
 | 
			
		||||
			}(c.Request.Context())
 | 
			
		||||
		}
 | 
			
		||||
		return relayErrorHandler(resp)
 | 
			
		||||
	}
 | 
			
		||||
	quotaDelta := quota - preConsumedQuota
 | 
			
		||||
	defer func(ctx context.Context) {
 | 
			
		||||
		go postConsumeQuota(ctx, tokenId, quotaDelta, quota, userId, channelId, modelRatio, groupRatio, audioModel, tokenName)
 | 
			
		||||
	}(c.Request.Context())
 | 
			
		||||
 | 
			
		||||
	for k, v := range resp.Header {
 | 
			
		||||
		c.Writer.Header().Set(k, v[0])
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,9 @@ func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest {
 | 
			
		||||
		} else if message.Role == "assistant" {
 | 
			
		||||
			prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
 | 
			
		||||
		} else if message.Role == "system" {
 | 
			
		||||
			prompt += fmt.Sprintf("\n\nSystem: %s", message.Content)
 | 
			
		||||
			if prompt == "" {
 | 
			
		||||
				prompt = message.StringContent()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	prompt += "\n\nAssistant:"
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"one-api/model"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
@@ -33,16 +34,13 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
	channelType := c.GetInt("channel")
 | 
			
		||||
	channelId := c.GetInt("channel_id")
 | 
			
		||||
	userId := c.GetInt("id")
 | 
			
		||||
	consumeQuota := c.GetBool("consume_quota")
 | 
			
		||||
	group := c.GetString("group")
 | 
			
		||||
 | 
			
		||||
	var imageRequest ImageRequest
 | 
			
		||||
	if consumeQuota {
 | 
			
		||||
	err := common.UnmarshalBodyReusable(c, &imageRequest)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
 | 
			
		||||
	}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Size validation
 | 
			
		||||
	if imageRequest.Size != "" {
 | 
			
		||||
@@ -104,8 +102,15 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
		baseURL = c.GetString("base_url")
 | 
			
		||||
	}
 | 
			
		||||
	fullRequestURL := getFullRequestURL(baseURL, requestURL, channelType)
 | 
			
		||||
	if channelType == common.ChannelTypeAzure && relayMode == RelayModeImagesGenerations {
 | 
			
		||||
		// https://learn.microsoft.com/en-us/azure/ai-services/openai/dall-e-quickstart?tabs=dalle3%2Ccommand-line&pivots=rest-api
 | 
			
		||||
		apiVersion := GetAPIVersion(c)
 | 
			
		||||
		// https://{resource_name}.openai.azure.com/openai/deployments/dall-e-3/images/generations?api-version=2023-06-01-preview
 | 
			
		||||
		fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", baseURL, imageModel, apiVersion)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var requestBody io.Reader
 | 
			
		||||
	if isModelMapped {
 | 
			
		||||
	if isModelMapped || channelType == common.ChannelTypeAzure { // make Azure channel request body
 | 
			
		||||
		jsonStr, err := json.Marshal(imageRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
@@ -122,7 +127,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
 | 
			
		||||
	quota := int(ratio*imageCostRatio*1000) * imageRequest.N
 | 
			
		||||
 | 
			
		||||
	if consumeQuota && userQuota-quota < 0 {
 | 
			
		||||
	if userQuota-quota < 0 {
 | 
			
		||||
		return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -130,7 +135,13 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
 | 
			
		||||
	token := c.Request.Header.Get("Authorization")
 | 
			
		||||
	if channelType == common.ChannelTypeAzure { // Azure authentication
 | 
			
		||||
		token = strings.TrimPrefix(token, "Bearer ")
 | 
			
		||||
		req.Header.Set("api-key", token)
 | 
			
		||||
	} else {
 | 
			
		||||
		req.Header.Set("Authorization", token)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
 | 
			
		||||
	req.Header.Set("Accept", c.Request.Header.Get("Accept"))
 | 
			
		||||
@@ -151,7 +162,6 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
	var textResponse ImageResponse
 | 
			
		||||
 | 
			
		||||
	defer func(ctx context.Context) {
 | 
			
		||||
		if consumeQuota {
 | 
			
		||||
		err := model.PostConsumeTokenQuota(tokenId, quota)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			common.SysError("error consuming token remain quota: " + err.Error())
 | 
			
		||||
@@ -168,10 +178,8 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
			channelId := c.GetInt("channel_id")
 | 
			
		||||
			model.UpdateChannelUsedQuota(channelId, quota)
 | 
			
		||||
		}
 | 
			
		||||
		}
 | 
			
		||||
	}(c.Request.Context())
 | 
			
		||||
 | 
			
		||||
	if consumeQuota {
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -187,7 +195,6 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range resp.Header {
 | 
			
		||||
		c.Writer.Header().Set(k, v[0])
 | 
			
		||||
 
 | 
			
		||||
@@ -88,9 +88,8 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
 | 
			
		||||
	return nil, responseText
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
func openaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var textResponse TextResponse
 | 
			
		||||
	if consumeQuota {
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
@@ -111,7 +110,7 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool, promp
 | 
			
		||||
	}
 | 
			
		||||
	// Reset response body
 | 
			
		||||
	resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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 httpClient will be confused by the response.
 | 
			
		||||
@@ -120,7 +119,7 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool, promp
 | 
			
		||||
		c.Writer.Header().Set(k, v[0])
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err := io.Copy(c.Writer, resp.Body)
 | 
			
		||||
	_, err = io.Copy(c.Writer, resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,15 +51,12 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	channelId := c.GetInt("channel_id")
 | 
			
		||||
	tokenId := c.GetInt("token_id")
 | 
			
		||||
	userId := c.GetInt("id")
 | 
			
		||||
	consumeQuota := c.GetBool("consume_quota")
 | 
			
		||||
	group := c.GetString("group")
 | 
			
		||||
	var textRequest GeneralOpenAIRequest
 | 
			
		||||
	if consumeQuota || channelType == common.ChannelTypeAzure || channelType == common.ChannelTypePaLM {
 | 
			
		||||
	err := common.UnmarshalBodyReusable(c, &textRequest)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
 | 
			
		||||
	}
 | 
			
		||||
	}
 | 
			
		||||
	if relayMode == RelayModeModerations && textRequest.Model == "" {
 | 
			
		||||
		textRequest.Model = "text-moderation-latest"
 | 
			
		||||
	}
 | 
			
		||||
@@ -132,11 +129,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	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")
 | 
			
		||||
			}
 | 
			
		||||
			apiVersion := GetAPIVersion(c)
 | 
			
		||||
			requestURL := strings.Split(requestURL, "?")[0]
 | 
			
		||||
			requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
 | 
			
		||||
			baseURL = c.GetString("base_url")
 | 
			
		||||
@@ -235,7 +228,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
		preConsumedQuota = 0
 | 
			
		||||
		common.LogInfo(c.Request.Context(), fmt.Sprintf("user %d has enough quota %d, trusted and no need to pre-consume", userId, userQuota))
 | 
			
		||||
	}
 | 
			
		||||
	if consumeQuota && preConsumedQuota > 0 {
 | 
			
		||||
	if preConsumedQuota > 0 {
 | 
			
		||||
		err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
 | 
			
		||||
@@ -414,7 +407,6 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
	defer func(ctx context.Context) {
 | 
			
		||||
		// c.Writer.Flush()
 | 
			
		||||
		go func() {
 | 
			
		||||
			if consumeQuota {
 | 
			
		||||
			quota := 0
 | 
			
		||||
			completionRatio := common.GetCompletionRatio(textRequest.Model)
 | 
			
		||||
			promptTokens = textResponse.Usage.PromptTokens
 | 
			
		||||
@@ -444,7 +436,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
				model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
 | 
			
		||||
				model.UpdateChannelUsedQuota(channelId, quota)
 | 
			
		||||
			}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}()
 | 
			
		||||
	}(c.Request.Context())
 | 
			
		||||
	switch apiType {
 | 
			
		||||
@@ -458,7 +450,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			err, usage := openaiHandler(c, resp, consumeQuota, promptTokens, textRequest.Model)
 | 
			
		||||
			err, usage := openaiHandler(c, resp, promptTokens, textRequest.Model)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -145,6 +145,19 @@ func shouldDisableChannel(err *OpenAIError, statusCode int) bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shouldEnableChannel(err error, openAIErr *OpenAIError) bool {
 | 
			
		||||
	if !common.AutomaticEnableChannelEnabled {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if openAIErr != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setEventStreamHeaders(c *gin.Context) {
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "text/event-stream")
 | 
			
		||||
	c.Writer.Header().Set("Cache-Control", "no-cache")
 | 
			
		||||
@@ -191,12 +204,12 @@ func getFullRequestURL(baseURL string, requestURL string, channelType int) strin
 | 
			
		||||
			fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/openai/deployments"))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fullRequestURL
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func postConsumeQuota(ctx context.Context, tokenId int, quota int, userId int, channelId int, modelRatio float64, groupRatio float64, modelName string, tokenName string) {
 | 
			
		||||
	err := model.PostConsumeTokenQuota(tokenId, quota)
 | 
			
		||||
func postConsumeQuota(ctx context.Context, tokenId int, quotaDelta int, totalQuota int, userId int, channelId int, modelRatio float64, groupRatio float64, modelName string, tokenName string) {
 | 
			
		||||
	// quotaDelta is remaining quota to be consumed
 | 
			
		||||
	err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		common.SysError("error consuming token remain quota: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
@@ -204,10 +217,23 @@ func postConsumeQuota(ctx context.Context, tokenId int, quota int, userId int, c
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		common.SysError("error update user quota cache: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	if quota != 0 {
 | 
			
		||||
	// totalQuota is total quota consumed
 | 
			
		||||
	if totalQuota != 0 {
 | 
			
		||||
		logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
 | 
			
		||||
		model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent)
 | 
			
		||||
		model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
 | 
			
		||||
		model.UpdateChannelUsedQuota(channelId, quota)
 | 
			
		||||
		model.RecordConsumeLog(ctx, userId, channelId, totalQuota, 0, modelName, tokenName, totalQuota, logContent)
 | 
			
		||||
		model.UpdateUserUsedQuotaAndRequestCount(userId, totalQuota)
 | 
			
		||||
		model.UpdateChannelUsedQuota(channelId, totalQuota)
 | 
			
		||||
	}
 | 
			
		||||
	if totalQuota <= 0 {
 | 
			
		||||
		common.LogError(ctx, fmt.Sprintf("totalQuota consumed is %d, something is wrong", totalQuota))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAPIVersion(c *gin.Context) string {
 | 
			
		||||
	query := c.Request.URL.Query()
 | 
			
		||||
	apiVersion := query.Get("api-version")
 | 
			
		||||
	if apiVersion == "" {
 | 
			
		||||
		apiVersion = c.GetString("api_version")
 | 
			
		||||
	}
 | 
			
		||||
	return apiVersion
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -133,12 +133,12 @@ type TextRequest struct {
 | 
			
		||||
type ImageRequest struct {
 | 
			
		||||
	Model          string `json:"model"`
 | 
			
		||||
	Prompt         string `json:"prompt" binding:"required"`
 | 
			
		||||
	N              int    `json:"n"`
 | 
			
		||||
	Size           string `json:"size"`
 | 
			
		||||
	Quality        string `json:"quality"`
 | 
			
		||||
	ResponseFormat string `json:"response_format"`
 | 
			
		||||
	Style          string `json:"style"`
 | 
			
		||||
	User           string `json:"user"`
 | 
			
		||||
	N              int    `json:"n,omitempty"`
 | 
			
		||||
	Size           string `json:"size,omitempty"`
 | 
			
		||||
	Quality        string `json:"quality,omitempty"`
 | 
			
		||||
	ResponseFormat string `json:"response_format,omitempty"`
 | 
			
		||||
	Style          string `json:"style,omitempty"`
 | 
			
		||||
	User           string `json:"user,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type WhisperResponse struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -119,6 +119,7 @@
 | 
			
		||||
  " 年 ": " y ",
 | 
			
		||||
  "未测试": "Not tested",
 | 
			
		||||
  "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.",
 | 
			
		||||
  "已成功开始测试所有通道,请刷新页面查看结果。": "All channels have been successfully tested, please refresh the page to view the results.",
 | 
			
		||||
  "已成功开始测试所有已启用通道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.",
 | 
			
		||||
  "通道 ${name} 余额更新成功!": "Channel ${name} balance updated successfully!",
 | 
			
		||||
  "已更新完毕所有已启用通道余额!": "The balance of all enabled channels has been updated!",
 | 
			
		||||
@@ -139,6 +140,7 @@
 | 
			
		||||
  "启用": "Enable",
 | 
			
		||||
  "编辑": "Edit",
 | 
			
		||||
  "添加新的渠道": "Add a new channel",
 | 
			
		||||
  "测试所有通道": "Test all channels",
 | 
			
		||||
  "测试所有已启用通道": "Test all enabled channels",
 | 
			
		||||
  "更新所有已启用通道余额": "Update the balance of all enabled channels",
 | 
			
		||||
  "刷新": "Refresh",
 | 
			
		||||
 
 | 
			
		||||
@@ -106,12 +106,6 @@ func TokenAuth() func(c *gin.Context) {
 | 
			
		||||
		c.Set("id", token.UserId)
 | 
			
		||||
		c.Set("token_id", token.Id)
 | 
			
		||||
		c.Set("token_name", token.Name)
 | 
			
		||||
		requestURL := c.Request.URL.String()
 | 
			
		||||
		consumeQuota := true
 | 
			
		||||
		if strings.HasPrefix(requestURL, "/v1/models") {
 | 
			
		||||
			consumeQuota = false
 | 
			
		||||
		}
 | 
			
		||||
		c.Set("consume_quota", consumeQuota)
 | 
			
		||||
		if len(parts) > 1 {
 | 
			
		||||
			if model.IsAdmin(token.UserId) {
 | 
			
		||||
				c.Set("channelId", parts[1])
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ func InitOptionMap() {
 | 
			
		||||
	common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
 | 
			
		||||
	common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
 | 
			
		||||
	common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
 | 
			
		||||
	common.OptionMap["AutomaticEnableChannelEnabled"] = strconv.FormatBool(common.AutomaticEnableChannelEnabled)
 | 
			
		||||
	common.OptionMap["ApproximateTokenEnabled"] = strconv.FormatBool(common.ApproximateTokenEnabled)
 | 
			
		||||
	common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
 | 
			
		||||
	common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
 | 
			
		||||
@@ -147,6 +148,8 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
			common.EmailDomainRestrictionEnabled = boolValue
 | 
			
		||||
		case "AutomaticDisableChannelEnabled":
 | 
			
		||||
			common.AutomaticDisableChannelEnabled = boolValue
 | 
			
		||||
		case "AutomaticEnableChannelEnabled":
 | 
			
		||||
			common.AutomaticEnableChannelEnabled = boolValue
 | 
			
		||||
		case "ApproximateTokenEnabled":
 | 
			
		||||
			common.ApproximateTokenEnabled = boolValue
 | 
			
		||||
		case "LogConsumeEnabled":
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								pull_request_template.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								pull_request_template.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
close #issue_number
 | 
			
		||||
 | 
			
		||||
我已确认该 PR 已自测通过,相关截图如下:
 | 
			
		||||
@@ -234,7 +234,7 @@ const ChannelsTable = () => {
 | 
			
		||||
    const res = await API.get(`/api/channel/test`);
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      showInfo('已成功开始测试所有已启用通道,请刷新页面查看结果。');
 | 
			
		||||
      showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ const OperationSetting = () => {
 | 
			
		||||
    ChatLink: '',
 | 
			
		||||
    QuotaPerUnit: 0,
 | 
			
		||||
    AutomaticDisableChannelEnabled: '',
 | 
			
		||||
    AutomaticEnableChannelEnabled: '',
 | 
			
		||||
    ChannelDisableThreshold: 0,
 | 
			
		||||
    LogConsumeEnabled: '',
 | 
			
		||||
    DisplayInCurrencyEnabled: '',
 | 
			
		||||
@@ -269,6 +270,12 @@ const OperationSetting = () => {
 | 
			
		||||
              name='AutomaticDisableChannelEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
                checked={inputs.AutomaticEnableChannelEnabled === 'true'}
 | 
			
		||||
                label='成功时自动启用通道'
 | 
			
		||||
                name='AutomaticEnableChannelEnabled'
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('monitor').then();
 | 
			
		||||
 
 | 
			
		||||
@@ -60,7 +60,7 @@ const EditChannel = () => {
 | 
			
		||||
      let localModels = [];
 | 
			
		||||
      switch (value) {
 | 
			
		||||
        case 14:
 | 
			
		||||
          localModels = ['claude-instant-1', 'claude-2'];
 | 
			
		||||
          localModels = ['claude-instant-1', 'claude-2', 'claude-2.0', 'claude-2.1'];
 | 
			
		||||
          break;
 | 
			
		||||
        case 11:
 | 
			
		||||
          localModels = ['PaLM-2'];
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user