Compare commits

...

11 Commits

Author SHA1 Message Date
JustSong
0941e294bf feat: support model remap now 2023-06-27 13:42:45 +08:00
JustSong
431d505f79 refactor: do not use redis to store session 2023-06-26 16:10:59 +08:00
mrhaoji
f0dc7f3f06 fix: InitChannelCache does not filter disabled channels (#201)
* chore: Show the HTTP status code in the test_time script to determine the success or failure of the request.

* fix: InitChannelCache does not filter disabled channels

* chore: do not hardcode

---------

Co-authored-by: JustSong <songquanpeng@foxmail.com>
2023-06-25 23:14:15 +08:00
mrhaoji
99fed1f850 chore: show the HTTP status code in the test_time script to determine the success or failure of the request (#200) 2023-06-25 22:58:16 +08:00
JustSong
4dc5388a80 chore: do not show completion ratio anymore 2023-06-25 20:29:42 +08:00
JustSong
f81f4c60b2 docs: update README 2023-06-25 15:14:52 +08:00
JustSong
c613d8b6b2 docs: update README 2023-06-25 15:14:09 +08:00
JustSong
7adac1c09c chore: update default ratio for text-embedding-ada-002 2023-06-25 12:07:42 +08:00
mrhaoji
6f05128368 chore: show equivalent amount next to remaining quota in the user editing page (#198) 2023-06-25 11:54:05 +08:00
JustSong
9b178a28a3 feat: support /v1/edits now (close #196) 2023-06-25 11:46:23 +08:00
JustSong
4a6a7f4635 chore: update the number that representing the unlimited quota 2023-06-25 10:52:46 +08:00
14 changed files with 110 additions and 40 deletions

View File

@@ -10,7 +10,7 @@
# One API # One API
_✨ The all-in-one OpenAI interface, integrates various API access methods, ready to use ✨_ _✨ An OpenAI key management & redistribution system, easy to deploy & use ✨_
</div> </div>

View File

@@ -12,14 +12,16 @@ total_time=0
times=() times=()
for ((i=1; i<=count; i++)); do for ((i=1; i<=count; i++)); do
result=$(curl -o /dev/null -s -w %{time_total}\\n \ result=$(curl -o /dev/null -s -w "%{http_code} %{time_total}\\n" \
https://"$domain"/v1/chat/completions \ https://"$domain"/v1/chat/completions \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer $key" \ -H "Authorization: Bearer $key" \
-d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "gpt-3.5-turbo", "stream": false, "max_tokens": 1}') -d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "gpt-3.5-turbo", "stream": false, "max_tokens": 1}')
echo "$result" http_code=$(echo "$result" | awk '{print $1}')
total_time=$(bc <<< "$total_time + $result") time=$(echo "$result" | awk '{print $2}')
times+=("$result") echo "HTTP status code: $http_code, Time taken: $time"
total_time=$(bc <<< "$total_time + $time")
times+=("$time")
done done
average_time=$(echo "scale=4; $total_time / $count" | bc) average_time=$(echo "scale=4; $total_time / $count" | bc)

View File

@@ -31,7 +31,7 @@ var ModelRatio = map[string]float64{
"curie": 10, "curie": 10,
"babbage": 10, "babbage": 10,
"ada": 10, "ada": 10,
"text-embedding-ada-002": 0.2, "text-embedding-ada-002": 0.05,
"text-search-ada-doc-001": 10, "text-search-ada-doc-001": 10,
"text-moderation-stable": 0.1, "text-moderation-stable": 0.1,
"text-moderation-latest": 0.1, "text-moderation-latest": 0.1,

View File

@@ -33,7 +33,7 @@ func GetSubscription(c *gin.Context) {
amount /= common.QuotaPerUnit amount /= common.QuotaPerUnit
} }
if token != nil && token.UnlimitedQuota { if token != nil && token.UnlimitedQuota {
amount = 99999999.9999 amount = 100000000
} }
subscription := OpenAISubscriptionResponse{ subscription := OpenAISubscriptionResponse{
Object: "billing_subscription", Object: "billing_subscription",

View File

@@ -224,6 +224,24 @@ func init() {
Root: "text-moderation-stable", Root: "text-moderation-stable",
Parent: nil, Parent: nil,
}, },
{
Id: "text-davinci-edit-001",
Object: "model",
Created: 1677649963,
OwnedBy: "openai",
Permission: permission,
Root: "text-davinci-edit-001",
Parent: nil,
},
{
Id: "code-davinci-edit-001",
Object: "model",
Created: 1677649963,
OwnedBy: "openai",
Permission: permission,
Root: "code-davinci-edit-001",
Parent: nil,
},
} }
openAIModelsMap = make(map[string]OpenAIModels) openAIModelsMap = make(map[string]OpenAIModels)
for _, model := range openAIModels { for _, model := range openAIModels {

View File

@@ -27,7 +27,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest) return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest)
} }
} }
if relayMode == RelayModeModeration && textRequest.Model == "" { if relayMode == RelayModeModerations && textRequest.Model == "" {
textRequest.Model = "text-moderation-latest" textRequest.Model = "text-moderation-latest"
} }
// request validation // request validation
@@ -37,16 +37,34 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
switch relayMode { switch relayMode {
case RelayModeCompletions: case RelayModeCompletions:
if textRequest.Prompt == "" { if textRequest.Prompt == "" {
return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest) return errorWrapper(errors.New("field prompt is required"), "required_field_missing", http.StatusBadRequest)
} }
case RelayModeChatCompletions: case RelayModeChatCompletions:
if len(textRequest.Messages) == 0 { if textRequest.Messages == nil || len(textRequest.Messages) == 0 {
return errorWrapper(errors.New("messages is required"), "required_field_missing", http.StatusBadRequest) return errorWrapper(errors.New("field messages is required"), "required_field_missing", http.StatusBadRequest)
} }
case RelayModeEmbeddings: case RelayModeEmbeddings:
case RelayModeModeration: case RelayModeModerations:
if textRequest.Input == "" { if textRequest.Input == "" {
return errorWrapper(errors.New("input is required"), "required_field_missing", http.StatusBadRequest) return errorWrapper(errors.New("field input is required"), "required_field_missing", http.StatusBadRequest)
}
case RelayModeEdits:
if textRequest.Instruction == "" {
return errorWrapper(errors.New("field instruction is required"), "required_field_missing", http.StatusBadRequest)
}
}
// map model name
modelMapping := c.GetString("model_mapping")
isModelMapped := false
if modelMapping != "" {
modelMap := make(map[string]string)
err := json.Unmarshal([]byte(modelMapping), &modelMap)
if err != nil {
return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
}
if modelMap[textRequest.Model] != "" {
textRequest.Model = modelMap[textRequest.Model]
isModelMapped = true
} }
} }
baseURL := common.ChannelBaseURLs[channelType] baseURL := common.ChannelBaseURLs[channelType]
@@ -84,7 +102,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model) promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model)
case RelayModeCompletions: case RelayModeCompletions:
promptTokens = countTokenInput(textRequest.Prompt, textRequest.Model) promptTokens = countTokenInput(textRequest.Prompt, textRequest.Model)
case RelayModeModeration: case RelayModeModerations:
promptTokens = countTokenInput(textRequest.Input, textRequest.Model) promptTokens = countTokenInput(textRequest.Input, textRequest.Model)
} }
preConsumedTokens := common.PreConsumedQuota preConsumedTokens := common.PreConsumedQuota
@@ -110,7 +128,17 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden) return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
} }
} }
req, err := http.NewRequest(c.Request.Method, fullRequestURL, c.Request.Body) var requestBody io.Reader
if isModelMapped {
jsonStr, err := json.Marshal(textRequest)
if err != nil {
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(jsonStr)
} else {
requestBody = c.Request.Body
}
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
if err != nil { if err != nil {
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError) return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
} }
@@ -144,7 +172,10 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
defer func() { defer func() {
if consumeQuota { if consumeQuota {
quota := 0 quota := 0
completionRatio := 1.333333 // default for gpt-3 completionRatio := 1.0
if strings.HasPrefix(textRequest.Model, "gpt-3.5") {
completionRatio = 1.333333
}
if strings.HasPrefix(textRequest.Model, "gpt-4") { if strings.HasPrefix(textRequest.Model, "gpt-4") {
completionRatio = 2 completionRatio = 2
} }

View File

@@ -19,22 +19,24 @@ const (
RelayModeChatCompletions RelayModeChatCompletions
RelayModeCompletions RelayModeCompletions
RelayModeEmbeddings RelayModeEmbeddings
RelayModeModeration RelayModeModerations
RelayModeImagesGenerations RelayModeImagesGenerations
RelayModeEdits
) )
// https://platform.openai.com/docs/api-reference/chat // https://platform.openai.com/docs/api-reference/chat
type GeneralOpenAIRequest struct { type GeneralOpenAIRequest struct {
Model string `json:"model"` Model string `json:"model,omitempty"`
Messages []Message `json:"messages"` Messages []Message `json:"messages,omitempty"`
Prompt any `json:"prompt"` Prompt any `json:"prompt,omitempty"`
Stream bool `json:"stream"` Stream bool `json:"stream,omitempty"`
MaxTokens int `json:"max_tokens"` MaxTokens int `json:"max_tokens,omitempty"`
Temperature float64 `json:"temperature"` Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p"` TopP float64 `json:"top_p,omitempty"`
N int `json:"n"` N int `json:"n,omitempty"`
Input any `json:"input"` Input any `json:"input,omitempty"`
Instruction string `json:"instruction,omitempty"`
} }
type ChatRequest struct { type ChatRequest struct {
@@ -99,9 +101,11 @@ func Relay(c *gin.Context) {
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") { } else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") {
relayMode = RelayModeEmbeddings relayMode = RelayModeEmbeddings
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") { } else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
relayMode = RelayModeModeration relayMode = RelayModeModerations
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") { } else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") {
relayMode = RelayModeImagesGenerations relayMode = RelayModeImagesGenerations
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/edits") {
relayMode = RelayModeEdits
} }
var err *OpenAIErrorWithStatusCode var err *OpenAIErrorWithStatusCode
switch relayMode { switch relayMode {

11
main.go
View File

@@ -4,7 +4,6 @@ import (
"embed" "embed"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"one-api/common" "one-api/common"
"one-api/controller" "one-api/controller"
@@ -82,14 +81,8 @@ func main() {
server.Use(middleware.CORS()) server.Use(middleware.CORS())
// Initialize session store // Initialize session store
if common.RedisEnabled { store := cookie.NewStore([]byte(common.SessionSecret))
opt := common.ParseRedisOption() server.Use(sessions.Sessions("session", store))
store, _ := redis.NewStore(opt.MinIdleConns, opt.Network, opt.Addr, opt.Password, []byte(common.SessionSecret))
server.Use(sessions.Sessions("session", store))
} else {
store := cookie.NewStore([]byte(common.SessionSecret))
server.Use(sessions.Sessions("session", store))
}
router.SetRouter(server, buildFS, indexPage) router.SetRouter(server, buildFS, indexPage)
var port = os.Getenv("PORT") var port = os.Getenv("PORT")

View File

@@ -88,6 +88,7 @@ func Distribute() func(c *gin.Context) {
c.Set("channel", channel.Type) c.Set("channel", channel.Type)
c.Set("channel_id", channel.Id) c.Set("channel_id", channel.Id)
c.Set("channel_name", channel.Name) c.Set("channel_name", channel.Name)
c.Set("model_mapping", channel.ModelMapping)
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
c.Set("base_url", channel.BaseURL) c.Set("base_url", channel.BaseURL)
if channel.Type == common.ChannelTypeAzure { if channel.Type == common.ChannelTypeAzure {

View File

@@ -108,7 +108,7 @@ var channelSyncLock sync.RWMutex
func InitChannelCache() { func InitChannelCache() {
newChannelId2channel := make(map[int]*Channel) newChannelId2channel := make(map[int]*Channel)
var channels []*Channel var channels []*Channel
DB.Find(&channels) DB.Where("status = ?", common.ChannelStatusEnabled).Find(&channels)
for _, channel := range channels { for _, channel := range channels {
newChannelId2channel[channel.Id] = channel newChannelId2channel[channel.Id] = channel
} }

View File

@@ -22,6 +22,7 @@ type Channel struct {
Models string `json:"models"` Models string `json:"models"`
Group string `json:"group" gorm:"type:varchar(32);default:'default'"` Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
ModelMapping string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
} }
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {

View File

@@ -19,7 +19,7 @@ func SetRelayRouter(router *gin.Engine) {
{ {
relayV1Router.POST("/completions", controller.Relay) relayV1Router.POST("/completions", controller.Relay)
relayV1Router.POST("/chat/completions", controller.Relay) relayV1Router.POST("/chat/completions", controller.Relay)
relayV1Router.POST("/edits", controller.RelayNotImplemented) relayV1Router.POST("/edits", controller.Relay)
relayV1Router.POST("/images/generations", controller.RelayNotImplemented) relayV1Router.POST("/images/generations", controller.RelayNotImplemented)
relayV1Router.POST("/images/edits", controller.RelayNotImplemented) relayV1Router.POST("/images/edits", controller.RelayNotImplemented)
relayV1Router.POST("/images/variations", controller.RelayNotImplemented) relayV1Router.POST("/images/variations", controller.RelayNotImplemented)

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react'; import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { API, showError, showInfo, showSuccess } from '../../helpers'; import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
import { CHANNEL_OPTIONS } from '../../constants'; import { CHANNEL_OPTIONS } from '../../constants';
const EditChannel = () => { const EditChannel = () => {
@@ -15,6 +15,7 @@ const EditChannel = () => {
key: '', key: '',
base_url: '', base_url: '',
other: '', other: '',
model_mapping:'',
models: [], models: [],
groups: ['default'] groups: ['default']
}; };
@@ -42,6 +43,9 @@ const EditChannel = () => {
} else { } else {
data.groups = data.group.split(','); data.groups = data.group.split(',');
} }
if (data.model_mapping !== '') {
data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
}
setInputs(data); setInputs(data);
} else { } else {
showError(message); showError(message);
@@ -94,6 +98,10 @@ const EditChannel = () => {
showInfo('请至少选择一个模型!'); showInfo('请至少选择一个模型!');
return; return;
} }
if (inputs.model_mapping !== "" && !verifyJSON(inputs.model_mapping)) {
showInfo('模型映射必须是合法的 JSON 格式!');
return;
}
let localInputs = inputs; let localInputs = inputs;
if (localInputs.base_url.endsWith('/')) { if (localInputs.base_url.endsWith('/')) {
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
@@ -246,6 +254,17 @@ const EditChannel = () => {
handleInputChange(null, { name: 'models', value: [] }); handleInputChange(null, { name: 'models', value: [] });
}}>清除所有模型</Button> }}>清除所有模型</Button>
</div> </div>
<Form.Field>
<Form.TextArea
label='模型映射'
placeholder={'为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称'}
name='model_mapping'
onChange={handleInputChange}
value={inputs.model_mapping}
style={{ minHeight: 100, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete='new-password'
/>
</Form.Field>
{ {
batch ? <Form.Field> batch ? <Form.Field>
<Form.TextArea <Form.TextArea

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Segment } from 'semantic-ui-react'; import { Button, Form, Header, Segment } from 'semantic-ui-react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { API, showError, showSuccess } from '../../helpers'; import { API, showError, showSuccess } from '../../helpers';
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
const EditUser = () => { const EditUser = () => {
const params = useParams(); const params = useParams();
@@ -134,7 +135,7 @@ const EditUser = () => {
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='剩余额度' label={`剩余额度${renderQuotaWithPrompt(quota)}`}
name='quota' name='quota'
placeholder={'请输入新的剩余额度'} placeholder={'请输入新的剩余额度'}
onChange={handleInputChange} onChange={handleInputChange}