mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	Compare commits
	
		
			12 Commits
		
	
	
		
			v0.5.2-alp
			...
			v0.5.3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					efeb9a16ce | ||
| 
						 | 
					05e4f2b439 | ||
| 
						 | 
					7e058bfb9b | ||
| 
						 | 
					dfaa0183b7 | ||
| 
						 | 
					1b56becfaa | ||
| 
						 | 
					23b1c63538 | ||
| 
						 | 
					49d1a63402 | ||
| 
						 | 
					2a7b82650c | ||
| 
						 | 
					8ea7b9aae2 | ||
| 
						 | 
					5136b12612 | ||
| 
						 | 
					80a49e01a3 | ||
| 
						 | 
					8fb082ba3b | 
@@ -52,7 +52,7 @@ _✨ 標準的な OpenAI API フォーマットを通じてすべての LLM に
 | 
			
		||||
 | 
			
		||||
> **警告**: この README は ChatGPT によって翻訳されています。翻訳ミスを発見した場合は遠慮なく PR を投稿してください。
 | 
			
		||||
 | 
			
		||||
> **警告**: 英語版の Docker イメージは `justsong/one-api-ja` です。
 | 
			
		||||
> **警告**: 英語版の Docker イメージは `justsong/one-api-en` です。
 | 
			
		||||
 | 
			
		||||
> **注**: Docker からプルされた最新のイメージは、`alpha` リリースかもしれません。安定性が必要な場合は、手動でバージョンを指定してください。
 | 
			
		||||
 | 
			
		||||
@@ -89,7 +89,7 @@ _✨ 標準的な OpenAI API フォーマットを通じてすべての LLM に
 | 
			
		||||
 | 
			
		||||
## デプロイメント
 | 
			
		||||
### Docker デプロイメント
 | 
			
		||||
デプロイコマンド: `docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api-ja`。
 | 
			
		||||
デプロイコマンド: `docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api-en`。
 | 
			
		||||
 | 
			
		||||
