Merge branch 'Calcium-Ion:main' into main

This commit is contained in:
H.
2025-01-22 13:12:14 +08:00
committed by GitHub
11 changed files with 118 additions and 70 deletions

View File

@@ -12,7 +12,7 @@ on:
jobs: jobs:
push_to_registries: push_to_registries:
name: Push Docker image to multiple registries name: Push Docker image to multiple registries
runs-on: self-hosted runs-on: ubuntu-latest
permissions: permissions:
packages: write packages: write
contents: read contents: read

View File

@@ -9,7 +9,7 @@ on:
- '!*-alpha*' - '!*-alpha*'
jobs: jobs:
release: release:
runs-on: self-hosted runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3

View File

@@ -9,7 +9,7 @@ on:
- '!*-alpha*' - '!*-alpha*'
jobs: jobs:
release: release:
runs-on: self-hosted runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3

View File

@@ -9,7 +9,7 @@ on:
- '!*-alpha*' - '!*-alpha*'
jobs: jobs:
release: release:
runs-on: self-hosted runs-on: ubuntu-latest
defaults: defaults:
run: run:
shell: bash shell: bash

View File

@@ -24,7 +24,7 @@ FROM alpine
RUN apk update \ RUN apk update \
&& apk upgrade \ && apk upgrade \
&& apk add --no-cache ca-certificates tzdata \ && apk add --no-cache ca-certificates tzdata ffmpeg\
&& update-ca-certificates 2>/dev/null || true && update-ca-certificates 2>/dev/null || true
COPY --from=builder2 /build/one-api / COPY --from=builder2 /build/one-api /

View File

@@ -401,10 +401,13 @@ func GetCompletionRatio(name string) float64 {
case "command-r-plus-08-2024": case "command-r-plus-08-2024":
return 4 return 4
default: default:
return 2 return 4
} }
} }
if strings.HasPrefix(name, "deepseek") { if strings.HasPrefix(name, "deepseek") {
if name == "deepseek-reasoner" {
return 4
}
return 2 return 2
} }
if strings.HasPrefix(name, "ERNIE-Speed-") { if strings.HasPrefix(name, "ERNIE-Speed-") {

View File

@@ -1,14 +1,19 @@
package common package common
import ( import (
"bytes"
"context"
crand "crypto/rand" crand "crypto/rand"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/pkg/errors"
"html/template" "html/template"
"io"
"log" "log"
"math/big" "math/big"
"math/rand" "math/rand"
"net" "net"
"os"
"os/exec" "os/exec"
"runtime" "runtime"
"strconv" "strconv"
@@ -207,3 +212,31 @@ func RandomSleep() {
// Sleep for 0-3000 ms // Sleep for 0-3000 ms
time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond) time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond)
} }
// SaveTmpFile saves data to a temporary file. The filename would be apppended with a random string.
func SaveTmpFile(filename string, data io.Reader) (string, error) {
f, err := os.CreateTemp(os.TempDir(), filename)
if err != nil {
return "", errors.Wrapf(err, "failed to create temporary file %s", filename)
}
defer f.Close()
_, err = io.Copy(f, data)
if err != nil {
return "", errors.Wrapf(err, "failed to copy data to temporary file %s", filename)
}
return f.Name(), nil
}
// GetAudioDuration returns the duration of an audio file in seconds.
func GetAudioDuration(ctx context.Context, filename string) (float64, error) {
// ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {{input}}
c := exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)
output, err := c.Output()
if err != nil {
return 0, errors.Wrap(err, "failed to get audio duration")
}
return strconv.ParseFloat(string(bytes.TrimSpace(output)), 64)
}

View File

@@ -27,6 +27,7 @@ type Log struct {
UseTime int `json:"use_time" gorm:"default:0"` UseTime int `json:"use_time" gorm:"default:0"`
IsStream bool `json:"is_stream" gorm:"default:false"` IsStream bool `json:"is_stream" gorm:"default:false"`
ChannelId int `json:"channel" gorm:"index"` ChannelId int `json:"channel" gorm:"index"`
ChannelName string `json:"channel_name" gorm:"->"`
TokenId int `json:"token_id" gorm:"default:0;index"` TokenId int `json:"token_id" gorm:"default:0;index"`
Group string `json:"group" gorm:"index"` Group string `json:"group" gorm:"index"`
Other string `json:"other"` Other string `json:"other"`
@@ -130,6 +131,10 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
} else { } else {
tx = LOG_DB.Where("type = ?", logType) tx = LOG_DB.Where("type = ?", logType)
} }
tx = tx.Joins("LEFT JOIN channels ON logs.channel_id = channels.id")
tx = tx.Select("logs.*, channels.name as channel_name")
if modelName != "" { if modelName != "" {
tx = tx.Where("model_name like ?", modelName) tx = tx.Where("model_name like ?", modelName)
} }
@@ -169,6 +174,10 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
} else { } else {
tx = LOG_DB.Where("user_id = ? and type = ?", userId, logType) tx = LOG_DB.Where("user_id = ? and type = ?", userId, logType)
} }
tx = tx.Joins("LEFT JOIN channels ON logs.channel_id = channels.id")
tx = tx.Select("logs.*, channels.name as channel_name")
if modelName != "" { if modelName != "" {
tx = tx.Where("model_name like ?", modelName) tx = tx.Where("model_name like ?", modelName)
} }

View File

@@ -1,7 +1,7 @@
package deepseek package deepseek
var ModelList = []string{ var ModelList = []string{
"deepseek-chat", "deepseek-coder", "deepseek-chat", "deepseek-reasoner",
} }
var ChannelName = "deepseek" var ChannelName = "deepseek"

View File

