mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 13:53:41 +08:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			v0.2.6-alp
			...
			v0.2.6-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | cc3072c4df | ||
|  | bffee4e91d | ||
|  | 79dc53ff0d | ||
|  | 68e53d3e10 | ||
|  | d267211ee7 | ||
|  | 570b3bc71c | ||
|  | 225176aae9 | 
| @@ -11,6 +11,7 @@ import ( | |||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -19,7 +20,7 @@ func GetAllChannels(c *gin.Context) { | |||||||
| 	if p < 0 { | 	if p < 0 { | ||||||
| 		p = 0 | 		p = 0 | ||||||
| 	} | 	} | ||||||
| 	channels, err := model.GetAllChannels(p*common.ItemsPerPage, common.ItemsPerPage) | 	channels, err := model.GetAllChannels(p*common.ItemsPerPage, common.ItemsPerPage, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| 			"success": false, | 			"success": false, | ||||||
| @@ -89,7 +90,6 @@ func AddChannel(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	channel.CreatedTime = common.GetTimestamp() | 	channel.CreatedTime = common.GetTimestamp() | ||||||
| 	channel.AccessedTime = common.GetTimestamp() |  | ||||||
| 	keys := strings.Split(channel.Key, "\n") | 	keys := strings.Split(channel.Key, "\n") | ||||||
| 	channels := make([]model.Channel, 0) | 	channels := make([]model.Channel, 0) | ||||||
| 	for _, key := range keys { | 	for _, key := range keys { | ||||||
| @@ -207,6 +207,19 @@ func testChannel(channel *model.Channel, request *ChatRequest) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func buildTestRequest(c *gin.Context) *ChatRequest { | ||||||
|  | 	model_ := c.Query("model") | ||||||
|  | 	testRequest := &ChatRequest{ | ||||||
|  | 		Model: model_, | ||||||
|  | 	} | ||||||
|  | 	testMessage := Message{ | ||||||
|  | 		Role:    "user", | ||||||
|  | 		Content: "echo hi", | ||||||
|  | 	} | ||||||
|  | 	testRequest.Messages = append(testRequest.Messages, testMessage) | ||||||
|  | 	return testRequest | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestChannel(c *gin.Context) { | func TestChannel(c *gin.Context) { | ||||||
| 	id, err := strconv.Atoi(c.Param("id")) | 	id, err := strconv.Atoi(c.Param("id")) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -224,19 +237,13 @@ func TestChannel(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	model_ := c.Query("model") | 	testRequest := buildTestRequest(c) | ||||||
| 	chatRequest := &ChatRequest{ |  | ||||||
| 		Model: model_, |  | ||||||
| 	} |  | ||||||
| 	testMessage := Message{ |  | ||||||
| 		Role:    "user", |  | ||||||
| 		Content: "echo hi", |  | ||||||
| 	} |  | ||||||
| 	chatRequest.Messages = append(chatRequest.Messages, testMessage) |  | ||||||
| 	tik := time.Now() | 	tik := time.Now() | ||||||
| 	err = testChannel(channel, chatRequest) | 	err = testChannel(channel, testRequest) | ||||||
| 	tok := time.Now() | 	tok := time.Now() | ||||||
| 	consumedTime := float64(tok.Sub(tik).Milliseconds()) / 1000.0 | 	milliseconds := tok.Sub(tik).Milliseconds() | ||||||
|  | 	go channel.UpdateResponseTime(milliseconds) | ||||||
|  | 	consumedTime := float64(milliseconds) / 1000.0 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| 			"success": false, | 			"success": false, | ||||||
| @@ -252,3 +259,70 @@ func TestChannel(c *gin.Context) { | |||||||
| 	}) | 	}) | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var testAllChannelsLock sync.Mutex | ||||||
|  |  | ||||||
|  | func testAllChannels(c *gin.Context) error { | ||||||
|  | 	ok := testAllChannelsLock.TryLock() | ||||||
|  | 	if !ok { | ||||||
|  | 		return errors.New("测试已在运行") | ||||||
|  | 	} | ||||||
|  | 	defer testAllChannelsLock.Unlock() | ||||||
|  | 	channels, err := model.GetAllChannels(0, 0, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	testRequest := buildTestRequest(c) | ||||||
|  | 	var disableThreshold int64 = 5000 // TODO: make it configurable | ||||||
|  | 	email := model.GetRootUserEmail() | ||||||
|  | 	go func() { | ||||||
|  | 		for _, channel := range channels { | ||||||
|  | 			if channel.Status != common.ChannelStatusEnabled { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			tik := time.Now() | ||||||
|  | 			err := testChannel(channel, testRequest) | ||||||
|  | 			tok := time.Now() | ||||||
|  | 			milliseconds := tok.Sub(tik).Milliseconds() | ||||||
|  | 			if err != nil || milliseconds > disableThreshold { | ||||||
|  | 				if milliseconds > disableThreshold { | ||||||
|  | 					err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)) | ||||||
|  | 				} | ||||||
|  | 				// disable & notify | ||||||
|  | 				channel.UpdateStatus(common.ChannelStatusDisabled) | ||||||
|  | 				subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channel.Name, channel.Id) | ||||||
|  | 				content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channel.Name, channel.Id, err.Error()) | ||||||
|  | 				err = common.SendEmail(subject, email, content) | ||||||
|  | 				if err != nil { | ||||||
|  | 					common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			channel.UpdateResponseTime(milliseconds) | ||||||
|  | 		} | ||||||
|  | 		err := common.SendEmail("通道测试完成", email, "通道测试完成") | ||||||
|  | 		if err != nil { | ||||||
|  | 			common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAllChannels(c *gin.Context) { | ||||||
|  | 	err := testAllChannels(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 		"success": true, | ||||||
|  | 		"message": "", | ||||||
|  | 	}) | ||||||
|  | 	return | ||||||
|  | } | ||||||
|   | |||||||
| @@ -117,6 +117,9 @@ func relayHelper(c *gin.Context) error { | |||||||
| 		task := strings.TrimPrefix(requestURL, "/v1/") | 		task := strings.TrimPrefix(requestURL, "/v1/") | ||||||
| 		model_ := textRequest.Model | 		model_ := textRequest.Model | ||||||
| 		model_ = strings.Replace(model_, ".", "", -1) | 		model_ = strings.Replace(model_, ".", "", -1) | ||||||
|  | 		// https://github.com/songquanpeng/one-api/issues/67 | ||||||
|  | 		model_ = strings.TrimSuffix(model_, "-0301") | ||||||
|  | 		model_ = strings.TrimSuffix(model_, "-0314") | ||||||
| 		fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task) | 		fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task) | ||||||
| 	} | 	} | ||||||
| 	req, err := http.NewRequest(c.Request.Method, fullRequestURL, c.Request.Body) | 	req, err := http.NewRequest(c.Request.Method, fullRequestURL, c.Request.Body) | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ func TokenAuth() func(c *gin.Context) { | |||||||
| 		c.Set("token_id", token.Id) | 		c.Set("token_id", token.Id) | ||||||
| 		requestURL := c.Request.URL.String() | 		requestURL := c.Request.URL.String() | ||||||
| 		consumeQuota := !token.UnlimitedQuota | 		consumeQuota := !token.UnlimitedQuota | ||||||
| 		if strings.HasPrefix(requestURL, "/models") { | 		if strings.HasPrefix(requestURL, "/v1/models") { | ||||||
| 			consumeQuota = false | 			consumeQuota = false | ||||||
| 		} | 		} | ||||||
| 		c.Set("consume_quota", consumeQuota) | 		c.Set("consume_quota", consumeQuota) | ||||||
|   | |||||||
| @@ -13,15 +13,20 @@ type Channel struct { | |||||||
| 	Name         string `json:"name" gorm:"index"` | 	Name         string `json:"name" gorm:"index"` | ||||||
| 	Weight       int    `json:"weight"` | 	Weight       int    `json:"weight"` | ||||||
| 	CreatedTime  int64  `json:"created_time" gorm:"bigint"` | 	CreatedTime  int64  `json:"created_time" gorm:"bigint"` | ||||||
| 	AccessedTime int64  `json:"accessed_time" gorm:"bigint"` | 	TestTime     int64  `json:"test_time" gorm:"bigint"` | ||||||
|  | 	ResponseTime int    `json:"response_time"` // in milliseconds | ||||||
| 	BaseURL      string `json:"base_url" gorm:"column:base_url"` | 	BaseURL      string `json:"base_url" gorm:"column:base_url"` | ||||||
| 	Other        string `json:"other"` | 	Other        string `json:"other"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetAllChannels(startIdx int, num int) ([]*Channel, error) { | func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) { | ||||||
| 	var channels []*Channel | 	var channels []*Channel | ||||||
| 	var err error | 	var err error | ||||||
| 	err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error | 	if selectAll { | ||||||
|  | 		err = DB.Order("id desc").Find(&channels).Error | ||||||
|  | 	} else { | ||||||
|  | 		err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error | ||||||
|  | 	} | ||||||
| 	return channels, err | 	return channels, err | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -71,6 +76,23 @@ func (channel *Channel) Update() error { | |||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (channel *Channel) UpdateResponseTime(responseTime int64) { | ||||||
|  | 	err := DB.Model(channel).Select("response_time", "test_time").Updates(Channel{ | ||||||
|  | 		TestTime:     common.GetTimestamp(), | ||||||
|  | 		ResponseTime: int(responseTime), | ||||||
|  | 	}).Error | ||||||
|  | 	if err != nil { | ||||||
|  | 		common.SysError("failed to update response time: " + err.Error()) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (channel *Channel) UpdateStatus(status int) { | ||||||
|  | 	err := DB.Model(channel).Update("status", status).Error | ||||||
|  | 	if err != nil { | ||||||
|  | 		common.SysError("failed to update response time: " + err.Error()) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func (channel *Channel) Delete() error { | func (channel *Channel) Delete() error { | ||||||
| 	var err error | 	var err error | ||||||
| 	err = DB.Delete(channel).Error | 	err = DB.Delete(channel).Error | ||||||
|   | |||||||
| @@ -234,3 +234,8 @@ func DecreaseUserQuota(id int, quota int) (err error) { | |||||||
| 	err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error | 	err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func GetRootUserEmail() (email string) { | ||||||
|  | 	DB.Model(&User{}).Where("role = ?", common.RoleRootUser).Select("email").Find(&email) | ||||||
|  | 	return email | ||||||
|  | } | ||||||
|   | |||||||
| @@ -63,6 +63,7 @@ func SetApiRouter(router *gin.Engine) { | |||||||
| 			channelRoute.GET("/", controller.GetAllChannels) | 			channelRoute.GET("/", controller.GetAllChannels) | ||||||
| 			channelRoute.GET("/search", controller.SearchChannels) | 			channelRoute.GET("/search", controller.SearchChannels) | ||||||
| 			channelRoute.GET("/:id", controller.GetChannel) | 			channelRoute.GET("/:id", controller.GetChannel) | ||||||
|  | 			channelRoute.GET("/test", controller.TestAllChannels) | ||||||
| 			channelRoute.GET("/test/:id", controller.TestChannel) | 			channelRoute.GET("/test/:id", controller.TestChannel) | ||||||
| 			channelRoute.POST("/", controller.AddChannel) | 			channelRoute.POST("/", controller.AddChannel) | ||||||
| 			channelRoute.PUT("/", controller.UpdateChannel) | 			channelRoute.PUT("/", controller.UpdateChannel) | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; | import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; | ||||||
| import { Link } from 'react-router-dom'; | import { Link } from 'react-router-dom'; | ||||||
| import { API, copy, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; | import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; | ||||||
|  |  | ||||||
| import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; | import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; | ||||||
|  |  | ||||||
| @@ -120,6 +120,22 @@ const ChannelsTable = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const renderResponseTime = (responseTime) => { | ||||||
|  |     let time = responseTime / 1000; | ||||||
|  |     time = time.toFixed(2) + " 秒"; | ||||||
|  |     if (responseTime === 0) { | ||||||
|  |       return <Label basic color='grey'>未测试</Label>; | ||||||
|  |     } else if (responseTime <= 1000) { | ||||||
|  |       return <Label basic color='green'>{time}</Label>; | ||||||
|  |     } else if (responseTime <= 3000) { | ||||||
|  |       return <Label basic color='olive'>{time}</Label>; | ||||||
|  |     } else if (responseTime <= 5000) { | ||||||
|  |       return <Label basic color='yellow'>{time}</Label>; | ||||||
|  |     } else { | ||||||
|  |       return <Label basic color='red'>{time}</Label>; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const searchChannels = async () => { |   const searchChannels = async () => { | ||||||
|     if (searchKeyword === '') { |     if (searchKeyword === '') { | ||||||
|       // if keyword is blank, load files instead. |       // if keyword is blank, load files instead. | ||||||
| @@ -139,11 +155,26 @@ const ChannelsTable = () => { | |||||||
|     setSearching(false); |     setSearching(false); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const testChannel = async (id, name) => { |   const testChannel = async (id, name, idx) => { | ||||||
|     const res = await API.get(`/api/channel/test/${id}/`); |     const res = await API.get(`/api/channel/test/${id}/`); | ||||||
|     const { success, message, time } = res.data; |     const { success, message, time } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
|       showInfo(`通道 ${name} 测试成功,耗时 ${time} 秒。`); |       let newChannels = [...channels]; | ||||||
|  |       let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; | ||||||
|  |       newChannels[realIdx].response_time = time * 1000; | ||||||
|  |       newChannels[realIdx].test_time = Date.now() / 1000; | ||||||
|  |       setChannels(newChannels); | ||||||
|  |       showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); | ||||||
|  |     } else { | ||||||
|  |       showError(message); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const testAllChannels = async () => { | ||||||
|  |     const res = await API.get(`/api/channel/test`); | ||||||
|  |     const { success, message } = res.data; | ||||||
|  |     if (success) { | ||||||
|  |       showSuccess("已成功开始测试所有已启用通道,请刷新页面查看结果。"); | ||||||
|     } else { |     } else { | ||||||
|       showError(message); |       showError(message); | ||||||
|     } |     } | ||||||
| @@ -219,18 +250,18 @@ const ChannelsTable = () => { | |||||||
|             <Table.HeaderCell |             <Table.HeaderCell | ||||||
|               style={{ cursor: 'pointer' }} |               style={{ cursor: 'pointer' }} | ||||||
|               onClick={() => { |               onClick={() => { | ||||||
|                 sortChannel('created_time'); |                 sortChannel('response_time'); | ||||||
|               }} |               }} | ||||||
|             > |             > | ||||||
|               创建时间 |               响应时间 | ||||||
|             </Table.HeaderCell> |             </Table.HeaderCell> | ||||||
|             <Table.HeaderCell |             <Table.HeaderCell | ||||||
|               style={{ cursor: 'pointer' }} |               style={{ cursor: 'pointer' }} | ||||||
|               onClick={() => { |               onClick={() => { | ||||||
|                 sortChannel('accessed_time'); |                 sortChannel('test_time'); | ||||||
|               }} |               }} | ||||||
|             > |             > | ||||||
|               访问时间 |               测试时间 | ||||||
|             </Table.HeaderCell> |             </Table.HeaderCell> | ||||||
|             <Table.HeaderCell>操作</Table.HeaderCell> |             <Table.HeaderCell>操作</Table.HeaderCell> | ||||||
|           </Table.Row> |           </Table.Row> | ||||||
| @@ -250,15 +281,15 @@ const ChannelsTable = () => { | |||||||
|                   <Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell> |                   <Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell> | ||||||
|                   <Table.Cell>{renderType(channel.type)}</Table.Cell> |                   <Table.Cell>{renderType(channel.type)}</Table.Cell> | ||||||
|                   <Table.Cell>{renderStatus(channel.status)}</Table.Cell> |                   <Table.Cell>{renderStatus(channel.status)}</Table.Cell> | ||||||
|                   <Table.Cell>{renderTimestamp(channel.created_time)}</Table.Cell> |                   <Table.Cell>{renderResponseTime(channel.response_time)}</Table.Cell> | ||||||
|                   <Table.Cell>{renderTimestamp(channel.accessed_time)}</Table.Cell> |                   <Table.Cell>{channel.test_time ? renderTimestamp(channel.test_time) : "未测试"}</Table.Cell> | ||||||
|                   <Table.Cell> |                   <Table.Cell> | ||||||
|                     <div> |                     <div> | ||||||
|                       <Button |                       <Button | ||||||
|                         size={'small'} |                         size={'small'} | ||||||
|                         positive |                         positive | ||||||
|                         onClick={() => { |                         onClick={() => { | ||||||
|                           testChannel(channel.id, channel.name); |                           testChannel(channel.id, channel.name, idx); | ||||||
|                         }} |                         }} | ||||||
|                       > |                       > | ||||||
|                         测试 |                         测试 | ||||||
| @@ -314,6 +345,9 @@ const ChannelsTable = () => { | |||||||
|               <Button size='small' as={Link} to='/channel/add' loading={loading}> |               <Button size='small' as={Link} to='/channel/add' loading={loading}> | ||||||
|                 添加新的渠道 |                 添加新的渠道 | ||||||
|               </Button> |               </Button> | ||||||
|  |               <Button size='small' loading={loading} onClick={testAllChannels}> | ||||||
|  |                 测试所有已启用通道 | ||||||
|  |               </Button> | ||||||
|               <Pagination |               <Pagination | ||||||
|                 floated='right' |                 floated='right' | ||||||
|                 activePage={activePage} |                 activePage={activePage} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user