mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-01 22:51:56 +00:00
Compare commits
22 Commits
v0.11.4-al
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ca4ff503e | ||
|
|
dea9353842 | ||
|
|
4deb1eb70f | ||
|
|
092ee07e94 | ||
|
|
4e1b05e987 | ||
|
|
3bd1a167c9 | ||
|
|
7a0ff73c1b | ||
|
|
902725eb66 | ||
|
|
cd1b3771bf | ||
|
|
122d5c00ef | ||
|
|
c3b9ae5f3b | ||
|
|
2c9b22153f | ||
|
|
80c09d7119 | ||
|
|
b0d8b563c3 | ||
|
|
6ff5a5dc99 | ||
|
|
9bb2b6a6ae | ||
|
|
c706a5c29a | ||
|
|
9b394231c8 | ||
|
|
05452c1558 | ||
|
|
f9b5ecc955 | ||
|
|
c3a96bc980 | ||
|
|
bb0d4b0f6d |
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,14 +7,23 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**例行检查**
|
||||
## 提交前必读(请勿删除本节)
|
||||
|
||||
- 文档:https://docs.newapi.ai/
|
||||
- 使用问题先看或先问:https://deepwiki.com/QuantumNous/new-api
|
||||
- 警告:删除本模板、删除小节标题或随意清空内容的 issue,可能会被直接关闭;重复恶意提交者可能会被 block。
|
||||
|
||||
**您当前的 newapi 版本**
|
||||
|
||||
请填写,例如:`v1.0.0`
|
||||
|
||||
**提交确认**
|
||||
|
||||
[//]: # (方框内删除已有的空格,填 x 号)
|
||||
+ [ ] 我已确认目前没有类似 issue
|
||||
+ [ ] 我已确认我已升级到最新版本
|
||||
+ [ ] 我已完整查看过项目 README,尤其是常见问题部分
|
||||
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
|
||||
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
|
||||
+ [ ] 我已完整查看过文档 https://docs.newapi.ai/ 和项目 README,尤其是常见问题部分
|
||||
+ [ ] 我未删除此模板中的任何引导内容或小节标题,并会按要求完整填写
|
||||
+ [ ] 我理解项目维护者精力有限,不遵循模板要求的 issue 可能会被无视或直接关闭
|
||||
|
||||
**问题描述**
|
||||
|
||||
@@ -23,4 +32,3 @@ assignees: ''
|
||||
**预期结果**
|
||||
|
||||
**相关截图**
|
||||
如果没有的话,请删除此节。
|
||||
22
.github/ISSUE_TEMPLATE/bug_report_en.md
vendored
22
.github/ISSUE_TEMPLATE/bug_report_en.md
vendored
@@ -7,14 +7,23 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Routine Checks**
|
||||
## Read This First (Do Not Remove This Section)
|
||||
|
||||
- Docs: https://docs.newapi.ai/
|
||||
- Usage questions first: https://deepwiki.com/QuantumNous/new-api
|
||||
- Warning: issues with this template removed, section headings deleted, or content cleared may be closed directly. Repeated abusive submissions may result in a block.
|
||||
|
||||
**Your current newapi version**
|
||||
|
||||
Please fill this in, for example: `v1.0.0`
|
||||
|
||||
**Submission Checks**
|
||||
|
||||
[//]: # (Remove the space in the box and fill with an x)
|
||||
+ [ ] I have confirmed there are no similar issues currently
|
||||
+ [ ] I have confirmed I have upgraded to the latest version
|
||||
+ [ ] I have thoroughly read the project README, especially the FAQ section
|
||||
+ [ ] I understand and am willing to follow up on this issue, assist with testing and provide feedback
|
||||
+ [ ] I understand and acknowledge the above, and understand that project maintainers have limited time and energy, **issues that do not follow the rules may be ignored or closed directly**
|
||||
+ [ ] I have confirmed there are no similar issues
|
||||
+ [ ] I have thoroughly read the docs at https://docs.newapi.ai/ and the project README, especially the FAQ section
|
||||
+ [ ] I have not removed any guidance or section headings from this template and will complete it as requested
|
||||
+ [ ] I understand that maintainers have limited time and issues that do not follow this template may be ignored or closed directly
|
||||
|
||||
**Issue Description**
|
||||
|
||||
@@ -23,4 +32,3 @@ assignees: ''
|
||||
**Expected Result**
|
||||
|
||||
**Related Screenshots**
|
||||
If none, please delete this section.
|
||||
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 项目群聊
|
||||
url: https://private-user-images.githubusercontent.com/61247483/283011625-de536a8a-0161-47a7-a0a2-66ef6de81266.jpeg?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTEiLCJleHAiOjE3MDIyMjQzOTAsIm5iZiI6MTcwMjIyNDA5MCwicGF0aCI6Ii82MTI0NzQ4My8yODMwMTE2MjUtZGU1MzZhOGEtMDE2MS00N2E3LWEwYTItNjZlZjZkZTgxMjY2LmpwZWc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBSVdOSllBWDRDU1ZFSDUzQSUyRjIwMjMxMjEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDIzMTIxMFQxNjAxMzBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT02MGIxYmM3ZDQyYzBkOTA2ZTYyYmVmMzQ1NjY4NjM1YjY0NTUzNTM5NjE1NDZkYTIzODdhYTk4ZjZjODJmYzY2JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZhY3Rvcl9pZD0wJmtleV9pZD0wJnJlcG9faWQ9MCJ9.TJ8CTfOSwR0-CHS1KLfomqgL0e4YH1luy8lSLrkv5Zg
|
||||
about: QQ 群:629454374
|
||||
- name: 使用文档 / Documentation
|
||||
url: https://docs.newapi.ai/
|
||||
about: 提交 issue 前请先查阅文档,确认现有说明无法解决你的问题。
|
||||
- name: 使用问题 / Usage Questions
|
||||
url: https://deepwiki.com/QuantumNous/new-api
|
||||
about: 使用、配置、接入等问题请优先在 DeepWiki 查询或提问。
|
||||
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,14 +7,23 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**例行检查**
|
||||
## 提交前必读(请勿删除本节)
|
||||
|
||||
- 文档:https://docs.newapi.ai/
|
||||
- 使用问题先看或先问:https://deepwiki.com/QuantumNous/new-api
|
||||
- 警告:删除本模板、删除小节标题或随意清空内容的 issue,可能会被直接关闭;重复恶意提交者可能会被 block。
|
||||
|
||||
**您当前的 newapi 版本**
|
||||
|
||||
请填写,例如:`v1.0.0`
|
||||
|
||||
**提交确认**
|
||||
|
||||
[//]: # (方框内删除已有的空格,填 x 号)
|
||||
+ [ ] 我已确认目前没有类似 issue
|
||||
+ [ ] 我已确认我已升级到最新版本
|
||||
+ [ ] 我已完整查看过项目 README,已确定现有版本无法满足需求
|
||||
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
|
||||
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
|
||||
+ [ ] 我已完整查看过文档 https://docs.newapi.ai/ 和项目 README,已确定现有版本无法满足需求
|
||||
+ [ ] 我未删除此模板中的任何引导内容或小节标题,并会按要求完整填写
|
||||
+ [ ] 我理解项目维护者精力有限,不遵循模板要求的 issue 可能会被无视或直接关闭
|
||||
|
||||
**功能描述**
|
||||
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/feature_request_en.md
vendored
22
.github/ISSUE_TEMPLATE/feature_request_en.md
vendored
@@ -7,16 +7,24 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Routine Checks**
|
||||
## Read This First (Do Not Remove This Section)
|
||||
|
||||
- Docs: https://docs.newapi.ai/
|
||||
- Usage questions first: https://deepwiki.com/QuantumNous/new-api
|
||||
- Warning: issues with this template removed, section headings deleted, or content cleared may be closed directly. Repeated abusive submissions may result in a block.
|
||||
|
||||
**Your current newapi version**
|
||||
|
||||
Please fill this in, for example: `v1.0.0`
|
||||
|
||||
**Submission Checks**
|
||||
|
||||
[//]: # (Remove the space in the box and fill with an x)
|
||||
+ [ ] I have confirmed there are no similar issues currently
|
||||
+ [ ] I have confirmed I have upgraded to the latest version
|
||||
+ [ ] I have thoroughly read the project README and confirmed the current version cannot meet my needs
|
||||
+ [ ] I understand and am willing to follow up on this issue, assist with testing and provide feedback
|
||||
+ [ ] I understand and acknowledge the above, and understand that project maintainers have limited time and energy, **issues that do not follow the rules may be ignored or closed directly**
|
||||
+ [ ] I have confirmed there are no similar issues
|
||||
+ [ ] I have thoroughly read the docs at https://docs.newapi.ai/ and the project README, and confirmed the current version cannot meet my needs
|
||||
+ [ ] I have not removed any guidance or section headings from this template and will complete it as requested
|
||||
+ [ ] I understand that maintainers have limited time and issues that do not follow this template may be ignored or closed directly
|
||||
|
||||
**Feature Description**
|
||||
|
||||
**Use Case**
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
.idea
|
||||
.vscode
|
||||
.zed
|
||||
.history
|
||||
upload
|
||||
*.exe
|
||||
*.db
|
||||
@@ -20,6 +21,7 @@ tiktoken_cache
|
||||
.cache
|
||||
web/bun.lock
|
||||
plans
|
||||
.claude
|
||||
|
||||
electron/node_modules
|
||||
electron/dist
|
||||
|
||||
@@ -14,6 +14,23 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func buildMaskedTokenResponse(token *model.Token) *model.Token {
|
||||
if token == nil {
|
||||
return nil
|
||||
}
|
||||
maskedToken := *token
|
||||
maskedToken.Key = token.GetMaskedKey()
|
||||
return &maskedToken
|
||||
}
|
||||
|
||||
func buildMaskedTokenResponses(tokens []*model.Token) []*model.Token {
|
||||
maskedTokens := make([]*model.Token, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
maskedTokens = append(maskedTokens, buildMaskedTokenResponse(token))
|
||||
}
|
||||
return maskedTokens
|
||||
}
|
||||
|
||||
func GetAllTokens(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
pageInfo := common.GetPageQuery(c)
|
||||
@@ -24,9 +41,8 @@ func GetAllTokens(c *gin.Context) {
|
||||
}
|
||||
total, _ := model.CountUserTokens(userId)
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(tokens)
|
||||
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
return
|
||||
}
|
||||
|
||||
func SearchTokens(c *gin.Context) {
|
||||
@@ -42,9 +58,8 @@ func SearchTokens(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
pageInfo.SetTotal(int(total))
|
||||
pageInfo.SetItems(tokens)
|
||||
pageInfo.SetItems(buildMaskedTokenResponses(tokens))
|
||||
common.ApiSuccess(c, pageInfo)
|
||||
return
|
||||
}
|
||||
|
||||
func GetToken(c *gin.Context) {
|
||||
@@ -59,12 +74,24 @@ func GetToken(c *gin.Context) {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": token,
|
||||
common.ApiSuccess(c, buildMaskedTokenResponse(token))
|
||||
}
|
||||
|
||||
func GetTokenKey(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
userId := c.GetInt("id")
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
token, err := model.GetTokenByIds(id, userId)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, gin.H{
|
||||
"key": token.GetFullKey(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func GetTokenStatus(c *gin.Context) {
|
||||
@@ -204,7 +231,6 @@ func AddToken(c *gin.Context) {
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func DeleteToken(c *gin.Context) {
|
||||
@@ -219,7 +245,6 @@ func DeleteToken(c *gin.Context) {
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func UpdateToken(c *gin.Context) {
|
||||
@@ -283,7 +308,7 @@ func UpdateToken(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": cleanToken,
|
||||
"data": buildMaskedTokenResponse(cleanToken),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
275
controller/token_test.go
Normal file
275
controller/token_test.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type tokenAPIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type tokenPageResponse struct {
|
||||
Items []tokenResponseItem `json:"items"`
|
||||
}
|
||||
|
||||
type tokenResponseItem struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
type tokenKeyResponse struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func setupTokenControllerTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
common.UsingSQLite = true
|
||||
common.UsingMySQL = false
|
||||
common.UsingPostgreSQL = false
|
||||
common.RedisEnabled = false
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open sqlite db: %v", err)
|
||||
}
|
||||
model.DB = db
|
||||
model.LOG_DB = db
|
||||
|
||||
if err := db.AutoMigrate(&model.Token{}); err != nil {
|
||||
t.Fatalf("failed to migrate token table: %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
sqlDB, err := db.DB()
|
||||
if err == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func seedToken(t *testing.T, db *gorm.DB, userID int, name string, rawKey string) *model.Token {
|
||||
t.Helper()
|
||||
|
||||
token := &model.Token{
|
||||
UserId: userID,
|
||||
Name: name,
|
||||
Key: rawKey,
|
||||
Status: common.TokenStatusEnabled,
|
||||
CreatedTime: 1,
|
||||
AccessedTime: 1,
|
||||
ExpiredTime: -1,
|
||||
RemainQuota: 100,
|
||||
UnlimitedQuota: true,
|
||||
Group: "default",
|
||||
}
|
||||
if err := db.Create(token).Error; err != nil {
|
||||
t.Fatalf("failed to create token: %v", err)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func newAuthenticatedContext(t *testing.T, method string, target string, body any, userID int) (*gin.Context, *httptest.ResponseRecorder) {
|
||||
t.Helper()
|
||||
|
||||
var requestBody *bytes.Reader
|
||||
if body != nil {
|
||||
payload, err := common.Marshal(body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal request body: %v", err)
|
||||
}
|
||||
requestBody = bytes.NewReader(payload)
|
||||
} else {
|
||||
requestBody = bytes.NewReader(nil)
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(method, target, requestBody)
|
||||
if body != nil {
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
ctx.Set("id", userID)
|
||||
return ctx, recorder
|
||||
}
|
||||
|
||||
func decodeAPIResponse(t *testing.T, recorder *httptest.ResponseRecorder) tokenAPIResponse {
|
||||
t.Helper()
|
||||
|
||||
var response tokenAPIResponse
|
||||
if err := common.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("failed to decode api response: %v", err)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func TestGetAllTokensMasksKeyInResponse(t *testing.T) {
|
||||
db := setupTokenControllerTestDB(t)
|
||||
token := seedToken(t, db, 1, "list-token", "abcd1234efgh5678")
|
||||
seedToken(t, db, 2, "other-user-token", "zzzz1234yyyy5678")
|
||||
|
||||
ctx, recorder := newAuthenticatedContext(t, http.MethodGet, "/api/token/?p=1&size=10", nil, 1)
|
||||
GetAllTokens(ctx)
|
||||
|
||||
response := decodeAPIResponse(t, recorder)
|
||||
if !response.Success {
|
||||
t.Fatalf("expected success response, got message: %s", response.Message)
|
||||
}
|
||||
|
||||
var page tokenPageResponse
|
||||
if err := common.Unmarshal(response.Data, &page); err != nil {
|
||||
t.Fatalf("failed to decode token page response: %v", err)
|
||||
}
|
||||
if len(page.Items) != 1 {
|
||||
t.Fatalf("expected exactly one token, got %d", len(page.Items))
|
||||
}
|
||||
if page.Items[0].Key != token.GetMaskedKey() {
|
||||
t.Fatalf("expected masked key %q, got %q", token.GetMaskedKey(), page.Items[0].Key)
|
||||
}
|
||||
if strings.Contains(recorder.Body.String(), token.Key) {
|
||||
t.Fatalf("list response leaked raw token key: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchTokensMasksKeyInResponse(t *testing.T) {
|
||||
db := setupTokenControllerTestDB(t)
|
||||
token := seedToken(t, db, 1, "searchable-token", "ijkl1234mnop5678")
|
||||
|
||||
ctx, recorder := newAuthenticatedContext(t, http.MethodGet, "/api/token/search?keyword=searchable-token&p=1&size=10", nil, 1)
|
||||
SearchTokens(ctx)
|
||||
|
||||
response := decodeAPIResponse(t, recorder)
|
||||
if !response.Success {
|
||||
t.Fatalf("expected success response, got message: %s", response.Message)
|
||||
}
|
||||
|
||||
var page tokenPageResponse
|
||||
if err := common.Unmarshal(response.Data, &page); err != nil {
|
||||
t.Fatalf("failed to decode search response: %v", err)
|
||||
}
|
||||
if len(page.Items) != 1 {
|
||||
t.Fatalf("expected exactly one search result, got %d", len(page.Items))
|
||||
}
|
||||
if page.Items[0].Key != token.GetMaskedKey() {
|
||||
t.Fatalf("expected masked search key %q, got %q", token.GetMaskedKey(), page.Items[0].Key)
|
||||
}
|
||||
if strings.Contains(recorder.Body.String(), token.Key) {
|
||||
t.Fatalf("search response leaked raw token key: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTokenMasksKeyInResponse(t *testing.T) {
|
||||
db := setupTokenControllerTestDB(t)
|
||||
token := seedToken(t, db, 1, "detail-token", "qrst1234uvwx5678")
|
||||
|
||||
ctx, recorder := newAuthenticatedContext(t, http.MethodGet, "/api/token/"+strconv.Itoa(token.Id), nil, 1)
|
||||
ctx.Params = gin.Params{{Key: "id", Value: strconv.Itoa(token.Id)}}
|
||||
GetToken(ctx)
|
||||
|
||||
response := decodeAPIResponse(t, recorder)
|
||||
if !response.Success {
|
||||
t.Fatalf("expected success response, got message: %s", response.Message)
|
||||
}
|
||||
|
||||
var detail tokenResponseItem
|
||||
if err := common.Unmarshal(response.Data, &detail); err != nil {
|
||||
t.Fatalf("failed to decode token detail response: %v", err)
|
||||
}
|
||||
if detail.Key != token.GetMaskedKey() {
|
||||
t.Fatalf("expected masked detail key %q, got %q", token.GetMaskedKey(), detail.Key)
|
||||
}
|
||||
if strings.Contains(recorder.Body.String(), token.Key) {
|
||||
t.Fatalf("detail response leaked raw token key: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTokenMasksKeyInResponse(t *testing.T) {
|
||||
db := setupTokenControllerTestDB(t)
|
||||
token := seedToken(t, db, 1, "editable-token", "yzab1234cdef5678")
|
||||
|
||||
body := map[string]any{
|
||||
"id": token.Id,
|
||||
"name": "updated-token",
|
||||
"expired_time": -1,
|
||||
"remain_quota": 100,
|
||||
"unlimited_quota": true,
|
||||
"model_limits_enabled": false,
|
||||
"model_limits": "",
|
||||
"group": "default",
|
||||
"cross_group_retry": false,
|
||||
}
|
||||
|
||||
ctx, recorder := newAuthenticatedContext(t, http.MethodPut, "/api/token/", body, 1)
|
||||
UpdateToken(ctx)
|
||||
|
||||
response := decodeAPIResponse(t, recorder)
|
||||
if !response.Success {
|
||||
t.Fatalf("expected success response, got message: %s", response.Message)
|
||||
}
|
||||
|
||||
var detail tokenResponseItem
|
||||
if err := common.Unmarshal(response.Data, &detail); err != nil {
|
||||
t.Fatalf("failed to decode token update response: %v", err)
|
||||
}
|
||||
if detail.Key != token.GetMaskedKey() {
|
||||
t.Fatalf("expected masked update key %q, got %q", token.GetMaskedKey(), detail.Key)
|
||||
}
|
||||
if strings.Contains(recorder.Body.String(), token.Key) {
|
||||
t.Fatalf("update response leaked raw token key: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTokenKeyRequiresOwnershipAndReturnsFullKey(t *testing.T) {
|
||||
db := setupTokenControllerTestDB(t)
|
||||
token := seedToken(t, db, 1, "owned-token", "owner1234token5678")
|
||||
|
||||
authorizedCtx, authorizedRecorder := newAuthenticatedContext(t, http.MethodPost, "/api/token/"+strconv.Itoa(token.Id)+"/key", nil, 1)
|
||||
authorizedCtx.Params = gin.Params{{Key: "id", Value: strconv.Itoa(token.Id)}}
|
||||
GetTokenKey(authorizedCtx)
|
||||
|
||||
authorizedResponse := decodeAPIResponse(t, authorizedRecorder)
|
||||
if !authorizedResponse.Success {
|
||||
t.Fatalf("expected authorized key fetch to succeed, got message: %s", authorizedResponse.Message)
|
||||
}
|
||||
|
||||
var keyData tokenKeyResponse
|
||||
if err := common.Unmarshal(authorizedResponse.Data, &keyData); err != nil {
|
||||
t.Fatalf("failed to decode token key response: %v", err)
|
||||
}
|
||||
if keyData.Key != token.GetFullKey() {
|
||||
t.Fatalf("expected full key %q, got %q", token.GetFullKey(), keyData.Key)
|
||||
}
|
||||
|
||||
unauthorizedCtx, unauthorizedRecorder := newAuthenticatedContext(t, http.MethodPost, "/api/token/"+strconv.Itoa(token.Id)+"/key", nil, 2)
|
||||
unauthorizedCtx.Params = gin.Params{{Key: "id", Value: strconv.Itoa(token.Id)}}
|
||||
GetTokenKey(unauthorizedCtx)
|
||||
|
||||
unauthorizedResponse := decodeAPIResponse(t, unauthorizedRecorder)
|
||||
if unauthorizedResponse.Success {
|
||||
t.Fatalf("expected unauthorized key fetch to fail")
|
||||
}
|
||||
if strings.Contains(unauthorizedRecorder.Body.String(), token.Key) {
|
||||
t.Fatalf("unauthorized key response leaked raw token key: %s", unauthorizedRecorder.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,8 @@ services:
|
||||
- redis
|
||||
- postgres
|
||||
# - mysql # Uncomment if using MySQL
|
||||
networks:
|
||||
- new-api-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"]
|
||||
interval: 30s
|
||||
@@ -53,6 +55,8 @@ services:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
restart: always
|
||||
networks:
|
||||
- new-api-network
|
||||
|
||||
postgres:
|
||||
image: postgres:15
|
||||
@@ -64,6 +68,8 @@ services:
|
||||
POSTGRES_DB: new-api
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- new-api-network
|
||||
# ports:
|
||||
# - "5432:5432" # Uncomment if you need to access PostgreSQL from outside Docker
|
||||
|
||||
@@ -76,9 +82,15 @@ services:
|
||||
# MYSQL_DATABASE: new-api
|
||||
# volumes:
|
||||
# - mysql_data:/var/lib/mysql
|
||||
# networks:
|
||||
# - new-api-network
|
||||
# ports:
|
||||
# - "3306:3306" # Uncomment if you need to access MySQL from outside Docker
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
# mysql_data:
|
||||
|
||||
networks:
|
||||
new-api-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -35,6 +35,27 @@ func (token *Token) Clean() {
|
||||
token.Key = ""
|
||||
}
|
||||
|
||||
func MaskTokenKey(key string) string {
|
||||
if key == "" {
|
||||
return ""
|
||||
}
|
||||
if len(key) <= 4 {
|
||||
return strings.Repeat("*", len(key))
|
||||
}
|
||||
if len(key) <= 8 {
|
||||
return key[:2] + "****" + key[len(key)-2:]
|
||||
}
|
||||
return key[:4] + "**********" + key[len(key)-4:]
|
||||
}
|
||||
|
||||
func (token *Token) GetFullKey() string {
|
||||
return token.Key
|
||||
}
|
||||
|
||||
func (token *Token) GetMaskedKey() string {
|
||||
return MaskTokenKey(token.Key)
|
||||
}
|
||||
|
||||
func (token *Token) GetIpLimits() []string {
|
||||
// delete empty spaces
|
||||
//split with \n
|
||||
@@ -201,7 +222,7 @@ func ValidateUserToken(key string) (token *Token, err error) {
|
||||
}
|
||||
keyPrefix := key[:3]
|
||||
keySuffix := key[len(key)-3:]
|
||||
return token, errors.New(fmt.Sprintf("[sk-%s***%s] 该令牌额度已用尽 !token.UnlimitedQuota && token.RemainQuota = %d", keyPrefix, keySuffix, token.RemainQuota))
|
||||
return token, fmt.Errorf("[sk-%s***%s] 该令牌额度已用尽 !token.UnlimitedQuota && token.RemainQuota = %d", keyPrefix, keySuffix, token.RemainQuota)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ var ModelList = []string{
|
||||
"claude-opus-4-6-high",
|
||||
"claude-opus-4-6-medium",
|
||||
"claude-opus-4-6-low",
|
||||
"claude-sonnet-4-6",
|
||||
}
|
||||
|
||||
var ChannelName = "claude"
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
var baseModelList = []string{
|
||||
"gpt-5", "gpt-5-codex", "gpt-5-codex-mini",
|
||||
"gpt-5.1", "gpt-5.1-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini",
|
||||
"gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex",
|
||||
"gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark",
|
||||
"gpt-5.4",
|
||||
}
|
||||
|
||||
var ModelList = withCompactModelSuffix(baseModelList)
|
||||
|
||||
@@ -2,29 +2,34 @@ package gemini
|
||||
|
||||
var ModelList = []string{
|
||||
// stable version
|
||||
"gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.5-flash-8b",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash",
|
||||
"gemini-2.0-flash-001", "gemini-2.0-flash-lite-001", "gemini-2.0-flash-lite",
|
||||
"gemini-2.5-flash-lite",
|
||||
// latest version
|
||||
"gemini-1.5-pro-latest", "gemini-1.5-flash-latest",
|
||||
"gemini-flash-latest", "gemini-flash-lite-latest", "gemini-pro-latest",
|
||||
"gemini-2.5-flash-native-audio-latest",
|
||||
// preview version
|
||||
"gemini-2.0-flash-lite-preview",
|
||||
"gemini-3-pro-preview",
|
||||
// gemini exp
|
||||
"gemini-exp-1206",
|
||||
// flash exp
|
||||
"gemini-2.0-flash-exp",
|
||||
// pro exp
|
||||
"gemini-2.0-pro-exp",
|
||||
// thinking exp
|
||||
"gemini-2.0-flash-thinking-exp",
|
||||
"gemini-2.5-pro-exp-03-25",
|
||||
"gemini-2.5-pro-preview-03-25",
|
||||
// imagen models
|
||||
"imagen-3.0-generate-002",
|
||||
"gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts",
|
||||
"gemini-2.5-flash-image", "gemini-2.5-flash-lite-preview-09-2025",
|
||||
"gemini-3-pro-preview", "gemini-3-flash-preview", "gemini-3.1-pro-preview",
|
||||
"gemini-3.1-pro-preview-customtools", "gemini-3.1-flash-lite-preview",
|
||||
"gemini-3-pro-image-preview", "nano-banana-pro-preview",
|
||||
"gemini-3.1-flash-image-preview", "gemini-robotics-er-1.5-preview",
|
||||
"gemini-2.5-computer-use-preview-10-2025", "deep-research-pro-preview-12-2025",
|
||||
"gemini-2.5-flash-native-audio-preview-09-2025", "gemini-2.5-flash-native-audio-preview-12-2025",
|
||||
// gemma models
|
||||
"gemma-3-1b-it", "gemma-3-4b-it", "gemma-3-12b-it",
|
||||
"gemma-3-27b-it", "gemma-3n-e4b-it", "gemma-3n-e2b-it",
|
||||
// embedding models
|
||||
"gemini-embedding-exp-03-07",
|
||||
"text-embedding-004",
|
||||
"embedding-001",
|
||||
"gemini-embedding-001", "gemini-embedding-2-preview",
|
||||
// imagen models
|
||||
"imagen-4.0-generate-001", "imagen-4.0-ultra-generate-001",
|
||||
"imagen-4.0-fast-generate-001",
|
||||
// veo models
|
||||
"veo-2.0-generate-001", "veo-3.0-generate-001", "veo-3.0-fast-generate-001",
|
||||
"veo-3.1-generate-preview", "veo-3.1-fast-generate-preview",
|
||||
// other models
|
||||
"aqa",
|
||||
}
|
||||
|
||||
var SafetySettingList = []string{
|
||||
|
||||
@@ -15,8 +15,10 @@ var ModelList = []string{
|
||||
"speech-01-hd",
|
||||
"speech-01-turbo",
|
||||
"MiniMax-M2.1",
|
||||
"MiniMax-M2.1-lightning",
|
||||
"MiniMax-M2.1-highspeed",
|
||||
"MiniMax-M2",
|
||||
"MiniMax-M2.5",
|
||||
"MiniMax-M2.5-highspeed",
|
||||
}
|
||||
|
||||
var ChannelName = "minimax"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package moonshot
|
||||
|
||||
var ModelList = []string{
|
||||
"moonshot-v1-8k",
|
||||
"moonshot-v1-32k",
|
||||
"moonshot-v1-128k",
|
||||
"kimi-k2.5",
|
||||
"kimi-k2-0905-preview",
|
||||
"kimi-k2-turbo-preview",
|
||||
"kimi-k2-thinking",
|
||||
"kimi-k2-thinking-turbo",
|
||||
}
|
||||
|
||||
var ChannelName = "moonshot"
|
||||
|
||||
@@ -225,8 +225,12 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info *
|
||||
}
|
||||
}
|
||||
if info.ChannelType == constant.ChannelTypeOpenRouter {
|
||||
header.Set("HTTP-Referer", "https://www.newapi.ai")
|
||||
header.Set("X-Title", "New API")
|
||||
if header.Get("HTTP-Referer") == "" {
|
||||
header.Set("HTTP-Referer", "https://www.newapi.ai")
|
||||
}
|
||||
if header.Get("X-OpenRouter-Title") == "" {
|
||||
header.Set("X-OpenRouter-Title", "New API")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,14 +3,19 @@ package openai
|
||||
var ModelList = []string{
|
||||
"gpt-3.5-turbo", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125",
|
||||
"gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613",
|
||||
"gpt-3.5-turbo-instruct",
|
||||
"gpt-3.5-turbo-instruct", "gpt-3.5-turbo-instruct-0914",
|
||||
"gpt-4", "gpt-4-0613", "gpt-4-1106-preview", "gpt-4-0125-preview",
|
||||
"gpt-4-32k", "gpt-4-32k-0613",
|
||||
"gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
|
||||
"gpt-4-vision-preview",
|
||||
"chatgpt-4o-latest",
|
||||
"gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06", "gpt-4o-2024-11-20",
|
||||
"gpt-4o-transcribe", "gpt-4o-transcribe-diarize",
|
||||
"gpt-4o-search-preview", "gpt-4o-search-preview-2025-03-11",
|
||||
"gpt-4o-mini", "gpt-4o-mini-2024-07-18",
|
||||
"gpt-4o-mini-transcribe", "gpt-4o-mini-transcribe-2025-03-20", "gpt-4o-mini-transcribe-2025-12-15",
|
||||
"gpt-4o-mini-tts", "gpt-4o-mini-tts-2025-03-20", "gpt-4o-mini-tts-2025-12-15",
|
||||
"gpt-4o-mini-search-preview", "gpt-4o-mini-search-preview-2025-03-11",
|
||||
"gpt-4.5-preview", "gpt-4.5-preview-2025-02-27",
|
||||
"gpt-4.1", "gpt-4.1-2025-04-14",
|
||||
"gpt-4.1-mini", "gpt-4.1-mini-2025-04-14",
|
||||
@@ -31,17 +36,41 @@ var ModelList = []string{
|
||||
"gpt-5", "gpt-5-2025-08-07", "gpt-5-chat-latest",
|
||||
"gpt-5-mini", "gpt-5-mini-2025-08-07",
|
||||
"gpt-5-nano", "gpt-5-nano-2025-08-07",
|
||||
"gpt-4o-audio-preview", "gpt-4o-audio-preview-2024-10-01",
|
||||
"gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01", "gpt-4o-realtime-preview-2024-12-17",
|
||||
"gpt-5-codex",
|
||||
"gpt-5-pro", "gpt-5-pro-2025-10-06",
|
||||
"gpt-5-search-api", "gpt-5-search-api-2025-10-14",
|
||||
"gpt-5.1", "gpt-5.1-2025-11-13", "gpt-5.1-chat-latest",
|
||||
"gpt-5.1-codex", "gpt-5.1-codex-mini", "gpt-5.1-codex-max",
|
||||
"gpt-5.2", "gpt-5.2-2025-12-11", "gpt-5.2-chat-latest",
|
||||
"gpt-5.2-pro", "gpt-5.2-pro-2025-12-11",
|
||||
"gpt-5.2-codex",
|
||||
"gpt-5.3-chat-latest",
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.4", "gpt-5.4-2026-03-05",
|
||||
"gpt-5.4-pro", "gpt-5.4-pro-2026-03-05",
|
||||
"gpt-4o-audio-preview", "gpt-4o-audio-preview-2024-10-01", "gpt-4o-audio-preview-2024-12-17", "gpt-4o-audio-preview-2025-06-03",
|
||||
"gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01", "gpt-4o-realtime-preview-2024-12-17", "gpt-4o-realtime-preview-2025-06-03",
|
||||
"gpt-4o-mini-realtime-preview", "gpt-4o-mini-realtime-preview-2024-12-17",
|
||||
"gpt-4o-mini-audio-preview", "gpt-4o-mini-audio-preview-2024-12-17",
|
||||
"gpt-audio", "gpt-audio-2025-08-28",
|
||||
"gpt-audio-mini", "gpt-audio-mini-2025-10-06", "gpt-audio-mini-2025-12-15",
|
||||
"gpt-audio-1.5",
|
||||
"gpt-realtime", "gpt-realtime-2025-08-28",
|
||||
"gpt-realtime-mini", "gpt-realtime-mini-2025-10-06", "gpt-realtime-mini-2025-12-15",
|
||||
"gpt-realtime-1.5",
|
||||
"text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large",
|
||||
"text-curie-001", "text-babbage-001", "text-ada-001",
|
||||
"text-moderation-latest", "text-moderation-stable",
|
||||
"omni-moderation-latest", "omni-moderation-2024-09-26",
|
||||
"text-davinci-edit-001",
|
||||
"davinci-002", "babbage-002",
|
||||
"dall-e-3", "gpt-image-1",
|
||||
"dall-e-2", "dall-e-3",
|
||||
"gpt-image-1", "gpt-image-1-mini", "gpt-image-1.5",
|
||||
"chatgpt-image-latest",
|
||||
"whisper-1",
|
||||
"tts-1", "tts-1-1106", "tts-1-hd", "tts-1-hd-1106",
|
||||
"computer-use-preview", "computer-use-preview-2025-03-11",
|
||||
"sora-2", "sora-2-pro",
|
||||
}
|
||||
|
||||
var ChannelName = "openai"
|
||||
|
||||
@@ -405,5 +405,12 @@ func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, erro
|
||||
Code: fmt.Sprintf("%d", klingResp.Code),
|
||||
}
|
||||
}
|
||||
|
||||
// https://app.klingai.com/cn/dev/document-api/apiReference/model/textToVideo
|
||||
if data := klingResp.Data; data.TaskStatus == "failed" {
|
||||
openAIVideo.Error = &dto.OpenAIVideoError{
|
||||
Message: data.TaskStatusMsg,
|
||||
}
|
||||
}
|
||||
return common.Marshal(openAIVideo)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package zhipu_4v
|
||||
|
||||
var ModelList = []string{
|
||||
"glm-4", "glm-4v", "glm-3-turbo", "glm-4-alltools", "glm-4-plus", "glm-4-0520", "glm-4-air", "glm-4-airx", "glm-4-long", "glm-4-flash", "glm-4v-plus", "glm-4.6",
|
||||
"glm-4", "glm-4v", "glm-3-turbo", "glm-4-alltools", "glm-4-plus", "glm-4-0520", "glm-4-air", "glm-4-airx", "glm-4-long", "glm-4-flash", "glm-4v-plus", "glm-4.6", "glm-4.6v", "glm-4.7", "glm-4.7-flash", "glm-5",
|
||||
}
|
||||
|
||||
var ChannelName = "zhipu_4v"
|
||||
|
||||
@@ -847,24 +847,30 @@ func resolveHeaderOverrideValueByMapping(context map[string]interface{}, headerN
|
||||
return "", false, fmt.Errorf("header value mapping cannot be empty")
|
||||
}
|
||||
|
||||
sourceValue, exists := getHeaderValueFromContext(context, headerName)
|
||||
if !exists {
|
||||
return "", false, nil
|
||||
appendTokens, err := parseHeaderAppendTokens(mapping)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
sourceTokens := splitHeaderListValue(sourceValue)
|
||||
if len(sourceTokens) == 0 {
|
||||
return "", false, nil
|
||||
keepOnlyDeclared := parseHeaderKeepOnlyDeclared(mapping)
|
||||
|
||||
sourceValue, exists := getHeaderValueFromContext(context, headerName)
|
||||
sourceTokens := make([]string, 0)
|
||||
if exists {
|
||||
sourceTokens = splitHeaderListValue(sourceValue)
|
||||
}
|
||||
|
||||
wildcardValue, hasWildcard := mapping["*"]
|
||||
resultTokens := make([]string, 0, len(sourceTokens))
|
||||
resultTokens := make([]string, 0, len(sourceTokens)+len(appendTokens))
|
||||
for _, token := range sourceTokens {
|
||||
replacementRaw, hasReplacement := mapping[token]
|
||||
if !hasReplacement && hasWildcard {
|
||||
if !hasReplacement && hasWildcard && !keepOnlyDeclared {
|
||||
replacementRaw = wildcardValue
|
||||
hasReplacement = true
|
||||
}
|
||||
if !hasReplacement {
|
||||
if keepOnlyDeclared {
|
||||
continue
|
||||
}
|
||||
resultTokens = append(resultTokens, token)
|
||||
continue
|
||||
}
|
||||
@@ -875,6 +881,7 @@ func resolveHeaderOverrideValueByMapping(context map[string]interface{}, headerN
|
||||
resultTokens = append(resultTokens, replacementTokens...)
|
||||
}
|
||||
|
||||
resultTokens = append(resultTokens, appendTokens...)
|
||||
resultTokens = lo.Uniq(resultTokens)
|
||||
if len(resultTokens) == 0 {
|
||||
return "", false, nil
|
||||
@@ -882,6 +889,26 @@ func resolveHeaderOverrideValueByMapping(context map[string]interface{}, headerN
|
||||
return strings.Join(resultTokens, ","), true, nil
|
||||
}
|
||||
|
||||
func parseHeaderAppendTokens(mapping map[string]interface{}) ([]string, error) {
|
||||
appendRaw, ok := mapping["$append"]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return parseHeaderReplacementTokens(appendRaw)
|
||||
}
|
||||
|
||||
func parseHeaderKeepOnlyDeclared(mapping map[string]interface{}) bool {
|
||||
keepOnlyDeclaredRaw, ok := mapping["$keep_only_declared"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
keepOnlyDeclared, ok := keepOnlyDeclaredRaw.(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return keepOnlyDeclared
|
||||
}
|
||||
|
||||
func parseHeaderReplacementTokens(value interface{}) ([]string, error) {
|
||||
switch raw := value.(type) {
|
||||
case nil:
|
||||
|
||||
@@ -1653,6 +1653,141 @@ func TestApplyParamOverrideSetHeaderMapDeleteWholeHeaderWhenAllTokensCleared(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideSetHeaderMapAppendsTokens(t *testing.T) {
|
||||
input := []byte(`{"temperature":0.7}`)
|
||||
override := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"mode": "set_header",
|
||||
"path": "anthropic-beta",
|
||||
"value": map[string]interface{}{
|
||||
"$append": []interface{}{"context-1m-2025-08-07", "computer-use-2025-01-24"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := map[string]interface{}{
|
||||
"header_override": map[string]interface{}{
|
||||
"anthropic-beta": "computer-use-2025-01-24",
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ApplyParamOverride(input, override, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, `{"temperature":0.7}`, string(out))
|
||||
|
||||
headers, ok := ctx["header_override"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected header_override context map")
|
||||
}
|
||||
if headers["anthropic-beta"] != "computer-use-2025-01-24,context-1m-2025-08-07" {
|
||||
t.Fatalf("expected anthropic-beta to append new token without duplicates, got: %v", headers["anthropic-beta"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideSetHeaderMapAppendsTokensWhenHeaderMissing(t *testing.T) {
|
||||
input := []byte(`{"temperature":0.7}`)
|
||||
override := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"mode": "set_header",
|
||||
"path": "anthropic-beta",
|
||||
"value": map[string]interface{}{
|
||||
"$append": []interface{}{"context-1m-2025-08-07", "computer-use-2025-01-24"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := map[string]interface{}{}
|
||||
out, err := ApplyParamOverride(input, override, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, `{"temperature":0.7}`, string(out))
|
||||
|
||||
headers, ok := ctx["header_override"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected header_override context map")
|
||||
}
|
||||
if headers["anthropic-beta"] != "context-1m-2025-08-07,computer-use-2025-01-24" {
|
||||
t.Fatalf("expected anthropic-beta to be created from appended tokens, got: %v", headers["anthropic-beta"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideSetHeaderMapKeepOnlyDeclaredDropsUndeclaredTokens(t *testing.T) {
|
||||
input := []byte(`{"temperature":0.7}`)
|
||||
override := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"mode": "set_header",
|
||||
"path": "anthropic-beta",
|
||||
"value": map[string]interface{}{
|
||||
"computer-use-2025-01-24": "computer-use-2025-01-24",
|
||||
"$append": []interface{}{"context-1m-2025-08-07"},
|
||||
"$keep_only_declared": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := map[string]interface{}{
|
||||
"header_override": map[string]interface{}{
|
||||
"anthropic-beta": "advanced-tool-use-2025-11-20,computer-use-2025-01-24",
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ApplyParamOverride(input, override, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, `{"temperature":0.7}`, string(out))
|
||||
|
||||
headers, ok := ctx["header_override"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected header_override context map")
|
||||
}
|
||||
if headers["anthropic-beta"] != "computer-use-2025-01-24,context-1m-2025-08-07" {
|
||||
t.Fatalf("expected anthropic-beta to keep only declared tokens, got: %v", headers["anthropic-beta"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideSetHeaderMapKeepOnlyDeclaredDeletesHeaderWhenNothingDeclaredMatches(t *testing.T) {
|
||||
input := []byte(`{"temperature":0.7}`)
|
||||
override := map[string]interface{}{
|
||||
"operations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"mode": "set_header",
|
||||
"path": "anthropic-beta",
|
||||
"value": map[string]interface{}{
|
||||
"computer-use-2025-01-24": "computer-use-2025-01-24",
|
||||
"$keep_only_declared": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := map[string]interface{}{
|
||||
"header_override": map[string]interface{}{
|
||||
"anthropic-beta": "advanced-tool-use-2025-11-20",
|
||||
},
|
||||
}
|
||||
|
||||
out, err := ApplyParamOverride(input, override, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyParamOverride returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, `{"temperature":0.7}`, string(out))
|
||||
|
||||
headers, ok := ctx["header_override"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected header_override context map")
|
||||
}
|
||||
if _, exists := headers["anthropic-beta"]; exists {
|
||||
t.Fatalf("expected anthropic-beta to be deleted when no declared tokens remain, got: %v", headers["anthropic-beta"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideConditionsObjectShorthand(t *testing.T) {
|
||||
input := []byte(`{"temperature":0.7}`)
|
||||
override := map[string]interface{}{
|
||||
|
||||
@@ -248,6 +248,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
tokenRoute.GET("/", controller.GetAllTokens)
|
||||
tokenRoute.GET("/search", middleware.SearchRateLimit(), controller.SearchTokens)
|
||||
tokenRoute.GET("/:id", controller.GetToken)
|
||||
tokenRoute.POST("/:id/key", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.GetTokenKey)
|
||||
tokenRoute.POST("/", controller.AddToken)
|
||||
tokenRoute.PUT("/", controller.UpdateToken)
|
||||
tokenRoute.DELETE("/:id", controller.DeleteToken)
|
||||
|
||||
@@ -214,7 +214,7 @@ func registerMjRouterGroup(relayMjRouter *gin.RouterGroup) {
|
||||
relayMjRouter.POST("/submit/blend", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/edits", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/video", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/notify", controller.RelayMidjourney)
|
||||
//relayMjRouter.POST("/notify", controller.RelayMidjourney)
|
||||
relayMjRouter.GET("/task/:id/fetch", controller.RelayMidjourney)
|
||||
relayMjRouter.GET("/task/:id/image-seed", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/task/list-by-condition", controller.RelayMidjourney)
|
||||
|
||||
@@ -2,6 +2,7 @@ package model_setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting/config"
|
||||
)
|
||||
@@ -50,23 +51,36 @@ func GetClaudeSettings() *ClaudeSettings {
|
||||
func (c *ClaudeSettings) WriteHeaders(originModel string, httpHeader *http.Header) {
|
||||
if headers, ok := c.HeadersSettings[originModel]; ok {
|
||||
for headerKey, headerValues := range headers {
|
||||
// get existing values for this header key
|
||||
existingValues := httpHeader.Values(headerKey)
|
||||
existingValuesMap := make(map[string]bool)
|
||||
for _, v := range existingValues {
|
||||
existingValuesMap[v] = true
|
||||
}
|
||||
|
||||
// add only values that don't already exist
|
||||
for _, headerValue := range headerValues {
|
||||
if !existingValuesMap[headerValue] {
|
||||
httpHeader.Add(headerKey, headerValue)
|
||||
}
|
||||
mergedValues := normalizeHeaderListValues(
|
||||
append(append([]string(nil), httpHeader.Values(headerKey)...), headerValues...),
|
||||
)
|
||||
if len(mergedValues) == 0 {
|
||||
continue
|
||||
}
|
||||
httpHeader.Set(headerKey, strings.Join(mergedValues, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeHeaderListValues(values []string) []string {
|
||||
normalizedValues := make([]string, 0, len(values))
|
||||
seenValues := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
for _, item := range strings.Split(value, ",") {
|
||||
normalizedItem := strings.TrimSpace(item)
|
||||
if normalizedItem == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seenValues[normalizedItem]; exists {
|
||||
continue
|
||||
}
|
||||
seenValues[normalizedItem] = struct{}{}
|
||||
normalizedValues = append(normalizedValues, normalizedItem)
|
||||
}
|
||||
}
|
||||
return normalizedValues
|
||||
}
|
||||
|
||||
func (c *ClaudeSettings) GetDefaultMaxTokens(model string) int {
|
||||
if maxTokens, ok := c.DefaultMaxTokens[model]; ok {
|
||||
return maxTokens
|
||||
|
||||
@@ -41,7 +41,7 @@ import { normalizeLanguage } from '../../i18n/language';
|
||||
const { Sider, Content, Header } = Layout;
|
||||
|
||||
const PageLayout = () => {
|
||||
const [, userDispatch] = useContext(UserContext);
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [, statusDispatch] = useContext(StatusContext);
|
||||
const isMobile = useIsMobile();
|
||||
const [collapsed, , setCollapsed] = useSidebarCollapsed();
|
||||
@@ -114,15 +114,34 @@ const PageLayout = () => {
|
||||
linkElement.href = logo;
|
||||
}
|
||||
}
|
||||
const savedLang = localStorage.getItem('i18nextLng');
|
||||
if (savedLang) {
|
||||
const normalizedLang = normalizeLanguage(savedLang);
|
||||
if (normalizedLang !== savedLang) {
|
||||
localStorage.setItem('i18nextLng', normalizedLang);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let preferredLang;
|
||||
|
||||
if (userState?.user?.setting) {
|
||||
try {
|
||||
const settings = JSON.parse(userState.user.setting);
|
||||
preferredLang = normalizeLanguage(settings.language);
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
i18n.changeLanguage(normalizedLang);
|
||||
}
|
||||
}, [i18n]);
|
||||
|
||||
if (!preferredLang) {
|
||||
const savedLang = localStorage.getItem('i18nextLng');
|
||||
if (savedLang) {
|
||||
preferredLang = normalizeLanguage(savedLang);
|
||||
}
|
||||
}
|
||||
|
||||
if (preferredLang) {
|
||||
localStorage.setItem('i18nextLng', preferredLang);
|
||||
if (preferredLang !== i18n.language) {
|
||||
i18n.changeLanguage(preferredLang);
|
||||
}
|
||||
}
|
||||
}, [i18n, userState?.user?.setting]);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
||||
@@ -73,6 +73,7 @@ const PreferencesSettings = ({ t }) => {
|
||||
// Update language immediately for responsive UX
|
||||
setCurrentLanguage(lang);
|
||||
i18n.changeLanguage(lang);
|
||||
localStorage.setItem('i18nextLng', lang);
|
||||
|
||||
// Save to backend
|
||||
const res = await API.put("/api/user/self", {
|
||||
@@ -81,33 +82,38 @@ const PreferencesSettings = ({ t }) => {
|
||||
|
||||
if (res.data.success) {
|
||||
showSuccess(t("语言偏好已保存"));
|
||||
// Update user context with new setting
|
||||
// Keep backend preference, context state, and local cache aligned.
|
||||
let settings = {};
|
||||
if (userState?.user?.setting) {
|
||||
try {
|
||||
const settings = JSON.parse(userState.user.setting);
|
||||
settings.language = lang;
|
||||
userDispatch({
|
||||
type: "login",
|
||||
payload: {
|
||||
...userState.user,
|
||||
setting: JSON.stringify(settings),
|
||||
},
|
||||
});
|
||||
settings = JSON.parse(userState.user.setting) || {};
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
settings = {};
|
||||
}
|
||||
}
|
||||
settings.language = lang;
|
||||
const nextUser = {
|
||||
...userState.user,
|
||||
setting: JSON.stringify(settings),
|
||||
};
|
||||
userDispatch({
|
||||
type: "login",
|
||||
payload: nextUser,
|
||||
});
|
||||
localStorage.setItem("user", JSON.stringify(nextUser));
|
||||
} else {
|
||||
showError(res.data.message || t("保存失败"));
|
||||
// Revert on error
|
||||
setCurrentLanguage(previousLang);
|
||||
i18n.changeLanguage(previousLang);
|
||||
localStorage.setItem("i18nextLng", previousLang);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t("保存失败,请重试"));
|
||||
// Revert on error
|
||||
setCurrentLanguage(previousLang);
|
||||
i18n.changeLanguage(previousLang);
|
||||
localStorage.setItem("i18nextLng", previousLang);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
TextArea,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
|
||||
import { IconDelete, IconMenu, IconPlus } from '@douyinfe/semi-icons';
|
||||
import { copy, showError, showSuccess, verifyJSON } from '../../../../helpers';
|
||||
import {
|
||||
CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE,
|
||||
@@ -163,7 +163,7 @@ const MODE_DESCRIPTIONS = {
|
||||
prune_objects: '按条件清理对象中的子项',
|
||||
pass_headers: '把指定请求头透传到上游请求',
|
||||
sync_fields: '在一个字段有值、另一个缺失时自动补齐',
|
||||
set_header: '设置运行期请求头(支持整值覆盖,或用 JSON 映射按逗号 token 替换/删除)',
|
||||
set_header: '设置运行期请求头:可直接覆盖整条值,也可对逗号分隔的 token 做删除、替换、追加或白名单保留',
|
||||
delete_header: '删除运行期请求头',
|
||||
copy_header: '复制请求头',
|
||||
move_header: '移动请求头',
|
||||
@@ -230,17 +230,29 @@ const getModeValueLabel = (mode) => {
|
||||
return '值(支持 JSON 或普通文本)';
|
||||
};
|
||||
|
||||
const HEADER_VALUE_JSONC_EXAMPLE = `{
|
||||
// 置空:删除 Bedrock 不支持的 beta特性
|
||||
"files-api-2025-04-14": null,
|
||||
|
||||
// 替换:把旧特性改成兼容特性
|
||||
"advanced-tool-use-2025-11-20": "tool-search-tool-2025-10-19",
|
||||
|
||||
// 追加:在末尾补一个需要的特性
|
||||
"$append": ["context-1m-2025-08-07"]
|
||||
}`;
|
||||
|
||||
const getModeValuePlaceholder = (mode) => {
|
||||
if (mode === 'set_header') {
|
||||
return [
|
||||
'String example:',
|
||||
'纯字符串(整条覆盖):',
|
||||
'Bearer sk-xxx',
|
||||
'',
|
||||
'JSON map example:',
|
||||
'{"advanced-tool-use-2025-11-20": null, "computer-use-2025-01-24": "computer-use-2025-01-24"}',
|
||||
'',
|
||||
'JSON map wildcard:',
|
||||
'{"*": null, "computer-use-2025-11-24": "computer-use-2025-11-24"}',
|
||||
'或使用 JSON 规则:',
|
||||
'{',
|
||||
' "files-api-2025-04-14": null,',
|
||||
' "advanced-tool-use-2025-11-20": "tool-search-tool-2025-10-19",',
|
||||
' "$append": ["context-1m-2025-08-07"]',
|
||||
'}',
|
||||
].join('\n');
|
||||
}
|
||||
if (mode === 'pass_headers') return 'Authorization, X-Request-Id';
|
||||
@@ -258,11 +270,6 @@ const getModeValuePlaceholder = (mode) => {
|
||||
return '0.7';
|
||||
};
|
||||
|
||||
const getModeValueHelp = (mode) => {
|
||||
if (mode !== 'set_header') return '';
|
||||
return '字符串:整条请求头直接覆盖。JSON 映射:按逗号分隔 token 逐项处理,null 表示删除,string/array 表示替换,* 表示兜底规则。';
|
||||
};
|
||||
|
||||
const SYNC_TARGET_TYPE_OPTIONS = [
|
||||
{ label: '请求体字段', value: 'json' },
|
||||
{ label: '请求头字段', value: 'header' },
|
||||
@@ -369,6 +376,7 @@ const AWS_BEDROCK_ANTHROPIC_COMPAT_TEMPLATE = {
|
||||
'tool-search-tool-2025-10-19': 'tool-search-tool-2025-10-19',
|
||||
'web-fetch-2025-09-10': null,
|
||||
'web-search-2025-03-05': null,
|
||||
'oauth-2025-04-20': null
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -800,6 +808,38 @@ const normalizeOperation = (operation = {}) => ({
|
||||
|
||||
const createDefaultOperation = () => normalizeOperation({ mode: 'set' });
|
||||
|
||||
const reorderOperations = (
|
||||
sourceOperations = [],
|
||||
sourceId,
|
||||
targetId,
|
||||
position = 'before',
|
||||
) => {
|
||||
if (!sourceId || !targetId || sourceId === targetId) {
|
||||
return sourceOperations;
|
||||
}
|
||||
|
||||
const sourceIndex = sourceOperations.findIndex((item) => item.id === sourceId);
|
||||
|
||||
if (sourceIndex < 0) {
|
||||
return sourceOperations;
|
||||
}
|
||||
|
||||
const nextOperations = [...sourceOperations];
|
||||
const [moved] = nextOperations.splice(sourceIndex, 1);
|
||||
let insertIndex = nextOperations.findIndex((item) => item.id === targetId);
|
||||
|
||||
if (insertIndex < 0) {
|
||||
return sourceOperations;
|
||||
}
|
||||
|
||||
if (position === 'after') {
|
||||
insertIndex += 1;
|
||||
}
|
||||
|
||||
nextOperations.splice(insertIndex, 0, moved);
|
||||
return nextOperations;
|
||||
};
|
||||
|
||||
const getOperationSummary = (operation = {}, index = 0) => {
|
||||
const mode = operation.mode || 'set';
|
||||
const modeLabel = OPERATION_MODE_LABEL_MAP[mode] || mode;
|
||||
@@ -1037,8 +1077,12 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
|
||||
const [operationSearch, setOperationSearch] = useState('');
|
||||
const [selectedOperationId, setSelectedOperationId] = useState('');
|
||||
const [expandedConditionMap, setExpandedConditionMap] = useState({});
|
||||
const [draggedOperationId, setDraggedOperationId] = useState('');
|
||||
const [dragOverOperationId, setDragOverOperationId] = useState('');
|
||||
const [dragOverPosition, setDragOverPosition] = useState('before');
|
||||
const [templateGroupKey, setTemplateGroupKey] = useState('basic');
|
||||
const [templatePresetKey, setTemplatePresetKey] = useState('operations_default');
|
||||
const [headerValueExampleVisible, setHeaderValueExampleVisible] = useState(false);
|
||||
const [fieldGuideVisible, setFieldGuideVisible] = useState(false);
|
||||
const [fieldGuideTarget, setFieldGuideTarget] = useState('path');
|
||||
const [fieldGuideKeyword, setFieldGuideKeyword] = useState('');
|
||||
@@ -1055,6 +1099,9 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
|
||||
setOperationSearch('');
|
||||
setSelectedOperationId(nextState.operations[0]?.id || '');
|
||||
setExpandedConditionMap({});
|
||||
setDraggedOperationId('');
|
||||
setDragOverOperationId('');
|
||||
setDragOverPosition('before');
|
||||
if (nextState.visualMode === 'legacy') {
|
||||
setTemplateGroupKey('basic');
|
||||
setTemplatePresetKey('legacy_default');
|
||||
@@ -1062,6 +1109,7 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
|
||||
setTemplateGroupKey('basic');
|
||||
setTemplatePresetKey('operations_default');
|
||||
}
|
||||
setHeaderValueExampleVisible(false);
|
||||
setFieldGuideVisible(false);
|
||||
setFieldGuideTarget('path');
|
||||
setFieldGuideKeyword('');
|
||||
@@ -1583,6 +1631,67 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
|
||||
setSelectedOperationId(created.id);
|
||||
};
|
||||
|
||||
const resetOperationDragState = useCallback(() => {
|
||||
setDraggedOperationId('');
|
||||
setDragOverOperationId('');
|
||||
setDragOverPosition('before');
|
||||
}, []);
|
||||
|
||||
const moveOperation = useCallback(
|
||||
(sourceId, targetId, position = 'before') => {
|
||||
if (!sourceId || !targetId || sourceId === targetId) {
|
||||
return;
|
||||
}
|
||||
setOperations((prev) =>
|
||||
reorderOperations(prev, sourceId, targetId, position),
|
||||
);
|
||||
setSelectedOperationId(sourceId);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleOperationDragStart = useCallback((event, operationId) => {
|
||||
setDraggedOperationId(operationId);
|
||||
setSelectedOperationId(operationId);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', operationId);
|
||||
}, []);
|
||||
|
||||
const handleOperationDragOver = useCallback(
|
||||
(event, operationId) => {
|
||||
event.preventDefault();
|
||||
if (!draggedOperationId || draggedOperationId === operationId) {
|
||||
return;
|
||||
}
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const position =
|
||||
event.clientY - rect.top > rect.height / 2 ? 'after' : 'before';
|
||||
setDragOverOperationId(operationId);
|
||||
setDragOverPosition(position);
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
},
|
||||
[draggedOperationId],
|
||||
);
|
||||
|
||||
const handleOperationDrop = useCallback(
|
||||
(event, operationId) => {
|
||||
event.preventDefault();
|
||||
const sourceId =
|
||||
draggedOperationId || event.dataTransfer.getData('text/plain');
|
||||
const position =
|
||||
dragOverOperationId === operationId ? dragOverPosition : 'before';
|
||||
moveOperation(sourceId, operationId, position);
|
||||
resetOperationDragState();
|
||||
},
|
||||
[
|
||||
dragOverOperationId,
|
||||
dragOverPosition,
|
||||
draggedOperationId,
|
||||
moveOperation,
|
||||
resetOperationDragState,
|
||||
],
|
||||
);
|
||||
|
||||
const duplicateOperation = (operationId) => {
|
||||
let insertedId = '';
|
||||
setOperations((prev) => {
|
||||
@@ -1941,14 +2050,31 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
|
||||
);
|
||||
const isActive =
|
||||
operation.id === selectedOperationId;
|
||||
const isDragging =
|
||||
operation.id === draggedOperationId;
|
||||
const isDropTarget =
|
||||
operation.id === dragOverOperationId &&
|
||||
draggedOperationId &&
|
||||
draggedOperationId !== operation.id;
|
||||
return (
|
||||
<div
|
||||
key={operation.id}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
draggable={operations.length > 1}
|
||||
onClick={() =>
|
||||
setSelectedOperationId(operation.id)
|
||||
}
|
||||
onDragStart={(event) =>
|
||||
handleOperationDragStart(event, operation.id)
|
||||
}
|
||||
onDragOver={(event) =>
|
||||
handleOperationDragOver(event, operation.id)
|
||||
}
|
||||
onDrop={(event) =>
|
||||
handleOperationDrop(event, operation.id)
|
||||
}
|
||||
onDragEnd={resetOperationDragState}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === 'Enter' ||
|
||||
@@ -1966,35 +2092,53 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
|
||||
border: isActive
|
||||
? '1px solid var(--semi-color-primary)'
|
||||
: '1px solid var(--semi-color-border)',
|
||||
opacity: isDragging ? 0.6 : 1,
|
||||
boxShadow: isDropTarget
|
||||
? dragOverPosition === 'after'
|
||||
? 'inset 0 -3px 0 var(--semi-color-primary)'
|
||||
: 'inset 0 3px 0 var(--semi-color-primary)'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
<div className='flex items-start justify-between gap-2'>
|
||||
<div>
|
||||
<Text strong>{`#${index + 1}`}</Text>
|
||||
<Text
|
||||
type='tertiary'
|
||||
size='small'
|
||||
className='block mt-1'
|
||||
<div className='flex items-start gap-2 min-w-0'>
|
||||
<div
|
||||
className='flex-shrink-0'
|
||||
style={{
|
||||
color: 'var(--semi-color-text-2)',
|
||||
cursor: operations.length > 1 ? 'grab' : 'default',
|
||||
marginTop: 1,
|
||||
}}
|
||||
>
|
||||
{getOperationSummary(operation, index)}
|
||||
</Text>
|
||||
{String(operation.description || '').trim() ? (
|
||||
<IconMenu />
|
||||
</div>
|
||||
<div className='min-w-0'>
|
||||
<Text strong>{`#${index + 1}`}</Text>
|
||||
<Text
|
||||
type='tertiary'
|
||||
size='small'
|
||||
className='block mt-1'
|
||||
style={{
|
||||
lineHeight: 1.5,
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{operation.description}
|
||||
{getOperationSummary(operation, index)}
|
||||
</Text>
|
||||
) : null}
|
||||
{String(operation.description || '').trim() ? (
|
||||
<Text
|
||||
type='tertiary'
|
||||
size='small'
|
||||
className='block mt-1'
|
||||
style={{
|
||||
lineHeight: 1.5,
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{operation.description}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Tag size='small' color='grey'>
|
||||
{(operation.conditions || []).length}
|
||||
@@ -2688,15 +2832,35 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
|
||||
{t(getModeValueLabel(mode))}
|
||||
</Text>
|
||||
{mode === 'set_header' ? (
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={formatSelectedOperationValueAsJson}
|
||||
>
|
||||
{t('格式化 JSON')}
|
||||
</Button>
|
||||
<Space spacing={6}>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() =>
|
||||
setHeaderValueExampleVisible(true)
|
||||
}
|
||||
>
|
||||
{t('查看 JSON 示例')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={formatSelectedOperationValueAsJson}
|
||||
>
|
||||
{t('格式化 JSON')}
|
||||
</Button>
|
||||
</Space>
|
||||
) : null}
|
||||
</div>
|
||||
{mode === 'set_header' ? (
|
||||
<Text
|
||||
type='tertiary'
|
||||
size='small'
|
||||
className='mt-1 mb-2 block'
|
||||
>
|
||||
{t('纯字符串会直接覆盖整条请求头,或者点击“查看 JSON 示例”按 token 规则处理。')}
|
||||
</Text>
|
||||
) : null}
|
||||
<TextArea
|
||||
value={selectedOperation.value_text}
|
||||
autosize={{ minRows: 1, maxRows: 4 }}
|
||||
@@ -2707,11 +2871,6 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
|
||||
})
|
||||
}
|
||||
/>
|
||||
{getModeValueHelp(mode) ? (
|
||||
<Text type='tertiary' size='small'>
|
||||
{t(getModeValueHelp(mode))}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
@@ -3167,6 +3326,27 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={t('anthropic-beta JSON 示例')}
|
||||
visible={headerValueExampleVisible}
|
||||
width={760}
|
||||
footer={null}
|
||||
onCancel={() => setHeaderValueExampleVisible(false)}
|
||||
bodyStyle={{ padding: 16, paddingBottom: 24 }}
|
||||
>
|
||||
<Space vertical align='start' spacing={12} style={{ width: '100%' }}>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('下面是带注释的示例,仅用于参考;实际保存时请删除注释。')}
|
||||
</Text>
|
||||
<TextArea
|
||||
value={HEADER_VALUE_JSONC_EXAMPLE}
|
||||
readOnly
|
||||
autosize={{ minRows: 16, maxRows: 20 }}
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={null}
|
||||
visible={fieldGuideVisible}
|
||||
|
||||
@@ -25,6 +25,7 @@ const PricingDisplaySettings = ({
|
||||
setShowWithRecharge,
|
||||
currency,
|
||||
setCurrency,
|
||||
siteDisplayType,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
@@ -34,11 +35,17 @@ const PricingDisplaySettings = ({
|
||||
loading = false,
|
||||
t,
|
||||
}) => {
|
||||
const supportsCurrencyDisplay = siteDisplayType !== 'TOKENS';
|
||||
|
||||
const items = [
|
||||
{
|
||||
value: 'recharge',
|
||||
label: t('充值价格显示'),
|
||||
},
|
||||
...(supportsCurrencyDisplay
|
||||
? [
|
||||
{
|
||||
value: 'recharge',
|
||||
label: t('充值价格显示'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
value: 'ratio',
|
||||
label: t('显示倍率'),
|
||||
@@ -78,7 +85,7 @@ const PricingDisplaySettings = ({
|
||||
|
||||
const getActiveValues = () => {
|
||||
const activeValues = [];
|
||||
if (showWithRecharge) activeValues.push('recharge');
|
||||
if (supportsCurrencyDisplay && showWithRecharge) activeValues.push('recharge');
|
||||
if (showRatio) activeValues.push('ratio');
|
||||
if (viewMode === 'table') activeValues.push('tableView');
|
||||
if (tokenUnit === 'K') activeValues.push('tokenUnit');
|
||||
@@ -98,7 +105,7 @@ const PricingDisplaySettings = ({
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{showWithRecharge && (
|
||||
{supportsCurrencyDisplay && showWithRecharge && (
|
||||
<SelectableButtonGroup
|
||||
title={t('货币单位')}
|
||||
items={currencyItems}
|
||||
|
||||
@@ -70,6 +70,7 @@ const PricingPage = () => {
|
||||
groupRatio={pricingData.groupRatio}
|
||||
usableGroup={pricingData.usableGroup}
|
||||
currency={pricingData.currency}
|
||||
siteDisplayType={pricingData.siteDisplayType}
|
||||
tokenUnit={pricingData.tokenUnit}
|
||||
displayPrice={pricingData.displayPrice}
|
||||
showRatio={allProps.showRatio}
|
||||
|
||||
@@ -40,6 +40,7 @@ const PricingTopSection = memo(
|
||||
setShowWithRecharge,
|
||||
currency,
|
||||
setCurrency,
|
||||
siteDisplayType,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
@@ -68,6 +69,7 @@ const PricingTopSection = memo(
|
||||
setShowWithRecharge={setShowWithRecharge}
|
||||
currency={currency}
|
||||
setCurrency={setCurrency}
|
||||
siteDisplayType={siteDisplayType}
|
||||
showRatio={showRatio}
|
||||
setShowRatio={setShowRatio}
|
||||
viewMode={viewMode}
|
||||
@@ -103,6 +105,7 @@ const PricingTopSection = memo(
|
||||
setShowWithRecharge={setShowWithRecharge}
|
||||
currency={currency}
|
||||
setCurrency={setCurrency}
|
||||
siteDisplayType={siteDisplayType}
|
||||
showRatio={showRatio}
|
||||
setShowRatio={setShowRatio}
|
||||
viewMode={viewMode}
|
||||
|
||||
@@ -35,6 +35,7 @@ const SearchActions = memo(
|
||||
setShowWithRecharge,
|
||||
currency,
|
||||
setCurrency,
|
||||
siteDisplayType,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
@@ -43,6 +44,8 @@ const SearchActions = memo(
|
||||
setTokenUnit,
|
||||
t,
|
||||
}) => {
|
||||
const supportsCurrencyDisplay = siteDisplayType !== 'TOKENS';
|
||||
|
||||
const handleCopyClick = useCallback(() => {
|
||||
if (copyText && selectedRowKeys.length > 0) {
|
||||
copyText(selectedRowKeys);
|
||||
@@ -91,16 +94,18 @@ const SearchActions = memo(
|
||||
<Divider layout='vertical' margin='8px' />
|
||||
|
||||
{/* 充值价格显示开关 */}
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-sm text-gray-600'>{t('充值价格显示')}</span>
|
||||
<Switch
|
||||
checked={showWithRecharge}
|
||||
onChange={setShowWithRecharge}
|
||||
/>
|
||||
</div>
|
||||
{supportsCurrencyDisplay && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-sm text-gray-600'>{t('充值价格显示')}</span>
|
||||
<Switch
|
||||
checked={showWithRecharge}
|
||||
onChange={setShowWithRecharge}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 货币单位选择 */}
|
||||
{showWithRecharge && (
|
||||
{supportsCurrencyDisplay && showWithRecharge && (
|
||||
<Select
|
||||
value={currency}
|
||||
onChange={setCurrency}
|
||||
|
||||
@@ -35,6 +35,7 @@ const ModelDetailSideSheet = ({
|
||||
modelData,
|
||||
groupRatio,
|
||||
currency,
|
||||
siteDisplayType,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
showRatio,
|
||||
@@ -92,6 +93,7 @@ const ModelDetailSideSheet = ({
|
||||
modelData={modelData}
|
||||
groupRatio={groupRatio}
|
||||
currency={currency}
|
||||
siteDisplayType={siteDisplayType}
|
||||
tokenUnit={tokenUnit}
|
||||
displayPrice={displayPrice}
|
||||
showRatio={showRatio}
|
||||
|
||||
@@ -32,6 +32,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
||||
setShowWithRecharge,
|
||||
currency,
|
||||
setCurrency,
|
||||
siteDisplayType,
|
||||
handleChange,
|
||||
setActiveKey,
|
||||
showRatio,
|
||||
@@ -77,6 +78,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
||||
setShowWithRecharge={setShowWithRecharge}
|
||||
currency={currency}
|
||||
setCurrency={setCurrency}
|
||||
siteDisplayType={siteDisplayType}
|
||||
showRatio={showRatio}
|
||||
setShowRatio={setShowRatio}
|
||||
viewMode={viewMode}
|
||||
|
||||
@@ -28,6 +28,7 @@ const ModelPricingTable = ({
|
||||
modelData,
|
||||
groupRatio,
|
||||
currency,
|
||||
siteDisplayType,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
showRatio,
|
||||
@@ -57,6 +58,7 @@ const ModelPricingTable = ({
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
currency,
|
||||
quotaDisplayType: siteDisplayType,
|
||||
})
|
||||
: { inputPrice: '-', outputPrice: '-', price: '-' };
|
||||
|
||||
@@ -74,7 +76,7 @@ const ModelPricingTable = ({
|
||||
: modelData?.quota_type === 1
|
||||
? t('按次计费')
|
||||
: '-',
|
||||
priceItems: getModelPriceItems(priceData, t),
|
||||
priceItems: getModelPriceItems(priceData, t, siteDisplayType),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -122,7 +124,7 @@ const ModelPricingTable = ({
|
||||
});
|
||||
|
||||
columns.push({
|
||||
title: t('价格摘要'),
|
||||
title: siteDisplayType === 'TOKENS' ? t('计费摘要') : t('价格摘要'),
|
||||
dataIndex: 'priceItems',
|
||||
render: (items) => (
|
||||
<div className='space-y-1'>
|
||||
|
||||
@@ -67,6 +67,7 @@ const PricingCardView = ({
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
siteDisplayType,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
showRatio,
|
||||
@@ -246,6 +247,7 @@ const PricingCardView = ({
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
currency,
|
||||
quotaDisplayType: siteDisplayType,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -265,7 +267,7 @@ const PricingCardView = ({
|
||||
{model.model_name}
|
||||
</h3>
|
||||
<div className='flex flex-col gap-1 text-xs mt-1'>
|
||||
{formatPriceInfo(priceData, t)}
|
||||
{formatPriceInfo(priceData, t, siteDisplayType)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@ const PricingTable = ({
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
siteDisplayType,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
searchValue,
|
||||
@@ -54,6 +55,7 @@ const PricingTable = ({
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
siteDisplayType,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
showRatio,
|
||||
@@ -66,6 +68,7 @@ const PricingTable = ({
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
siteDisplayType,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
showRatio,
|
||||
|
||||
@@ -109,6 +109,7 @@ export const getPricingTableColumns = ({
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
siteDisplayType,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
showRatio,
|
||||
@@ -126,6 +127,7 @@ export const getPricingTableColumns = ({
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
currency,
|
||||
quotaDisplayType: siteDisplayType,
|
||||
});
|
||||
priceDataCache.set(record, cache);
|
||||
}
|
||||
@@ -227,12 +229,12 @@ export const getPricingTableColumns = ({
|
||||
};
|
||||
|
||||
const priceColumn = {
|
||||
title: t('模型价格'),
|
||||
title: siteDisplayType === 'TOKENS' ? t('计费摘要') : t('模型价格'),
|
||||
dataIndex: 'model_price',
|
||||
...(isMobile ? {} : { fixed: 'right' }),
|
||||
render: (text, record, index) => {
|
||||
const priceData = getPriceData(record);
|
||||
const priceItems = getModelPriceItems(priceData, t);
|
||||
const priceItems = getModelPriceItems(priceData, t, siteDisplayType);
|
||||
|
||||
return (
|
||||
<div className='space-y-1'>
|
||||
|
||||
@@ -29,7 +29,6 @@ const TokensActions = ({
|
||||
setShowEdit,
|
||||
batchCopyTokens,
|
||||
batchDeleteTokens,
|
||||
copyText,
|
||||
t,
|
||||
}) => {
|
||||
// Modal states
|
||||
@@ -99,8 +98,7 @@ const TokensActions = ({
|
||||
<CopyTokensModal
|
||||
visible={showCopyModal}
|
||||
onCancel={() => setShowCopyModal(false)}
|
||||
selectedKeys={selectedKeys}
|
||||
copyText={copyText}
|
||||
batchCopyTokens={batchCopyTokens}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
|
||||
@@ -108,17 +108,28 @@ const renderGroupColumn = (text, record, t) => {
|
||||
};
|
||||
|
||||
// Render token key column with show/hide and copy functionality
|
||||
const renderTokenKey = (text, record, showKeys, setShowKeys, copyText) => {
|
||||
const fullKey = 'sk-' + record.key;
|
||||
const maskedKey =
|
||||
'sk-' + record.key.slice(0, 4) + '**********' + record.key.slice(-4);
|
||||
const renderTokenKey = (
|
||||
text,
|
||||
record,
|
||||
showKeys,
|
||||
resolvedTokenKeys,
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
) => {
|
||||
const revealed = !!showKeys[record.id];
|
||||
const loading = !!loadingTokenKeys[record.id];
|
||||
const keyValue =
|
||||
revealed && resolvedTokenKeys[record.id]
|
||||
? resolvedTokenKeys[record.id]
|
||||
: record.key || '';
|
||||
const displayedKey = keyValue ? `sk-${keyValue}` : '';
|
||||
|
||||
return (
|
||||
<div className='w-[200px]'>
|
||||
<Input
|
||||
readOnly
|
||||
value={revealed ? fullKey : maskedKey}
|
||||
value={displayedKey}
|
||||
size='small'
|
||||
suffix={
|
||||
<div className='flex items-center'>
|
||||
@@ -127,10 +138,11 @@ const renderTokenKey = (text, record, showKeys, setShowKeys, copyText) => {
|
||||
size='small'
|
||||
type='tertiary'
|
||||
icon={revealed ? <IconEyeClosed /> : <IconEyeOpened />}
|
||||
loading={loading}
|
||||
aria-label='toggle token visibility'
|
||||
onClick={(e) => {
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
setShowKeys((prev) => ({ ...prev, [record.id]: !revealed }));
|
||||
await toggleTokenVisibility(record);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@@ -138,10 +150,11 @@ const renderTokenKey = (text, record, showKeys, setShowKeys, copyText) => {
|
||||
size='small'
|
||||
type='tertiary'
|
||||
icon={<IconCopy />}
|
||||
loading={loading}
|
||||
aria-label='copy token key'
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await copyText(fullKey);
|
||||
await copyTokenKey(record);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -427,8 +440,10 @@ const renderOperations = (
|
||||
export const getTokensColumns = ({
|
||||
t,
|
||||
showKeys,
|
||||
setShowKeys,
|
||||
copyText,
|
||||
resolvedTokenKeys,
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
@@ -461,7 +476,15 @@ export const getTokensColumns = ({
|
||||
title: t('密钥'),
|
||||
key: 'token_key',
|
||||
render: (text, record) =>
|
||||
renderTokenKey(text, record, showKeys, setShowKeys, copyText),
|
||||
renderTokenKey(
|
||||
text,
|
||||
record,
|
||||
showKeys,
|
||||
resolvedTokenKeys,
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('可用模型'),
|
||||
|
||||
@@ -39,8 +39,10 @@ const TokensTable = (tokensData) => {
|
||||
rowSelection,
|
||||
handleRow,
|
||||
showKeys,
|
||||
setShowKeys,
|
||||
copyText,
|
||||
resolvedTokenKeys,
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
@@ -54,8 +56,10 @@ const TokensTable = (tokensData) => {
|
||||
return getTokensColumns({
|
||||
t,
|
||||
showKeys,
|
||||
setShowKeys,
|
||||
copyText,
|
||||
resolvedTokenKeys,
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
@@ -65,8 +69,10 @@ const TokensTable = (tokensData) => {
|
||||
}, [
|
||||
t,
|
||||
showKeys,
|
||||
setShowKeys,
|
||||
copyText,
|
||||
resolvedTokenKeys,
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
|
||||
@@ -58,6 +58,7 @@ function TokensPage() {
|
||||
t: (k) => k,
|
||||
selectedModel: '',
|
||||
prefillKey: '',
|
||||
fetchTokenKey: async () => '',
|
||||
});
|
||||
const [modelOptions, setModelOptions] = useState([]);
|
||||
const [selectedModel, setSelectedModel] = useState('');
|
||||
@@ -74,6 +75,7 @@ function TokensPage() {
|
||||
t: tokensData.t,
|
||||
selectedModel,
|
||||
prefillKey,
|
||||
fetchTokenKey: tokensData.fetchTokenKey,
|
||||
};
|
||||
}, [
|
||||
tokensData.tokens,
|
||||
@@ -81,6 +83,7 @@ function TokensPage() {
|
||||
tokensData.t,
|
||||
selectedModel,
|
||||
prefillKey,
|
||||
tokensData.fetchTokenKey,
|
||||
]);
|
||||
|
||||
const loadModels = async () => {
|
||||
@@ -198,13 +201,14 @@ function TokensPage() {
|
||||
openCCSwitchModalRef.current = openCCSwitchModal;
|
||||
|
||||
// Prefill to Fluent handler
|
||||
const handlePrefillToFluent = () => {
|
||||
const handlePrefillToFluent = async () => {
|
||||
const {
|
||||
tokens,
|
||||
selectedKeys,
|
||||
t,
|
||||
selectedModel: chosenModel,
|
||||
prefillKey: overrideKey,
|
||||
fetchTokenKey,
|
||||
} = latestRef.current;
|
||||
const container = document.getElementById('fluent-new-api-container');
|
||||
if (!container) {
|
||||
@@ -241,7 +245,11 @@ function TokensPage() {
|
||||
Toast.warning(t('没有可用令牌用于填充'));
|
||||
return;
|
||||
}
|
||||
apiKeyToUse = 'sk-' + token.key;
|
||||
try {
|
||||
apiKeyToUse = 'sk-' + (await fetchTokenKey(token));
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
@@ -351,7 +359,6 @@ function TokensPage() {
|
||||
setShowEdit,
|
||||
batchCopyTokens,
|
||||
batchDeleteTokens,
|
||||
copyText,
|
||||
|
||||
// Filters state
|
||||
formInitValues,
|
||||
@@ -401,7 +408,6 @@ function TokensPage() {
|
||||
setShowEdit={setShowEdit}
|
||||
batchCopyTokens={batchCopyTokens}
|
||||
batchDeleteTokens={batchDeleteTokens}
|
||||
copyText={copyText}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
|
||||
@@ -116,8 +116,7 @@ export default function CCSwitchModal({
|
||||
Toast.warning(t('请选择主模型'));
|
||||
return;
|
||||
}
|
||||
const apiKey = 'sk-' + tokenKey;
|
||||
const url = buildCCSwitchURL(app, name, models, apiKey);
|
||||
const url = buildCCSwitchURL(app, name, models, 'sk-' + tokenKey);
|
||||
window.open(url, '_blank');
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -20,24 +20,21 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import React from 'react';
|
||||
import { Modal, Button, Space } from '@douyinfe/semi-ui';
|
||||
|
||||
const CopyTokensModal = ({ visible, onCancel, selectedKeys, copyText, t }) => {
|
||||
const CopyTokensModal = ({
|
||||
visible,
|
||||
onCancel,
|
||||
batchCopyTokens,
|
||||
t,
|
||||
}) => {
|
||||
// Handle copy with name and key format
|
||||
const handleCopyWithName = async () => {
|
||||
let content = '';
|
||||
for (let i = 0; i < selectedKeys.length; i++) {
|
||||
content += selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
|
||||
}
|
||||
await copyText(content);
|
||||
await batchCopyTokens('name+key');
|
||||
onCancel();
|
||||
};
|
||||
|
||||
// Handle copy with key only format
|
||||
const handleCopyKeyOnly = async () => {
|
||||
let content = '';
|
||||
for (let i = 0; i < selectedKeys.length; i++) {
|
||||
content += 'sk-' + selectedKeys[i].key + '\n';
|
||||
}
|
||||
await copyText(content);
|
||||
await batchCopyTokens('key-only');
|
||||
onCancel();
|
||||
};
|
||||
|
||||
|
||||
@@ -337,6 +337,7 @@ export const getLogsColumns = ({
|
||||
showUserInfoFunc,
|
||||
openChannelAffinityUsageCacheModal,
|
||||
isAdminUser,
|
||||
billingDisplayMode = 'price',
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
@@ -761,11 +762,10 @@ export const getLogsColumns = ({
|
||||
Boolean(other?.violation_fee_marker)
|
||||
) {
|
||||
const feeQuota = other?.fee_quota ?? record?.quota;
|
||||
const ratioText = formatRatio(other?.group_ratio);
|
||||
const summary = [
|
||||
t('违规扣费'),
|
||||
`${t('分组倍率')}:${ratioText}`,
|
||||
`${t('扣费')}:${renderQuota(feeQuota, 6)}`,
|
||||
`${t('分组倍率')}:${formatRatio(other?.group_ratio)}`,
|
||||
text ? `${t('详情')}:${text}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -808,6 +808,7 @@ export const getLogsColumns = ({
|
||||
1.0,
|
||||
other?.is_system_prompt_overwritten,
|
||||
'claude',
|
||||
billingDisplayMode,
|
||||
)
|
||||
: renderModelPriceSimple(
|
||||
other.model_ratio,
|
||||
@@ -826,6 +827,7 @@ export const getLogsColumns = ({
|
||||
1.0,
|
||||
other?.is_system_prompt_overwritten,
|
||||
'openai',
|
||||
billingDisplayMode,
|
||||
);
|
||||
return (
|
||||
<Typography.Paragraph
|
||||
|
||||
@@ -43,6 +43,7 @@ const LogsTable = (logsData) => {
|
||||
openChannelAffinityUsageCacheModal,
|
||||
hasExpandableRows,
|
||||
isAdminUser,
|
||||
billingDisplayMode,
|
||||
t,
|
||||
COLUMN_KEYS,
|
||||
} = logsData;
|
||||
@@ -56,6 +57,7 @@ const LogsTable = (logsData) => {
|
||||
showUserInfoFunc,
|
||||
openChannelAffinityUsageCacheModal,
|
||||
isAdminUser,
|
||||
billingDisplayMode,
|
||||
});
|
||||
}, [
|
||||
t,
|
||||
@@ -64,6 +66,7 @@ const LogsTable = (logsData) => {
|
||||
showUserInfoFunc,
|
||||
openChannelAffinityUsageCacheModal,
|
||||
isAdminUser,
|
||||
billingDisplayMode,
|
||||
]);
|
||||
|
||||
// Filter columns based on visibility settings
|
||||
|
||||
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Modal, Button, Checkbox } from '@douyinfe/semi-ui';
|
||||
import { Modal, Button, Checkbox, RadioGroup, Radio } from '@douyinfe/semi-ui';
|
||||
import { getLogsColumns } from '../UsageLogsColumnDefs';
|
||||
|
||||
const ColumnSelectorModal = ({
|
||||
@@ -28,12 +28,18 @@ const ColumnSelectorModal = ({
|
||||
handleColumnVisibilityChange,
|
||||
handleSelectAll,
|
||||
initDefaultColumns,
|
||||
billingDisplayMode,
|
||||
setBillingDisplayMode,
|
||||
COLUMN_KEYS,
|
||||
isAdminUser,
|
||||
copyText,
|
||||
showUserInfoFunc,
|
||||
t,
|
||||
}) => {
|
||||
const isTokensDisplay =
|
||||
typeof localStorage !== 'undefined' &&
|
||||
localStorage.getItem('quota_display_type') === 'TOKENS';
|
||||
|
||||
// Get all columns for display in selector
|
||||
const allColumns = getLogsColumns({
|
||||
t,
|
||||
@@ -41,6 +47,7 @@ const ColumnSelectorModal = ({
|
||||
copyText,
|
||||
showUserInfoFunc,
|
||||
isAdminUser,
|
||||
billingDisplayMode,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -61,6 +68,21 @@ const ColumnSelectorModal = ({
|
||||
}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 600 }}>{t('计费显示模式')}</div>
|
||||
<RadioGroup
|
||||
type='button'
|
||||
value={billingDisplayMode}
|
||||
onChange={(value) => setBillingDisplayMode(value)}
|
||||
>
|
||||
<Radio value='price'>
|
||||
{isTokensDisplay ? t('价格模式') : t('价格模式(默认)')}
|
||||
</Radio>
|
||||
<Radio value='ratio'>
|
||||
{isTokensDisplay ? t('倍率模式(默认)') : t('倍率模式')}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={Object.values(visibleColumns).every((v) => v === true)}
|
||||
indeterminate={
|
||||
|
||||
3
web/src/context/User/index.jsx
vendored
3
web/src/context/User/index.jsx
vendored
@@ -40,6 +40,9 @@ export const UserProvider = ({ children }) => {
|
||||
if (normalizedLanguage && normalizedLanguage !== i18n.language) {
|
||||
i18n.changeLanguage(normalizedLanguage);
|
||||
}
|
||||
if (normalizedLanguage) {
|
||||
localStorage.setItem('i18nextLng', normalizedLanguage);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
|
||||
1444
web/src/helpers/render.jsx
vendored
1444
web/src/helpers/render.jsx
vendored
File diff suppressed because it is too large
Load Diff
25
web/src/helpers/token.js
vendored
25
web/src/helpers/token.js
vendored
@@ -20,8 +20,22 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { API } from './api';
|
||||
|
||||
/**
|
||||
* 获取可用的token keys
|
||||
* @returns {Promise<string[]>} 返回active状态的token key数组
|
||||
* 按需获取单个令牌的真实 key
|
||||
* @param {number|string} tokenId
|
||||
* @returns {Promise<string>} 返回不带 sk- 前缀的真实 token key
|
||||
*/
|
||||
export async function fetchTokenKey(tokenId) {
|
||||
const response = await API.post(`/api/token/${tokenId}/key`);
|
||||
const { success, data, message } = response.data || {};
|
||||
if (!success || !data?.key) {
|
||||
throw new Error(message || 'Failed to fetch token key');
|
||||
}
|
||||
return data.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的 token keys
|
||||
* @returns {Promise<string[]>} 返回 active 状态的不带 sk- 前缀的真实 token key 数组
|
||||
*/
|
||||
export async function fetchTokenKeys() {
|
||||
try {
|
||||
@@ -31,7 +45,12 @@ export async function fetchTokenKeys() {
|
||||
|
||||
const tokenItems = Array.isArray(data) ? data : data.items || [];
|
||||
const activeTokens = tokenItems.filter((token) => token.status === 1);
|
||||
return activeTokens.map((token) => token.key);
|
||||
const keyResults = await Promise.allSettled(
|
||||
activeTokens.map((token) => fetchTokenKey(token.id)),
|
||||
);
|
||||
return keyResults
|
||||
.filter((result) => result.status === 'fulfilled' && result.value)
|
||||
.map((result) => result.value);
|
||||
} catch (error) {
|
||||
console.error('Error fetching token keys:', error);
|
||||
return [];
|
||||
|
||||
95
web/src/helpers/utils.jsx
vendored
95
web/src/helpers/utils.jsx
vendored
@@ -615,6 +615,7 @@ export const calculateModelPrice = ({
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
currency,
|
||||
quotaDisplayType = 'USD',
|
||||
precision = 4,
|
||||
}) => {
|
||||
// 1. 选择实际使用的分组
|
||||
@@ -647,9 +648,34 @@ export const calculateModelPrice = ({
|
||||
// 2. 根据计费类型计算价格
|
||||
if (record.quota_type === 0) {
|
||||
// 按量计费
|
||||
const isTokensDisplay = quotaDisplayType === 'TOKENS';
|
||||
const inputRatioPriceUSD = record.model_ratio * 2 * usedGroupRatio;
|
||||
const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
|
||||
const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
|
||||
const hasRatioValue = (value) =>
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
value !== '' &&
|
||||
Number.isFinite(Number(value));
|
||||
|
||||
const formatRatio = (value) =>
|
||||
hasRatioValue(value) ? Number(Number(value).toFixed(6)) : null;
|
||||
|
||||
if (isTokensDisplay) {
|
||||
return {
|
||||
inputRatio: formatRatio(record.model_ratio),
|
||||
completionRatio: formatRatio(record.completion_ratio),
|
||||
cacheRatio: formatRatio(record.cache_ratio),
|
||||
createCacheRatio: formatRatio(record.create_cache_ratio),
|
||||
imageRatio: formatRatio(record.image_ratio),
|
||||
audioInputRatio: formatRatio(record.audio_ratio),
|
||||
audioOutputRatio: formatRatio(record.audio_completion_ratio),
|
||||
isPerToken: true,
|
||||
isTokensDisplay: true,
|
||||
usedGroup,
|
||||
usedGroupRatio,
|
||||
};
|
||||
}
|
||||
|
||||
let symbol = '$';
|
||||
if (currency === 'CNY') {
|
||||
@@ -675,12 +701,6 @@ export const calculateModelPrice = ({
|
||||
return `${symbol}${numericPrice.toFixed(precision)}`;
|
||||
};
|
||||
|
||||
const hasRatioValue = (value) =>
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
value !== '' &&
|
||||
Number.isFinite(Number(value));
|
||||
|
||||
const inputPrice = formatTokenPrice(inputRatioPriceUSD);
|
||||
const audioInputPrice = hasRatioValue(record.audio_ratio)
|
||||
? formatTokenPrice(inputRatioPriceUSD * Number(record.audio_ratio))
|
||||
@@ -711,6 +731,7 @@ export const calculateModelPrice = ({
|
||||
: null,
|
||||
unitLabel,
|
||||
isPerToken: true,
|
||||
isTokensDisplay: false,
|
||||
usedGroup,
|
||||
usedGroupRatio,
|
||||
};
|
||||
@@ -724,6 +745,7 @@ export const calculateModelPrice = ({
|
||||
return {
|
||||
price: displayVal,
|
||||
isPerToken: false,
|
||||
isTokensDisplay: false,
|
||||
usedGroup,
|
||||
usedGroupRatio,
|
||||
};
|
||||
@@ -733,13 +755,68 @@ export const calculateModelPrice = ({
|
||||
return {
|
||||
price: '-',
|
||||
isPerToken: false,
|
||||
isTokensDisplay: false,
|
||||
usedGroup,
|
||||
usedGroupRatio,
|
||||
};
|
||||
};
|
||||
|
||||
export const getModelPriceItems = (priceData, t) => {
|
||||
export const getModelPriceItems = (
|
||||
priceData,
|
||||
t,
|
||||
quotaDisplayType = 'USD',
|
||||
) => {
|
||||
if (priceData.isPerToken) {
|
||||
if (quotaDisplayType === 'TOKENS' || priceData.isTokensDisplay) {
|
||||
return [
|
||||
{
|
||||
key: 'input-ratio',
|
||||
label: t('输入倍率'),
|
||||
value: priceData.inputRatio,
|
||||
suffix: 'x',
|
||||
},
|
||||
{
|
||||
key: 'completion-ratio',
|
||||
label: t('补全倍率'),
|
||||
value: priceData.completionRatio,
|
||||
suffix: 'x',
|
||||
},
|
||||
{
|
||||
key: 'cache-ratio',
|
||||
label: t('缓存读取倍率'),
|
||||
value: priceData.cacheRatio,
|
||||
suffix: 'x',
|
||||
},
|
||||
{
|
||||
key: 'create-cache-ratio',
|
||||
label: t('缓存创建倍率'),
|
||||
value: priceData.createCacheRatio,
|
||||
suffix: 'x',
|
||||
},
|
||||
{
|
||||
key: 'image-ratio',
|
||||
label: t('图片输入倍率'),
|
||||
value: priceData.imageRatio,
|
||||
suffix: 'x',
|
||||
},
|
||||
{
|
||||
key: 'audio-input-ratio',
|
||||
label: t('音频输入倍率'),
|
||||
value: priceData.audioInputRatio,
|
||||
suffix: 'x',
|
||||
},
|
||||
{
|
||||
key: 'audio-output-ratio',
|
||||
label: t('音频补全倍率'),
|
||||
value: priceData.audioOutputRatio,
|
||||
suffix: 'x',
|
||||
},
|
||||
].filter(
|
||||
(item) =>
|
||||
item.value !== null && item.value !== undefined && item.value !== '',
|
||||
);
|
||||
}
|
||||
|
||||
const unitSuffix = ` / 1${priceData.unitLabel} Tokens`;
|
||||
return [
|
||||
{
|
||||
@@ -798,8 +875,8 @@ export const getModelPriceItems = (priceData, t) => {
|
||||
};
|
||||
|
||||
// 格式化价格信息(用于卡片视图)
|
||||
export const formatPriceInfo = (priceData, t) => {
|
||||
const items = getModelPriceItems(priceData, t);
|
||||
export const formatPriceInfo = (priceData, t, quotaDisplayType = 'USD') => {
|
||||
const items = getModelPriceItems(priceData, t, quotaDisplayType);
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
|
||||
35
web/src/hooks/common/useHeaderBar.js
vendored
35
web/src/hooks/common/useHeaderBar.js
vendored
@@ -150,7 +150,9 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
||||
const handleLanguageChange = useCallback(
|
||||
async (lang) => {
|
||||
// Change language immediately for responsive UX
|
||||
const previousLang = normalizeLanguage(i18n.language);
|
||||
i18n.changeLanguage(lang);
|
||||
localStorage.setItem('i18nextLng', lang);
|
||||
|
||||
// If user is logged in, save preference to backend
|
||||
if (userState?.user?.id) {
|
||||
@@ -159,25 +161,34 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
||||
language: lang,
|
||||
});
|
||||
if (res.data.success) {
|
||||
// Update user context with new setting
|
||||
// Keep user preference and local cache in sync so route changes
|
||||
// don't reapply an older remembered language.
|
||||
let settings = {};
|
||||
if (userState?.user?.setting) {
|
||||
try {
|
||||
const settings = JSON.parse(userState.user.setting);
|
||||
settings.language = lang;
|
||||
userDispatch({
|
||||
type: 'login',
|
||||
payload: {
|
||||
...userState.user,
|
||||
setting: JSON.stringify(settings),
|
||||
},
|
||||
});
|
||||
settings = JSON.parse(userState.user.setting) || {};
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
settings = {};
|
||||
}
|
||||
}
|
||||
|
||||
settings.language = lang;
|
||||
const nextUser = {
|
||||
...userState.user,
|
||||
setting: JSON.stringify(settings),
|
||||
};
|
||||
|
||||
userDispatch({
|
||||
type: 'login',
|
||||
payload: nextUser,
|
||||
});
|
||||
localStorage.setItem('user', JSON.stringify(nextUser));
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore errors - language was already changed locally
|
||||
if (previousLang) {
|
||||
i18n.changeLanguage(previousLang);
|
||||
localStorage.setItem('i18nextLng', previousLang);
|
||||
}
|
||||
console.error('Failed to save language preference:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export const useModelPricingData = () => {
|
||||
[statusState],
|
||||
);
|
||||
|
||||
// 默认货币与站点展示类型同步(USD/CNY),TOKENS 时仍允许切换视图内货币
|
||||
// 默认货币与站点展示类型同步;TOKENS 由视图层走倍率展示
|
||||
const siteDisplayType = useMemo(
|
||||
() => statusState?.status?.quota_display_type || 'USD',
|
||||
[statusState],
|
||||
@@ -88,6 +88,13 @@ export const useModelPricingData = () => {
|
||||
}
|
||||
}, [siteDisplayType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (siteDisplayType === 'TOKENS') {
|
||||
setShowWithRecharge(false);
|
||||
setCurrency('USD');
|
||||
}
|
||||
}, [siteDisplayType]);
|
||||
|
||||
const filteredModels = useMemo(() => {
|
||||
let result = models;
|
||||
|
||||
@@ -356,6 +363,7 @@ export const useModelPricingData = () => {
|
||||
setCurrentPage,
|
||||
currency,
|
||||
setCurrency,
|
||||
siteDisplayType,
|
||||
showWithRecharge,
|
||||
setShowWithRecharge,
|
||||
tokenUnit,
|
||||
|
||||
150
web/src/hooks/tokens/useTokensData.jsx
vendored
150
web/src/hooks/tokens/useTokensData.jsx
vendored
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
import { fetchTokenKey as fetchTokenKeyById } from '../../helpers/token';
|
||||
|
||||
export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -54,6 +55,9 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
// UI state
|
||||
const [compactMode, setCompactMode] = useTableCompactMode('tokens');
|
||||
const [showKeys, setShowKeys] = useState({});
|
||||
const [resolvedTokenKeys, setResolvedTokenKeys] = useState({});
|
||||
const [loadingTokenKeys, setLoadingTokenKeys] = useState({});
|
||||
const keyRequestsRef = useRef({});
|
||||
|
||||
// Form state
|
||||
const [formApi, setFormApi] = useState(null);
|
||||
@@ -87,6 +91,7 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
setTokenCount(payload.total || 0);
|
||||
setActivePage(payload.page || 1);
|
||||
setPageSize(payload.page_size || pageSize);
|
||||
setShowKeys({});
|
||||
};
|
||||
|
||||
// Load tokens function
|
||||
@@ -122,14 +127,86 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTokenKey = async (tokenOrId, options = {}) => {
|
||||
const { suppressError = false } = options;
|
||||
const tokenId =
|
||||
typeof tokenOrId === 'object' ? tokenOrId?.id : Number(tokenOrId);
|
||||
|
||||
if (!tokenId) {
|
||||
const error = new Error(t('令牌不存在'));
|
||||
if (!suppressError) {
|
||||
showError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (resolvedTokenKeys[tokenId]) {
|
||||
return resolvedTokenKeys[tokenId];
|
||||
}
|
||||
|
||||
if (keyRequestsRef.current[tokenId]) {
|
||||
return keyRequestsRef.current[tokenId];
|
||||
}
|
||||
|
||||
const request = (async () => {
|
||||
setLoadingTokenKeys((prev) => ({ ...prev, [tokenId]: true }));
|
||||
try {
|
||||
const fullKey = await fetchTokenKeyById(tokenId);
|
||||
setResolvedTokenKeys((prev) => ({ ...prev, [tokenId]: fullKey }));
|
||||
return fullKey;
|
||||
} catch (error) {
|
||||
const normalizedError = new Error(
|
||||
error?.message || t('获取令牌密钥失败'),
|
||||
);
|
||||
if (!suppressError) {
|
||||
showError(normalizedError.message);
|
||||
}
|
||||
throw normalizedError;
|
||||
} finally {
|
||||
delete keyRequestsRef.current[tokenId];
|
||||
setLoadingTokenKeys((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[tokenId];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
keyRequestsRef.current[tokenId] = request;
|
||||
return request;
|
||||
};
|
||||
|
||||
const toggleTokenVisibility = async (record) => {
|
||||
const tokenId = record?.id;
|
||||
if (!tokenId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (showKeys[tokenId]) {
|
||||
setShowKeys((prev) => ({ ...prev, [tokenId]: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
const fullKey = await fetchTokenKey(record);
|
||||
if (fullKey) {
|
||||
setShowKeys((prev) => ({ ...prev, [tokenId]: true }));
|
||||
}
|
||||
};
|
||||
|
||||
const copyTokenKey = async (record) => {
|
||||
const fullKey = await fetchTokenKey(record);
|
||||
await copyText(`sk-${fullKey}`);
|
||||
};
|
||||
|
||||
// Open link function for chat integrations
|
||||
const onOpenLink = async (type, url, record) => {
|
||||
const fullKey = await fetchTokenKey(record);
|
||||
if (url && url.startsWith('ccswitch')) {
|
||||
openCCSwitchModal(record.key);
|
||||
openCCSwitchModal(fullKey);
|
||||
return;
|
||||
}
|
||||
if (url && url.startsWith('fluent')) {
|
||||
openFluentNotification(record.key);
|
||||
openFluentNotification(fullKey);
|
||||
return;
|
||||
}
|
||||
let status = localStorage.getItem('status');
|
||||
@@ -145,7 +222,7 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
let cherryConfig = {
|
||||
id: 'new-api',
|
||||
baseUrl: serverAddress,
|
||||
apiKey: 'sk-' + record.key,
|
||||
apiKey: `sk-${fullKey}`,
|
||||
};
|
||||
let encodedConfig = encodeURIComponent(
|
||||
encodeToBase64(JSON.stringify(cherryConfig)),
|
||||
@@ -155,7 +232,7 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
let aionuiConfig = {
|
||||
platform: 'new-api',
|
||||
baseUrl: serverAddress,
|
||||
apiKey: 'sk-' + record.key,
|
||||
apiKey: `sk-${fullKey}`,
|
||||
};
|
||||
let encodedConfig = encodeURIComponent(
|
||||
encodeToBase64(JSON.stringify(aionuiConfig)),
|
||||
@@ -164,7 +241,7 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
} else {
|
||||
let encodedServerAddress = encodeURIComponent(serverAddress);
|
||||
url = url.replaceAll('{address}', encodedServerAddress);
|
||||
url = url.replaceAll('{key}', 'sk-' + record.key);
|
||||
url = url.replaceAll('{key}', `sk-${fullKey}`);
|
||||
}
|
||||
|
||||
window.open(url, '_blank');
|
||||
@@ -314,48 +391,28 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
};
|
||||
|
||||
// Batch copy tokens
|
||||
const batchCopyTokens = (copyType) => {
|
||||
const batchCopyTokens = async (copyType) => {
|
||||
if (selectedKeys.length === 0) {
|
||||
showError(t('请至少选择一个令牌!'));
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.info({
|
||||
title: t('复制令牌'),
|
||||
icon: null,
|
||||
content: t('请选择你的复制方式'),
|
||||
footer: (
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
className='px-3 py-1 bg-gray-200 rounded'
|
||||
onClick={async () => {
|
||||
let content = '';
|
||||
for (let i = 0; i < selectedKeys.length; i++) {
|
||||
content +=
|
||||
selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
|
||||
}
|
||||
await copyText(content);
|
||||
Modal.destroyAll();
|
||||
}}
|
||||
>
|
||||
{t('名称+密钥')}
|
||||
</button>
|
||||
<button
|
||||
className='px-3 py-1 bg-blue-500 text-white rounded'
|
||||
onClick={async () => {
|
||||
let content = '';
|
||||
for (let i = 0; i < selectedKeys.length; i++) {
|
||||
content += 'sk-' + selectedKeys[i].key + '\n';
|
||||
}
|
||||
await copyText(content);
|
||||
Modal.destroyAll();
|
||||
}}
|
||||
>
|
||||
{t('仅密钥')}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
try {
|
||||
const keys = await Promise.all(
|
||||
selectedKeys.map((token) => fetchTokenKey(token, { suppressError: true })),
|
||||
);
|
||||
let content = '';
|
||||
for (let i = 0; i < selectedKeys.length; i++) {
|
||||
const fullKey = keys[i];
|
||||
if (copyType === 'name+key') {
|
||||
content += `${selectedKeys[i].name} sk-${fullKey}\n`;
|
||||
} else {
|
||||
content += `sk-${fullKey}\n`;
|
||||
}
|
||||
}
|
||||
await copyText(content);
|
||||
} catch (error) {
|
||||
showError(error?.message || t('复制令牌失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize data
|
||||
@@ -392,6 +449,8 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
setCompactMode,
|
||||
showKeys,
|
||||
setShowKeys,
|
||||
resolvedTokenKeys,
|
||||
loadingTokenKeys,
|
||||
|
||||
// Form state
|
||||
formApi,
|
||||
@@ -403,6 +462,9 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
loadTokens,
|
||||
refresh,
|
||||
copyText,
|
||||
fetchTokenKey,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
onOpenLink,
|
||||
manageToken,
|
||||
searchTokens,
|
||||
|
||||
115
web/src/hooks/usage-logs/useUsageLogsData.jsx
vendored
115
web/src/hooks/usage-logs/useUsageLogsData.jsx
vendored
@@ -78,6 +78,9 @@ export const useLogsData = () => {
|
||||
const STORAGE_KEY = isAdminUser
|
||||
? 'logs-table-columns-admin'
|
||||
: 'logs-table-columns-user';
|
||||
const BILLING_DISPLAY_MODE_STORAGE_KEY = isAdminUser
|
||||
? 'logs-billing-display-mode-admin'
|
||||
: 'logs-billing-display-mode-user';
|
||||
|
||||
// Statistics state
|
||||
const [stat, setStat] = useState({
|
||||
@@ -102,50 +105,6 @@ export const useLogsData = () => {
|
||||
logType: '0',
|
||||
};
|
||||
|
||||
// Column visibility state
|
||||
const [visibleColumns, setVisibleColumns] = useState({});
|
||||
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
||||
|
||||
// Compact mode
|
||||
const [compactMode, setCompactMode] = useTableCompactMode('logs');
|
||||
|
||||
// User info modal state
|
||||
const [showUserInfo, setShowUserInfoModal] = useState(false);
|
||||
const [userInfoData, setUserInfoData] = useState(null);
|
||||
|
||||
// Channel affinity usage cache stats modal state (admin only)
|
||||
const [
|
||||
showChannelAffinityUsageCacheModal,
|
||||
setShowChannelAffinityUsageCacheModal,
|
||||
] = useState(false);
|
||||
const [channelAffinityUsageCacheTarget, setChannelAffinityUsageCacheTarget] =
|
||||
useState(null);
|
||||
|
||||
// Load saved column preferences from localStorage
|
||||
useEffect(() => {
|
||||
const savedColumns = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedColumns) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedColumns);
|
||||
const defaults = getDefaultColumnVisibility();
|
||||
const merged = { ...defaults, ...parsed };
|
||||
|
||||
// For non-admin users, force-hide admin-only columns (does not touch admin settings)
|
||||
if (!isAdminUser) {
|
||||
merged[COLUMN_KEYS.CHANNEL] = false;
|
||||
merged[COLUMN_KEYS.USERNAME] = false;
|
||||
merged[COLUMN_KEYS.RETRY] = false;
|
||||
}
|
||||
setVisibleColumns(merged);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved column preferences', e);
|
||||
initDefaultColumns();
|
||||
}
|
||||
} else {
|
||||
initDefaultColumns();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Get default column visibility based on user role
|
||||
const getDefaultColumnVisibility = () => {
|
||||
return {
|
||||
@@ -166,6 +125,63 @@ export const useLogsData = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const getInitialVisibleColumns = () => {
|
||||
const defaults = getDefaultColumnVisibility();
|
||||
const savedColumns = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
if (!savedColumns) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(savedColumns);
|
||||
const merged = { ...defaults, ...parsed };
|
||||
|
||||
if (!isAdminUser) {
|
||||
merged[COLUMN_KEYS.CHANNEL] = false;
|
||||
merged[COLUMN_KEYS.USERNAME] = false;
|
||||
merged[COLUMN_KEYS.RETRY] = false;
|
||||
}
|
||||
|
||||
return merged;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved column preferences', e);
|
||||
return defaults;
|
||||
}
|
||||
};
|
||||
|
||||
const getInitialBillingDisplayMode = () => {
|
||||
const savedMode = localStorage.getItem(BILLING_DISPLAY_MODE_STORAGE_KEY);
|
||||
if (savedMode === 'price' || savedMode === 'ratio') {
|
||||
return savedMode;
|
||||
}
|
||||
return localStorage.getItem('quota_display_type') === 'TOKENS'
|
||||
? 'ratio'
|
||||
: 'price';
|
||||
};
|
||||
|
||||
// Column visibility state
|
||||
const [visibleColumns, setVisibleColumns] = useState(getInitialVisibleColumns);
|
||||
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
||||
const [billingDisplayMode, setBillingDisplayMode] = useState(
|
||||
getInitialBillingDisplayMode,
|
||||
);
|
||||
|
||||
// Compact mode
|
||||
const [compactMode, setCompactMode] = useTableCompactMode('logs');
|
||||
|
||||
// User info modal state
|
||||
const [showUserInfo, setShowUserInfoModal] = useState(false);
|
||||
const [userInfoData, setUserInfoData] = useState(null);
|
||||
|
||||
// Channel affinity usage cache stats modal state (admin only)
|
||||
const [
|
||||
showChannelAffinityUsageCacheModal,
|
||||
setShowChannelAffinityUsageCacheModal,
|
||||
] = useState(false);
|
||||
const [channelAffinityUsageCacheTarget, setChannelAffinityUsageCacheTarget] =
|
||||
useState(null);
|
||||
|
||||
// Initialize default column visibility
|
||||
const initDefaultColumns = () => {
|
||||
const defaults = getDefaultColumnVisibility();
|
||||
@@ -207,6 +223,10 @@ export const useLogsData = () => {
|
||||
}
|
||||
}, [visibleColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(BILLING_DISPLAY_MODE_STORAGE_KEY, billingDisplayMode);
|
||||
}, [BILLING_DISPLAY_MODE_STORAGE_KEY, billingDisplayMode]);
|
||||
|
||||
// 获取表单值的辅助函数,确保所有值都是字符串
|
||||
const getFormValues = () => {
|
||||
const formValues = formApi ? formApi.getValues() : {};
|
||||
@@ -406,6 +426,7 @@ export const useLogsData = () => {
|
||||
other.cache_creation_ratio_1h ||
|
||||
other.cache_creation_ratio ||
|
||||
1.0,
|
||||
billingDisplayMode,
|
||||
)
|
||||
: renderLogContent(
|
||||
other?.model_ratio,
|
||||
@@ -420,6 +441,7 @@ export const useLogsData = () => {
|
||||
other.web_search_call_count || 0,
|
||||
other.file_search || false,
|
||||
other.file_search_call_count || 0,
|
||||
billingDisplayMode,
|
||||
),
|
||||
});
|
||||
if (logs[i]?.content) {
|
||||
@@ -473,6 +495,7 @@ export const useLogsData = () => {
|
||||
other?.user_group_ratio,
|
||||
other?.cache_tokens || 0,
|
||||
other?.cache_ratio || 1.0,
|
||||
billingDisplayMode,
|
||||
);
|
||||
} else if (other?.claude) {
|
||||
content = renderClaudeModelPrice(
|
||||
@@ -495,6 +518,7 @@ export const useLogsData = () => {
|
||||
other.cache_creation_ratio_1h ||
|
||||
other.cache_creation_ratio ||
|
||||
1.0,
|
||||
billingDisplayMode,
|
||||
);
|
||||
} else {
|
||||
content = renderModelPrice(
|
||||
@@ -521,6 +545,7 @@ export const useLogsData = () => {
|
||||
other?.audio_input_price || 0,
|
||||
other?.image_generation_call || false,
|
||||
other?.image_generation_call_price || 0,
|
||||
billingDisplayMode,
|
||||
);
|
||||
}
|
||||
expandDataLocal.push({
|
||||
@@ -764,6 +789,8 @@ export const useLogsData = () => {
|
||||
visibleColumns,
|
||||
showColumnSelector,
|
||||
setShowColumnSelector,
|
||||
billingDisplayMode,
|
||||
setBillingDisplayMode,
|
||||
handleColumnVisibilityChange,
|
||||
handleSelectAll,
|
||||
initDefaultColumns,
|
||||
|
||||
67
web/src/i18n/locales/en.json
vendored
67
web/src/i18n/locales/en.json
vendored
@@ -85,6 +85,8 @@
|
||||
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude thinking adaptation BudgetTokens = MaxTokens * BudgetTokens percentage",
|
||||
"Claude设置": "Claude settings",
|
||||
"Claude请求头覆盖": "Claude request header override",
|
||||
"Claude请求头追加": "Claude request header append",
|
||||
"Claude会在原有请求头基础上追加这些值,不会覆盖已有同名请求头;重复值会自动忽略。": "Claude appends these values on top of existing request headers. Existing headers are not overwritten, and duplicate values are ignored automatically.",
|
||||
"Client ID": "Client ID",
|
||||
"Client Secret": "Client Secret",
|
||||
"Codex 授权": "",
|
||||
@@ -513,6 +515,8 @@
|
||||
"倍率信息": "Ratio information",
|
||||
"倍率是为了方便换算不同价格的模型": "The magnification is to facilitate the conversion of models with different prices.",
|
||||
"倍率模式": "Ratio Mode",
|
||||
"计费显示模式": "Billing Display Mode",
|
||||
"价格模式(默认)": "Price Mode (Default)",
|
||||
"倍率类型": "Ratio type",
|
||||
"偏好设置": "Preferences",
|
||||
"停止测试": "Stop Testing",
|
||||
@@ -909,6 +913,9 @@
|
||||
"图片生成调用:{{symbol}}{{price}} / 1次": "Image generation call: {{symbol}}{{price}} / 1 time",
|
||||
"图片输入: {{imageRatio}}": "Image input: {{imageRatio}}",
|
||||
"图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Image input price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Image ratio: {{imageRatio}})",
|
||||
"图片输入价格:{{symbol}}{{price}} / 1M tokens": "Image input price: {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格 {{symbol}}{{price}} / 1M tokens": "Image input price {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入 {{price}}": "Image input {{price}}",
|
||||
"图片输入倍率(仅部分模型支持该计费)": "Image input ratio (only supported by some models for billing)",
|
||||
"图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "Ratio settings related to image input, key is model name, value is ratio, only supported by some models for billing",
|
||||
"图生文": "Describe",
|
||||
@@ -1399,6 +1406,7 @@
|
||||
"按倍率类型筛选": "Filter by ratio type",
|
||||
"按倍率设置": "Set by ratio",
|
||||
"按次": "Per request",
|
||||
"按次 {{price}} / 次": "Per request {{price}} / request",
|
||||
"按次计费": "Pay per request",
|
||||
"按照如下格式输入:AccessKey|SecretAccessKey|Region": "Enter in the format: AccessKey|SecretAccessKey|Region",
|
||||
"按量计费": "Pay as you go",
|
||||
@@ -1775,6 +1783,9 @@
|
||||
"格式化 JSON": "Format JSON",
|
||||
"格式正确": "Format Correct",
|
||||
"格式示例:": "Format example:",
|
||||
"前:": "Before:",
|
||||
"配置:": "Config:",
|
||||
"后:": "After:",
|
||||
"格式错误": "Format Error",
|
||||
"检查更新": "Check for updates",
|
||||
"检测到 FluentRead(流畅阅读)": "FluentRead (smooth reading) detected",
|
||||
@@ -1787,7 +1798,10 @@
|
||||
"模型专用区域": "Model-specific area",
|
||||
"模型价格": "Model price",
|
||||
"模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "Model price {{symbol}}{{price}}, {{ratioType}} {{ratio}}",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "Model price {{symbol}}{{price}} / request",
|
||||
"模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Model price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "Model price: {{symbol}}{{price}} / request",
|
||||
"输入 {{price}} / 1M tokens": "Input {{price}} / 1M tokens",
|
||||
"模型倍率": "Model ratio",
|
||||
"模型倍率 {{modelRatio}}": "Model ratio {{modelRatio}}",
|
||||
"模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "Model ratio {{modelRatio}}, cache ratio {{cacheRatio}}, completion ratio {{completionRatio}}, {{ratioType}} {{ratio}}",
|
||||
@@ -2338,11 +2352,16 @@
|
||||
"统计次数": "Statistical count",
|
||||
"统计额度": "Statistical quota",
|
||||
"继续": "Continue",
|
||||
"缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})": "Cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (ratio: {{ratio}})",
|
||||
"缓存 Tokens": "Cache Tokens",
|
||||
"缓存: {{cacheRatio}}": "Cache: {{cacheRatio}}",
|
||||
"缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache price: {{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (Cache ratio: {{cacheRatio}})",
|
||||
"缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Cache ratio: {{cacheRatio}})",
|
||||
"缓存读取价格:{{symbol}}{{price}} / 1M tokens": "Cache read price: {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取价格 {{symbol}}{{price}} / 1M tokens": "Cache read price {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取 {{price}}": "Cache read {{price}}",
|
||||
"缓存读取 {{price}}": "Cache read {{price}}",
|
||||
"缓存倍率": "Cache ratio",
|
||||
"缓存倍率 {{cacheRatio}}": "Cache ratio {{cacheRatio}}",
|
||||
"缓存写": "Cache Write",
|
||||
@@ -2352,8 +2371,20 @@
|
||||
"缓存创建: 1h {{cacheCreationRatio1h}}": "Cache creation: 1h {{cacheCreationRatio1h}}",
|
||||
"缓存创建: 5m {{cacheCreationRatio5m}}": "Cache creation: 5m {{cacheCreationRatio5m}}",
|
||||
"缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Cache creation: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}",
|
||||
"缓存创建 {{price}}": "Cache creation {{price}}",
|
||||
"5m缓存创建 {{price}}": "5m cache creation {{price}}",
|
||||
"1h缓存创建 {{price}}": "1h cache creation {{price}}",
|
||||
"缓存创建 {{price}}": "Cache creation {{price}}",
|
||||
"5m缓存创建 {{price}}": "5m cache creation {{price}}",
|
||||
"1h缓存创建 {{price}}": "1h cache creation {{price}}",
|
||||
"缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Cache creation price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Cache creation ratio: {{cacheCreationRatio}})",
|
||||
"缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Cache creation price: {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Cache creation price {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens": "Cache creation price total: 5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "5m cache creation price: {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "5m cache creation price {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "1h cache creation price: {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "1h cache creation price {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建倍率": "Cache creation ratio",
|
||||
"缓存创建倍率 {{cacheCreationRatio}}": "Cache creation ratio {{cacheCreationRatio}}",
|
||||
"缓存创建倍率 1h {{cacheCreationRatio1h}}": "Cache creation multiplier 1h {{cacheCreationRatio1h}}",
|
||||
@@ -2470,8 +2501,16 @@
|
||||
"获得": "Received",
|
||||
"补全": "Completion",
|
||||
"补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}": "Completion {{completion}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Model price {{symbol}}{{price}} / request * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "Input {{input}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"图片输入 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Image input {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"Web 搜索 {{count}} 次 * {{symbol}}{{price}} / 1K 次": "Web search {{count}} calls * {{symbol}}{{price}} / 1K calls",
|
||||
"文件搜索 {{count}} 次 * {{symbol}}{{price}} / 1K 次": "File search {{count}} calls * {{symbol}}{{price}} / 1K calls",
|
||||
"文字价格 {{textPrice}} + 音频价格 {{audioPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Text price {{textPrice}} + Audio price {{audioPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"输入与缓存价格合计 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Input and cache pricing subtotal * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Completion price: {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (Completion ratio: {{completionRatio}})",
|
||||
"补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "Completion price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens",
|
||||
"补全价格 {{symbol}}{{price}} / 1M tokens": "Completion price {{symbol}}{{price}} / 1M tokens",
|
||||
"补全倍率": "Completion ratio",
|
||||
"补全倍率值": "Completion Ratio Value",
|
||||
"补单": "Complete Order",
|
||||
@@ -2854,6 +2893,7 @@
|
||||
"输入JSON对象": "Enter JSON Object",
|
||||
"输入价格": "Input Price",
|
||||
"输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}": "Input Price: {{symbol}}{{price}} / 1M tokens{{audioPrice}}",
|
||||
"输入价格 {{symbol}}{{price}} / 1M tokens": "Input Price {{symbol}}{{price}} / 1M tokens",
|
||||
"输入你注册的 LinuxDO OAuth APP 的 ID": "Enter the ID of your registered LinuxDO OAuth APP",
|
||||
"输入你的账户名{{username}}以确认删除": "Enter your account name{{username}} to confirm deletion",
|
||||
"输入域名后回车": "Enter domain and press Enter",
|
||||
@@ -3228,9 +3268,36 @@
|
||||
"缓存创建价格": "Input Cache Creation Price",
|
||||
"图片输入价格": "Image Input Price",
|
||||
"音频输入价格": "Audio Input Price",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "Audio input price: {{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格": "Audio Completion Price",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "Audio completion price: {{symbol}}{{price}} / 1M tokens",
|
||||
"适合 MJ / 任务类等按次收费模型。": "Suitable for MJ and other task-based models billed per request.",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "This model's completion ratio is fixed to {{ratio}} by the backend. The completion price cannot be changed here.",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Web search called {{webSearchCallCount}} times",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "File search called {{fileSearchCallCount}} times",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Actual charge: {{symbol}}{{total}} (group pricing adjustment included)",
|
||||
"图片倍率 {{imageRatio}}": "Image ratio {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "Audio ratio {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Standard input: {{tokens}} / 1M * model ratio {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Cached input: {{tokens}} / 1M * model ratio {{modelRatio}} * cache ratio {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Image input: {{tokens}} / 1M * model ratio {{modelRatio}} * image ratio {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Audio input: {{tokens}} / 1M * model ratio {{modelRatio}} * audio ratio {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Output: {{tokens}} / 1M * model ratio {{modelRatio}} * completion ratio {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web search: {{count}} / 1K * unit price {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "File search: {{count}} / 1K * unit price {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Image generation: 1 call * unit price {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "Total: {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "Model ratio {{modelRatio}}, completion ratio {{completionRatio}}, audio ratio {{audioRatio}}, audio completion ratio {{audioCompletionRatio}}, {{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Text output: {{tokens}} / 1M * model ratio {{modelRatio}} * completion ratio {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Audio output: {{tokens}} / 1M * model ratio {{modelRatio}} * audio ratio {{audioRatio}} * audio completion ratio {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Total: text {{textTotal}} + audio {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "Model ratio {{modelRatio}}, output ratio {{completionRatio}}, cache ratio {{cacheRatio}}, {{ratioType}} {{ratio}}",
|
||||
"缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Cache creation ratio 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Cache read: {{tokens}} / 1M * model ratio {{modelRatio}} * cache ratio {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Cache creation: {{tokens}} / 1M * model ratio {{modelRatio}} * cache creation ratio {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m cache creation: {{tokens}} / 1M * model ratio {{modelRatio}} * 5m cache creation ratio {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h cache creation: {{tokens}} / 1M * model ratio {{modelRatio}} * 1h cache creation ratio {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Output: {{tokens}} / 1M * model ratio {{modelRatio}} * output ratio {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "Empty"
|
||||
}
|
||||
}
|
||||
|
||||
48
web/src/i18n/locales/fr.json
vendored
48
web/src/i18n/locales/fr.json
vendored
@@ -86,6 +86,8 @@
|
||||
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Adaptation de la pensée Claude BudgetTokens = MaxTokens * BudgetTokens pourcentage",
|
||||
"Claude设置": "Paramètres Claude",
|
||||
"Claude请求头覆盖": "Remplacement de l'en-tête de la requête Claude",
|
||||
"Claude请求头追加": "Ajout des en-tetes de requete Claude",
|
||||
"Claude会在原有请求头基础上追加这些值,不会覆盖已有同名请求头;重复值会自动忽略。": "Claude ajoute ces valeurs aux en-tetes de requete existants. Les en-tetes existants ne sont pas remplaces et les valeurs en double sont ignorees automatiquement.",
|
||||
"Client ID": "ID client",
|
||||
"Client Secret": "Secret client",
|
||||
"Codex 授权": "",
|
||||
@@ -1765,6 +1767,9 @@
|
||||
"格式化 JSON": "Formater le JSON",
|
||||
"格式正确": "Format valide",
|
||||
"格式示例:": "Exemple de format :",
|
||||
"前:": "Avant :",
|
||||
"配置:": "Configuration :",
|
||||
"后:": "Apres :",
|
||||
"格式错误": "Format invalide",
|
||||
"检查更新": "Vérifier les mises à jour",
|
||||
"检测到 FluentRead(流畅阅读)": "FluentRead détecté",
|
||||
@@ -3200,6 +3205,49 @@
|
||||
"音频补全价格": "Prix de complétion audio",
|
||||
"适合 MJ / 任务类等按次收费模型。": "Convient aux modèles MJ et autres modèles facturés à la requête.",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "Le ratio de complétion de ce modèle est fixé à {{ratio}} par le backend. Le prix de complétion ne peut pas être modifié ici.",
|
||||
"计费显示模式": "Mode d'affichage de la facturation",
|
||||
"价格模式(默认)": "Mode prix (par défaut)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "Prix du modèle {{symbol}}{{price}} / requête",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "Prix du modèle : {{symbol}}{{price}} / requête",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Montant facturé réel : {{symbol}}{{total}} (ajustement tarifaire de groupe inclus)",
|
||||
"缓存读取价格:{{symbol}}{{price}} / 1M tokens": "Prix de lecture du cache : {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取价格 {{symbol}}{{price}} / 1M tokens": "Prix de lecture du cache {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Prix de création du cache : {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Prix de création du cache {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Prix de création du cache 5m : {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Prix de création du cache 5m {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Prix de création du cache 1h : {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Prix de création du cache 1h {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格:{{symbol}}{{price}} / 1M tokens": "Prix d'entrée image : {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格 {{symbol}}{{price}} / 1M tokens": "Prix d'entrée image {{symbol}}{{price}} / 1M tokens",
|
||||
"输入价格 {{symbol}}{{price}} / 1M tokens": "Prix d'entrée {{symbol}}{{price}} / 1M tokens",
|
||||
"补全价格 {{symbol}}{{price}} / 1M tokens": "Prix de complétion {{symbol}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "Prix d'entrée audio : {{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "Prix de complétion audio : {{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Recherche Web appelée {{webSearchCallCount}} fois",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "Recherche de fichier appelée {{fileSearchCallCount}} fois",
|
||||
"图片倍率 {{imageRatio}}": "Ratio d'image {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "Ratio audio {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée standard : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée en cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio du cache {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée d'image : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio d'image {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée audio : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio audio {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de complétion {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Recherche Web : {{count}} / 1K * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Recherche de fichier : {{count}} / 1K * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Génération d'image : 1 appel * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "Total : {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "Ratio du modèle {{modelRatio}}, ratio de complétion {{completionRatio}}, ratio audio {{audioRatio}}, ratio de complétion audio {{audioCompletionRatio}}, {{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie texte : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de complétion {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie audio : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio audio {{audioRatio}} * ratio de complétion audio {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Total : partie texte {{textTotal}} + partie audio {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "Ratio du modèle {{modelRatio}}, ratio de sortie {{completionRatio}}, ratio du cache {{cacheRatio}}, {{ratioType}} {{ratio}}",
|
||||
"缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Ratio de création du cache 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Lecture du cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio du cache {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache 5m : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache 1h : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de sortie {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "Vide"
|
||||
}
|
||||
}
|
||||
|
||||
48
web/src/i18n/locales/ja.json
vendored
48
web/src/i18n/locales/ja.json
vendored
@@ -82,6 +82,8 @@
|
||||
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude思考モード:BudgetTokens = MaxTokens * BudgetTokensの割合",
|
||||
"Claude设置": "Claude設定",
|
||||
"Claude请求头覆盖": "Claudeリクエストヘッダーの上書き",
|
||||
"Claude请求头追加": "Claudeリクエストヘッダーの追加",
|
||||
"Claude会在原有请求头基础上追加这些值,不会覆盖已有同名请求头;重复值会自动忽略。": "Claude は既存のリクエストヘッダーにこれらの値を追加します。既存の同名ヘッダーは上書きされず、重複した値は自動的に無視されます。",
|
||||
"Client ID": "Client ID",
|
||||
"Client Secret": "Client Secret",
|
||||
"Codex 授权": "",
|
||||
@@ -1748,6 +1750,9 @@
|
||||
"格式化 JSON": "JSON を整形",
|
||||
"格式正确": "有効な形式",
|
||||
"格式示例:": "フォーマット例:",
|
||||
"前:": "前:",
|
||||
"配置:": "設定:",
|
||||
"后:": "後:",
|
||||
"格式错误": "無効な形式",
|
||||
"检查更新": "更新を確認",
|
||||
"检测到 FluentRead(流畅阅读)": "FluentReadが検出されました",
|
||||
@@ -3181,6 +3186,49 @@
|
||||
"音频补全价格": "音声補完価格",
|
||||
"适合 MJ / 任务类等按次收费模型。": "MJ やその他のリクエスト単位課金モデルに適しています。",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "このモデルの補完倍率はバックエンドで {{ratio}} に固定されています。ここでは補完価格を変更できません。",
|
||||
"计费显示模式": "課金表示モード",
|
||||
"价格模式(默认)": "価格モード(デフォルト)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "モデル価格 {{symbol}}{{price}} / リクエスト",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "モデル価格:{{symbol}}{{price}} / リクエスト",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "実際の請求額:{{symbol}}{{total}}(グループ価格調整込み)",
|
||||
"缓存读取价格:{{symbol}}{{price}} / 1M tokens": "キャッシュ読み取り価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取价格 {{symbol}}{{price}} / 1M tokens": "キャッシュ読み取り価格 {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格:{{symbol}}{{price}} / 1M tokens": "キャッシュ作成価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格 {{symbol}}{{price}} / 1M tokens": "キャッシュ作成価格 {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "5m キャッシュ作成価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "5m キャッシュ作成価格 {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "1h キャッシュ作成価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "1h キャッシュ作成価格 {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格:{{symbol}}{{price}} / 1M tokens": "画像入力価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格 {{symbol}}{{price}} / 1M tokens": "画像入力価格 {{symbol}}{{price}} / 1M tokens",
|
||||
"输入价格 {{symbol}}{{price}} / 1M tokens": "入力価格 {{symbol}}{{price}} / 1M tokens",
|
||||
"补全价格 {{symbol}}{{price}} / 1M tokens": "補完価格 {{symbol}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "音声入力価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "音声補完価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Web 検索呼び出し {{webSearchCallCount}} 回",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "ファイル検索呼び出し {{fileSearchCallCount}} 回",
|
||||
"图片倍率 {{imageRatio}}": "画像倍率 {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "音声倍率 {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "通常入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "画像入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 画像倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音声入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 音声倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 補完倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web 検索: {{count}} / 1K * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "ファイル検索: {{count}} / 1K * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "画像生成: 1 回 * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "合計: {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "モデル倍率 {{modelRatio}}、補完倍率 {{completionRatio}}、音声倍率 {{audioRatio}}、音声補完倍率 {{audioCompletionRatio}}、{{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "テキスト出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 補完倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音声出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 音声倍率 {{audioRatio}} * 音声補完倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "合計: テキスト部分 {{textTotal}} + 音声部分 {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "モデル倍率 {{modelRatio}}、出力倍率 {{completionRatio}}、キャッシュ倍率 {{cacheRatio}}、{{ratioType}} {{ratio}}",
|
||||
"缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "キャッシュ作成倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ読み取り: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ作成倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 5m キャッシュ作成倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 1h キャッシュ作成倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 出力倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "空"
|
||||
}
|
||||
}
|
||||
|
||||
48
web/src/i18n/locales/ru.json
vendored
48
web/src/i18n/locales/ru.json
vendored
@@ -89,6 +89,8 @@
|
||||
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Адаптация мышления Claude BudgetTokens = MaxTokens * процент BudgetTokens",
|
||||
"Claude设置": "Настройки Claude",
|
||||
"Claude请求头覆盖": "Переопределение заголовков запроса Claude",
|
||||
"Claude请求头追加": "Добавление заголовков запроса Claude",
|
||||
"Claude会在原有请求头基础上追加这些值,不会覆盖已有同名请求头;重复值会自动忽略。": "Claude добавляет эти значения поверх существующих заголовков запроса. Уже существующие заголовки не перезаписываются, а дублирующиеся значения автоматически игнорируются.",
|
||||
"Client ID": "ID клиента",
|
||||
"Client Secret": "Секрет клиента",
|
||||
"Codex 授权": "",
|
||||
@@ -1777,6 +1779,9 @@
|
||||
"格式化 JSON": "Форматировать JSON",
|
||||
"格式正确": "Действительный формат",
|
||||
"格式示例:": "Пример формата: ",
|
||||
"前:": "До:",
|
||||
"配置:": "Конфиг:",
|
||||
"后:": "После:",
|
||||
"格式错误": "Недействительный формат",
|
||||
"检查更新": "Проверить обновления",
|
||||
"检测到 FluentRead(流畅阅读)": "Обнаружен FluentRead (плавное чтение)",
|
||||
@@ -3214,6 +3219,49 @@
|
||||
"音频补全价格": "Цена завершения аудио",
|
||||
"适合 MJ / 任务类等按次收费模型。": "Подходит для MJ и других моделей с тарификацией за запрос.",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "Коэффициент завершения для этой модели зафиксирован на уровне {{ratio}} на бэкенде. Цену завершения нельзя изменить здесь.",
|
||||
"计费显示模式": "Режим отображения тарификации",
|
||||
"价格模式(默认)": "Режим цен (по умолчанию)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "Цена модели {{symbol}}{{price}} / запрос",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "Цена модели: {{symbol}}{{price}} / запрос",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Фактическое списание: {{symbol}}{{total}} (включая групповую ценовую корректировку)",
|
||||
"缓存读取价格:{{symbol}}{{price}} / 1M tokens": "Цена чтения кеша: {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取价格 {{symbol}}{{price}} / 1M tokens": "Цена чтения кеша {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Цена создания кеша: {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Цена создания кеша {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Цена создания кеша 5m: {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Цена создания кеша 5m {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Цена создания кеша 1h: {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Цена создания кеша 1h {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格:{{symbol}}{{price}} / 1M tokens": "Цена входного изображения: {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格 {{symbol}}{{price}} / 1M tokens": "Цена входного изображения {{symbol}}{{price}} / 1M tokens",
|
||||
"输入价格 {{symbol}}{{price}} / 1M tokens": "Цена ввода {{symbol}}{{price}} / 1M tokens",
|
||||
"补全价格 {{symbol}}{{price}} / 1M tokens": "Цена завершения {{symbol}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "Цена входного аудио: {{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "Цена завершения аудио: {{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Web-поиск вызван {{webSearchCallCount}} раз",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "Поиск файлов вызван {{fileSearchCallCount}} раз",
|
||||
"图片倍率 {{imageRatio}}": "Коэффициент изображения {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "Аудио-коэффициент {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Обычный ввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Кэшированный ввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент кэша {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Ввод изображения: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент изображения {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Аудиоввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * аудио-коэффициент {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент завершения {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web-поиск: {{count}} / 1K * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Поиск файлов: {{count}} / 1K * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Генерация изображения: 1 вызов * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "Итого: {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "Коэффициент модели {{modelRatio}}, коэффициент завершения {{completionRatio}}, аудио-коэффициент {{audioRatio}}, коэффициент аудиозавершения {{audioCompletionRatio}}, {{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Текстовый вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент завершения {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Аудиовывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * аудио-коэффициент {{audioRatio}} * коэффициент аудиозавершения {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Итого: текстовая часть {{textTotal}} + аудиочасть {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "Коэффициент модели {{modelRatio}}, коэффициент вывода {{completionRatio}}, коэффициент кэша {{cacheRatio}}, {{ratioType}} {{ratio}}",
|
||||
"缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Коэффициент создания кэша 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Чтение кэша: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент кэша {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша 5m: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша 1h: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент вывода {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "Пусто"
|
||||
}
|
||||
}
|
||||
|
||||
48
web/src/i18n/locales/vi.json
vendored
48
web/src/i18n/locales/vi.json
vendored
@@ -82,6 +82,8 @@
|
||||
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Thích ứng tư duy Claude BudgetTokens = MaxTokens * Tỷ lệ phần trăm BudgetTokens",
|
||||
"Claude设置": "Cài đặt Claude",
|
||||
"Claude请求头覆盖": "Ghi đè tiêu đề yêu cầu Claude",
|
||||
"Claude请求头追加": "Thêm tiêu đề yêu cầu Claude",
|
||||
"Claude会在原有请求头基础上追加这些值,不会覆盖已有同名请求头;重复值会自动忽略。": "Claude sẽ thêm các giá trị này vào các tiêu đề yêu cầu hiện có. Các tiêu đề cùng tên sẽ không bị ghi đè và các giá trị trùng lặp sẽ tự động bị bỏ qua.",
|
||||
"Client ID": "Client ID",
|
||||
"Client Secret": "Client Secret",
|
||||
"Codex 授权": "",
|
||||
@@ -1749,6 +1751,9 @@
|
||||
"格式化 JSON": "Định dạng JSON",
|
||||
"格式正确": "Định dạng hợp lệ",
|
||||
"格式示例:": "Ví dụ định dạng:",
|
||||
"前:": "Trước:",
|
||||
"配置:": "Cấu hình:",
|
||||
"后:": "Sau:",
|
||||
"格式错误": "Định dạng không hợp lệ",
|
||||
"检查更新": "Kiểm tra cập nhật",
|
||||
"检测到 FluentRead(流畅阅读)": "Đã phát hiện FluentRead (đọc trôi chảy)",
|
||||
@@ -3753,6 +3758,49 @@
|
||||
"音频补全价格": "Giá hoàn thành âm thanh",
|
||||
"适合 MJ / 任务类等按次收费模型。": "Phù hợp cho MJ và các mô hình tính phí theo lượt gọi tương tự.",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "Tỷ lệ hoàn thành của mô hình này được backend cố định ở {{ratio}}. Không thể chỉnh giá hoàn thành tại đây.",
|
||||
"计费显示模式": "Chế độ hiển thị tính phí",
|
||||
"价格模式(默认)": "Chế độ giá (mặc định)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "Giá mô hình {{symbol}}{{price}} / lượt gọi",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "Giá mô hình: {{symbol}}{{price}} / lượt gọi",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Khoản phí thực tế: {{symbol}}{{total}} (đã bao gồm điều chỉnh giá theo nhóm)",
|
||||
"缓存读取价格:{{symbol}}{{price}} / 1M tokens": "Giá đọc bộ nhớ đệm: {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取价格 {{symbol}}{{price}} / 1M tokens": "Giá đọc bộ nhớ đệm {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm: {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 5m: {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 5m {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 1h: {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 1h {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格:{{symbol}}{{price}} / 1M tokens": "Giá đầu vào hình ảnh: {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格 {{symbol}}{{price}} / 1M tokens": "Giá đầu vào hình ảnh {{symbol}}{{price}} / 1M tokens",
|
||||
"输入价格 {{symbol}}{{price}} / 1M tokens": "Giá đầu vào {{symbol}}{{price}} / 1M tokens",
|
||||
"补全价格 {{symbol}}{{price}} / 1M tokens": "Giá hoàn thành {{symbol}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "Giá đầu vào âm thanh: {{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "Giá hoàn thành âm thanh: {{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Đã gọi tìm kiếm Web {{webSearchCallCount}} lần",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "Đã gọi tìm kiếm tệp {{fileSearchCallCount}} lần",
|
||||
"图片倍率 {{imageRatio}}": "Hệ số hình ảnh {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "Hệ số âm thanh {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào thường: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số bộ nhớ đệm {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào hình ảnh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hình ảnh {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào âm thanh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số âm thanh {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hoàn thành {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tìm kiếm Web: {{count}} / 1K * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tìm kiếm tệp: {{count}} / 1K * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo ảnh: 1 lần gọi * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "Tổng cộng: {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "Hệ số mô hình {{modelRatio}}, hệ số hoàn thành {{completionRatio}}, hệ số âm thanh {{audioRatio}}, hệ số hoàn thành âm thanh {{audioCompletionRatio}}, {{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra văn bản: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hoàn thành {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra âm thanh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số âm thanh {{audioRatio}} * hệ số hoàn thành âm thanh {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Tổng cộng: phần văn bản {{textTotal}} + phần âm thanh {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "Hệ số mô hình {{modelRatio}}, hệ số đầu ra {{completionRatio}}, hệ số bộ nhớ đệm {{cacheRatio}}, {{ratioType}} {{ratio}}",
|
||||
"缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "Hệ số tạo bộ nhớ đệm 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đọc bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số bộ nhớ đệm {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm 5m: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm 1h: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số đầu ra {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "Trống"
|
||||
}
|
||||
}
|
||||
|
||||
55
web/src/i18n/locales/zh-CN.json
vendored
55
web/src/i18n/locales/zh-CN.json
vendored
@@ -69,6 +69,8 @@
|
||||
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比",
|
||||
"Claude设置": "Claude设置",
|
||||
"Claude请求头覆盖": "Claude请求头覆盖",
|
||||
"Claude请求头追加": "Claude请求头追加",
|
||||
"Claude会在原有请求头基础上追加这些值,不会覆盖已有同名请求头;重复值会自动忽略。": "Claude会在原有请求头基础上追加这些值,不会覆盖已有同名请求头;重复值会自动忽略。",
|
||||
"Client ID": "Client ID",
|
||||
"Client Secret": "Client Secret",
|
||||
"common.changeLanguage": "common.changeLanguage",
|
||||
@@ -1104,6 +1106,7 @@
|
||||
"按倍率类型筛选": "按倍率类型筛选",
|
||||
"按倍率设置": "按倍率设置",
|
||||
"按次": "按次",
|
||||
"按次 {{price}} / 次": "按次 {{price}} / 次",
|
||||
"按次计费": "按次计费",
|
||||
"按照如下格式输入:AccessKey|SecretAccessKey|Region": "按照如下格式输入:AccessKey|SecretAccessKey|Region",
|
||||
"按量计费": "按量计费",
|
||||
@@ -1414,6 +1417,7 @@
|
||||
"格式化": "格式化",
|
||||
"格式正确": "格式正确",
|
||||
"格式示例:": "格式示例:",
|
||||
"前:": "前:",
|
||||
"格式错误": "格式错误",
|
||||
"检查更新": "检查更新",
|
||||
"检测到 FluentRead(流畅阅读)": "检测到 FluentRead(流畅阅读)",
|
||||
@@ -2829,7 +2833,9 @@
|
||||
"下面展示这个模型保存后会写入哪些后端字段,便于和原始 JSON 编辑框保持一致。": "下面展示这个模型保存后会写入哪些后端字段,便于和原始 JSON 编辑框保持一致。",
|
||||
"补全价格已锁定": "补全价格已锁定",
|
||||
"后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。",
|
||||
"后:": "后:",
|
||||
"这些价格都是可选项,不填也可以。": "这些价格都是可选项,不填也可以。",
|
||||
"配置:": "配置:",
|
||||
"请先开启并填写音频输入价格。": "请先开启并填写音频输入价格。",
|
||||
"输入模型名称,例如 gpt-4.1": "输入模型名称,例如 gpt-4.1",
|
||||
"当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。": "当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。",
|
||||
@@ -2858,6 +2864,55 @@
|
||||
"音频补全价格": "音频补全价格",
|
||||
"适合 MJ / 任务类等按次收费模型。": "适合 MJ / 任务类等按次收费模型。",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。",
|
||||
"计费显示模式": "计费显示模式",
|
||||
"价格模式(默认)": "价格模式(默认)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "模型价格 {{symbol}}{{price}} / 次",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "模型价格:{{symbol}}{{price}} / 次",
|
||||
"输入 {{price}} / 1M tokens": "输入 {{price}} / 1M tokens",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)",
|
||||
"缓存读取价格:{{symbol}}{{price}} / 1M tokens": "缓存读取价格:{{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取价格 {{symbol}}{{price}} / 1M tokens": "缓存读取价格 {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取 {{price}}": "缓存读取 {{price}}",
|
||||
"缓存创建价格:{{symbol}}{{price}} / 1M tokens": "缓存创建价格:{{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格 {{symbol}}{{price}} / 1M tokens": "缓存创建价格 {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建 {{price}}": "缓存创建 {{price}}",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "5m缓存创建价格:{{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "5m缓存创建价格 {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建 {{price}}": "5m缓存创建 {{price}}",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "1h缓存创建价格:{{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "1h缓存创建价格 {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建 {{price}}": "1h缓存创建 {{price}}",
|
||||
"图片输入价格:{{symbol}}{{price}} / 1M tokens": "图片输入价格:{{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格 {{symbol}}{{price}} / 1M tokens": "图片输入价格 {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入 {{price}}": "图片输入 {{price}}",
|
||||
"输入价格 {{symbol}}{{price}} / 1M tokens": "输入价格 {{symbol}}{{price}} / 1M tokens",
|
||||
"补全价格 {{symbol}}{{price}} / 1M tokens": "补全价格 {{symbol}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "音频输入价格:{{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "音频补全价格:{{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Web 搜索调用 {{webSearchCallCount}} 次",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "文件搜索调用 {{fileSearchCallCount}} 次",
|
||||
"图片倍率 {{imageRatio}}": "图片倍率 {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "音频倍率 {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "合计:{{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}",
|
||||
"缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "空"
|
||||
}
|
||||
}
|
||||
|
||||
55
web/src/i18n/locales/zh-TW.json
vendored
55
web/src/i18n/locales/zh-TW.json
vendored
@@ -69,6 +69,8 @@
|
||||
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude思考相容 BudgetTokens = MaxTokens * BudgetTokens 百分比",
|
||||
"Claude设置": "Claude設定",
|
||||
"Claude请求头覆盖": "Claude請求頭覆蓋",
|
||||
"Claude请求头追加": "Claude請求頭追加",
|
||||
"Claude会在原有请求头基础上追加这些值,不会覆盖已有同名请求头;重复值会自动忽略。": "Claude會在原有請求頭基礎上追加這些值,不會覆蓋已有同名請求頭;重複值會自動忽略。",
|
||||
"Client ID": "Client ID",
|
||||
"Client Secret": "Client Secret",
|
||||
"common.changeLanguage": "common.changeLanguage",
|
||||
@@ -1107,6 +1109,7 @@
|
||||
"按倍率类型筛选": "按倍率類型篩選",
|
||||
"按倍率设置": "按倍率設定",
|
||||
"按次": "按次",
|
||||
"按次 {{price}} / 次": "按次 {{price}} / 次",
|
||||
"按次计费": "按次計費",
|
||||
"按照如下格式输入:AccessKey|SecretAccessKey|Region": "按照如下格式輸入:AccessKey|SecretAccessKey|Region",
|
||||
"按量计费": "按量計費",
|
||||
@@ -1418,6 +1421,9 @@
|
||||
"格式化": "格式化",
|
||||
"格式正确": "格式正確",
|
||||
"格式示例:": "格式示例:",
|
||||
"前:": "前:",
|
||||
"配置:": "配置:",
|
||||
"后:": "後:",
|
||||
"格式错误": "格式錯誤",
|
||||
"检查更新": "檢查更新",
|
||||
"检测到 FluentRead(流畅阅读)": "檢測到 FluentRead(流暢閱讀)",
|
||||
@@ -2851,6 +2857,55 @@
|
||||
"音频补全价格": "音訊補全價格",
|
||||
"适合 MJ / 任务类等按次收费模型。": "適合 MJ / 任務類等按次收費模型。",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "該模型補全倍率由後端固定為 {{ratio}}。補全價格不能在這裡修改。",
|
||||
"计费显示模式": "計費顯示模式",
|
||||
"价格模式(默认)": "價格模式(預設)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "模型價格 {{symbol}}{{price}} / 次",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "模型價格:{{symbol}}{{price}} / 次",
|
||||
"输入 {{price}} / 1M tokens": "輸入 {{price}} / 1M tokens",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "實際結算金額:{{symbol}}{{total}}(已包含分組價格調整)",
|
||||
"缓存读取价格:{{symbol}}{{price}} / 1M tokens": "快取讀取價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取价格 {{symbol}}{{price}} / 1M tokens": "快取讀取價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取 {{price}}": "快取讀取 {{price}}",
|
||||
"缓存创建价格:{{symbol}}{{price}} / 1M tokens": "快取建立價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格 {{symbol}}{{price}} / 1M tokens": "快取建立價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建 {{price}}": "快取建立 {{price}}",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "5m快取建立價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "5m快取建立價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建 {{price}}": "5m快取建立 {{price}}",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "1h快取建立價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "1h快取建立價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建 {{price}}": "1h快取建立 {{price}}",
|
||||
"图片输入价格:{{symbol}}{{price}} / 1M tokens": "圖片輸入價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格 {{symbol}}{{price}} / 1M tokens": "圖片輸入價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入 {{price}}": "圖片輸入 {{price}}",
|
||||
"输入价格 {{symbol}}{{price}} / 1M tokens": "輸入價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"补全价格 {{symbol}}{{price}} / 1M tokens": "補全價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "音訊輸入價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "音訊補全價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Web 搜尋呼叫 {{webSearchCallCount}} 次",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "檔案搜尋呼叫 {{fileSearchCallCount}} 次",
|
||||
"图片倍率 {{imageRatio}}": "圖片倍率 {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "音訊倍率 {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "普通輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "圖片輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 圖片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音訊輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音訊倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 補全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web 搜尋:{{count}} / 1K * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "檔案搜尋:{{count}} / 1K * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "圖片生成:1 次 * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "合計:{{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},補全倍率 {{completionRatio}},音訊倍率 {{audioRatio}},音訊補全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "文字輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 補全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音訊輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音訊倍率 {{audioRatio}} * 音訊補全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "合計:文字部分 {{textTotal}} + 音訊部分 {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},輸出倍率 {{completionRatio}},快取倍率 {{cacheRatio}},{{ratioType}} {{ratio}}",
|
||||
"缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}": "快取建立倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取讀取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取建立倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m快取建立倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h快取建立倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 輸出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "空"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,16 @@ const CLAUDE_HEADER = {
|
||||
},
|
||||
};
|
||||
|
||||
const CLAUDE_HEADER_APPEND_CONFIG = {
|
||||
'claude-3-7-sonnet-20250219-thinking': {
|
||||
'anthropic-beta': ['token-efficient-tools-2025-02-19'],
|
||||
},
|
||||
};
|
||||
|
||||
const CLAUDE_HEADER_APPEND_BEFORE = `anthropic-beta: output-128k-2025-02-19`;
|
||||
|
||||
const CLAUDE_HEADER_APPEND_AFTER = `anthropic-beta: output-128k-2025-02-19,token-efficient-tools-2025-02-19`;
|
||||
|
||||
const CLAUDE_DEFAULT_MAX_TOKENS = {
|
||||
default: 8192,
|
||||
'claude-3-haiku-20240307': 4096,
|
||||
@@ -114,7 +124,7 @@ export default function SettingClaudeModel(props) {
|
||||
<Row>
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
<Form.TextArea
|
||||
label={t('Claude请求头覆盖')}
|
||||
label={t('Claude请求头追加')}
|
||||
field={'claude.model_headers_settings'}
|
||||
placeholder={
|
||||
t('为一个 JSON 文本,例如:') +
|
||||
@@ -122,7 +132,20 @@ export default function SettingClaudeModel(props) {
|
||||
JSON.stringify(CLAUDE_HEADER, null, 2)
|
||||
}
|
||||
extraText={
|
||||
t('示例') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)
|
||||
<div>
|
||||
<div>
|
||||
{t(
|
||||
'Claude会在原有请求头基础上追加这些值,不会覆盖已有同名请求头;重复值会自动忽略。',
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-2 whitespace-pre-wrap font-mono text-xs'>
|
||||
{`${t('前:')}\n${CLAUDE_HEADER_APPEND_BEFORE}\n\n${t('配置:')}\n${JSON.stringify(
|
||||
CLAUDE_HEADER_APPEND_CONFIG,
|
||||
null,
|
||||
2,
|
||||
)}\n\n${t('后:')}\n${CLAUDE_HEADER_APPEND_AFTER}`}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
|
||||
@@ -59,6 +59,11 @@ const formatNumber = (value) => {
|
||||
return parseFloat(num.toFixed(12)).toString();
|
||||
};
|
||||
|
||||
const toNormalizedNumber = (value) => {
|
||||
const formatted = formatNumber(value);
|
||||
return formatted === '' ? null : Number(formatted);
|
||||
};
|
||||
|
||||
const parseOptionJSON = (rawValue) => {
|
||||
if (!rawValue || rawValue.trim() === '') {
|
||||
return {};
|
||||
@@ -123,7 +128,11 @@ const buildModelState = (name, sourceMaps) => {
|
||||
lockedCompletionRatio: completionRatioMeta.ratio,
|
||||
completionPrice:
|
||||
inputPriceNumber !== null &&
|
||||
hasValue(completionRatioMeta.locked ? completionRatioMeta.ratio : completionRatio)
|
||||
hasValue(
|
||||
completionRatioMeta.locked
|
||||
? completionRatioMeta.ratio
|
||||
: completionRatio,
|
||||
)
|
||||
? formatNumber(
|
||||
inputPriceNumber *
|
||||
Number(
|
||||
@@ -192,7 +201,9 @@ export const getModelWarnings = (model, t) => {
|
||||
].some(hasValue);
|
||||
|
||||
if (model.hasConflict) {
|
||||
warnings.push(t('当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。'));
|
||||
warnings.push(
|
||||
t('当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。'),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -207,11 +218,17 @@ export const getModelWarnings = (model, t) => {
|
||||
].some(hasValue)
|
||||
) {
|
||||
warnings.push(
|
||||
t('当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。'),
|
||||
t(
|
||||
'当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (model.billingMode === 'per-token' && hasDerivedPricing && !hasValue(model.inputPrice)) {
|
||||
if (
|
||||
model.billingMode === 'per-token' &&
|
||||
hasDerivedPricing &&
|
||||
!hasValue(model.inputPrice)
|
||||
) {
|
||||
warnings.push(t('按量计费下需要先填写输入价格,才能保存其它价格项。'));
|
||||
}
|
||||
|
||||
@@ -249,7 +266,8 @@ export const buildSummaryText = (model, t) => {
|
||||
};
|
||||
|
||||
export const buildOptionalFieldToggles = (model) => ({
|
||||
completionPrice: model.completionRatioLocked || hasValue(model.completionPrice),
|
||||
completionPrice:
|
||||
model.completionRatioLocked || hasValue(model.completionPrice),
|
||||
cachePrice: hasValue(model.cachePrice),
|
||||
createCachePrice: hasValue(model.createCachePrice),
|
||||
imagePrice: hasValue(model.imagePrice),
|
||||
@@ -271,7 +289,7 @@ const serializeModel = (model, t) => {
|
||||
|
||||
if (model.billingMode === 'per-request') {
|
||||
if (hasValue(model.fixedPrice)) {
|
||||
result.ModelPrice = Number(model.fixedPrice);
|
||||
result.ModelPrice = toNormalizedNumber(model.fixedPrice);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -296,57 +314,68 @@ const serializeModel = (model, t) => {
|
||||
if (inputPrice === null) {
|
||||
if (hasDependentPrice) {
|
||||
throw new Error(
|
||||
t('模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率', {
|
||||
name: model.name,
|
||||
}),
|
||||
t(
|
||||
'模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率',
|
||||
{
|
||||
name: model.name,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (hasValue(model.rawRatios.modelRatio)) {
|
||||
result.ModelRatio = Number(model.rawRatios.modelRatio);
|
||||
result.ModelRatio = toNormalizedNumber(model.rawRatios.modelRatio);
|
||||
}
|
||||
if (hasValue(model.rawRatios.completionRatio)) {
|
||||
result.CompletionRatio = Number(model.rawRatios.completionRatio);
|
||||
result.CompletionRatio = toNormalizedNumber(
|
||||
model.rawRatios.completionRatio,
|
||||
);
|
||||
}
|
||||
if (hasValue(model.rawRatios.cacheRatio)) {
|
||||
result.CacheRatio = Number(model.rawRatios.cacheRatio);
|
||||
result.CacheRatio = toNormalizedNumber(model.rawRatios.cacheRatio);
|
||||
}
|
||||
if (hasValue(model.rawRatios.createCacheRatio)) {
|
||||
result.CreateCacheRatio = Number(model.rawRatios.createCacheRatio);
|
||||
result.CreateCacheRatio = toNormalizedNumber(
|
||||
model.rawRatios.createCacheRatio,
|
||||
);
|
||||
}
|
||||
if (hasValue(model.rawRatios.imageRatio)) {
|
||||
result.ImageRatio = Number(model.rawRatios.imageRatio);
|
||||
result.ImageRatio = toNormalizedNumber(model.rawRatios.imageRatio);
|
||||
}
|
||||
if (hasValue(model.rawRatios.audioRatio)) {
|
||||
result.AudioRatio = Number(model.rawRatios.audioRatio);
|
||||
result.AudioRatio = toNormalizedNumber(model.rawRatios.audioRatio);
|
||||
}
|
||||
if (hasValue(model.rawRatios.audioCompletionRatio)) {
|
||||
result.AudioCompletionRatio = Number(model.rawRatios.audioCompletionRatio);
|
||||
result.AudioCompletionRatio = toNormalizedNumber(
|
||||
model.rawRatios.audioCompletionRatio,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
result.ModelRatio = inputPrice / 2;
|
||||
result.ModelRatio = toNormalizedNumber(inputPrice / 2);
|
||||
|
||||
if (!model.completionRatioLocked && completionPrice !== null) {
|
||||
result.CompletionRatio = completionPrice / inputPrice;
|
||||
result.CompletionRatio = toNormalizedNumber(completionPrice / inputPrice);
|
||||
} else if (
|
||||
model.completionRatioLocked &&
|
||||
hasValue(model.rawRatios.completionRatio)
|
||||
) {
|
||||
result.CompletionRatio = Number(model.rawRatios.completionRatio);
|
||||
result.CompletionRatio = toNormalizedNumber(
|
||||
model.rawRatios.completionRatio,
|
||||
);
|
||||
}
|
||||
if (cachePrice !== null) {
|
||||
result.CacheRatio = cachePrice / inputPrice;
|
||||
result.CacheRatio = toNormalizedNumber(cachePrice / inputPrice);
|
||||
}
|
||||
if (createCachePrice !== null) {
|
||||
result.CreateCacheRatio = createCachePrice / inputPrice;
|
||||
result.CreateCacheRatio = toNormalizedNumber(createCachePrice / inputPrice);
|
||||
}
|
||||
if (imagePrice !== null) {
|
||||
result.ImageRatio = imagePrice / inputPrice;
|
||||
result.ImageRatio = toNormalizedNumber(imagePrice / inputPrice);
|
||||
}
|
||||
if (audioInputPrice !== null) {
|
||||
result.AudioRatio = audioInputPrice / inputPrice;
|
||||
result.AudioRatio = toNormalizedNumber(audioInputPrice / inputPrice);
|
||||
}
|
||||
if (audioOutputPrice !== null) {
|
||||
if (audioInputPrice === null || audioInputPrice === 0) {
|
||||
@@ -356,7 +385,9 @@ const serializeModel = (model, t) => {
|
||||
}),
|
||||
);
|
||||
}
|
||||
result.AudioCompletionRatio = audioOutputPrice / audioInputPrice;
|
||||
result.AudioCompletionRatio = toNormalizedNumber(
|
||||
audioOutputPrice / audioInputPrice,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -455,7 +486,8 @@ export const buildPreviewRows = (model, t) => {
|
||||
{
|
||||
key: 'CacheRatio',
|
||||
label: 'CacheRatio',
|
||||
value: cachePrice !== null ? formatNumber(cachePrice / inputPrice) : t('空'),
|
||||
value:
|
||||
cachePrice !== null ? formatNumber(cachePrice / inputPrice) : t('空'),
|
||||
},
|
||||
{
|
||||
key: 'CreateCacheRatio',
|
||||
@@ -468,7 +500,8 @@ export const buildPreviewRows = (model, t) => {
|
||||
{
|
||||
key: 'ImageRatio',
|
||||
label: 'ImageRatio',
|
||||
value: imagePrice !== null ? formatNumber(imagePrice / inputPrice) : t('空'),
|
||||
value:
|
||||
imagePrice !== null ? formatNumber(imagePrice / inputPrice) : t('空'),
|
||||
},
|
||||
{
|
||||
key: 'AudioRatio',
|
||||
@@ -482,7 +515,9 @@ export const buildPreviewRows = (model, t) => {
|
||||
key: 'AudioCompletionRatio',
|
||||
label: 'AudioCompletionRatio',
|
||||
value:
|
||||
audioOutputPrice !== null && audioInputPrice !== null && audioInputPrice !== 0
|
||||
audioOutputPrice !== null &&
|
||||
audioInputPrice !== null &&
|
||||
audioInputPrice !== 0
|
||||
? formatNumber(audioOutputPrice / audioInputPrice)
|
||||
: t('空'),
|
||||
},
|
||||
@@ -585,7 +620,8 @@ export function useModelPricingEditorState({
|
||||
}, [currentPage, filteredModels]);
|
||||
|
||||
const selectedModel = useMemo(
|
||||
() => visibleModels.find((model) => model.name === selectedModelName) || null,
|
||||
() =>
|
||||
visibleModels.find((model) => model.name === selectedModelName) || null,
|
||||
[selectedModelName, visibleModels],
|
||||
);
|
||||
|
||||
@@ -605,7 +641,9 @@ export function useModelPricingEditorState({
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedModelNames((previous) =>
|
||||
previous.filter((name) => visibleModels.some((model) => model.name === name)),
|
||||
previous.filter((name) =>
|
||||
visibleModels.some((model) => model.name === name),
|
||||
),
|
||||
);
|
||||
}, [visibleModels]);
|
||||
|
||||
@@ -779,7 +817,9 @@ export function useModelPricingEditorState({
|
||||
delete next[name];
|
||||
return next;
|
||||
});
|
||||
setSelectedModelNames((previous) => previous.filter((item) => item !== name));
|
||||
setSelectedModelNames((previous) =>
|
||||
previous.filter((item) => item !== name),
|
||||
);
|
||||
if (selectedModelName === name) {
|
||||
setSelectedModelName(nextModels[0]?.name || '');
|
||||
}
|
||||
@@ -823,7 +863,8 @@ export function useModelPricingEditorState({
|
||||
hasValue(nextModel.lockedCompletionRatio)
|
||||
) {
|
||||
nextModel.completionPrice = formatNumber(
|
||||
Number(nextModel.inputPrice) * Number(nextModel.lockedCompletionRatio),
|
||||
Number(nextModel.inputPrice) *
|
||||
Number(nextModel.lockedCompletionRatio),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user