mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-09 02:03:42 +08:00
Compare commits
10 Commits
v0.5.2-alp
...
v0.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b464e2907a | ||
|
|
d96cf2e84d | ||
|
|
446337c329 | ||
|
|
1dfa190e79 | ||
|
|
2d49ca6a07 | ||
|
|
89bcaaf989 | ||
|
|
afcd1bd27b | ||
|
|
c2c455c980 | ||
|
|
30a7f1a1c7 | ||
|
|
c9d2e42a9e |
@@ -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>
|
<summary><strong>Deploy on Sealos</strong></summary>
|
||||||
<div>
|
<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>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -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>
|
<summary><strong>部署到 Sealos </strong></summary>
|
||||||
<div>
|
<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>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -314,6 +316,7 @@ https://openai.justsong.cn
|
|||||||
+ 额度 = 分组倍率 * 模型倍率 * (提示 token 数 + 补全 token 数 * 补全倍率)
|
+ 额度 = 分组倍率 * 模型倍率 * (提示 token 数 + 补全 token 数 * 补全倍率)
|
||||||
+ 其中补全倍率对于 GPT3.5 固定为 1.33,GPT4 为 2,与官方保持一致。
|
+ 其中补全倍率对于 GPT3.5 固定为 1.33,GPT4 为 2,与官方保持一致。
|
||||||
+ 如果是非流模式,官方接口会返回消耗的总 token,但是你要注意提示和补全的消耗倍率不一样。
|
+ 如果是非流模式,官方接口会返回消耗的总 token,但是你要注意提示和补全的消耗倍率不一样。
|
||||||
|
+ 注意,One API 的默认倍率就是官方倍率,是已经调整过的。
|
||||||
2. 账户额度足够为什么提示额度不足?
|
2. 账户额度足够为什么提示额度不足?
|
||||||
+ 请检查你的令牌额度是否足够,这个和账户额度是分开的。
|
+ 请检查你的令牌额度是否足够,这个和账户额度是分开的。
|
||||||
+ 令牌额度仅供用户设置最大使用量,用户可自由设置。
|
+ 令牌额度仅供用户设置最大使用量,用户可自由设置。
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
|
|||||||
err := json.Unmarshal([]byte(data), &streamResponse)
|
err := json.Unmarshal([]byte(data), &streamResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
return
|
continue // just ignore the error
|
||||||
}
|
}
|
||||||
for _, choice := range streamResponse.Choices {
|
for _, choice := range streamResponse.Choices {
|
||||||
responseText += choice.Delta.Content
|
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)
|
err := json.Unmarshal([]byte(data), &streamResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
for _, choice := range streamResponse.Choices {
|
for _, choice := range streamResponse.Choices {
|
||||||
responseText += choice.Text
|
responseText += choice.Text
|
||||||
@@ -92,7 +92,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
|
|||||||
return nil, responseText
|
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
|
var textResponse TextResponse
|
||||||
if consumeQuota {
|
if consumeQuota {
|
||||||
responseBody, err := io.ReadAll(resp.Body)
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
@@ -132,5 +132,17 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), 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
|
return nil, &textResponse.Usage
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
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
|
var textResponse TextResponse
|
||||||
@@ -362,7 +362,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
|
textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
err, usage := openaiHandler(c, resp, consumeQuota)
|
err, usage := openaiHandler(c, resp, consumeQuota, promptTokens, textRequest.Model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ type XunfeiChatResponse struct {
|
|||||||
Seq int `json:"seq"`
|
Seq int `json:"seq"`
|
||||||
Text []XunfeiChatResponseTextItem `json:"text"`
|
Text []XunfeiChatResponseTextItem `json:"text"`
|
||||||
} `json:"choices"`
|
} `json:"choices"`
|
||||||
} `json:"payload"`
|
|
||||||
Usage struct {
|
Usage struct {
|
||||||
//Text struct {
|
//Text struct {
|
||||||
// QuestionTokens string `json:"question_tokens"`
|
// QuestionTokens string `json:"question_tokens"`
|
||||||
@@ -73,6 +72,7 @@ type XunfeiChatResponse struct {
|
|||||||
//} `json:"text"`
|
//} `json:"text"`
|
||||||
Text Usage `json:"text"`
|
Text Usage `json:"text"`
|
||||||
} `json:"usage"`
|
} `json:"usage"`
|
||||||
|
} `json:"payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest {
|
func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest {
|
||||||
@@ -123,7 +123,7 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *OpenAITextResponse {
|
|||||||
Object: "chat.completion",
|
Object: "chat.completion",
|
||||||
Created: common.GetTimestamp(),
|
Created: common.GetTimestamp(),
|
||||||
Choices: []OpenAITextResponseChoice{choice},
|
Choices: []OpenAITextResponseChoice{choice},
|
||||||
Usage: response.Usage.Text,
|
Usage: response.Payload.Usage.Text,
|
||||||
}
|
}
|
||||||
return &fullTextResponse
|
return &fullTextResponse
|
||||||
}
|
}
|
||||||
@@ -222,9 +222,9 @@ func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId
|
|||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
select {
|
select {
|
||||||
case xunfeiResponse := <-dataChan:
|
case xunfeiResponse := <-dataChan:
|
||||||
usage.PromptTokens += xunfeiResponse.Usage.Text.PromptTokens
|
usage.PromptTokens += xunfeiResponse.Payload.Usage.Text.PromptTokens
|
||||||
usage.CompletionTokens += xunfeiResponse.Usage.Text.CompletionTokens
|
usage.CompletionTokens += xunfeiResponse.Payload.Usage.Text.CompletionTokens
|
||||||
usage.TotalTokens += xunfeiResponse.Usage.Text.TotalTokens
|
usage.TotalTokens += xunfeiResponse.Payload.Usage.Text.TotalTokens
|
||||||
response := streamResponseXunfei2OpenAI(&xunfeiResponse)
|
response := streamResponseXunfei2OpenAI(&xunfeiResponse)
|
||||||
jsonResponse, err := json.Marshal(response)
|
jsonResponse, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -194,8 +194,8 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt
|
|||||||
if atEOF && len(data) == 0 {
|
if atEOF && len(data) == 0 {
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
if i := strings.Index(string(data), "\n"); i >= 0 {
|
if i := strings.Index(string(data), "\n\n"); i >= 0 && strings.Index(string(data), ":") >= 0 {
|
||||||
return i + 1, data[0:i], nil
|
return i + 2, data[0:i], nil
|
||||||
}
|
}
|
||||||
if atEOF {
|
if atEOF {
|
||||||
return len(data), data, nil
|
return len(data), data, nil
|
||||||
@@ -208,14 +208,19 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt
|
|||||||
go func() {
|
go func() {
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
data := scanner.Text()
|
data := scanner.Text()
|
||||||
data = strings.Trim(data, "\"")
|
lines := strings.Split(data, "\n")
|
||||||
if len(data) < 5 { // ignore blank line or wrong format
|
for i, line := range lines {
|
||||||
|
if len(line) < 5 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if data[:5] == "data:" {
|
if line[:5] == "data:" {
|
||||||
dataChan <- data[5:]
|
dataChan <- line[5:]
|
||||||
} else if data[:5] == "meta:" {
|
if i != len(lines)-1 {
|
||||||
metaChan <- data[5:]
|
dataChan <- "\n"
|
||||||
|
}
|
||||||
|
} else if line[:5] == "meta:" {
|
||||||
|
metaChan <- line[5:]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stopChan <- true
|
stopChan <- true
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ type OpenAIErrorWithStatusCode struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TextResponse struct {
|
type TextResponse struct {
|
||||||
|
Choices []OpenAITextResponseChoice `json:"choices"`
|
||||||
Usage `json:"usage"`
|
Usage `json:"usage"`
|
||||||
Error OpenAIError `json:"error"`
|
Error OpenAIError `json:"error"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
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 { Link } from 'react-router-dom';
|
||||||
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
|
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
|
||||||
|
|
||||||
import { ITEMS_PER_PAGE } from '../constants';
|
import { ITEMS_PER_PAGE } from '../constants';
|
||||||
import { renderQuota } from '../helpers/render';
|
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) {
|
function renderTimestamp(timestamp) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -68,7 +74,40 @@ const TokensTable = () => {
|
|||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await loadTokens(activePage - 1);
|
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(() => {
|
useEffect(() => {
|
||||||
loadTokens(0)
|
loadTokens(0)
|
||||||
@@ -235,21 +274,28 @@ const TokensTable = () => {
|
|||||||
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
|
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<div>
|
<div>
|
||||||
|
<Button.Group color='green' size={'small'}>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
positive
|
positive
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
let key = "sk-" + token.key;
|
await onCopy('', token.key);
|
||||||
if (await copy(key)) {
|
}
|
||||||
showSuccess('已复制到剪贴板!');
|
|
||||||
} else {
|
|
||||||
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
|
|
||||||
setSearchKeyword(key);
|
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
复制
|
复制
|
||||||
</Button>
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
className='button icon'
|
||||||
|
floating
|
||||||
|
options={COPY_OPTIONS}
|
||||||
|
onChange={async (e, { value } = {}) => {
|
||||||
|
await onCopy(value, token.key);
|
||||||
|
}}
|
||||||
|
trigger={<></>}
|
||||||
|
/>
|
||||||
|
</Button.Group>
|
||||||
|
{' '}
|
||||||
<Popup
|
<Popup
|
||||||
trigger={
|
trigger={
|
||||||
<Button size='small' negative>
|
<Button size='small' negative>
|
||||||
|
|||||||
Reference in New Issue
Block a user