mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-04 21:58:19 +00:00
Compare commits
21 Commits
task-model
...
logger
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13ff448049 | ||
|
|
3dc4d6c39e | ||
|
|
019412c27a | ||
|
|
96a2b81aaa | ||
|
|
fb610e62a0 | ||
|
|
736f7b55b7 | ||
|
|
2fd33ea294 | ||
|
|
53123aaf94 | ||
|
|
f8f5d26600 | ||
|
|
c86bc94d9d | ||
|
|
50e8639a40 | ||
|
|
424325162e | ||
|
|
a9a8676f7c | ||
|
|
14295f0035 | ||
|
|
29e70acc55 | ||
|
|
8599b348c0 | ||
|
|
6a761c2dba | ||
|
|
df2ee649ab | ||
|
|
00782aae88 | ||
|
|
70f8a59a65 | ||
|
|
a4cf9bb6fe |
260
LOGGING.md
Normal file
260
LOGGING.md
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
# 日志系统说明
|
||||||
|
|
||||||
|
本项目使用 Go 标准库的 `log/slog` 实现结构化日志记录。
|
||||||
|
|
||||||
|
## 📋 功能特性
|
||||||
|
|
||||||
|
### 1. 标准的文件存储结构
|
||||||
|
|
||||||
|
- **当前日志文件**: `oneapi.log` - 实时写入的日志文件
|
||||||
|
- **归档日志文件**: `oneapi.2024-01-02-153045.log` - 自动轮转后的历史日志
|
||||||
|
|
||||||
|
### 2. 自动日志轮转
|
||||||
|
|
||||||
|
日志文件会在以下情况自动轮转:
|
||||||
|
|
||||||
|
- **按大小轮转**: 当日志文件超过指定大小时(默认 100MB)
|
||||||
|
- **启动时日期检查**: 程序启动时如果检测到日志文件是旧日期创建的,会自动轮转
|
||||||
|
- **自动清理**: 只保留最近 N 个日志文件(默认 7 个)
|
||||||
|
|
||||||
|
### 3. 结构化日志
|
||||||
|
|
||||||
|
所有日志都包含以下结构化字段:
|
||||||
|
|
||||||
|
```
|
||||||
|
time=2024-01-02T15:30:45 level=INFO msg="user logged in" request_id=abc123 user_id=1001
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 多种输出格式
|
||||||
|
|
||||||
|
- **Text 格式** (默认): 人类可读的文本格式
|
||||||
|
- **JSON 格式**: 便于日志分析工具解析
|
||||||
|
|
||||||
|
### 5. 灵活的日志级别
|
||||||
|
|
||||||
|
支持四个日志级别:
|
||||||
|
- `DEBUG`: 调试信息
|
||||||
|
- `INFO`: 一般信息
|
||||||
|
- `WARN`: 警告信息
|
||||||
|
- `ERROR`: 错误信息
|
||||||
|
|
||||||
|
## ⚙️ 配置方式
|
||||||
|
|
||||||
|
### 环境变量配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 日志目录(必需,否则只输出到控制台)
|
||||||
|
--log-dir=./logs
|
||||||
|
|
||||||
|
# 日志级别(可选,默认: INFO,DEBUG 模式除外)
|
||||||
|
export LOG_LEVEL=DEBUG # 可选值: DEBUG, INFO, WARN, ERROR
|
||||||
|
|
||||||
|
# 日志格式(可选,默认: text)
|
||||||
|
export LOG_FORMAT=json # 可选值: text, json
|
||||||
|
|
||||||
|
# 单个日志文件最大大小(可选,默认: 100,单位: MB)
|
||||||
|
export LOG_MAX_SIZE_MB=200
|
||||||
|
|
||||||
|
# 保留的日志文件数量(可选,默认: 7)
|
||||||
|
export LOG_MAX_FILES=14
|
||||||
|
|
||||||
|
# 启用调试模式(会自动将日志级别设为 DEBUG)
|
||||||
|
export DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命令行参数
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动时指定日志目录
|
||||||
|
./new-api --log-dir=./logs
|
||||||
|
|
||||||
|
# 如果不指定日志目录,日志只输出到控制台
|
||||||
|
./new-api
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 使用示例
|
||||||
|
|
||||||
|
### 基础使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/QuantumNous/new-api/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 记录信息日志
|
||||||
|
logger.LogInfo(ctx, "user registered successfully")
|
||||||
|
|
||||||
|
// 记录警告日志
|
||||||
|
logger.LogWarn(ctx, "API rate limit approaching")
|
||||||
|
|
||||||
|
// 记录错误日志
|
||||||
|
logger.LogError(ctx, "failed to connect to database")
|
||||||
|
|
||||||
|
// 记录调试日志(只在 DEBUG 模式下输出)
|
||||||
|
logger.LogDebug(ctx, "processing request with params: %v", params)
|
||||||
|
|
||||||
|
// 记录系统日志(无 context)
|
||||||
|
logger.LogSystemInfo("application started")
|
||||||
|
logger.LogSystemError("critical system error")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志输出示例
|
||||||
|
|
||||||
|
**Text 格式** (易读格式):
|
||||||
|
```
|
||||||
|
[INFO] 2024/01/02 - 15:30:45 | SYSTEM | application started
|
||||||
|
[INFO] 2024/01/02 - 15:30:46 | abc123 | user registered successfully
|
||||||
|
[WARN] 2024/01/02 - 15:30:47 | def456 | API rate limit approaching | remaining=10, limit=100
|
||||||
|
[ERROR] 2024/01/02 - 15:30:48 | ghi789 | failed to connect to database | error="connection timeout"
|
||||||
|
```
|
||||||
|
|
||||||
|
格式说明:`[级别] 时间 | 请求ID/组件 | 消息 | 额外属性(如有)`
|
||||||
|
|
||||||
|
**JSON 格式**:
|
||||||
|
```json
|
||||||
|
{"time":"2024-01-02 15:30:45","level":"INFO","msg":"application started","request_id":"SYSTEM"}
|
||||||
|
{"time":"2024-01-02 15:30:46","level":"INFO","msg":"user registered successfully","request_id":"abc123"}
|
||||||
|
{"time":"2024-01-02 15:30:47","level":"WARN","msg":"API rate limit approaching","request_id":"def456"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📂 日志文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
logs/
|
||||||
|
├── oneapi.log # 当前活动日志文件
|
||||||
|
├── oneapi.2024-01-01-090000.log # 昨天的日志
|
||||||
|
├── oneapi.2024-01-01-150000.log # 昨天下午的日志(如果超过大小限制)
|
||||||
|
├── oneapi.2023-12-31-090000.log # 更早的日志
|
||||||
|
└── ... # 最多保留配置数量的历史文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 日志轮转机制
|
||||||
|
|
||||||
|
### 轮转触发条件
|
||||||
|
|
||||||
|
1. **文件大小检查**: 每写入 1000 条日志后检查一次文件大小
|
||||||
|
2. **启动时日期检查**: 程序启动时检查日志文件的修改日期,如果不是今天则轮转
|
||||||
|
3. **自动清理**: 轮转时自动删除超过保留数量的旧日志文件
|
||||||
|
|
||||||
|
> **注意**: 日志不会在运行时动态检查日期变化。如果需要每天自动轮转日志,建议:
|
||||||
|
> - 使用定时任务(如 cron)每天重启服务
|
||||||
|
> - 或者配置较小的日志文件大小,让它自动按大小轮转
|
||||||
|
|
||||||
|
### 轮转流程
|
||||||
|
|
||||||
|
1. 检测到需要轮转时,关闭当前日志文件
|
||||||
|
2. 将 `oneapi.log` 重命名为 `oneapi.YYYY-MM-DD-HHmmss.log`
|
||||||
|
3. 创建新的 `oneapi.log` 文件
|
||||||
|
4. 异步清理超过数量限制的旧日志文件
|
||||||
|
5. 记录轮转事件到新日志文件
|
||||||
|
|
||||||
|
## 🎯 最佳实践
|
||||||
|
|
||||||
|
### 1. 生产环境配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 INFO 级别,避免过多调试信息
|
||||||
|
export LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
# 使用 JSON 格式,便于日志分析工具处理
|
||||||
|
export LOG_FORMAT=json
|
||||||
|
|
||||||
|
# 设置合适的文件大小和保留数量
|
||||||
|
export LOG_MAX_SIZE_MB=500
|
||||||
|
export LOG_MAX_FILES=30
|
||||||
|
|
||||||
|
# 指定日志目录
|
||||||
|
./new-api --log-dir=/var/log/oneapi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 开发环境配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 DEBUG 级别查看详细信息
|
||||||
|
export DEBUG=true
|
||||||
|
|
||||||
|
# 使用 Text 格式,便于阅读
|
||||||
|
export LOG_FORMAT=text
|
||||||
|
|
||||||
|
# 较小的文件大小和保留数量
|
||||||
|
export LOG_MAX_SIZE_MB=50
|
||||||
|
export LOG_MAX_FILES=7
|
||||||
|
|
||||||
|
./new-api --log-dir=./logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 容器环境配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 只输出到标准输出,由容器运行时管理日志
|
||||||
|
./new-api
|
||||||
|
|
||||||
|
# 或者使用 JSON 格式便于日志收集系统处理
|
||||||
|
export LOG_FORMAT=json
|
||||||
|
./new-api
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 日志分析
|
||||||
|
|
||||||
|
### 使用 grep 分析文本日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查找错误日志
|
||||||
|
grep '\[ERROR\]' logs/oneapi.log
|
||||||
|
|
||||||
|
# 查找特定请求的所有日志
|
||||||
|
grep 'abc123' logs/*.log
|
||||||
|
|
||||||
|
# 查看最近的警告和错误
|
||||||
|
tail -f logs/oneapi.log | grep -E '\[(WARN|ERROR)\]'
|
||||||
|
|
||||||
|
# 查找包含特定关键词的日志
|
||||||
|
grep 'database' logs/oneapi.log
|
||||||
|
|
||||||
|
# 查看今天的所有错误
|
||||||
|
grep "\[ERROR\] $(date +%Y/%m/%d)" logs/oneapi.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 jq 分析 JSON 日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 提取所有错误日志
|
||||||
|
cat logs/oneapi.log | jq 'select(.level=="ERROR")'
|
||||||
|
|
||||||
|
# 统计各级别日志数量
|
||||||
|
cat logs/oneapi.log | jq -r '.level' | sort | uniq -c
|
||||||
|
|
||||||
|
# 查找特定时间范围的日志
|
||||||
|
cat logs/oneapi.log | jq 'select(.time >= "2024-01-02 15:00:00" and .time <= "2024-01-02 16:00:00")'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 性能优化
|
||||||
|
|
||||||
|
1. **异步日志轮转**: 轮转操作在后台 goroutine 中执行,不阻塞主程序
|
||||||
|
2. **批量写入检查**: 每 1000 次写入才检查一次轮转条件,减少 I/O 开销
|
||||||
|
3. **读写锁**: 使用 `sync.RWMutex` 保护日志器,提高并发性能
|
||||||
|
4. **零分配**: `slog` 库在大多数情况下实现零内存分配
|
||||||
|
|
||||||
|
## 🚨 故障排查
|
||||||
|
|
||||||
|
### 日志文件未创建
|
||||||
|
|
||||||
|
- 检查日志目录是否存在且有写入权限
|
||||||
|
- 确认启动时指定了 `--log-dir` 参数
|
||||||
|
|
||||||
|
### 日志文件过多
|
||||||
|
|
||||||
|
- 调整 `LOG_MAX_FILES` 环境变量
|
||||||
|
- 手动清理不需要的旧日志文件
|
||||||
|
|
||||||
|
### 日志级别不正确
|
||||||
|
|
||||||
|
- 检查 `LOG_LEVEL` 环境变量是否正确设置
|
||||||
|
- 确认 `DEBUG` 环境变量的值(会覆盖 LOG_LEVEL)
|
||||||
|
|
||||||
|
## 📖 相关文档
|
||||||
|
|
||||||
|
- [Go slog 官方文档](https://pkg.go.dev/log/slog)
|
||||||
|
- [结构化日志最佳实践](https://go.dev/blog/slog)
|
||||||
|
|
||||||
@@ -141,6 +141,7 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
|
|||||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE`:邮件等通知限制持续时间,默认 `10`分钟
|
- `NOTIFICATION_LIMIT_DURATION_MINUTE`:邮件等通知限制持续时间,默认 `10`分钟
|
||||||
- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2`
|
- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2`
|
||||||
- `ERROR_LOG_ENABLED=true`: 是否记录并显示错误日志,默认`false`
|
- `ERROR_LOG_ENABLED=true`: 是否记录并显示错误日志,默认`false`
|
||||||
|
- `TASK_PRICE_PATCH=sora-2-all,sora-2-pro-all`: 异步任务设置某些模型按次计费,多个模型用逗号分隔,例如`sora-2-all,sora-2-pro-all`,表示sora-2-all和sora-2-pro-all模型异步任务仅按次计费,不按秒等计费。
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
|
|||||||
@@ -159,14 +159,15 @@ var (
|
|||||||
GlobalWebRateLimitNum int
|
GlobalWebRateLimitNum int
|
||||||
GlobalWebRateLimitDuration int64
|
GlobalWebRateLimitDuration int64
|
||||||
|
|
||||||
|
CriticalRateLimitEnable bool
|
||||||
|
CriticalRateLimitNum = 20
|
||||||
|
CriticalRateLimitDuration int64 = 20 * 60
|
||||||
|
|
||||||
UploadRateLimitNum = 10
|
UploadRateLimitNum = 10
|
||||||
UploadRateLimitDuration int64 = 60
|
UploadRateLimitDuration int64 = 60
|
||||||
|
|
||||||
DownloadRateLimitNum = 10
|
DownloadRateLimitNum = 10
|
||||||
DownloadRateLimitDuration int64 = 60
|
DownloadRateLimitDuration int64 = 60
|
||||||
|
|
||||||
CriticalRateLimitNum = 20
|
|
||||||
CriticalRateLimitDuration int64 = 20 * 60
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var RateLimitKeyExpirationDuration = 20 * time.Minute
|
var RateLimitKeyExpirationDuration = 20 * time.Minute
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package common
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -43,9 +43,10 @@ func InitEnv() {
|
|||||||
if os.Getenv("SESSION_SECRET") != "" {
|
if os.Getenv("SESSION_SECRET") != "" {
|
||||||
ss := os.Getenv("SESSION_SECRET")
|
ss := os.Getenv("SESSION_SECRET")
|
||||||
if ss == "random_string" {
|
if ss == "random_string" {
|
||||||
log.Println("WARNING: SESSION_SECRET is set to the default value 'random_string', please change it to a random string.")
|
slog.Warn("SESSION_SECRET is set to the default value 'random_string', please change it to a random string.")
|
||||||
log.Println("警告:SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。")
|
slog.Warn("警告:SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。")
|
||||||
log.Fatal("Please set SESSION_SECRET to a random string.")
|
slog.Error("Please set SESSION_SECRET to a random string.")
|
||||||
|
os.Exit(1)
|
||||||
} else {
|
} else {
|
||||||
SessionSecret = ss
|
SessionSecret = ss
|
||||||
}
|
}
|
||||||
@@ -62,12 +63,14 @@ func InitEnv() {
|
|||||||
var err error
|
var err error
|
||||||
*LogDir, err = filepath.Abs(*LogDir)
|
*LogDir, err = filepath.Abs(*LogDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
slog.Error("failed to get absolute path for log directory", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(*LogDir); os.IsNotExist(err) {
|
if _, err := os.Stat(*LogDir); os.IsNotExist(err) {
|
||||||
err = os.Mkdir(*LogDir, 0777)
|
err = os.Mkdir(*LogDir, 0777)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
slog.Error("failed to create log directory", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +102,9 @@ func InitEnv() {
|
|||||||
GlobalWebRateLimitNum = GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT", 60)
|
GlobalWebRateLimitNum = GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT", 60)
|
||||||
GlobalWebRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT_DURATION", 180))
|
GlobalWebRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT_DURATION", 180))
|
||||||
|
|
||||||
|
CriticalRateLimitEnable = GetEnvOrDefaultBool("CRITICAL_RATE_LIMIT_ENABLE", true)
|
||||||
|
CriticalRateLimitNum = GetEnvOrDefault("CRITICAL_RATE_LIMIT", 20)
|
||||||
|
CriticalRateLimitDuration = int64(GetEnvOrDefault("CRITICAL_RATE_LIMIT_DURATION", 20*60))
|
||||||
initConstantEnv()
|
initConstantEnv()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -9,18 +10,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func SysLog(s string) {
|
func SysLog(s string) {
|
||||||
t := time.Now()
|
slog.Info(s, "component", "system")
|
||||||
_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SysError(s string) {
|
func SysError(s string) {
|
||||||
t := time.Now()
|
slog.Error(s, "component", "system")
|
||||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FatalLog(v ...any) {
|
func FatalLog(v ...any) {
|
||||||
t := time.Now()
|
msg := fmt.Sprint(v...)
|
||||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
slog.Error(msg, "component", "system", "level", "fatal")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -617,6 +617,10 @@ func TestAllChannels(c *gin.Context) {
|
|||||||
var autoTestChannelsOnce sync.Once
|
var autoTestChannelsOnce sync.Once
|
||||||
|
|
||||||
func AutomaticallyTestChannels() {
|
func AutomaticallyTestChannels() {
|
||||||
|
// 只在Master节点定时测试渠道
|
||||||
|
if !common.IsMasterNode {
|
||||||
|
return
|
||||||
|
}
|
||||||
autoTestChannelsOnce.Do(func() {
|
autoTestChannelsOnce.Do(func() {
|
||||||
for {
|
for {
|
||||||
if !operation_setting.GetMonitorSetting().AutoTestChannelEnabled {
|
if !operation_setting.GetMonitorSetting().AutoTestChannelEnabled {
|
||||||
|
|||||||
@@ -649,13 +649,15 @@ func DeleteDisabledChannel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChannelTag struct {
|
type ChannelTag struct {
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
NewTag *string `json:"new_tag"`
|
NewTag *string `json:"new_tag"`
|
||||||
Priority *int64 `json:"priority"`
|
Priority *int64 `json:"priority"`
|
||||||
Weight *uint `json:"weight"`
|
Weight *uint `json:"weight"`
|
||||||
ModelMapping *string `json:"model_mapping"`
|
ModelMapping *string `json:"model_mapping"`
|
||||||
Models *string `json:"models"`
|
Models *string `json:"models"`
|
||||||
Groups *string `json:"groups"`
|
Groups *string `json:"groups"`
|
||||||
|
ParamOverride *string `json:"param_override"`
|
||||||
|
HeaderOverride *string `json:"header_override"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DisableTagChannels(c *gin.Context) {
|
func DisableTagChannels(c *gin.Context) {
|
||||||
@@ -721,7 +723,29 @@ func EditTagChannels(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = model.EditChannelByTag(channelTag.Tag, channelTag.NewTag, channelTag.ModelMapping, channelTag.Models, channelTag.Groups, channelTag.Priority, channelTag.Weight)
|
if channelTag.ParamOverride != nil {
|
||||||
|
trimmed := strings.TrimSpace(*channelTag.ParamOverride)
|
||||||
|
if trimmed != "" && !json.Valid([]byte(trimmed)) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "参数覆盖必须是合法的 JSON 格式",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channelTag.ParamOverride = common.GetPointer[string](trimmed)
|
||||||
|
}
|
||||||
|
if channelTag.HeaderOverride != nil {
|
||||||
|
trimmed := strings.TrimSpace(*channelTag.HeaderOverride)
|
||||||
|
if trimmed != "" && !json.Valid([]byte(trimmed)) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "请求头覆盖必须是合法的 JSON 格式",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channelTag.HeaderOverride = common.GetPointer[string](trimmed)
|
||||||
|
}
|
||||||
|
err = model.EditChannelByTag(channelTag.Tag, channelTag.NewTag, channelTag.ModelMapping, channelTag.Models, channelTag.Groups, channelTag.Priority, channelTag.Weight, channelTag.ParamOverride, channelTag.HeaderOverride)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -510,11 +510,44 @@ func (c *ClaudeResponse) GetClaudeError() *types.ClaudeError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClaudeUsage struct {
|
type ClaudeUsage struct {
|
||||||
InputTokens int `json:"input_tokens"`
|
InputTokens int `json:"input_tokens"`
|
||||||
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
|
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
|
||||||
CacheReadInputTokens int `json:"cache_read_input_tokens"`
|
CacheReadInputTokens int `json:"cache_read_input_tokens"`
|
||||||
OutputTokens int `json:"output_tokens"`
|
OutputTokens int `json:"output_tokens"`
|
||||||
ServerToolUse *ClaudeServerToolUse `json:"server_tool_use,omitempty"`
|
CacheCreation *ClaudeCacheCreationUsage `json:"cache_creation,omitempty"`
|
||||||
|
// claude cache 1h
|
||||||
|
ClaudeCacheCreation5mTokens int `json:"claude_cache_creation_5_m_tokens"`
|
||||||
|
ClaudeCacheCreation1hTokens int `json:"claude_cache_creation_1_h_tokens"`
|
||||||
|
ServerToolUse *ClaudeServerToolUse `json:"server_tool_use,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaudeCacheCreationUsage struct {
|
||||||
|
Ephemeral5mInputTokens int `json:"ephemeral_5m_input_tokens,omitempty"`
|
||||||
|
Ephemeral1hInputTokens int `json:"ephemeral_1h_input_tokens,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClaudeUsage) GetCacheCreation5mTokens() int {
|
||||||
|
if u == nil || u.CacheCreation == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return u.CacheCreation.Ephemeral5mInputTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClaudeUsage) GetCacheCreation1hTokens() int {
|
||||||
|
if u == nil || u.CacheCreation == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return u.CacheCreation.Ephemeral1hInputTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClaudeUsage) GetCacheCreationTotalTokens() int {
|
||||||
|
if u == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if u.CacheCreationInputTokens > 0 {
|
||||||
|
return u.CacheCreationInputTokens
|
||||||
|
}
|
||||||
|
return u.GetCacheCreation5mTokens() + u.GetCacheCreation1hTokens()
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClaudeServerToolUse struct {
|
type ClaudeServerToolUse struct {
|
||||||
|
|||||||
@@ -232,10 +232,13 @@ func (r *GeneralOpenAIRequest) GetSystemRoleName() string {
|
|||||||
return "system"
|
return "system"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CustomType = "custom"
|
||||||
|
|
||||||
type ToolCallRequest struct {
|
type ToolCallRequest struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Function FunctionRequest `json:"function"`
|
Function FunctionRequest `json:"function,omitempty"`
|
||||||
|
Custom json.RawMessage `json:"custom,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FunctionRequest struct {
|
type FunctionRequest struct {
|
||||||
|
|||||||
@@ -230,6 +230,11 @@ type Usage struct {
|
|||||||
InputTokens int `json:"input_tokens"`
|
InputTokens int `json:"input_tokens"`
|
||||||
OutputTokens int `json:"output_tokens"`
|
OutputTokens int `json:"output_tokens"`
|
||||||
InputTokensDetails *InputTokenDetails `json:"input_tokens_details"`
|
InputTokensDetails *InputTokenDetails `json:"input_tokens_details"`
|
||||||
|
|
||||||
|
// claude cache 1h
|
||||||
|
ClaudeCacheCreation5mTokens int `json:"claude_cache_creation_5_m_tokens"`
|
||||||
|
ClaudeCacheCreation1hTokens int `json:"claude_cache_creation_1_h_tokens"`
|
||||||
|
|
||||||
// OpenRouter Params
|
// OpenRouter Params
|
||||||
Cost any `json:"cost,omitempty"`
|
Cost any `json:"cost,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
582
logger/logger.go
582
logger/logger.go
@@ -5,9 +5,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -19,79 +21,561 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
loggerINFO = "INFO"
|
// 日志轮转配置
|
||||||
loggerWarn = "WARN"
|
defaultMaxLogSize = 100 * 1024 * 1024 // 100MB
|
||||||
loggerError = "ERR"
|
defaultMaxLogFiles = 7 // 保留最近7个日志文件
|
||||||
loggerDebug = "DEBUG"
|
defaultLogFileName = "newapi.log"
|
||||||
|
checkRotateInterval = 1000 // 每1000次写入检查一次是否需要轮转
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxLogCount = 1000000
|
var (
|
||||||
|
logMutex sync.RWMutex
|
||||||
|
rotateCheckLock sync.Mutex
|
||||||
|
defaultLogger *slog.Logger
|
||||||
|
logFile *os.File
|
||||||
|
logFilePath string
|
||||||
|
logDirPath string
|
||||||
|
writeCount int64
|
||||||
|
maxLogSize int64 = defaultMaxLogSize
|
||||||
|
maxLogFiles int = defaultMaxLogFiles
|
||||||
|
useJSONFormat bool
|
||||||
|
)
|
||||||
|
|
||||||
var logCount int
|
func init() {
|
||||||
var setupLogLock sync.Mutex
|
// Initialize with a text handler to stdout
|
||||||
var setupLogWorking bool
|
handler := createHandler(os.Stdout)
|
||||||
|
defaultLogger = slog.New(handler)
|
||||||
|
slog.SetDefault(defaultLogger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupLogger 初始化日志系统
|
||||||
func SetupLogger() {
|
func SetupLogger() {
|
||||||
defer func() {
|
logMutex.Lock()
|
||||||
setupLogWorking = false
|
defer logMutex.Unlock()
|
||||||
}()
|
|
||||||
if *common.LogDir != "" {
|
// 读取环境变量配置
|
||||||
ok := setupLogLock.TryLock()
|
if maxSize := os.Getenv("LOG_MAX_SIZE_MB"); maxSize != "" {
|
||||||
if !ok {
|
if size, err := fmt.Sscanf(maxSize, "%d", &maxLogSize); err == nil && size > 0 {
|
||||||
log.Println("setup log is already working")
|
maxLogSize = maxLogSize * 1024 * 1024 // 转换为字节
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer func() {
|
}
|
||||||
setupLogLock.Unlock()
|
if maxFiles := os.Getenv("LOG_MAX_FILES"); maxFiles != "" {
|
||||||
}()
|
fmt.Sscanf(maxFiles, "%d", &maxLogFiles)
|
||||||
logPath := filepath.Join(*common.LogDir, fmt.Sprintf("oneapi-%s.log", time.Now().Format("20060102150405")))
|
}
|
||||||
fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
if os.Getenv("LOG_FORMAT") == "json" {
|
||||||
if err != nil {
|
useJSONFormat = true
|
||||||
log.Fatal("failed to open log file")
|
}
|
||||||
|
|
||||||
|
if *common.LogDir == "" {
|
||||||
|
// 如果没有配置日志目录,只输出到标准输出
|
||||||
|
handler := createHandler(os.Stdout)
|
||||||
|
defaultLogger = slog.New(handler)
|
||||||
|
slog.SetDefault(defaultLogger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logDirPath = *common.LogDir
|
||||||
|
logFilePath = filepath.Join(logDirPath, defaultLogFileName)
|
||||||
|
|
||||||
|
// 检查日志文件是否需要按日期轮转(仅在启动时检查)
|
||||||
|
if err := checkAndRotateOnStartup(); err != nil {
|
||||||
|
slog.Error("failed to check log file on startup", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开或创建日志文件
|
||||||
|
if err := openLogFile(); err != nil {
|
||||||
|
slog.Error("failed to open log file", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建多路输出(控制台 + 文件)
|
||||||
|
multiWriter := io.MultiWriter(os.Stdout, logFile)
|
||||||
|
|
||||||
|
// 更新 gin 的默认输出
|
||||||
|
gin.DefaultWriter = multiWriter
|
||||||
|
gin.DefaultErrorWriter = multiWriter
|
||||||
|
|
||||||
|
// 更新 slog handler
|
||||||
|
handler := createHandler(multiWriter)
|
||||||
|
defaultLogger = slog.New(handler)
|
||||||
|
slog.SetDefault(defaultLogger)
|
||||||
|
|
||||||
|
slog.Info("logger initialized",
|
||||||
|
"log_dir", logDirPath,
|
||||||
|
"max_size_mb", maxLogSize/(1024*1024),
|
||||||
|
"max_files", maxLogFiles,
|
||||||
|
"format", getLogFormat())
|
||||||
|
}
|
||||||
|
|
||||||
|
// createHandler 创建日志处理器
|
||||||
|
func createHandler(w io.Writer) slog.Handler {
|
||||||
|
if useJSONFormat {
|
||||||
|
opts := &slog.HandlerOptions{
|
||||||
|
Level: getLogLevel(),
|
||||||
}
|
}
|
||||||
gin.DefaultWriter = io.MultiWriter(os.Stdout, fd)
|
return slog.NewJSONHandler(w, opts)
|
||||||
gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd)
|
}
|
||||||
|
return NewReadableTextHandler(w, getLogLevel())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadableTextHandler 自定义的易读文本处理器
|
||||||
|
type ReadableTextHandler struct {
|
||||||
|
w io.Writer
|
||||||
|
level slog.Level
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReadableTextHandler 创建一个新的易读文本处理器
|
||||||
|
func NewReadableTextHandler(w io.Writer, level slog.Level) *ReadableTextHandler {
|
||||||
|
return &ReadableTextHandler{
|
||||||
|
w: w,
|
||||||
|
level: level,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogInfo(ctx context.Context, msg string) {
|
// Enabled 检查是否启用该级别
|
||||||
logHelper(ctx, loggerINFO, msg)
|
func (h *ReadableTextHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||||
|
return level >= h.level
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogWarn(ctx context.Context, msg string) {
|
// Handle 处理日志记录
|
||||||
logHelper(ctx, loggerWarn, msg)
|
func (h *ReadableTextHandler) Handle(_ context.Context, r slog.Record) error {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
// 格式: [LEVEL] YYYY/MM/DD - HH:mm:ss | request_id | message | key=value ...
|
||||||
|
buf := make([]byte, 0, 256)
|
||||||
|
|
||||||
|
// 日志级别
|
||||||
|
level := r.Level.String()
|
||||||
|
switch r.Level {
|
||||||
|
case slog.LevelDebug:
|
||||||
|
level = "DEBUG"
|
||||||
|
case slog.LevelInfo:
|
||||||
|
level = "INFO"
|
||||||
|
case slog.LevelWarn:
|
||||||
|
level = "WARN"
|
||||||
|
case slog.LevelError:
|
||||||
|
level = "ERROR"
|
||||||
|
}
|
||||||
|
buf = append(buf, '[')
|
||||||
|
buf = append(buf, level...)
|
||||||
|
buf = append(buf, "] "...)
|
||||||
|
|
||||||
|
// 时间
|
||||||
|
buf = append(buf, r.Time.Format("2006/01/02 - 15:04:05")...)
|
||||||
|
buf = append(buf, " | "...)
|
||||||
|
|
||||||
|
// 提取 request_id 和 component
|
||||||
|
var requestID, component string
|
||||||
|
otherAttrs := make([]slog.Attr, 0)
|
||||||
|
|
||||||
|
r.Attrs(func(a slog.Attr) bool {
|
||||||
|
switch a.Key {
|
||||||
|
case "request_id":
|
||||||
|
requestID = a.Value.String()
|
||||||
|
case "component":
|
||||||
|
component = a.Value.String()
|
||||||
|
default:
|
||||||
|
otherAttrs = append(otherAttrs, a)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 输出 request_id 或 component
|
||||||
|
if requestID != "" {
|
||||||
|
buf = append(buf, requestID...)
|
||||||
|
buf = append(buf, " | "...)
|
||||||
|
} else if component != "" {
|
||||||
|
buf = append(buf, component...)
|
||||||
|
buf = append(buf, " | "...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息
|
||||||
|
buf = append(buf, r.Message...)
|
||||||
|
|
||||||
|
// 其他属性
|
||||||
|
if len(otherAttrs) > 0 {
|
||||||
|
buf = append(buf, " | "...)
|
||||||
|
for i, a := range otherAttrs {
|
||||||
|
if i > 0 {
|
||||||
|
buf = append(buf, ", "...)
|
||||||
|
}
|
||||||
|
buf = append(buf, a.Key...)
|
||||||
|
buf = append(buf, '=')
|
||||||
|
buf = appendValue(buf, a.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = append(buf, '\n')
|
||||||
|
_, err := h.w.Write(buf)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogError(ctx context.Context, msg string) {
|
// appendValue 追加值到缓冲区
|
||||||
logHelper(ctx, loggerError, msg)
|
func appendValue(buf []byte, v slog.Value) []byte {
|
||||||
|
switch v.Kind() {
|
||||||
|
case slog.KindString:
|
||||||
|
s := v.String()
|
||||||
|
// 如果字符串包含空格或特殊字符,加引号
|
||||||
|
if strings.ContainsAny(s, " \t\n\r,=") {
|
||||||
|
buf = append(buf, '"')
|
||||||
|
buf = append(buf, s...)
|
||||||
|
buf = append(buf, '"')
|
||||||
|
} else {
|
||||||
|
buf = append(buf, s...)
|
||||||
|
}
|
||||||
|
case slog.KindInt64:
|
||||||
|
buf = append(buf, fmt.Sprintf("%d", v.Int64())...)
|
||||||
|
case slog.KindUint64:
|
||||||
|
buf = append(buf, fmt.Sprintf("%d", v.Uint64())...)
|
||||||
|
case slog.KindFloat64:
|
||||||
|
buf = append(buf, fmt.Sprintf("%g", v.Float64())...)
|
||||||
|
case slog.KindBool:
|
||||||
|
buf = append(buf, fmt.Sprintf("%t", v.Bool())...)
|
||||||
|
case slog.KindDuration:
|
||||||
|
buf = append(buf, v.Duration().String()...)
|
||||||
|
case slog.KindTime:
|
||||||
|
buf = append(buf, v.Time().Format("2006-01-02 15:04:05")...)
|
||||||
|
default:
|
||||||
|
buf = append(buf, fmt.Sprintf("%v", v.Any())...)
|
||||||
|
}
|
||||||
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogDebug(ctx context.Context, msg string, args ...any) {
|
// WithAttrs 返回一个新的处理器,包含指定的属性
|
||||||
msg = fmt.Sprintf(msg, args...)
|
func (h *ReadableTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
// 简化实现:不支持 With
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGroup 返回一个新的处理器,使用指定的组
|
||||||
|
func (h *ReadableTextHandler) WithGroup(name string) slog.Handler {
|
||||||
|
// 简化实现:不支持组
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAndRotateOnStartup 启动时检查日志文件是否需要按日期轮转
|
||||||
|
func checkAndRotateOnStartup() error {
|
||||||
|
// 检查日志文件是否存在
|
||||||
|
fileInfo, err := os.Stat(logFilePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// 文件不存在,不需要轮转
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to stat log file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件的修改时间
|
||||||
|
modTime := fileInfo.ModTime()
|
||||||
|
modDate := modTime.Format("2006-01-02")
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
|
||||||
|
// 如果文件的日期和今天不同,进行轮转
|
||||||
|
if modDate != today {
|
||||||
|
// 生成归档文件名(使用文件的修改日期)
|
||||||
|
timestamp := modTime.Format("2006-01-02-150405")
|
||||||
|
archivePath := filepath.Join(logDirPath, fmt.Sprintf("newapi.%s.log", timestamp))
|
||||||
|
|
||||||
|
// 重命名日志文件
|
||||||
|
if err := os.Rename(logFilePath, archivePath); err != nil {
|
||||||
|
return fmt.Errorf("failed to archive old log file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("rotated old log file on startup",
|
||||||
|
"archive", archivePath,
|
||||||
|
"reason", "date changed")
|
||||||
|
|
||||||
|
// 清理旧的日志文件
|
||||||
|
gopool.Go(func() {
|
||||||
|
cleanOldLogFiles()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// openLogFile 打开日志文件
|
||||||
|
func openLogFile() error {
|
||||||
|
// 关闭旧的日志文件
|
||||||
|
if logFile != nil {
|
||||||
|
logFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开新的日志文件
|
||||||
|
fd, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open log file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logFile = fd
|
||||||
|
writeCount = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotateLogFile 轮转日志文件
|
||||||
|
func rotateLogFile() error {
|
||||||
|
if logFile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateCheckLock.Lock()
|
||||||
|
defer rotateCheckLock.Unlock()
|
||||||
|
|
||||||
|
// 获取当前日志文件信息
|
||||||
|
fileInfo, err := logFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat log file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小是否需要轮转
|
||||||
|
if fileInfo.Size() < maxLogSize {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭当前日志文件
|
||||||
|
logFile.Close()
|
||||||
|
|
||||||
|
// 生成归档文件名
|
||||||
|
timestamp := time.Now().Format("2006-01-02-150405")
|
||||||
|
archivePath := filepath.Join(logDirPath, fmt.Sprintf("newapi.%s.log", timestamp))
|
||||||
|
|
||||||
|
// 重命名当前日志文件为归档文件
|
||||||
|
if err := os.Rename(logFilePath, archivePath); err != nil {
|
||||||
|
// 如果重命名失败,尝试复制
|
||||||
|
if copyErr := copyFile(logFilePath, archivePath); copyErr != nil {
|
||||||
|
return fmt.Errorf("failed to archive log file: %w", err)
|
||||||
|
}
|
||||||
|
os.Truncate(logFilePath, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理旧的日志文件
|
||||||
|
gopool.Go(func() {
|
||||||
|
cleanOldLogFiles()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打开新的日志文件
|
||||||
|
if err := openLogFile(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新设置日志输出
|
||||||
|
multiWriter := io.MultiWriter(os.Stdout, logFile)
|
||||||
|
gin.DefaultWriter = multiWriter
|
||||||
|
gin.DefaultErrorWriter = multiWriter
|
||||||
|
|
||||||
|
handler := createHandler(multiWriter)
|
||||||
|
logMutex.Lock()
|
||||||
|
defaultLogger = slog.New(handler)
|
||||||
|
slog.SetDefault(defaultLogger)
|
||||||
|
logMutex.Unlock()
|
||||||
|
|
||||||
|
slog.Info("log file rotated",
|
||||||
|
"reason", "size limit reached",
|
||||||
|
"archive", archivePath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanOldLogFiles 清理旧的日志文件
|
||||||
|
func cleanOldLogFiles() {
|
||||||
|
if logDirPath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := os.ReadDir(logDirPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to read log directory", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有归档日志文件
|
||||||
|
var logFiles []os.DirEntry
|
||||||
|
for _, file := range files {
|
||||||
|
if !file.IsDir() && strings.HasPrefix(file.Name(), "newapi.") &&
|
||||||
|
strings.HasSuffix(file.Name(), ".log") &&
|
||||||
|
file.Name() != defaultLogFileName {
|
||||||
|
logFiles = append(logFiles, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果归档文件数量超过限制,删除最旧的
|
||||||
|
if len(logFiles) > maxLogFiles {
|
||||||
|
// 按名称排序(文件名包含时间戳)
|
||||||
|
sort.Slice(logFiles, func(i, j int) bool {
|
||||||
|
return logFiles[i].Name() < logFiles[j].Name()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除最旧的文件
|
||||||
|
deleteCount := len(logFiles) - maxLogFiles
|
||||||
|
for i := 0; i < deleteCount; i++ {
|
||||||
|
filePath := filepath.Join(logDirPath, logFiles[i].Name())
|
||||||
|
if err := os.Remove(filePath); err != nil {
|
||||||
|
slog.Error("failed to remove old log file",
|
||||||
|
"file", filePath,
|
||||||
|
"error", err)
|
||||||
|
} else {
|
||||||
|
slog.Info("removed old log file", "file", logFiles[i].Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile 复制文件
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
sourceFile, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(destFile, sourceFile)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLogLevel 获取日志级别
|
||||||
|
func getLogLevel() slog.Level {
|
||||||
|
// 支持环境变量配置
|
||||||
|
if level := os.Getenv("LOG_LEVEL"); level != "" {
|
||||||
|
switch strings.ToUpper(level) {
|
||||||
|
case "DEBUG":
|
||||||
|
return slog.LevelDebug
|
||||||
|
case "INFO":
|
||||||
|
return slog.LevelInfo
|
||||||
|
case "WARN", "WARNING":
|
||||||
|
return slog.LevelWarn
|
||||||
|
case "ERROR":
|
||||||
|
return slog.LevelError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if common.DebugEnabled {
|
if common.DebugEnabled {
|
||||||
logHelper(ctx, loggerDebug, msg)
|
return slog.LevelDebug
|
||||||
|
}
|
||||||
|
return slog.LevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLogFormat 获取日志格式
|
||||||
|
func getLogFormat() string {
|
||||||
|
if useJSONFormat {
|
||||||
|
return "json"
|
||||||
|
}
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAndRotateLog 检查并轮转日志
|
||||||
|
func checkAndRotateLog() {
|
||||||
|
if logFile == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeCount++
|
||||||
|
if writeCount%checkRotateInterval == 0 {
|
||||||
|
gopool.Go(func() {
|
||||||
|
if err := rotateLogFile(); err != nil {
|
||||||
|
slog.Error("failed to rotate log file", "error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logHelper(ctx context.Context, level string, msg string) {
|
// LogInfo 记录信息级别日志
|
||||||
writer := gin.DefaultErrorWriter
|
func LogInfo(ctx context.Context, msg string) {
|
||||||
if level == loggerINFO {
|
if ctx == nil {
|
||||||
writer = gin.DefaultWriter
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
id := getRequestID(ctx)
|
||||||
|
logMutex.RLock()
|
||||||
|
logger := defaultLogger
|
||||||
|
logMutex.RUnlock()
|
||||||
|
logger.InfoContext(ctx, msg, "request_id", id)
|
||||||
|
checkAndRotateLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogWarn 记录警告级别日志
|
||||||
|
func LogWarn(ctx context.Context, msg string) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
id := getRequestID(ctx)
|
||||||
|
logMutex.RLock()
|
||||||
|
logger := defaultLogger
|
||||||
|
logMutex.RUnlock()
|
||||||
|
logger.WarnContext(ctx, msg, "request_id", id)
|
||||||
|
checkAndRotateLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogError 记录错误级别日志
|
||||||
|
func LogError(ctx context.Context, msg string) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
id := getRequestID(ctx)
|
||||||
|
logMutex.RLock()
|
||||||
|
logger := defaultLogger
|
||||||
|
logMutex.RUnlock()
|
||||||
|
logger.ErrorContext(ctx, msg, "request_id", id)
|
||||||
|
checkAndRotateLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogSystemInfo 记录系统信息
|
||||||
|
func LogSystemInfo(msg string) {
|
||||||
|
logMutex.RLock()
|
||||||
|
logger := defaultLogger
|
||||||
|
logMutex.RUnlock()
|
||||||
|
logger.Info(msg, "request_id", "SYSTEM")
|
||||||
|
checkAndRotateLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogSystemError 记录系统错误
|
||||||
|
func LogSystemError(msg string) {
|
||||||
|
logMutex.RLock()
|
||||||
|
logger := defaultLogger
|
||||||
|
logMutex.RUnlock()
|
||||||
|
logger.Error(msg, "request_id", "SYSTEM")
|
||||||
|
checkAndRotateLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogDebug 记录调试级别日志
|
||||||
|
func LogDebug(ctx context.Context, msg string, args ...any) {
|
||||||
|
if !common.DebugEnabled && getLogLevel() > slog.LevelDebug {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
id := getRequestID(ctx)
|
||||||
|
if len(args) > 0 {
|
||||||
|
msg = fmt.Sprintf(msg, args...)
|
||||||
|
}
|
||||||
|
logMutex.RLock()
|
||||||
|
logger := defaultLogger
|
||||||
|
logMutex.RUnlock()
|
||||||
|
logger.DebugContext(ctx, msg, "request_id", id)
|
||||||
|
checkAndRotateLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRequestID 从上下文中获取请求ID
|
||||||
|
func getRequestID(ctx context.Context) string {
|
||||||
|
if ctx == nil {
|
||||||
|
return "SYSTEM"
|
||||||
}
|
}
|
||||||
id := ctx.Value(common.RequestIdKey)
|
id := ctx.Value(common.RequestIdKey)
|
||||||
if id == nil {
|
if id == nil {
|
||||||
id = "SYSTEM"
|
return "SYSTEM"
|
||||||
}
|
}
|
||||||
now := time.Now()
|
if strID, ok := id.(string); ok {
|
||||||
_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
|
return strID
|
||||||
logCount++ // we don't need accurate count, so no lock here
|
|
||||||
if logCount > maxLogCount && !setupLogWorking {
|
|
||||||
logCount = 0
|
|
||||||
setupLogWorking = true
|
|
||||||
gopool.Go(func() {
|
|
||||||
SetupLogger()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return "SYSTEM"
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogQuota(quota int) string {
|
func LogQuota(quota int) string {
|
||||||
|
|||||||
8
main.go
8
main.go
@@ -4,7 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -118,7 +118,9 @@ func main() {
|
|||||||
|
|
||||||
if os.Getenv("ENABLE_PPROF") == "true" {
|
if os.Getenv("ENABLE_PPROF") == "true" {
|
||||||
gopool.Go(func() {
|
gopool.Go(func() {
|
||||||
log.Println(http.ListenAndServe("0.0.0.0:8005", nil))
|
if err := http.ListenAndServe("0.0.0.0:8005", nil); err != nil {
|
||||||
|
slog.Error("pprof server failed", "error", err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
go common.Monitor()
|
go common.Monitor()
|
||||||
common.SysLog("pprof enabled")
|
common.SysLog("pprof enabled")
|
||||||
@@ -127,7 +129,7 @@ func main() {
|
|||||||
// Initialize HTTP server
|
// Initialize HTTP server
|
||||||
server := gin.New()
|
server := gin.New()
|
||||||
server.Use(gin.CustomRecovery(func(c *gin.Context, err any) {
|
server.Use(gin.CustomRecovery(func(c *gin.Context, err any) {
|
||||||
common.SysLog(fmt.Sprintf("panic detected: %v", err))
|
logger.LogSystemError(fmt.Sprintf("panic detected: %v", err))
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
|
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
|
||||||
|
|||||||
@@ -102,7 +102,10 @@ func GlobalAPIRateLimit() func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CriticalRateLimit() func(c *gin.Context) {
|
func CriticalRateLimit() func(c *gin.Context) {
|
||||||
return rateLimitFactory(common.CriticalRateLimitNum, common.CriticalRateLimitDuration, "CT")
|
if common.CriticalRateLimitEnable {
|
||||||
|
return rateLimitFactory(common.CriticalRateLimitNum, common.CriticalRateLimitDuration, "CT")
|
||||||
|
}
|
||||||
|
return defNext
|
||||||
}
|
}
|
||||||
|
|
||||||
func DownloadRateLimit() func(c *gin.Context) {
|
func DownloadRateLimit() func(c *gin.Context) {
|
||||||
|
|||||||
@@ -688,7 +688,7 @@ func DisableChannelByTag(tag string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *string, group *string, priority *int64, weight *uint) error {
|
func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *string, group *string, priority *int64, weight *uint, paramOverride *string, headerOverride *string) error {
|
||||||
updateData := Channel{}
|
updateData := Channel{}
|
||||||
shouldReCreateAbilities := false
|
shouldReCreateAbilities := false
|
||||||
updatedTag := tag
|
updatedTag := tag
|
||||||
@@ -714,6 +714,12 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
|
|||||||
if weight != nil {
|
if weight != nil {
|
||||||
updateData.Weight = weight
|
updateData.Weight = weight
|
||||||
}
|
}
|
||||||
|
if paramOverride != nil {
|
||||||
|
updateData.ParamOverride = paramOverride
|
||||||
|
}
|
||||||
|
if headerOverride != nil {
|
||||||
|
updateData.HeaderOverride = headerOverride
|
||||||
|
}
|
||||||
|
|
||||||
err := DB.Model(&Channel{}).Where("tag = ?", tag).Updates(updateData).Error
|
err := DB.Model(&Channel{}).Where("tag = ?", tag).Updates(updateData).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ func oaiFormEdit2AliImageEdit(c *gin.Context, info *relaycommon.RelayInfo, reque
|
|||||||
return nil, errors.New("image is required")
|
return nil, errors.New("image is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(imageFiles) > 1 {
|
//if len(imageFiles) > 1 {
|
||||||
return nil, errors.New("only one image is supported for qwen edit")
|
// return nil, errors.New("only one image is supported for qwen edit")
|
||||||
}
|
//}
|
||||||
|
|
||||||
// 获取base64编码的图片
|
// 获取base64编码的图片
|
||||||
var imageBase64s []string
|
var imageBase64s []string
|
||||||
|
|||||||
@@ -596,6 +596,8 @@ func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeRespons
|
|||||||
claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
|
claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
|
||||||
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
|
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
|
||||||
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
|
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
|
||||||
|
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Message.Usage.GetCacheCreation5mTokens()
|
||||||
|
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Message.Usage.GetCacheCreation1hTokens()
|
||||||
claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
|
claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
|
||||||
} else if claudeResponse.Type == "content_block_delta" {
|
} else if claudeResponse.Type == "content_block_delta" {
|
||||||
if claudeResponse.Delta.Text != nil {
|
if claudeResponse.Delta.Text != nil {
|
||||||
@@ -740,6 +742,8 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
|
|||||||
claudeInfo.Usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens
|
claudeInfo.Usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens
|
||||||
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
|
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
|
||||||
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
|
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
|
||||||
|
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Usage.GetCacheCreation5mTokens()
|
||||||
|
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Usage.GetCacheCreation1hTokens()
|
||||||
}
|
}
|
||||||
var responseData []byte
|
var responseData []byte
|
||||||
switch info.RelayFormat {
|
switch info.RelayFormat {
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re
|
|||||||
var usage = &dto.Usage{}
|
var usage = &dto.Usage{}
|
||||||
var streamItems []string // store stream items
|
var streamItems []string // store stream items
|
||||||
var lastStreamData string
|
var lastStreamData string
|
||||||
|
var secondLastStreamData string // 存储倒数第二个stream data,用于音频模型
|
||||||
|
|
||||||
|
// 检查是否为音频模型
|
||||||
|
isAudioModel := strings.Contains(strings.ToLower(model), "audio")
|
||||||
|
|
||||||
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
||||||
if lastStreamData != "" {
|
if lastStreamData != "" {
|
||||||
@@ -131,12 +135,35 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(data) > 0 {
|
if len(data) > 0 {
|
||||||
|
// 对音频模型,保存倒数第二个stream data
|
||||||
|
if isAudioModel && lastStreamData != "" {
|
||||||
|
secondLastStreamData = lastStreamData
|
||||||
|
}
|
||||||
|
|
||||||
lastStreamData = data
|
lastStreamData = data
|
||||||
streamItems = append(streamItems, data)
|
streamItems = append(streamItems, data)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 对音频模型,从倒数第二个stream data中提取usage信息
|
||||||
|
if isAudioModel && secondLastStreamData != "" {
|
||||||
|
var streamResp struct {
|
||||||
|
Usage *dto.Usage `json:"usage"`
|
||||||
|
}
|
||||||
|
err := json.Unmarshal([]byte(secondLastStreamData), &streamResp)
|
||||||
|
if err == nil && streamResp.Usage != nil && service.ValidUsage(streamResp.Usage) {
|
||||||
|
usage = streamResp.Usage
|
||||||
|
containStreamUsage = true
|
||||||
|
|
||||||
|
if common.DebugEnabled {
|
||||||
|
logger.LogDebug(c, fmt.Sprintf("Audio model usage extracted from second last SSE: PromptTokens=%d, CompletionTokens=%d, TotalTokens=%d, InputTokens=%d, OutputTokens=%d",
|
||||||
|
usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens,
|
||||||
|
usage.InputTokens, usage.OutputTokens))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理最后的响应
|
// 处理最后的响应
|
||||||
shouldSendLastResp := true
|
shouldSendLastResp := true
|
||||||
if err := handleLastResponse(lastStreamData, &responseId, &createAt, &systemFingerprint, &model, &usage,
|
if err := handleLastResponse(lastStreamData, &responseId, &createAt, &systemFingerprint, &model, &usage,
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/QuantumNous/new-api/common"
|
"github.com/QuantumNous/new-api/common"
|
||||||
"github.com/QuantumNous/new-api/dto"
|
"github.com/QuantumNous/new-api/dto"
|
||||||
|
"github.com/QuantumNous/new-api/logger"
|
||||||
"github.com/QuantumNous/new-api/model"
|
"github.com/QuantumNous/new-api/model"
|
||||||
"github.com/QuantumNous/new-api/relay/channel"
|
"github.com/QuantumNous/new-api/relay/channel"
|
||||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||||
"github.com/QuantumNous/new-api/service"
|
"github.com/QuantumNous/new-api/service"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -108,6 +111,7 @@ type TaskAdaptor struct {
|
|||||||
ChannelType int
|
ChannelType int
|
||||||
apiKey string
|
apiKey string
|
||||||
baseURL string
|
baseURL string
|
||||||
|
aliReq *AliVideoRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
|
func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
|
||||||
@@ -118,6 +122,16 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
|
|||||||
|
|
||||||
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
|
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
|
||||||
// 阿里通义万相支持 JSON 格式,不使用 multipart
|
// 阿里通义万相支持 JSON 格式,不使用 multipart
|
||||||
|
var taskReq relaycommon.TaskSubmitReq
|
||||||
|
if err := common.UnmarshalBodyReusable(c, &taskReq); err != nil {
|
||||||
|
return service.TaskErrorWrapper(err, "unmarshal_task_request_failed", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
aliReq, err := a.convertToAliRequest(info, taskReq)
|
||||||
|
if err != nil {
|
||||||
|
return service.TaskErrorWrapper(err, "convert_to_ali_request_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
a.aliReq = aliReq
|
||||||
|
logger.LogJson(c, "ali video request body", aliReq)
|
||||||
return relaycommon.ValidateMultipartDirect(c, info)
|
return relaycommon.ValidateMultipartDirect(c, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,13 +148,7 @@ func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) {
|
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) {
|
||||||
var taskReq relaycommon.TaskSubmitReq
|
bodyBytes, err := common.Marshal(a.aliReq)
|
||||||
if err := common.UnmarshalBodyReusable(c, &taskReq); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "unmarshal_task_request_failed")
|
|
||||||
}
|
|
||||||
aliReq := a.convertToAliRequest(taskReq)
|
|
||||||
|
|
||||||
bodyBytes, err := common.Marshal(aliReq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "marshal_ali_request_failed")
|
return nil, errors.Wrap(err, "marshal_ali_request_failed")
|
||||||
}
|
}
|
||||||
@@ -148,7 +156,98 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn
|
|||||||
return bytes.NewReader(bodyBytes), nil
|
return bytes.NewReader(bodyBytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TaskAdaptor) convertToAliRequest(req relaycommon.TaskSubmitReq) *AliVideoRequest {
|
var (
|
||||||
|
size480p = []string{
|
||||||
|
"832*480",
|
||||||
|
"480*832",
|
||||||
|
"624*624",
|
||||||
|
}
|
||||||
|
size720p = []string{
|
||||||
|
"1280*720",
|
||||||
|
"720*1280",
|
||||||
|
"960*960",
|
||||||
|
"1088*832",
|
||||||
|
"832*1088",
|
||||||
|
}
|
||||||
|
size1080p = []string{
|
||||||
|
"1920*1080",
|
||||||
|
"1080*1920",
|
||||||
|
"1440*1440",
|
||||||
|
"1632*1248",
|
||||||
|
"1248*1632",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func sizeToResolution(size string) (string, error) {
|
||||||
|
if lo.Contains(size480p, size) {
|
||||||
|
return "480P", nil
|
||||||
|
} else if lo.Contains(size720p, size) {
|
||||||
|
return "720P", nil
|
||||||
|
} else if lo.Contains(size1080p, size) {
|
||||||
|
return "1080P", nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("invalid size: %s", size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessAliOtherRatios(aliReq *AliVideoRequest) (map[string]float64, error) {
|
||||||
|
otherRatios := make(map[string]float64)
|
||||||
|
aliRatios := map[string]map[string]float64{
|
||||||
|
"wan2.5-t2v-preview": {
|
||||||
|
"480P": 1,
|
||||||
|
"720P": 2,
|
||||||
|
"1080P": 1 / 0.3,
|
||||||
|
},
|
||||||
|
"wan2.2-t2v-plus": {
|
||||||
|
"480P": 1,
|
||||||
|
"1080P": 0.7 / 0.14,
|
||||||
|
},
|
||||||
|
"wan2.5-i2v-preview": {
|
||||||
|
"480P": 1,
|
||||||
|
"720P": 2,
|
||||||
|
"1080P": 1 / 0.3,
|
||||||
|
},
|
||||||
|
"wan2.2-i2v-plus": {
|
||||||
|
"480P": 1,
|
||||||
|
"1080P": 0.7 / 0.14,
|
||||||
|
},
|
||||||
|
"wan2.2-kf2v-flash": {
|
||||||
|
"480P": 1,
|
||||||
|
"720P": 2,
|
||||||
|
"1080P": 4.8,
|
||||||
|
},
|
||||||
|
"wan2.2-i2v-flash": {
|
||||||
|
"480P": 1,
|
||||||
|
"720P": 2,
|
||||||
|
},
|
||||||
|
"wan2.2-s2v": {
|
||||||
|
"480P": 1,
|
||||||
|
"720P": 0.9 / 0.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var resolution string
|
||||||
|
|
||||||
|
// size match
|
||||||
|
if aliReq.Parameters.Size != "" {
|
||||||
|
toResolution, err := sizeToResolution(aliReq.Parameters.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resolution = toResolution
|
||||||
|
} else {
|
||||||
|
resolution = strings.ToUpper(aliReq.Parameters.Resolution)
|
||||||
|
if !strings.HasSuffix(resolution, "P") {
|
||||||
|
resolution = resolution + "P"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if otherRatio, ok := aliRatios[aliReq.Model]; ok {
|
||||||
|
if ratio, ok := otherRatio[resolution]; ok {
|
||||||
|
otherRatios[fmt.Sprintf("resolution-%s", resolution)] = ratio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return otherRatios, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TaskAdaptor) convertToAliRequest(info *relaycommon.RelayInfo, req relaycommon.TaskSubmitReq) (*AliVideoRequest, error) {
|
||||||
aliReq := &AliVideoRequest{
|
aliReq := &AliVideoRequest{
|
||||||
Model: req.Model,
|
Model: req.Model,
|
||||||
Input: AliVideoInput{
|
Input: AliVideoInput{
|
||||||
@@ -163,28 +262,53 @@ func (a *TaskAdaptor) convertToAliRequest(req relaycommon.TaskSubmitReq) *AliVid
|
|||||||
|
|
||||||
// 处理分辨率映射
|
// 处理分辨率映射
|
||||||
if req.Size != "" {
|
if req.Size != "" {
|
||||||
resolution := strings.ToUpper(req.Size)
|
// text to video size must be contained *
|
||||||
// 支持 480p, 720p, 1080p 或 480P, 720P, 1080P
|
if strings.Contains(req.Model, "t2v") && !strings.Contains(req.Size, "*") {
|
||||||
if !strings.HasSuffix(resolution, "P") {
|
return nil, fmt.Errorf("invalid size: %s, example: %s", req.Size, "1920*1080")
|
||||||
resolution = resolution + "P"
|
}
|
||||||
|
if strings.Contains(req.Size, "*") {
|
||||||
|
aliReq.Parameters.Size = req.Size
|
||||||
|
} else {
|
||||||
|
resolution := strings.ToUpper(req.Size)
|
||||||
|
// 支持 480p, 720p, 1080p 或 480P, 720P, 1080P
|
||||||
|
if !strings.HasSuffix(resolution, "P") {
|
||||||
|
resolution = resolution + "P"
|
||||||
|
}
|
||||||
|
aliReq.Parameters.Resolution = resolution
|
||||||
}
|
}
|
||||||
aliReq.Parameters.Resolution = resolution
|
|
||||||
} else {
|
} else {
|
||||||
// 根据模型设置默认分辨率
|
// 根据模型设置默认分辨率
|
||||||
if strings.HasPrefix(req.Model, "wan2.5") {
|
if strings.Contains(req.Model, "t2v") { // image to video
|
||||||
aliReq.Parameters.Resolution = "1080P"
|
if strings.HasPrefix(req.Model, "wan2.5") {
|
||||||
} else if strings.HasPrefix(req.Model, "wan2.2-i2v-flash") {
|
aliReq.Parameters.Size = "1920*1080"
|
||||||
aliReq.Parameters.Resolution = "720P"
|
} else if strings.HasPrefix(req.Model, "wan2.2") {
|
||||||
} else if strings.HasPrefix(req.Model, "wan2.2-i2v-plus") {
|
aliReq.Parameters.Size = "1920*1080"
|
||||||
aliReq.Parameters.Resolution = "1080P"
|
} else {
|
||||||
|
aliReq.Parameters.Size = "1280*720"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
aliReq.Parameters.Resolution = "720P"
|
if strings.HasPrefix(req.Model, "wan2.5") {
|
||||||
|
aliReq.Parameters.Resolution = "1080P"
|
||||||
|
} else if strings.HasPrefix(req.Model, "wan2.2-i2v-flash") {
|
||||||
|
aliReq.Parameters.Resolution = "720P"
|
||||||
|
} else if strings.HasPrefix(req.Model, "wan2.2-i2v-plus") {
|
||||||
|
aliReq.Parameters.Resolution = "1080P"
|
||||||
|
} else {
|
||||||
|
aliReq.Parameters.Resolution = "720P"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理时长
|
// 处理时长
|
||||||
if req.Duration > 0 {
|
if req.Duration > 0 {
|
||||||
aliReq.Parameters.Duration = req.Duration
|
aliReq.Parameters.Duration = req.Duration
|
||||||
|
} else if req.Seconds != "" {
|
||||||
|
seconds, err := strconv.Atoi(req.Seconds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "convert seconds to int failed")
|
||||||
|
} else {
|
||||||
|
aliReq.Parameters.Duration = seconds
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
aliReq.Parameters.Duration = 5 // 默认5秒
|
aliReq.Parameters.Duration = 5 // 默认5秒
|
||||||
}
|
}
|
||||||
@@ -192,11 +316,32 @@ func (a *TaskAdaptor) convertToAliRequest(req relaycommon.TaskSubmitReq) *AliVid
|
|||||||
// 从 metadata 中提取额外参数
|
// 从 metadata 中提取额外参数
|
||||||
if req.Metadata != nil {
|
if req.Metadata != nil {
|
||||||
if metadataBytes, err := common.Marshal(req.Metadata); err == nil {
|
if metadataBytes, err := common.Marshal(req.Metadata); err == nil {
|
||||||
_ = common.Unmarshal(metadataBytes, aliReq)
|
err = common.Unmarshal(metadataBytes, aliReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unmarshal metadata failed")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, errors.Wrap(err, "marshal metadata failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return aliReq
|
if aliReq.Model != req.Model {
|
||||||
|
return nil, errors.New("can't change model with metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
info.PriceData.OtherRatios = map[string]float64{
|
||||||
|
"seconds": float64(aliReq.Parameters.Duration),
|
||||||
|
}
|
||||||
|
|
||||||
|
ratios, err := ProcessAliOtherRatios(aliReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for s, f := range ratios {
|
||||||
|
info.PriceData.OtherRatios[s] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoRequest delegates to common helper
|
// DoRequest delegates to common helper
|
||||||
|
|||||||
@@ -406,12 +406,15 @@ func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*
|
|||||||
// 即梦视频3.0 ReqKey转换
|
// 即梦视频3.0 ReqKey转换
|
||||||
// https://www.volcengine.com/docs/85621/1792707
|
// https://www.volcengine.com/docs/85621/1792707
|
||||||
if strings.Contains(r.ReqKey, "jimeng_v30") {
|
if strings.Contains(r.ReqKey, "jimeng_v30") {
|
||||||
if len(req.Images) > 1 {
|
if r.ReqKey == "jimeng_v30_pro" {
|
||||||
|
// 3.0 pro只有固定的jimeng_ti2v_v30_pro
|
||||||
|
r.ReqKey = "jimeng_ti2v_v30_pro"
|
||||||
|
} else if len(req.Images) > 1 {
|
||||||
// 多张图片:首尾帧生成
|
// 多张图片:首尾帧生成
|
||||||
r.ReqKey = strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_tail_v30", 1)
|
r.ReqKey = strings.TrimSuffix(strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_tail_v30", 1), "p")
|
||||||
} else if len(req.Images) == 1 {
|
} else if len(req.Images) == 1 {
|
||||||
// 单张图片:图生视频
|
// 单张图片:图生视频
|
||||||
r.ReqKey = strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_v30", 1)
|
r.ReqKey = strings.TrimSuffix(strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_v30", 1), "p")
|
||||||
} else {
|
} else {
|
||||||
// 无图片:文生视频
|
// 无图片:文生视频
|
||||||
r.ReqKey = strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_t2v_v30", 1)
|
r.ReqKey = strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_t2v_v30", 1)
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ func ValidateMultipartDirect(c *gin.Context, info *RelayInfo) *dto.TaskError {
|
|||||||
|
|
||||||
prompt = req.Prompt
|
prompt = req.Prompt
|
||||||
model = req.Model
|
model = req.Model
|
||||||
|
size = req.Size
|
||||||
seconds, _ = strconv.Atoi(req.Seconds)
|
seconds, _ = strconv.Atoi(req.Seconds)
|
||||||
if seconds == 0 {
|
if seconds == 0 {
|
||||||
seconds = req.Duration
|
seconds = req.Duration
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// https://docs.claude.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration
|
||||||
|
const claudeCacheCreation1hMultiplier = 6 / 3.75
|
||||||
|
|
||||||
// HandleGroupRatio checks for "auto_group" in the context and updates the group ratio and relayInfo.UsingGroup if present
|
// HandleGroupRatio checks for "auto_group" in the context and updates the group ratio and relayInfo.UsingGroup if present
|
||||||
func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.GroupRatioInfo {
|
func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.GroupRatioInfo {
|
||||||
groupRatioInfo := types.GroupRatioInfo{
|
groupRatioInfo := types.GroupRatioInfo{
|
||||||
@@ -53,6 +56,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
|||||||
var cacheRatio float64
|
var cacheRatio float64
|
||||||
var imageRatio float64
|
var imageRatio float64
|
||||||
var cacheCreationRatio float64
|
var cacheCreationRatio float64
|
||||||
|
var cacheCreationRatio5m float64
|
||||||
|
var cacheCreationRatio1h float64
|
||||||
var audioRatio float64
|
var audioRatio float64
|
||||||
var audioCompletionRatio float64
|
var audioCompletionRatio float64
|
||||||
var freeModel bool
|
var freeModel bool
|
||||||
@@ -76,6 +81,9 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
|||||||
completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName)
|
completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName)
|
||||||
cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName)
|
cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName)
|
||||||
cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName)
|
cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName)
|
||||||
|
cacheCreationRatio5m = cacheCreationRatio
|
||||||
|
// 固定1h和5min缓存写入价格的比例
|
||||||
|
cacheCreationRatio1h = cacheCreationRatio * claudeCacheCreation1hMultiplier
|
||||||
imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName)
|
imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName)
|
||||||
audioRatio = ratio_setting.GetAudioRatio(info.OriginModelName)
|
audioRatio = ratio_setting.GetAudioRatio(info.OriginModelName)
|
||||||
audioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName)
|
audioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName)
|
||||||
@@ -116,6 +124,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
|||||||
AudioRatio: audioRatio,
|
AudioRatio: audioRatio,
|
||||||
AudioCompletionRatio: audioCompletionRatio,
|
AudioCompletionRatio: audioCompletionRatio,
|
||||||
CacheCreationRatio: cacheCreationRatio,
|
CacheCreationRatio: cacheCreationRatio,
|
||||||
|
CacheCreation5mRatio: cacheCreationRatio5m,
|
||||||
|
CacheCreation1hRatio: cacheCreationRatio1h,
|
||||||
QuotaToPreConsume: preConsumedQuota,
|
QuotaToPreConsume: preConsumedQuota,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,11 +92,23 @@ func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GenerateClaudeOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64,
|
func GenerateClaudeOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64,
|
||||||
cacheTokens int, cacheRatio float64, cacheCreationTokens int, cacheCreationRatio float64, modelPrice float64, userGroupRatio float64) map[string]interface{} {
|
cacheTokens int, cacheRatio float64,
|
||||||
|
cacheCreationTokens int, cacheCreationRatio float64,
|
||||||
|
cacheCreationTokens5m int, cacheCreationRatio5m float64,
|
||||||
|
cacheCreationTokens1h int, cacheCreationRatio1h float64,
|
||||||
|
modelPrice float64, userGroupRatio float64) map[string]interface{} {
|
||||||
info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice, userGroupRatio)
|
info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice, userGroupRatio)
|
||||||
info["claude"] = true
|
info["claude"] = true
|
||||||
info["cache_creation_tokens"] = cacheCreationTokens
|
info["cache_creation_tokens"] = cacheCreationTokens
|
||||||
info["cache_creation_ratio"] = cacheCreationRatio
|
info["cache_creation_ratio"] = cacheCreationRatio
|
||||||
|
if cacheCreationTokens5m != 0 {
|
||||||
|
info["cache_creation_tokens_5m"] = cacheCreationTokens5m
|
||||||
|
info["cache_creation_ratio_5m"] = cacheCreationRatio5m
|
||||||
|
}
|
||||||
|
if cacheCreationTokens1h != 0 {
|
||||||
|
info["cache_creation_tokens_1h"] = cacheCreationTokens1h
|
||||||
|
info["cache_creation_ratio_1h"] = cacheCreationRatio1h
|
||||||
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -251,7 +251,11 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
|||||||
cacheTokens := usage.PromptTokensDetails.CachedTokens
|
cacheTokens := usage.PromptTokensDetails.CachedTokens
|
||||||
|
|
||||||
cacheCreationRatio := relayInfo.PriceData.CacheCreationRatio
|
cacheCreationRatio := relayInfo.PriceData.CacheCreationRatio
|
||||||
|
cacheCreationRatio5m := relayInfo.PriceData.CacheCreation5mRatio
|
||||||
|
cacheCreationRatio1h := relayInfo.PriceData.CacheCreation1hRatio
|
||||||
cacheCreationTokens := usage.PromptTokensDetails.CachedCreationTokens
|
cacheCreationTokens := usage.PromptTokensDetails.CachedCreationTokens
|
||||||
|
cacheCreationTokens5m := usage.ClaudeCacheCreation5mTokens
|
||||||
|
cacheCreationTokens1h := usage.ClaudeCacheCreation1hTokens
|
||||||
|
|
||||||
if relayInfo.ChannelType == constant.ChannelTypeOpenRouter {
|
if relayInfo.ChannelType == constant.ChannelTypeOpenRouter {
|
||||||
promptTokens -= cacheTokens
|
promptTokens -= cacheTokens
|
||||||
@@ -269,7 +273,12 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
|||||||
if !relayInfo.PriceData.UsePrice {
|
if !relayInfo.PriceData.UsePrice {
|
||||||
calculateQuota = float64(promptTokens)
|
calculateQuota = float64(promptTokens)
|
||||||
calculateQuota += float64(cacheTokens) * cacheRatio
|
calculateQuota += float64(cacheTokens) * cacheRatio
|
||||||
calculateQuota += float64(cacheCreationTokens) * cacheCreationRatio
|
calculateQuota += float64(cacheCreationTokens5m) * cacheCreationRatio5m
|
||||||
|
calculateQuota += float64(cacheCreationTokens1h) * cacheCreationRatio1h
|
||||||
|
remainingCacheCreationTokens := cacheCreationTokens - cacheCreationTokens5m - cacheCreationTokens1h
|
||||||
|
if remainingCacheCreationTokens > 0 {
|
||||||
|
calculateQuota += float64(remainingCacheCreationTokens) * cacheCreationRatio
|
||||||
|
}
|
||||||
calculateQuota += float64(completionTokens) * completionRatio
|
calculateQuota += float64(completionTokens) * completionRatio
|
||||||
calculateQuota = calculateQuota * groupRatio * modelRatio
|
calculateQuota = calculateQuota * groupRatio * modelRatio
|
||||||
} else {
|
} else {
|
||||||
@@ -322,7 +331,11 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
other := GenerateClaudeOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio,
|
other := GenerateClaudeOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio,
|
||||||
cacheTokens, cacheRatio, cacheCreationTokens, cacheCreationRatio, modelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio)
|
cacheTokens, cacheRatio,
|
||||||
|
cacheCreationTokens, cacheCreationRatio,
|
||||||
|
cacheCreationTokens5m, cacheCreationRatio5m,
|
||||||
|
cacheCreationTokens1h, cacheCreationRatio1h,
|
||||||
|
modelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio)
|
||||||
model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{
|
model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{
|
||||||
ChannelId: relayInfo.ChannelId,
|
ChannelId: relayInfo.ChannelId,
|
||||||
PromptTokens: promptTokens,
|
PromptTokens: promptTokens,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ type PriceData struct {
|
|||||||
CompletionRatio float64
|
CompletionRatio float64
|
||||||
CacheRatio float64
|
CacheRatio float64
|
||||||
CacheCreationRatio float64
|
CacheCreationRatio float64
|
||||||
|
CacheCreation5mRatio float64
|
||||||
|
CacheCreation1hRatio float64
|
||||||
ImageRatio float64
|
ImageRatio float64
|
||||||
AudioRatio float64
|
AudioRatio float64
|
||||||
AudioCompletionRatio float64
|
AudioCompletionRatio float64
|
||||||
@@ -31,5 +33,5 @@ type PerCallPriceData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p PriceData) ToSetting() string {
|
func (p PriceData) ToSetting() string {
|
||||||
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio)
|
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, CacheCreation5mRatio: %f, CacheCreation1hRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.CacheCreation5mRatio, p.CacheCreation1hRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconUser,
|
IconUser,
|
||||||
IconCode,
|
IconCode,
|
||||||
|
IconSetting,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { getChannelModels } from '../../../../helpers';
|
import { getChannelModels } from '../../../../helpers';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -69,6 +70,8 @@ const EditTagModal = (props) => {
|
|||||||
model_mapping: null,
|
model_mapping: null,
|
||||||
groups: [],
|
groups: [],
|
||||||
models: [],
|
models: [],
|
||||||
|
param_override: null,
|
||||||
|
header_override: null,
|
||||||
};
|
};
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
const formApiRef = useRef(null);
|
const formApiRef = useRef(null);
|
||||||
@@ -190,12 +193,48 @@ const EditTagModal = (props) => {
|
|||||||
if (formVals.models && formVals.models.length > 0) {
|
if (formVals.models && formVals.models.length > 0) {
|
||||||
data.models = formVals.models.join(',');
|
data.models = formVals.models.join(',');
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
formVals.param_override !== undefined &&
|
||||||
|
formVals.param_override !== null
|
||||||
|
) {
|
||||||
|
if (typeof formVals.param_override !== 'string') {
|
||||||
|
showInfo('参数覆盖必须是合法的 JSON 格式!');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trimmedParamOverride = formVals.param_override.trim();
|
||||||
|
if (trimmedParamOverride !== '' && !verifyJSON(trimmedParamOverride)) {
|
||||||
|
showInfo('参数覆盖必须是合法的 JSON 格式!');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.param_override = trimmedParamOverride;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
formVals.header_override !== undefined &&
|
||||||
|
formVals.header_override !== null
|
||||||
|
) {
|
||||||
|
if (typeof formVals.header_override !== 'string') {
|
||||||
|
showInfo('请求头覆盖必须是合法的 JSON 格式!');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trimmedHeaderOverride = formVals.header_override.trim();
|
||||||
|
if (trimmedHeaderOverride !== '' && !verifyJSON(trimmedHeaderOverride)) {
|
||||||
|
showInfo('请求头覆盖必须是合法的 JSON 格式!');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.header_override = trimmedHeaderOverride;
|
||||||
|
}
|
||||||
data.new_tag = formVals.new_tag;
|
data.new_tag = formVals.new_tag;
|
||||||
if (
|
if (
|
||||||
data.model_mapping === undefined &&
|
data.model_mapping === undefined &&
|
||||||
data.groups === undefined &&
|
data.groups === undefined &&
|
||||||
data.models === undefined &&
|
data.models === undefined &&
|
||||||
data.new_tag === undefined
|
data.new_tag === undefined &&
|
||||||
|
data.param_override === undefined &&
|
||||||
|
data.header_override === undefined
|
||||||
) {
|
) {
|
||||||
showWarning('没有任何修改!');
|
showWarning('没有任何修改!');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -491,6 +530,157 @@ const EditTagModal = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
|
||||||
|
{/* Header: Advanced Settings */}
|
||||||
|
<div className='flex items-center mb-2'>
|
||||||
|
<Avatar size='small' color='orange' className='mr-2 shadow-md'>
|
||||||
|
<IconSetting size={16} />
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<Text className='text-lg font-medium'>{t('高级设置')}</Text>
|
||||||
|
<div className='text-xs text-gray-600'>
|
||||||
|
{t('渠道的高级配置选项')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<Form.TextArea
|
||||||
|
field='param_override'
|
||||||
|
label={t('参数覆盖')}
|
||||||
|
placeholder={
|
||||||
|
t(
|
||||||
|
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
|
||||||
|
) +
|
||||||
|
'\n' +
|
||||||
|
t('旧格式(直接覆盖):') +
|
||||||
|
'\n{\n "temperature": 0,\n "max_tokens": 1000\n}' +
|
||||||
|
'\n\n' +
|
||||||
|
t('新格式(支持条件判断与json自定义):') +
|
||||||
|
'\n{\n "operations": [\n {\n "path": "temperature",\n "mode": "set",\n "value": 0.7,\n "conditions": [\n {\n "path": "model",\n "mode": "prefix",\n "value": "gpt"\n }\n ]\n }\n ]\n}'
|
||||||
|
}
|
||||||
|
autosize
|
||||||
|
showClear
|
||||||
|
onChange={(value) =>
|
||||||
|
handleInputChange('param_override', value)
|
||||||
|
}
|
||||||
|
extraText={
|
||||||
|
<div className='flex gap-2 flex-wrap'>
|
||||||
|
<Text
|
||||||
|
className='!text-semi-color-primary cursor-pointer'
|
||||||
|
onClick={() =>
|
||||||
|
handleInputChange(
|
||||||
|
'param_override',
|
||||||
|
JSON.stringify({ temperature: 0 }, null, 2),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('旧格式模板')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
className='!text-semi-color-primary cursor-pointer'
|
||||||
|
onClick={() =>
|
||||||
|
handleInputChange(
|
||||||
|
'param_override',
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
path: 'temperature',
|
||||||
|
mode: 'set',
|
||||||
|
value: 0.7,
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
path: 'model',
|
||||||
|
mode: 'prefix',
|
||||||
|
value: 'gpt',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
logic: 'AND',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('新格式模板')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
className='!text-semi-color-primary cursor-pointer'
|
||||||
|
onClick={() =>
|
||||||
|
handleInputChange('param_override', null)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('不更改')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.TextArea
|
||||||
|
field='header_override'
|
||||||
|
label={t('请求头覆盖')}
|
||||||
|
placeholder={
|
||||||
|
t('此项可选,用于覆盖请求头参数') +
|
||||||
|
'\n' +
|
||||||
|
t('格式示例:') +
|
||||||
|
'\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",\n "Authorization": "Bearer {api_key}"\n}'
|
||||||
|
}
|
||||||
|
autosize
|
||||||
|
showClear
|
||||||
|
onChange={(value) =>
|
||||||
|
handleInputChange('header_override', value)
|
||||||
|
}
|
||||||
|
extraText={
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<div className='flex gap-2 flex-wrap items-center'>
|
||||||
|
<Text
|
||||||
|
className='!text-semi-color-primary cursor-pointer'
|
||||||
|
onClick={() =>
|
||||||
|
handleInputChange(
|
||||||
|
'header_override',
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0',
|
||||||
|
Authorization: 'Bearer {api_key}',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('填入模板')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
className='!text-semi-color-primary cursor-pointer'
|
||||||
|
onClick={() =>
|
||||||
|
handleInputChange('header_override', null)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('不更改')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text type='tertiary' size='small'>
|
||||||
|
{t('支持变量:')}
|
||||||
|
</Text>
|
||||||
|
<div className='text-xs text-tertiary ml-2'>
|
||||||
|
<div>
|
||||||
|
{t('渠道密钥')}: {'{api_key}'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||||
{/* Header: Group Settings */}
|
{/* Header: Group Settings */}
|
||||||
<div className='flex items-center mb-2'>
|
<div className='flex items-center mb-2'>
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ const EditTokenModal = (props) => {
|
|||||||
|
|
||||||
const getInitValues = () => ({
|
const getInitValues = () => ({
|
||||||
name: '',
|
name: '',
|
||||||
remain_quota: 500000,
|
remain_quota: 0,
|
||||||
expired_time: -1,
|
expired_time: -1,
|
||||||
unlimited_quota: false,
|
unlimited_quota: true,
|
||||||
model_limits_enabled: false,
|
model_limits_enabled: false,
|
||||||
model_limits: [],
|
model_limits: [],
|
||||||
allow_ips: '',
|
allow_ips: '',
|
||||||
|
|||||||
@@ -551,6 +551,10 @@ export const getLogsColumns = ({
|
|||||||
other.cache_ratio || 1.0,
|
other.cache_ratio || 1.0,
|
||||||
other.cache_creation_tokens || 0,
|
other.cache_creation_tokens || 0,
|
||||||
other.cache_creation_ratio || 1.0,
|
other.cache_creation_ratio || 1.0,
|
||||||
|
other.cache_creation_tokens_5m || 0,
|
||||||
|
other.cache_creation_ratio_5m || other.cache_creation_ratio || 1.0,
|
||||||
|
other.cache_creation_tokens_1h || 0,
|
||||||
|
other.cache_creation_ratio_1h || other.cache_creation_ratio || 1.0,
|
||||||
false,
|
false,
|
||||||
1.0,
|
1.0,
|
||||||
other?.is_system_prompt_overwritten,
|
other?.is_system_prompt_overwritten,
|
||||||
@@ -565,6 +569,10 @@ export const getLogsColumns = ({
|
|||||||
other.cache_ratio || 1.0,
|
other.cache_ratio || 1.0,
|
||||||
0,
|
0,
|
||||||
1.0,
|
1.0,
|
||||||
|
0,
|
||||||
|
1.0,
|
||||||
|
0,
|
||||||
|
1.0,
|
||||||
false,
|
false,
|
||||||
1.0,
|
1.0,
|
||||||
other?.is_system_prompt_overwritten,
|
other?.is_system_prompt_overwritten,
|
||||||
|
|||||||
56
web/src/helpers/base64.js
Normal file
56
web/src/helpers/base64.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
const toBinaryString = (text) => {
|
||||||
|
if (typeof TextEncoder !== 'undefined') {
|
||||||
|
const bytes = new TextEncoder().encode(text);
|
||||||
|
let binary = '';
|
||||||
|
|
||||||
|
bytes.forEach((byte) => {
|
||||||
|
binary += String.fromCharCode(byte);
|
||||||
|
});
|
||||||
|
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, hex) =>
|
||||||
|
String.fromCharCode(parseInt(hex, 16)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encodeToBase64 = (value) => {
|
||||||
|
const input = value == null ? '' : String(value);
|
||||||
|
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
if (typeof Buffer !== 'undefined') {
|
||||||
|
return Buffer.from(input, 'utf-8').toString('base64');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof globalThis !== 'undefined' &&
|
||||||
|
typeof globalThis.btoa === 'function'
|
||||||
|
) {
|
||||||
|
return globalThis.btoa(toBinaryString(input));
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'Base64 encoding is unavailable in the current environment',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.btoa(toBinaryString(input));
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
export * from './history';
|
export * from './history';
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
export * from './base64';
|
||||||
export * from './api';
|
export * from './api';
|
||||||
export * from './render';
|
export * from './render';
|
||||||
export * from './log';
|
export * from './log';
|
||||||
|
|||||||
@@ -1046,6 +1046,10 @@ function renderPriceSimpleCore({
|
|||||||
cacheRatio = 1.0,
|
cacheRatio = 1.0,
|
||||||
cacheCreationTokens = 0,
|
cacheCreationTokens = 0,
|
||||||
cacheCreationRatio = 1.0,
|
cacheCreationRatio = 1.0,
|
||||||
|
cacheCreationTokens5m = 0,
|
||||||
|
cacheCreationRatio5m = 1.0,
|
||||||
|
cacheCreationTokens1h = 0,
|
||||||
|
cacheCreationRatio1h = 1.0,
|
||||||
image = false,
|
image = false,
|
||||||
imageRatio = 1.0,
|
imageRatio = 1.0,
|
||||||
isSystemPromptOverride = false,
|
isSystemPromptOverride = false,
|
||||||
@@ -1064,17 +1068,40 @@ function renderPriceSimpleCore({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasSplitCacheCreation =
|
||||||
|
cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
|
||||||
|
|
||||||
|
const shouldShowLegacyCacheCreation =
|
||||||
|
!hasSplitCacheCreation && cacheCreationTokens !== 0;
|
||||||
|
|
||||||
|
const shouldShowCache = cacheTokens !== 0;
|
||||||
|
const shouldShowCacheCreation5m =
|
||||||
|
hasSplitCacheCreation && cacheCreationTokens5m > 0;
|
||||||
|
const shouldShowCacheCreation1h =
|
||||||
|
hasSplitCacheCreation && cacheCreationTokens1h > 0;
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
// base: model ratio
|
// base: model ratio
|
||||||
parts.push(i18next.t('模型: {{ratio}}'));
|
parts.push(i18next.t('模型: {{ratio}}'));
|
||||||
|
|
||||||
// cache part (label differs when with image)
|
// cache part (label differs when with image)
|
||||||
if (cacheTokens !== 0) {
|
if (shouldShowCache) {
|
||||||
parts.push(i18next.t('缓存: {{cacheRatio}}'));
|
parts.push(i18next.t('缓存: {{cacheRatio}}'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache creation part (Claude specific if passed)
|
if (hasSplitCacheCreation) {
|
||||||
if (cacheCreationTokens !== 0) {
|
if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
|
||||||
|
parts.push(
|
||||||
|
i18next.t(
|
||||||
|
'缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (shouldShowCacheCreation5m) {
|
||||||
|
parts.push(i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}'));
|
||||||
|
} else if (shouldShowCacheCreation1h) {
|
||||||
|
parts.push(i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}'));
|
||||||
|
}
|
||||||
|
} else if (shouldShowLegacyCacheCreation) {
|
||||||
parts.push(i18next.t('缓存创建: {{cacheCreationRatio}}'));
|
parts.push(i18next.t('缓存创建: {{cacheCreationRatio}}'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1091,6 +1118,8 @@ function renderPriceSimpleCore({
|
|||||||
groupRatio: finalGroupRatio,
|
groupRatio: finalGroupRatio,
|
||||||
cacheRatio: cacheRatio,
|
cacheRatio: cacheRatio,
|
||||||
cacheCreationRatio: cacheCreationRatio,
|
cacheCreationRatio: cacheCreationRatio,
|
||||||
|
cacheCreationRatio5m: cacheCreationRatio5m,
|
||||||
|
cacheCreationRatio1h: cacheCreationRatio1h,
|
||||||
imageRatio: imageRatio,
|
imageRatio: imageRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1450,6 +1479,10 @@ export function renderModelPriceSimple(
|
|||||||
cacheRatio = 1.0,
|
cacheRatio = 1.0,
|
||||||
cacheCreationTokens = 0,
|
cacheCreationTokens = 0,
|
||||||
cacheCreationRatio = 1.0,
|
cacheCreationRatio = 1.0,
|
||||||
|
cacheCreationTokens5m = 0,
|
||||||
|
cacheCreationRatio5m = 1.0,
|
||||||
|
cacheCreationTokens1h = 0,
|
||||||
|
cacheCreationRatio1h = 1.0,
|
||||||
image = false,
|
image = false,
|
||||||
imageRatio = 1.0,
|
imageRatio = 1.0,
|
||||||
isSystemPromptOverride = false,
|
isSystemPromptOverride = false,
|
||||||
@@ -1464,6 +1497,10 @@ export function renderModelPriceSimple(
|
|||||||
cacheRatio,
|
cacheRatio,
|
||||||
cacheCreationTokens,
|
cacheCreationTokens,
|
||||||
cacheCreationRatio,
|
cacheCreationRatio,
|
||||||
|
cacheCreationTokens5m,
|
||||||
|
cacheCreationRatio5m,
|
||||||
|
cacheCreationTokens1h,
|
||||||
|
cacheCreationRatio1h,
|
||||||
image,
|
image,
|
||||||
imageRatio,
|
imageRatio,
|
||||||
isSystemPromptOverride,
|
isSystemPromptOverride,
|
||||||
@@ -1681,6 +1718,10 @@ export function renderClaudeModelPrice(
|
|||||||
cacheRatio = 1.0,
|
cacheRatio = 1.0,
|
||||||
cacheCreationTokens = 0,
|
cacheCreationTokens = 0,
|
||||||
cacheCreationRatio = 1.0,
|
cacheCreationRatio = 1.0,
|
||||||
|
cacheCreationTokens5m = 0,
|
||||||
|
cacheCreationRatio5m = 1.0,
|
||||||
|
cacheCreationTokens1h = 0,
|
||||||
|
cacheCreationRatio1h = 1.0,
|
||||||
) {
|
) {
|
||||||
const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
|
const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
|
||||||
groupRatio,
|
groupRatio,
|
||||||
@@ -1710,20 +1751,121 @@ export function renderClaudeModelPrice(
|
|||||||
const completionRatioValue = completionRatio || 0;
|
const completionRatioValue = completionRatio || 0;
|
||||||
const inputRatioPrice = modelRatio * 2.0;
|
const inputRatioPrice = modelRatio * 2.0;
|
||||||
const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
|
const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
|
||||||
let cacheRatioPrice = (modelRatio * 2.0 * cacheRatio).toFixed(2);
|
const cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
|
||||||
let cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
|
const cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
|
||||||
|
const cacheCreationRatioPrice5m = modelRatio * 2.0 * cacheCreationRatio5m;
|
||||||
|
const cacheCreationRatioPrice1h = modelRatio * 2.0 * cacheCreationRatio1h;
|
||||||
|
|
||||||
|
const hasSplitCacheCreation =
|
||||||
|
cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
|
||||||
|
|
||||||
|
const shouldShowCache = cacheTokens > 0;
|
||||||
|
const shouldShowLegacyCacheCreation =
|
||||||
|
!hasSplitCacheCreation && cacheCreationTokens > 0;
|
||||||
|
const shouldShowCacheCreation5m =
|
||||||
|
hasSplitCacheCreation && cacheCreationTokens5m > 0;
|
||||||
|
const shouldShowCacheCreation1h =
|
||||||
|
hasSplitCacheCreation && cacheCreationTokens1h > 0;
|
||||||
|
|
||||||
// Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
|
// Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
|
||||||
const nonCachedTokens = inputTokens;
|
const nonCachedTokens = inputTokens;
|
||||||
const effectiveInputTokens =
|
const effectiveInputTokens =
|
||||||
nonCachedTokens +
|
nonCachedTokens +
|
||||||
cacheTokens * cacheRatio +
|
cacheTokens * cacheRatio +
|
||||||
cacheCreationTokens * cacheCreationRatio;
|
cacheCreationTokens * cacheCreationRatio +
|
||||||
|
cacheCreationTokens5m * cacheCreationRatio5m +
|
||||||
|
cacheCreationTokens1h * cacheCreationRatio1h;
|
||||||
|
|
||||||
let price =
|
let price =
|
||||||
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
||||||
(completionTokens / 1000000) * completionRatioPrice * groupRatio;
|
(completionTokens / 1000000) * completionRatioPrice * groupRatio;
|
||||||
|
|
||||||
|
const inputUnitPrice = inputRatioPrice * rate;
|
||||||
|
const completionUnitPrice = completionRatioPrice * rate;
|
||||||
|
const cacheUnitPrice = cacheRatioPrice * rate;
|
||||||
|
const cacheCreationUnitPrice = cacheCreationRatioPrice * rate;
|
||||||
|
const cacheCreationUnitPrice5m = cacheCreationRatioPrice5m * rate;
|
||||||
|
const cacheCreationUnitPrice1h = cacheCreationRatioPrice1h * rate;
|
||||||
|
const cacheCreationUnitPriceTotal =
|
||||||
|
cacheCreationUnitPrice5m + cacheCreationUnitPrice1h;
|
||||||
|
|
||||||
|
const breakdownSegments = [
|
||||||
|
i18next.t('提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', {
|
||||||
|
input: inputTokens,
|
||||||
|
symbol,
|
||||||
|
price: inputUnitPrice.toFixed(6),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (shouldShowCache) {
|
||||||
|
breakdownSegments.push(
|
||||||
|
i18next.t(
|
||||||
|
'缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})',
|
||||||
|
{
|
||||||
|
tokens: cacheTokens,
|
||||||
|
symbol,
|
||||||
|
price: cacheUnitPrice.toFixed(6),
|
||||||
|
ratio: cacheRatio,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldShowLegacyCacheCreation) {
|
||||||
|
breakdownSegments.push(
|
||||||
|
i18next.t(
|
||||||
|
'缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})',
|
||||||
|
{
|
||||||
|
tokens: cacheCreationTokens,
|
||||||
|
symbol,
|
||||||
|
price: cacheCreationUnitPrice.toFixed(6),
|
||||||
|
ratio: cacheCreationRatio,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldShowCacheCreation5m) {
|
||||||
|
breakdownSegments.push(
|
||||||
|
i18next.t(
|
||||||
|
'5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})',
|
||||||
|
{
|
||||||
|
tokens: cacheCreationTokens5m,
|
||||||
|
symbol,
|
||||||
|
price: cacheCreationUnitPrice5m.toFixed(6),
|
||||||
|
ratio: cacheCreationRatio5m,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldShowCacheCreation1h) {
|
||||||
|
breakdownSegments.push(
|
||||||
|
i18next.t(
|
||||||
|
'1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})',
|
||||||
|
{
|
||||||
|
tokens: cacheCreationTokens1h,
|
||||||
|
symbol,
|
||||||
|
price: cacheCreationUnitPrice1h.toFixed(6),
|
||||||
|
ratio: cacheCreationRatio1h,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
breakdownSegments.push(
|
||||||
|
i18next.t(
|
||||||
|
'补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}',
|
||||||
|
{
|
||||||
|
completion: completionTokens,
|
||||||
|
symbol,
|
||||||
|
price: completionUnitPrice.toFixed(6),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const breakdownText = breakdownSegments.join(' + ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<article>
|
<article>
|
||||||
@@ -1744,7 +1886,7 @@ export function renderClaudeModelPrice(
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{cacheTokens > 0 && (
|
{shouldShowCache && (
|
||||||
<p>
|
<p>
|
||||||
{i18next.t(
|
{i18next.t(
|
||||||
'缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
|
'缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
|
||||||
@@ -1752,13 +1894,13 @@ export function renderClaudeModelPrice(
|
|||||||
symbol: symbol,
|
symbol: symbol,
|
||||||
price: (inputRatioPrice * rate).toFixed(6),
|
price: (inputRatioPrice * rate).toFixed(6),
|
||||||
ratio: cacheRatio,
|
ratio: cacheRatio,
|
||||||
total: (cacheRatioPrice * rate).toFixed(2),
|
total: cacheUnitPrice.toFixed(6),
|
||||||
cacheRatio: cacheRatio,
|
cacheRatio: cacheRatio,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{cacheCreationTokens > 0 && (
|
{shouldShowLegacyCacheCreation && (
|
||||||
<p>
|
<p>
|
||||||
{i18next.t(
|
{i18next.t(
|
||||||
'缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
|
'缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
|
||||||
@@ -1766,49 +1908,65 @@ export function renderClaudeModelPrice(
|
|||||||
symbol: symbol,
|
symbol: symbol,
|
||||||
price: (inputRatioPrice * rate).toFixed(6),
|
price: (inputRatioPrice * rate).toFixed(6),
|
||||||
ratio: cacheCreationRatio,
|
ratio: cacheCreationRatio,
|
||||||
total: (cacheCreationRatioPrice * rate).toFixed(6),
|
total: cacheCreationUnitPrice.toFixed(6),
|
||||||
cacheCreationRatio: cacheCreationRatio,
|
cacheCreationRatio: cacheCreationRatio,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{shouldShowCacheCreation5m && (
|
||||||
|
<p>
|
||||||
|
{i18next.t(
|
||||||
|
'5m缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m缓存创建倍率: {{cacheCreationRatio5m}})',
|
||||||
|
{
|
||||||
|
symbol: symbol,
|
||||||
|
price: (inputRatioPrice * rate).toFixed(6),
|
||||||
|
ratio: cacheCreationRatio5m,
|
||||||
|
total: cacheCreationUnitPrice5m.toFixed(6),
|
||||||
|
cacheCreationRatio5m: cacheCreationRatio5m,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{shouldShowCacheCreation1h && (
|
||||||
|
<p>
|
||||||
|
{i18next.t(
|
||||||
|
'1h缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h缓存创建倍率: {{cacheCreationRatio1h}})',
|
||||||
|
{
|
||||||
|
symbol: symbol,
|
||||||
|
price: (inputRatioPrice * rate).toFixed(6),
|
||||||
|
ratio: cacheCreationRatio1h,
|
||||||
|
total: cacheCreationUnitPrice1h.toFixed(6),
|
||||||
|
cacheCreationRatio1h: cacheCreationRatio1h,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{shouldShowCacheCreation5m && shouldShowCacheCreation1h && (
|
||||||
|
<p>
|
||||||
|
{i18next.t(
|
||||||
|
'缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens',
|
||||||
|
{
|
||||||
|
symbol: symbol,
|
||||||
|
five: cacheCreationUnitPrice5m.toFixed(6),
|
||||||
|
one: cacheCreationUnitPrice1h.toFixed(6),
|
||||||
|
total: cacheCreationUnitPriceTotal.toFixed(6),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p></p>
|
<p></p>
|
||||||
<p>
|
<p>
|
||||||
{cacheTokens > 0 || cacheCreationTokens > 0
|
{i18next.t(
|
||||||
? i18next.t(
|
'{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
|
||||||
'提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
|
{
|
||||||
{
|
breakdown: breakdownText,
|
||||||
nonCacheInput: nonCachedTokens,
|
ratioType: ratioLabel,
|
||||||
cacheInput: cacheTokens,
|
ratio: groupRatio,
|
||||||
cacheRatio: cacheRatio,
|
symbol: symbol,
|
||||||
cacheCreationInput: cacheCreationTokens,
|
total: (price * rate).toFixed(6),
|
||||||
cacheCreationRatio: cacheCreationRatio,
|
},
|
||||||
symbol: symbol,
|
)}
|
||||||
cachePrice: (cacheRatioPrice * rate).toFixed(2),
|
|
||||||
cacheCreationPrice: (
|
|
||||||
cacheCreationRatioPrice * rate
|
|
||||||
).toFixed(6),
|
|
||||||
price: (inputRatioPrice * rate).toFixed(6),
|
|
||||||
completion: completionTokens,
|
|
||||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
|
||||||
ratio: groupRatio,
|
|
||||||
ratioType: ratioLabel,
|
|
||||||
total: (price * rate).toFixed(6),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: i18next.t(
|
|
||||||
'提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
|
|
||||||
{
|
|
||||||
input: inputTokens,
|
|
||||||
symbol: symbol,
|
|
||||||
price: (inputRatioPrice * rate).toFixed(6),
|
|
||||||
completion: completionTokens,
|
|
||||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
|
||||||
ratio: groupRatio,
|
|
||||||
ratioType: ratioLabel,
|
|
||||||
total: (price * rate).toFixed(6),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||||
</article>
|
</article>
|
||||||
@@ -1825,6 +1983,10 @@ export function renderClaudeLogContent(
|
|||||||
user_group_ratio,
|
user_group_ratio,
|
||||||
cacheRatio = 1.0,
|
cacheRatio = 1.0,
|
||||||
cacheCreationRatio = 1.0,
|
cacheCreationRatio = 1.0,
|
||||||
|
cacheCreationTokens5m = 0,
|
||||||
|
cacheCreationRatio5m = 1.0,
|
||||||
|
cacheCreationTokens1h = 0,
|
||||||
|
cacheCreationRatio1h = 1.0,
|
||||||
) {
|
) {
|
||||||
const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
|
const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
|
||||||
groupRatio,
|
groupRatio,
|
||||||
@@ -1843,17 +2005,58 @@ export function renderClaudeLogContent(
|
|||||||
ratio: groupRatio,
|
ratio: groupRatio,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return i18next.t(
|
const hasSplitCacheCreation =
|
||||||
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}',
|
cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
|
||||||
{
|
const shouldShowCacheCreation5m =
|
||||||
modelRatio: modelRatio,
|
hasSplitCacheCreation && cacheCreationTokens5m > 0;
|
||||||
completionRatio: completionRatio,
|
const shouldShowCacheCreation1h =
|
||||||
cacheRatio: cacheRatio,
|
hasSplitCacheCreation && cacheCreationTokens1h > 0;
|
||||||
cacheCreationRatio: cacheCreationRatio,
|
|
||||||
|
let cacheCreationPart = null;
|
||||||
|
if (hasSplitCacheCreation) {
|
||||||
|
if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
|
||||||
|
cacheCreationPart = i18next.t(
|
||||||
|
'缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
|
||||||
|
{
|
||||||
|
cacheCreationRatio5m,
|
||||||
|
cacheCreationRatio1h,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (shouldShowCacheCreation5m) {
|
||||||
|
cacheCreationPart = i18next.t(
|
||||||
|
'缓存创建倍率 5m {{cacheCreationRatio5m}}',
|
||||||
|
{
|
||||||
|
cacheCreationRatio5m,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (shouldShowCacheCreation1h) {
|
||||||
|
cacheCreationPart = i18next.t(
|
||||||
|
'缓存创建倍率 1h {{cacheCreationRatio1h}}',
|
||||||
|
{
|
||||||
|
cacheCreationRatio1h,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cacheCreationPart) {
|
||||||
|
cacheCreationPart = i18next.t('缓存创建倍率 {{cacheCreationRatio}}', {
|
||||||
|
cacheCreationRatio,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
i18next.t('模型倍率 {{modelRatio}}', { modelRatio }),
|
||||||
|
i18next.t('输出倍率 {{completionRatio}}', { completionRatio }),
|
||||||
|
i18next.t('缓存倍率 {{cacheRatio}}', { cacheRatio }),
|
||||||
|
cacheCreationPart,
|
||||||
|
i18next.t('{{ratioType}} {{ratio}}', {
|
||||||
ratioType: ratioLabel,
|
ratioType: ratioLabel,
|
||||||
ratio: groupRatio,
|
ratio: groupRatio,
|
||||||
},
|
}),
|
||||||
);
|
];
|
||||||
|
|
||||||
|
return parts.join(',');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@douyinfe/semi-ui';
|
import { Modal } from '@douyinfe/semi-ui';
|
||||||
import { API, copy, showError, showSuccess } from '../../helpers';
|
import {
|
||||||
|
API,
|
||||||
|
copy,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
encodeToBase64,
|
||||||
|
} from '../../helpers';
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
import { ITEMS_PER_PAGE } from '../../constants';
|
||||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||||
|
|
||||||
@@ -136,7 +142,7 @@ export const useTokensData = (openFluentNotification) => {
|
|||||||
apiKey: 'sk-' + record.key,
|
apiKey: 'sk-' + record.key,
|
||||||
};
|
};
|
||||||
let encodedConfig = encodeURIComponent(
|
let encodedConfig = encodeURIComponent(
|
||||||
btoa(JSON.stringify(cherryConfig)),
|
encodeToBase64(JSON.stringify(cherryConfig)),
|
||||||
);
|
);
|
||||||
url = url.replaceAll('{cherryConfig}', encodedConfig);
|
url = url.replaceAll('{cherryConfig}', encodedConfig);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -361,6 +361,10 @@ export const useLogsData = () => {
|
|||||||
other?.user_group_ratio,
|
other?.user_group_ratio,
|
||||||
other.cache_ratio || 1.0,
|
other.cache_ratio || 1.0,
|
||||||
other.cache_creation_ratio || 1.0,
|
other.cache_creation_ratio || 1.0,
|
||||||
|
other.cache_creation_tokens_5m || 0,
|
||||||
|
other.cache_creation_ratio_5m || other.cache_creation_ratio || 1.0,
|
||||||
|
other.cache_creation_tokens_1h || 0,
|
||||||
|
other.cache_creation_ratio_1h || other.cache_creation_ratio || 1.0,
|
||||||
)
|
)
|
||||||
: renderLogContent(
|
: renderLogContent(
|
||||||
other?.model_ratio,
|
other?.model_ratio,
|
||||||
@@ -429,6 +433,10 @@ export const useLogsData = () => {
|
|||||||
other.cache_ratio || 1.0,
|
other.cache_ratio || 1.0,
|
||||||
other.cache_creation_tokens || 0,
|
other.cache_creation_tokens || 0,
|
||||||
other.cache_creation_ratio || 1.0,
|
other.cache_creation_ratio || 1.0,
|
||||||
|
other.cache_creation_tokens_5m || 0,
|
||||||
|
other.cache_creation_ratio_5m || other.cache_creation_ratio || 1.0,
|
||||||
|
other.cache_creation_tokens_1h || 0,
|
||||||
|
other.cache_creation_ratio_1h || other.cache_creation_ratio || 1.0,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content = renderModelPrice(
|
content = renderModelPrice(
|
||||||
|
|||||||
@@ -1516,6 +1516,10 @@
|
|||||||
"缓存倍率": "Cache ratio",
|
"缓存倍率": "Cache ratio",
|
||||||
"缓存创建 Tokens": "Cache Creation Tokens",
|
"缓存创建 Tokens": "Cache Creation Tokens",
|
||||||
"缓存创建: {{cacheCreationRatio}}": "Cache creation: {{cacheCreationRatio}}",
|
"缓存创建: {{cacheCreationRatio}}": "Cache creation: {{cacheCreationRatio}}",
|
||||||
|
"缓存创建: 5m {{cacheCreationRatio5m}}": "Cache creation: 5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建: 1h {{cacheCreationRatio1h}}": "Cache creation: 1h {{cacheCreationRatio1h}}",
|
||||||
|
"缓存创建倍率 5m {{cacheCreationRatio5m}}": "Cache creation multiplier 5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建倍率 1h {{cacheCreationRatio1h}}": "Cache creation multiplier 1h {{cacheCreationRatio1h}}",
|
||||||
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Cache creation price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Cache creation ratio: {{cacheCreationRatio}})",
|
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Cache creation price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Cache creation ratio: {{cacheCreationRatio}})",
|
||||||
"编辑": "Edit",
|
"编辑": "Edit",
|
||||||
"编辑API": "Edit API",
|
"编辑API": "Edit API",
|
||||||
@@ -2104,4 +2108,4 @@
|
|||||||
"统一的": "The Unified",
|
"统一的": "The Unified",
|
||||||
"大模型接口网关": "LLM API Gateway"
|
"大模型接口网关": "LLM API Gateway"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1525,6 +1525,10 @@
|
|||||||
"缓存倍率": "Ratio de cache",
|
"缓存倍率": "Ratio de cache",
|
||||||
"缓存创建 Tokens": "Jetons de création de cache",
|
"缓存创建 Tokens": "Jetons de création de cache",
|
||||||
"缓存创建: {{cacheCreationRatio}}": "Création de cache : {{cacheCreationRatio}}",
|
"缓存创建: {{cacheCreationRatio}}": "Création de cache : {{cacheCreationRatio}}",
|
||||||
|
"缓存创建: 5m {{cacheCreationRatio5m}}": "Création de cache : 5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建: 1h {{cacheCreationRatio1h}}": "Création de cache : 1h {{cacheCreationRatio1h}}",
|
||||||
|
"缓存创建倍率 5m {{cacheCreationRatio5m}}": "Multiplicateur de création de cache 5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建倍率 1h {{cacheCreationRatio1h}}": "Multiplicateur de création de cache 1h {{cacheCreationRatio1h}}",
|
||||||
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Prix de création du cache : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (taux de création de cache : {{cacheCreationRatio}})",
|
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Prix de création du cache : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (taux de création de cache : {{cacheCreationRatio}})",
|
||||||
"编辑": "Modifier",
|
"编辑": "Modifier",
|
||||||
"编辑API": "Modifier l'API",
|
"编辑API": "Modifier l'API",
|
||||||
|
|||||||
@@ -1516,6 +1516,10 @@
|
|||||||
"缓存倍率": "キャッシュ倍率",
|
"缓存倍率": "キャッシュ倍率",
|
||||||
"缓存创建 Tokens": "キャッシュ作成トークン",
|
"缓存创建 Tokens": "キャッシュ作成トークン",
|
||||||
"缓存创建: {{cacheCreationRatio}}": "キャッシュ作成:{{cacheCreationRatio}}",
|
"缓存创建: {{cacheCreationRatio}}": "キャッシュ作成:{{cacheCreationRatio}}",
|
||||||
|
"缓存创建: 5m {{cacheCreationRatio5m}}": "キャッシュ作成:5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建: 1h {{cacheCreationRatio1h}}": "キャッシュ作成:1h {{cacheCreationRatio1h}}",
|
||||||
|
"缓存创建倍率 5m {{cacheCreationRatio5m}}": "キャッシュ作成倍率 5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建倍率 1h {{cacheCreationRatio1h}}": "キャッシュ作成倍率 1h {{cacheCreationRatio1h}}",
|
||||||
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "キャッシュ作成料金:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1Mtokens(キャッシュ作成倍率:{{cacheCreationRatio}})",
|
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "キャッシュ作成料金:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1Mtokens(キャッシュ作成倍率:{{cacheCreationRatio}})",
|
||||||
"编辑": "編集",
|
"编辑": "編集",
|
||||||
"编辑API": "API編集",
|
"编辑API": "API編集",
|
||||||
@@ -2075,4 +2079,4 @@
|
|||||||
"统一的": "統合型",
|
"统一的": "統合型",
|
||||||
"大模型接口网关": "LLM APIゲートウェイ"
|
"大模型接口网关": "LLM APIゲートウェイ"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1534,6 +1534,10 @@
|
|||||||
"缓存倍率": "Коэффициент кэширования",
|
"缓存倍率": "Коэффициент кэширования",
|
||||||
"缓存创建 Tokens": "Создание кэша токенов",
|
"缓存创建 Tokens": "Создание кэша токенов",
|
||||||
"缓存创建: {{cacheCreationRatio}}": "Создание кэша: {{cacheCreationRatio}}",
|
"缓存创建: {{cacheCreationRatio}}": "Создание кэша: {{cacheCreationRatio}}",
|
||||||
|
"缓存创建: 5m {{cacheCreationRatio5m}}": "Создание кэша: 5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建: 1h {{cacheCreationRatio1h}}": "Создание кэша: 1h {{cacheCreationRatio1h}}",
|
||||||
|
"缓存创建倍率 5m {{cacheCreationRatio5m}}": "Множитель создания кэша 5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建倍率 1h {{cacheCreationRatio1h}}": "Множитель создания кэша 1h {{cacheCreationRatio1h}}",
|
||||||
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Цена создания кэша: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (коэффициент создания кэша: {{cacheCreationRatio}})",
|
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Цена создания кэша: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (коэффициент создания кэша: {{cacheCreationRatio}})",
|
||||||
"编辑": "Редактировать",
|
"编辑": "Редактировать",
|
||||||
"编辑API": "Редактировать API",
|
"编辑API": "Редактировать API",
|
||||||
|
|||||||
@@ -1507,6 +1507,10 @@
|
|||||||
"缓存倍率": "缓存倍率",
|
"缓存倍率": "缓存倍率",
|
||||||
"缓存创建 Tokens": "缓存创建 Tokens",
|
"缓存创建 Tokens": "缓存创建 Tokens",
|
||||||
"缓存创建: {{cacheCreationRatio}}": "缓存创建: {{cacheCreationRatio}}",
|
"缓存创建: {{cacheCreationRatio}}": "缓存创建: {{cacheCreationRatio}}",
|
||||||
|
"缓存创建: 5m {{cacheCreationRatio5m}}": "缓存创建: 5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建: 1h {{cacheCreationRatio1h}}": "缓存创建: 1h {{cacheCreationRatio1h}}",
|
||||||
|
"缓存创建倍率 5m {{cacheCreationRatio5m}}": "缓存创建倍率 5m {{cacheCreationRatio5m}}",
|
||||||
|
"缓存创建倍率 1h {{cacheCreationRatio1h}}": "缓存创建倍率 1h {{cacheCreationRatio1h}}",
|
||||||
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})",
|
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})",
|
||||||
"编辑": "编辑",
|
"编辑": "编辑",
|
||||||
"编辑API": "编辑API",
|
"编辑API": "编辑API",
|
||||||
@@ -2066,4 +2070,4 @@
|
|||||||
"Creem 介绍": "Creem 是一个简单的支付处理平台,支持固定金额产品销售,以及订阅销售。",
|
"Creem 介绍": "Creem 是一个简单的支付处理平台,支持固定金额产品销售,以及订阅销售。",
|
||||||
"Creem Setting Tips": "Creem 只支持预设的固定金额产品,这产品以及价格需要提前在Creem网站内创建配置,所以不支持自定义动态金额充值。在Creem端配置产品的名字以及价格,获取Product Id 后填到下面的产品,在new-api为该产品设置充值额度,以及展示价格。"
|
"Creem Setting Tips": "Creem 只支持预设的固定金额产品,这产品以及价格需要提前在Creem网站内创建配置,所以不支持自定义动态金额充值。在Creem端配置产品的名字以及价格,获取Product Id 后填到下面的产品,在new-api为该产品设置充值额度,以及展示价格。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
createLoadingAssistantMessage,
|
createLoadingAssistantMessage,
|
||||||
getTextContent,
|
getTextContent,
|
||||||
buildApiPayload,
|
buildApiPayload,
|
||||||
|
encodeToBase64,
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
@@ -72,7 +73,7 @@ const generateAvatarDataUrl = (username) => {
|
|||||||
<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="16" fill="#ffffff" font-family="sans-serif">${firstLetter}</text>
|
<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="16" fill="#ffffff" font-family="sans-serif">${firstLetter}</text>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
return `data:image/svg+xml;base64,${encodeToBase64(svg)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Playground = () => {
|
const Playground = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user