Compare commits

..

10 Commits

Author SHA1 Message Date
JustSong
b464e2907a chore: update domain 2023-08-06 18:14:13 +08:00
JustSong
d96cf2e84d fix: fix stream mode determine related logic (close #360) 2023-08-06 18:09:00 +08:00
glzjin
446337c329 fix: calculate usage if not given in non-stream mode (#352) 2023-08-06 17:40:31 +08:00
Miniers
1dfa190e79 feat: able to copy scheme of ama, opencat & chatgpt next web (#343)
* Token Adds Option to Quickly Copy AMA and OpenCat URL Scheme

* feat: add ChatGPT Next Web

---------

Co-authored-by: JustSong <songquanpeng@foxmail.com>
2023-08-06 13:56:59 +08:00
glzjin
2d49ca6a07 fix: fix SparkDesk not billed (#344) 2023-08-06 13:24:49 +08:00
a497625414
89bcaaf989 docs: update readme (#359)
* update-deploy-on-sealos

* update-deploy-on-sealos
2023-08-06 13:19:54 +08:00
a497625414
afcd1bd27b docs: update deploy-on-sealos (#351) 2023-08-02 19:11:21 +08:00
glzjin
c2c455c980 fix: fix zhipu streaming (#349)
* Fix #348

* chore: update implementation

---------

Co-authored-by: JustSong <songquanpeng@foxmail.com>
2023-08-01 23:51:28 +08:00
JustSong
30a7f1a1c7 docs: update README 2023-07-30 22:24:07 +08:00
JustSong
c9d2e42a9e fix: fix sse not ending properly in some case 2023-07-30 22:20:42 +08:00
8 changed files with 123 additions and 51 deletions

View File

@@ -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://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
</div>
</details>

View File

@@ -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 步。
点击以下按钮一键部署:
[![Deploy-on-Sealos.svg](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](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.33GPT4 为 2与官方保持一致。
+ 如果是非流模式,官方接口会返回消耗的总 token但是你要注意提示和补全的消耗倍率不一样。
+ 注意One API 的默认倍率就是官方倍率,是已经调整过的。
2. 账户额度足够为什么提示额度不足?
+ 请检查你的令牌额度是否足够,这个和账户额度是分开的。
+ 令牌额度仅供用户设置最大使用量,用户可自由设置。

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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>