@@ -5,7 +5,10 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/pkg/errors"
"io" "io"
"math"
"mime/multipart"
"net/http" "net/http"
"one-api/common" "one-api/common"
"one-api/constant" "one-api/constant"
@@ -13,6 +16,7 @@ import (
relaycommon "one-api/relay/common" relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant" relayconstant "one-api/relay/constant"
"one-api/service" "one-api/service"
"os"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -316,6 +320,11 @@ func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
} }
func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, responseFormat string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, responseFormat string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
// count tokens by audio file duration
audioTokens, err := countAudioTokens(c)
if err != nil {
return service.OpenAIErrorWrapper(err, "count_audio_tokens_failed", http.StatusInternalServerError), nil
}
responseBody, err := io.ReadAll(resp.Body) responseBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
@@ -340,70 +349,52 @@ func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
} }
resp.Body.Close() resp.Body.Close()
var text string
switch responseFormat {
case "json":
text, err = getTextFromJSON(responseBody)
case "text":
text, err = getTextFromText(responseBody)
case "srt":
text, err = getTextFromSRT(responseBody)
case "verbose_json":
text, err = getTextFromVerboseJSON(responseBody)
case "vtt":
text, err = getTextFromVTT(responseBody)
}
usage := &dto.Usage{} usage := &dto.Usage{}
usage.PromptTokens = info.PromptTokens usage.PromptTokens = audioTokens
usage.CompletionTokens, _ = service.CountTextToken(text, info.UpstreamModelName) usage.CompletionTokens = 0
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
return nil, usage return nil, usage
} }
func getTextFromVTT(body []byte) (string, error) { func countAudioTokens(c *gin.Context) (int, error) {
return getTextFromSRT(body) body, err := common.GetRequestBody(c)
} if err != nil {
return 0, errors.WithStack(err)
func getTextFromVerboseJSON(body []byte) (string, error) {
var whisperResponse dto.WhisperVerboseJSONResponse
if err := json.Unmarshal(body, &whisperResponse); err != nil {
return "", fmt.Errorf("unmarshal_response_body_failed err :%w", err)
} }
return whisperResponse.Text, nil
}
func getTextFromSRT(body []byte) (string, error) { var reqBody struct {
scanner := bufio.NewScanner(strings.NewReader(string(body))) File *multipart.FileHeader `form:"file" binding:"required"`
var builder strings.Builder
var textLine bool
for scanner.Scan() {
line := scanner.Text()
if textLine {
builder.WriteString(line)
textLine = false
continue
} else if strings.Contains(line, "-->") {
textLine = true
continue
}
} }
if err := scanner.Err(); err != nil { c.Request.Body = io.NopCloser(bytes.NewReader(body))
return "", err if err = c.ShouldBind(&reqBody); err != nil {
return 0, errors.WithStack(err)
} }
return builder.String(), nil
}
func getTextFromText(body []byte) (string, error) { reqFp, err := reqBody.File.Open()
return strings.TrimSuffix(string(body), "\n"), nil if err != nil {
} return 0, errors.WithStack(err)
func getTextFromJSON(body []byte) (string, error) {
var whisperResponse dto.AudioResponse
if err := json.Unmarshal(body, &whisperResponse); err != nil {
return "", fmt.Errorf("unmarshal_response_body_failed err :%w", err)
} }
return whisperResponse.Text, nil
tmpFp, err := os.CreateTemp("", "audio-*")
if err != nil {
return 0, errors.WithStack(err)
}
defer os.Remove(tmpFp.Name())
_, err = io.Copy(tmpFp, reqFp)
if err != nil {
return 0, errors.WithStack(err)
}
if err = tmpFp.Close(); err != nil {
return 0, errors.WithStack(err)
}
duration, err := common.GetAudioDuration(c.Request.Context(), tmpFp.Name())
if err != nil {
return 0, errors.WithStack(err)
}
return int(math.Round(math.Ceil(duration) / 60.0 * 1000)), nil // 1 minute 相当于 1k tokens
} }
func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.RealtimeUsage) { func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.RealtimeUsage) {

View File

@@ -157,13 +157,15 @@ const LogsTable = () => {
record.type === 0 || record.type === 2 ? ( record.type === 0 || record.type === 2 ? (
<div> <div>
{ {
<Tag <Tooltip content={record.channel_name || '[未知]'}>
color={colors[parseInt(text) % colors.length]} <Tag
size='large' color={colors[parseInt(text) % colors.length]}
> size='large'
{' '} >
{text}{' '} {' '}
</Tag> {text}{' '}
</Tag>
</Tooltip>
} }
</div> </div>
) : ( ) : (
@@ -234,7 +236,12 @@ const LogsTable = () => {
</> </>
); );
} else { } else {
let other = JSON.parse(record.other); let other = null;
try {
other = JSON.parse(record.other);
} catch (e) {
console.error(`Failed to parse record.other: "${record.other}".`, e);
}
if (other === null) { if (other === null) {
return <></>; return <></>;
} }
@@ -543,6 +550,12 @@ const LogsTable = () => {
// key: '渠道重试', // key: '渠道重试',
// value: content, // value: content,
// }) // })
}
if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
expandDataLocal.push({
key: t('渠道信息'),
value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`
});
} }
if (other?.ws || other?.audio) { if (other?.ws || other?.audio) {
expandDataLocal.push({ expandDataLocal.push({
@@ -595,13 +608,12 @@ const LogsTable = () => {
key: t('计费过程'), key: t('计费过程'),
value: content, value: content,
}); });
}
}
expandDatesLocal[logs[i].key] = expandDataLocal; expandDatesLocal[logs[i].key] = expandDataLocal;
} }
setExpandData(expandDatesLocal); setExpandData(expandDatesLocal);
setLogs(logs); setLogs(logs);
}; };