mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 07:43:41 +08:00 
			
		
		
		
	Compare commits
	
		
			10 Commits
		
	
	
		
			v0.5.1-alp
			...
			v0.5.2-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b464e2907a | ||
| 
						 | 
					d96cf2e84d | ||
| 
						 | 
					446337c329 | ||
| 
						 | 
					1dfa190e79 | ||
| 
						 | 
					2d49ca6a07 | ||
| 
						 | 
					89bcaaf989 | ||
| 
						 | 
					afcd1bd27b | ||
| 
						 | 
					c2c455c980 | ||
| 
						 | 
					30a7f1a1c7 | ||
| 
						 | 
					c9d2e42a9e | 
@@ -137,7 +137,7 @@ The initial account username is `root` and password is `123456`.
 | 
			
		||||
   cd one-api/web
 | 
			
		||||
   npm install
 | 
			
		||||
   npm run build
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||
   # Build the backend
 | 
			
		||||
   cd ..
 | 
			
		||||
   go mod download
 | 
			
		||||
@@ -173,7 +173,12 @@ If you encounter a blank page after deployment, refer to [#97](https://github.co
 | 
			
		||||
<summary><strong>Deploy on Sealos</strong></summary>
 | 
			
		||||
<div>
 | 
			
		||||
 | 
			
		||||
Please refer to [this tutorial](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md).
 | 
			
		||||
> Sealos supports high concurrency, dynamic scaling, and stable operations for millions of users.
 | 
			
		||||
 | 
			
		||||
> Click the button below to deploy with one click.👇
 | 
			
		||||
 | 
			
		||||
[](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
</details>
 | 
			
		||||
 
 | 
			
		||||
@@ -153,7 +153,7 @@ sudo service nginx restart
 | 
			
		||||
   cd one-api/web
 | 
			
		||||
   npm install
 | 
			
		||||
   npm run build
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||
   # 构建后端
 | 
			
		||||
   cd ..
 | 
			
		||||
   go mod download
 | 
			
		||||
@@ -211,9 +211,11 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
 | 
			
		||||
<summary><strong>部署到 Sealos </strong></summary>
 | 
			
		||||
<div>
 | 
			
		||||
 | 
			
		||||
> Sealos 可视化部署,仅需 1 分钟。
 | 
			
		||||
> Sealos 的服务器在国外,不需要额外处理网络问题,支持高并发 & 动态伸缩。
 | 
			
		||||
 | 
			
		||||
参考这个[教程](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md)中 1~5 步。
 | 
			
		||||
点击以下按钮一键部署:
 | 
			
		||||
 | 
			
		||||
[](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
</details>
 | 
			
		||||
@@ -314,6 +316,7 @@ https://openai.justsong.cn
 | 
			
		||||
   + 额度 = 分组倍率 * 模型倍率 * (提示 token 数 + 补全 token 数 * 补全倍率)
 | 
			
		||||
   + 其中补全倍率对于 GPT3.5 固定为 1.33,GPT4 为 2,与官方保持一致。
 | 
			
		||||
   + 如果是非流模式,官方接口会返回消耗的总 token,但是你要注意提示和补全的消耗倍率不一样。
 | 
			
		||||
   + 注意,One API 的默认倍率就是官方倍率,是已经调整过的。
 | 
			
		||||
2. 账户额度足够为什么提示额度不足?
 | 
			
		||||
   + 请检查你的令牌额度是否足够,这个和账户额度是分开的。
 | 
			
		||||
   + 令牌额度仅供用户设置最大使用量,用户可自由设置。
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
 | 
			
		||||
					err := json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
						return
 | 
			
		||||
						continue // just ignore the error
 | 
			
		||||
					}
 | 
			
		||||
					for _, choice := range streamResponse.Choices {
 | 
			
		||||
						responseText += choice.Delta.Content
 | 
			
		||||
@@ -56,7 +56,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
 | 
			
		||||
					err := json.Unmarshal([]byte(data), &streamResponse)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
						return
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					for _, choice := range streamResponse.Choices {
 | 
			
		||||
						responseText += choice.Text
 | 
			
		||||
@@ -92,7 +92,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
 | 
			
		||||
	return nil, responseText
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var textResponse TextResponse
 | 
			
		||||
	if consumeQuota {
 | 
			
		||||
		responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
@@ -132,5 +132,17 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if textResponse.Usage.TotalTokens == 0 {
 | 
			
		||||
		completionTokens := 0
 | 
			
		||||
		for _, choice := range textResponse.Choices {
 | 
			
		||||
			completionTokens += countTokenText(choice.Message.Content, model)
 | 
			
		||||
		}
 | 
			
		||||
		textResponse.Usage = Usage{
 | 
			
		||||
			PromptTokens:     promptTokens,
 | 
			
		||||
			CompletionTokens: completionTokens,
 | 
			
		||||
			TotalTokens:      promptTokens + completionTokens,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil, &textResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -302,7 +302,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		isStream = strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
 | 
			
		||||
		isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var textResponse TextResponse
 | 
			
		||||
@@ -362,7 +362,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)
 | 
			
		||||
			err, usage := openaiHandler(c, resp, consumeQuota, promptTokens, textRequest.Model)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -63,16 +63,16 @@ type XunfeiChatResponse struct {
 | 
			
		||||
			Seq    int                          `json:"seq"`
 | 
			
		||||
			Text   []XunfeiChatResponseTextItem `json:"text"`
 | 
			
		||||
		} `json:"choices"`
 | 
			
		||||
		Usage struct {
 | 
			
		||||
			//Text struct {
 | 
			
		||||
			//	QuestionTokens   string `json:"question_tokens"`
 | 
			
		||||
			//	PromptTokens     string `json:"prompt_tokens"`
 | 
			
		||||
			//	CompletionTokens string `json:"completion_tokens"`
 | 
			
		||||
			//	TotalTokens      string `json:"total_tokens"`
 | 
			
		||||
			//} `json:"text"`
 | 
			
		||||
			Text Usage `json:"text"`
 | 
			
		||||
		} `json:"usage"`
 | 
			
		||||
	} `json:"payload"`
 | 
			
		||||
	Usage struct {
 | 
			
		||||
		//Text struct {
 | 
			
		||||
		//	QuestionTokens   string `json:"question_tokens"`
 | 
			
		||||
		//	PromptTokens     string `json:"prompt_tokens"`
 | 
			
		||||
		//	CompletionTokens string `json:"completion_tokens"`
 | 
			
		||||
		//	TotalTokens      string `json:"total_tokens"`
 | 
			
		||||
		//} `json:"text"`
 | 
			
		||||
		Text Usage `json:"text"`
 | 
			
		||||
	} `json:"usage"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest {
 | 
			
		||||
@@ -123,7 +123,7 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *OpenAITextResponse {
 | 
			
		||||
		Object:  "chat.completion",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Choices: []OpenAITextResponseChoice{choice},
 | 
			
		||||
		Usage:   response.Usage.Text,
 | 
			
		||||
		Usage:   response.Payload.Usage.Text,
 | 
			
		||||
	}
 | 
			
		||||
	return &fullTextResponse
 | 
			
		||||
}
 | 
			
		||||
@@ -222,9 +222,9 @@ func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId
 | 
			
		||||
	c.Stream(func(w io.Writer) bool {
 | 
			
		||||
		select {
 | 
			
		||||
		case xunfeiResponse := <-dataChan:
 | 
			
		||||
			usage.PromptTokens += xunfeiResponse.Usage.Text.PromptTokens
 | 
			
		||||
			usage.CompletionTokens += xunfeiResponse.Usage.Text.CompletionTokens
 | 
			
		||||
			usage.TotalTokens += xunfeiResponse.Usage.Text.TotalTokens
 | 
			
		||||
			usage.PromptTokens += xunfeiResponse.Payload.Usage.Text.PromptTokens
 | 
			
		||||
			usage.CompletionTokens += xunfeiResponse.Payload.Usage.Text.CompletionTokens
 | 
			
		||||
			usage.TotalTokens += xunfeiResponse.Payload.Usage.Text.TotalTokens
 | 
			
		||||
			response := streamResponseXunfei2OpenAI(&xunfeiResponse)
 | 
			
		||||
			jsonResponse, err := json.Marshal(response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -194,8 +194,8 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt
 | 
			
		||||
		if atEOF && len(data) == 0 {
 | 
			
		||||
			return 0, nil, nil
 | 
			
		||||
		}
 | 
			
		||||
		if i := strings.Index(string(data), "\n"); i >= 0 {
 | 
			
		||||
			return i + 1, data[0:i], nil
 | 
			
		||||
		if i := strings.Index(string(data), "\n\n"); i >= 0 && strings.Index(string(data), ":") >= 0 {
 | 
			
		||||
			return i + 2, data[0:i], nil
 | 
			
		||||
		}
 | 
			
		||||
		if atEOF {
 | 
			
		||||
			return len(data), data, nil
 | 
			
		||||
@@ -208,14 +208,19 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt
 | 
			
		||||
	go func() {
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			data := scanner.Text()
 | 
			
		||||
			data = strings.Trim(data, "\"")
 | 
			
		||||
			if len(data) < 5 { // ignore blank line or wrong format
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if data[:5] == "data:" {
 | 
			
		||||
				dataChan <- data[5:]
 | 
			
		||||
			} else if data[:5] == "meta:" {
 | 
			
		||||
				metaChan <- data[5:]
 | 
			
		||||
			lines := strings.Split(data, "\n")
 | 
			
		||||
			for i, line := range lines {
 | 
			
		||||
				if len(line) < 5 {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				if line[:5] == "data:" {
 | 
			
		||||
					dataChan <- line[5:]
 | 
			
		||||
					if i != len(lines)-1 {
 | 
			
		||||
						dataChan <- "\n"
 | 
			
		||||
					}
 | 
			
		||||
				} else if line[:5] == "meta:" {
 | 
			
		||||
					metaChan <- line[5:]
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		stopChan <- true
 | 
			
		||||
 
 | 
			
		||||
@@ -81,8 +81,9 @@ type OpenAIErrorWithStatusCode struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TextResponse struct {
 | 
			
		||||
	Usage `json:"usage"`
 | 
			
		||||
	Error OpenAIError `json:"error"`
 | 
			
		||||
	Choices []OpenAITextResponseChoice `json:"choices"`
 | 
			
		||||
	Usage   `json:"usage"`
 | 
			
		||||
	Error   OpenAIError `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OpenAITextResponseChoice struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,17 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-react';
 | 
			
		||||
import { Button, Dropdown, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
 | 
			
		||||
const COPY_OPTIONS = [
 | 
			
		||||
  { key: 'next', text: 'ChatGPT Next Web', value: 'next' },
 | 
			
		||||
  { key: 'ama', text: 'AMA 问天', value: 'ama' },
 | 
			
		||||
  { key: 'opencat', text: 'OpenCat', value: 'opencat' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
@@ -68,7 +74,40 @@ const TokensTable = () => {
 | 
			
		||||
  const refresh = async () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    await loadTokens(activePage - 1);
 | 
			
		||||
  }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onCopy = async (type, key) => {
 | 
			
		||||
    let status = localStorage.getItem('status');
 | 
			
		||||
    let serverAddress = '';
 | 
			
		||||
    if (status) {
 | 
			
		||||
      status = JSON.parse(status);
 | 
			
		||||
      serverAddress = status.server_address;
 | 
			
		||||
    }
 | 
			
		||||
    if (serverAddress === '') {
 | 
			
		||||
      serverAddress = window.location.origin;
 | 
			
		||||
    }
 | 
			
		||||
    let encodedServerAddress = encodeURIComponent(serverAddress);
 | 
			
		||||
    let url;
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'ama':
 | 
			
		||||
        url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'opencat':
 | 
			
		||||
        url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'next':
 | 
			
		||||
        url = `https://chat.oneapi.pro/#/?settings=%7B%22key%22:%22sk-${key}%22,%22url%22:%22${serverAddress}%22%7D`;
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        url = `sk-${key}`;
 | 
			
		||||
    }
 | 
			
		||||
    if (await copy(url)) {
 | 
			
		||||
      showSuccess('已复制到剪贴板!');
 | 
			
		||||
    } else {
 | 
			
		||||
      showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
 | 
			
		||||
      setSearchKeyword(url);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    loadTokens(0)
 | 
			
		||||
@@ -235,21 +274,28 @@ const TokensTable = () => {
 | 
			
		||||
                  <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <Button
 | 
			
		||||
                        size={'small'}
 | 
			
		||||
                        positive
 | 
			
		||||
                        onClick={async () => {
 | 
			
		||||
                          let key = "sk-" + token.key;
 | 
			
		||||
                          if (await copy(key)) {
 | 
			
		||||
                            showSuccess('已复制到剪贴板!');
 | 
			
		||||
                          } else {
 | 
			
		||||
                            showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
 | 
			
		||||
                            setSearchKeyword(key);
 | 
			
		||||
                      <Button.Group color='green' size={'small'}>
 | 
			
		||||
                        <Button
 | 
			
		||||
                          size={'small'}
 | 
			
		||||
                          positive
 | 
			
		||||
                          onClick={async () => {
 | 
			
		||||
                            await onCopy('', token.key);
 | 
			
		||||
                          }
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        复制
 | 
			
		||||
                      </Button>
 | 
			
		||||
                          }
 | 
			
		||||
                        >
 | 
			
		||||
                          复制
 | 
			
		||||
                        </Button>
 | 
			
		||||
                        <Dropdown
 | 
			
		||||
                          className='button icon'
 | 
			
		||||
                          floating
 | 
			
		||||
                          options={COPY_OPTIONS}
 | 
			
		||||
                          onChange={async (e, { value } = {}) => {
 | 
			
		||||
                            await onCopy(value, token.key);
 | 
			
		||||
                          }}
 | 
			
		||||
                          trigger={<></>}
 | 
			
		||||
                        />
 | 
			
		||||
                      </Button.Group>
 | 
			
		||||
                      {' '}
 | 
			
		||||
                      <Popup
 | 
			
		||||
                        trigger={
 | 
			
		||||
                          <Button size='small' negative>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user