コマンドを更新する: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrr/watchtower -cR`。
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							@@ -51,11 +51,13 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 
 | 
			
		||||
  <a href="https://iamazing.cn/page/reward">赞赏支持</a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
> **Note**:本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
 | 
			
		||||
> **Note**
 | 
			
		||||
> 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
 | 
			
		||||
> 
 | 
			
		||||
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
 | 
			
		||||
 | 
			
		||||
> **Note**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
 | 
			
		||||
 | 
			
		||||
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
 | 
			
		||||
> **Warning**
 | 
			
		||||
> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
 | 
			
		||||
 | 
			
		||||
## 功能
 | 
			
		||||
1. 支持多种大模型:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
package common
 | 
			
		||||
 | 
			
		||||
import "encoding/json"
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ModelRatio
 | 
			
		||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
 | 
			
		||||
@@ -38,8 +41,8 @@ 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,
 | 
			
		||||
	"claude-instant-1":        0.815,  // $1.63 / 1M tokens
 | 
			
		||||
	"claude-2":                5.51,   // $11.02 / 1M tokens
 | 
			
		||||
	"ERNIE-Bot":               0.8572, // ¥0.012 / 1k tokens
 | 
			
		||||
	"ERNIE-Bot-turbo":         0.5715, // ¥0.008 / 1k tokens
 | 
			
		||||
	"Embedding-V1":            0.1429, // ¥0.002 / 1k tokens
 | 
			
		||||
@@ -73,3 +76,19 @@ func GetModelRatio(name string) float64 {
 | 
			
		||||
	}
 | 
			
		||||
	return ratio
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetCompletionRatio(name string) float64 {
 | 
			
		||||
	if strings.HasPrefix(name, "gpt-3.5") {
 | 
			
		||||
		return 1.333333
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(name, "gpt-4") {
 | 
			
		||||
		return 2
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(name, "claude-instant-1") {
 | 
			
		||||
		return 3.38
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(name, "claude-2") {
 | 
			
		||||
		return 2.965517
 | 
			
		||||
	}
 | 
			
		||||
	return 1
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -177,9 +177,11 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStat
 | 
			
		||||
				common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			usage.PromptTokens += aliResponse.Usage.InputTokens
 | 
			
		||||
			usage.CompletionTokens += aliResponse.Usage.OutputTokens
 | 
			
		||||
			usage.TotalTokens += aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens
 | 
			
		||||
			if aliResponse.Usage.OutputTokens != 0 {
 | 
			
		||||
				usage.PromptTokens = aliResponse.Usage.InputTokens
 | 
			
		||||
				usage.CompletionTokens = aliResponse.Usage.OutputTokens
 | 
			
		||||
				usage.TotalTokens = aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens
 | 
			
		||||
			}
 | 
			
		||||
			response := streamResponseAli2OpenAI(&aliResponse)
 | 
			
		||||
			response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText)
 | 
			
		||||
			lastResponseText = aliResponse.Output.Text
 | 
			
		||||
 
 | 
			
		||||
@@ -215,9 +215,11 @@ func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt
 | 
			
		||||
				common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			usage.PromptTokens += baiduResponse.Usage.PromptTokens
 | 
			
		||||
			usage.CompletionTokens += baiduResponse.Usage.CompletionTokens
 | 
			
		||||
			usage.TotalTokens += baiduResponse.Usage.TotalTokens
 | 
			
		||||
			if baiduResponse.Usage.TotalTokens != 0 {
 | 
			
		||||
				usage.TotalTokens = baiduResponse.Usage.TotalTokens
 | 
			
		||||
				usage.PromptTokens = baiduResponse.Usage.PromptTokens
 | 
			
		||||
				usage.CompletionTokens = baiduResponse.Usage.TotalTokens - baiduResponse.Usage.PromptTokens
 | 
			
		||||
			}
 | 
			
		||||
			response := streamResponseBaidu2OpenAI(&baiduResponse)
 | 
			
		||||
			jsonResponse, err := json.Marshal(response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -315,6 +315,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
 | 
			
		||||
 | 
			
		||||
		if resp.StatusCode != http.StatusOK {
 | 
			
		||||
			return errorWrapper(
 | 
			
		||||
				fmt.Errorf("bad status code: %d", resp.StatusCode), "bad_status_code", resp.StatusCode)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var textResponse TextResponse
 | 
			
		||||
@@ -326,14 +331,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
		go func() {
 | 
			
		||||
			if consumeQuota {
 | 
			
		||||
				quota := 0
 | 
			
		||||
				completionRatio := 1.0
 | 
			
		||||
				if strings.HasPrefix(textRequest.Model, "gpt-3.5") {
 | 
			
		||||
					completionRatio = 1.333333
 | 
			
		||||
				}
 | 
			
		||||
				if strings.HasPrefix(textRequest.Model, "gpt-4") {
 | 
			
		||||
					completionRatio = 2
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				completionRatio := common.GetCompletionRatio(textRequest.Model)
 | 
			
		||||
				promptTokens = textResponse.Usage.PromptTokens
 | 
			
		||||
				completionTokens = textResponse.Usage.CompletionTokens
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ type XunfeiChatResponse struct {
 | 
			
		||||
	} `json:"payload"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest {
 | 
			
		||||
func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string, domain string) *XunfeiChatRequest {
 | 
			
		||||
	messages := make([]XunfeiMessage, 0, len(request.Messages))
 | 
			
		||||
	for _, message := range request.Messages {
 | 
			
		||||
		if message.Role == "system" {
 | 
			
		||||
@@ -96,7 +96,7 @@ func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *Xun
 | 
			
		||||
	}
 | 
			
		||||
	xunfeiRequest := XunfeiChatRequest{}
 | 
			
		||||
	xunfeiRequest.Header.AppId = xunfeiAppId
 | 
			
		||||
	xunfeiRequest.Parameter.Chat.Domain = "general"
 | 
			
		||||
	xunfeiRequest.Parameter.Chat.Domain = domain
 | 
			
		||||
	xunfeiRequest.Parameter.Chat.Temperature = request.Temperature
 | 
			
		||||
	xunfeiRequest.Parameter.Chat.TopK = request.N
 | 
			
		||||
	xunfeiRequest.Parameter.Chat.MaxTokens = request.MaxTokens
 | 
			
		||||
@@ -178,15 +178,28 @@ func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string {
 | 
			
		||||
 | 
			
		||||
func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var usage Usage
 | 
			
		||||
	query := c.Request.URL.Query()
 | 
			
		||||
	apiVersion := query.Get("api-version")
 | 
			
		||||
	if apiVersion == "" {
 | 
			
		||||
		apiVersion = c.GetString("api_version")
 | 
			
		||||
	}
 | 
			
		||||
	if apiVersion == "" {
 | 
			
		||||
		apiVersion = "v1.1"
 | 
			
		||||
		common.SysLog("api_version not found, use default: " + apiVersion)
 | 
			
		||||
	}
 | 
			
		||||
	domain := "general"
 | 
			
		||||
	if apiVersion == "v2.1" {
 | 
			
		||||
		domain = "generalv2"
 | 
			
		||||
	}
 | 
			
		||||
	hostUrl := fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion)
 | 
			
		||||
	d := websocket.Dialer{
 | 
			
		||||
		HandshakeTimeout: 5 * time.Second,
 | 
			
		||||
	}
 | 
			
		||||
	hostUrl := "wss://aichat.xf-yun.com/v1/chat"
 | 
			
		||||
	conn, resp, err := d.Dial(buildXunfeiAuthUrl(hostUrl, apiKey, apiSecret), nil)
 | 
			
		||||
	if err != nil || resp.StatusCode != 101 {
 | 
			
		||||
		return errorWrapper(err, "dial_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	data := requestOpenAI2Xunfei(textRequest, appId)
 | 
			
		||||
	data := requestOpenAI2Xunfei(textRequest, appId, domain)
 | 
			
		||||
	err = conn.WriteJSON(data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "write_json_failed", http.StatusInternalServerError), nil
 | 
			
		||||
 
 | 
			
		||||
@@ -520,5 +520,8 @@
 | 
			
		||||
  "代理": "Proxy",
 | 
			
		||||
  "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com",
 | 
			
		||||
  "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?",
 | 
			
		||||
  "按照如下格式输入:": "Enter in the following format:"
 | 
			
		||||
  "按照如下格式输入:": "Enter in the following format:",
 | 
			
		||||
  "模型版本": "Model version",
 | 
			
		||||
  "请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1",
 | 
			
		||||
  "点击查看": "click to view"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -107,7 +107,7 @@ func Distribute() func(c *gin.Context) {
 | 
			
		||||
		c.Set("model_mapping", channel.ModelMapping)
 | 
			
		||||
		c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
 | 
			
		||||
		c.Set("base_url", channel.BaseURL)
 | 
			
		||||
		if channel.Type == common.ChannelTypeAzure {
 | 
			
		||||
		if channel.Type == common.ChannelTypeAzure || channel.Type == common.ChannelTypeXunfei {
 | 
			
		||||
			c.Set("api_version", channel.Other)
 | 
			
		||||
		}
 | 
			
		||||
		c.Next()
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ function renderType(type) {
 | 
			
		||||
 | 
			
		||||
const LogsTable = () => {
 | 
			
		||||
  const [logs, setLogs] = useState([]);
 | 
			
		||||
  const [showStat, setShowStat] = useState(false);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [activePage, setActivePage] = useState(1);
 | 
			
		||||
  const [searchKeyword, setSearchKeyword] = useState('');
 | 
			
		||||
@@ -92,6 +93,17 @@ const LogsTable = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleEyeClick = async () => {
 | 
			
		||||
    if (!showStat) {
 | 
			
		||||
      if (isAdminUser) {
 | 
			
		||||
        await getLogStat();
 | 
			
		||||
      } else {
 | 
			
		||||
        await getLogSelfStat();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    setShowStat(!showStat);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const loadLogs = async (startIdx) => {
 | 
			
		||||
    let url = '';
 | 
			
		||||
    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
 | 
			
		||||
@@ -129,13 +141,8 @@ const LogsTable = () => {
 | 
			
		||||
 | 
			
		||||
  const refresh = async () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    setActivePage(1)
 | 
			
		||||
    setActivePage(1);
 | 
			
		||||
    await loadLogs(0);
 | 
			
		||||
    if (isAdminUser) {
 | 
			
		||||
      getLogStat().then();
 | 
			
		||||
    } else {
 | 
			
		||||
      getLogSelfStat().then();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@@ -169,7 +176,7 @@ const LogsTable = () => {
 | 
			
		||||
    if (logs.length === 0) return;
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    let sortedLogs = [...logs];
 | 
			
		||||
    if (typeof sortedLogs[0][key] === 'string'){
 | 
			
		||||
    if (typeof sortedLogs[0][key] === 'string') {
 | 
			
		||||
      sortedLogs.sort((a, b) => {
 | 
			
		||||
        return ('' + a[key]).localeCompare(b[key]);
 | 
			
		||||
      });
 | 
			
		||||
@@ -190,7 +197,12 @@ const LogsTable = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Segment>
 | 
			
		||||
        <Header as='h3'>使用明细(总消耗额度:{renderQuota(stat.quota)})</Header>
 | 
			
		||||
        <Header as='h3'>
 | 
			
		||||
          使用明细(总消耗额度:
 | 
			
		||||
          {showStat && renderQuota(stat.quota)}
 | 
			
		||||
          {!showStat && <span onClick={handleEyeClick} style={{ cursor: 'pointer', color: 'gray' }}>点击查看</span>}
 | 
			
		||||
          )
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form>
 | 
			
		||||
          <Form.Group>
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -163,6 +163,9 @@ const EditChannel = () => {
 | 
			
		||||
    if (localInputs.type === 3 && localInputs.other === '') {
 | 
			
		||||
      localInputs.other = '2023-06-01-preview';
 | 
			
		||||
    }
 | 
			
		||||
    if (localInputs.type === 18 && localInputs.other === '') {
 | 
			
		||||
      localInputs.other = 'v2.1';
 | 
			
		||||
    }
 | 
			
		||||
    if (localInputs.model_mapping === '') {
 | 
			
		||||
      localInputs.model_mapping = '{}';
 | 
			
		||||
    }
 | 
			
		||||
@@ -275,6 +278,20 @@ const EditChannel = () => {
 | 
			
		||||
              options={groupOptions}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 18 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='模型版本'
 | 
			
		||||
                  name='other'
 | 
			
		||||
                  placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.other}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Dropdown
 | 
			
		||||
              label='模型'
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user