Compare commits

...

14 Commits

Author SHA1 Message Date
QuentinHsu d92d1fadaf fix(usage-logs): preserve mobile log row field access 2026-06-02 13:04:51 +08:00
同語 0ff9c35e62 feat(web): support classic Rsbuild dev and build
Merge pull request #5232 from QuantumNous/feat/classic-rsbuild-dev-workflow
2026-06-02 11:33:33 +08:00
QuentinHsu 0bbcaa8999 fix(classic): inject Semi React 19 adapter 2026-06-02 00:50:29 +08:00
QuentinHsu 1e9ff8a0de feat(web): support classic Rsbuild dev and build
- migrate the classic frontend from Vite to Rsbuild with JSX, Semi UI, proxy, and production build config.
- update make dev-web to run both default and classic frontends for local theme switching.
- fix classic public page height, footer, CORS proxy, error handling, and constant export warnings.
- update Dockerfile and release workflow to install from the web workspace root with the shared lockfile.
2026-06-02 00:32:16 +08:00
同語 9a2e60dff2 chore(web): centralize shared frontend dependency versions #5227
Merge pull request #5227 from QuantumNous/chore/web-shared-dependency-catalog
2026-06-01 19:19:13 +08:00
QuentinHsu b596de739d chore(web): centralize shared frontend dependency versions
- add a web workspace catalog to manage dependency versions shared by default and classic frontends.
- switch shared dependencies including @lobehub/icons to catalog references and align @lobehub/icons on 5.10.0.
- replace separate frontend Bun lockfiles with a unified web/bun.lock to reduce duplicate maintenance.
2026-06-01 19:12:39 +08:00
同語 45d54c1613 fix(pricing): sync custom model icons #5224
Merge pull request #5224 from QuantumNous/fix/pricing-model-icons
2026-06-01 18:17:58 +08:00
QuentinHsu 086044650d fix(pricing): sync custom model icons
- add the icon field to the pricing model type to consume model-level icons returned by the backend.
- prefer model icons in cards, table model cells, and detail headers while falling back to vendor icons.
2026-06-01 17:58:02 +08:00
GGXH 0c7aceb831 feat: add claude opus 4.8 support (#5177) 2026-05-31 13:50:52 +08:00
dependabot[bot] b2e25b7df2 chore(deps): bump axios from 1.15.2 to 1.16.0 in /web/classic (#5185)
Bumps [axios](https://github.com/axios/axios) from 1.15.2 to 1.16.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.2...v1.16.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 13:49:50 +08:00
skynono 230a3592f8 perf: order admin logs by created_at to use composite index (#5116) 2026-05-30 20:00:02 +08:00
YuPeng Wu afb470e405 fix(model): correct idx_created_at_id index column order to (created_at, id) (#5191)
The idx_created_at_id composite index on the logs table was defined as
(id, created_at) because the GORM `priority` values on Id and CreatedAt were
swapped. Since `id` is the auto-increment primary key, a secondary composite
index leading with `id` is redundant with the PK and cannot accelerate
`created_at` range scans (a range column must sit at the index prefix).

This defeats the common log-listing queries
(`WHERE created_at BETWEEN ? AND ? ORDER BY id DESC LIMIT n` in
GetAllLogs/GetUserLogs) that the index name implies it should serve — the
optimizer falls back to scanning the primary key, degrading to near full-table
scans on large logs tables.

Swap the priorities so the column order becomes (created_at, id), matching the
index name and its intended purpose. idx_user_id_id and idx_created_at_type are
unaffected.

Note: GORM AutoMigrate does not change the column order of an already-existing
index with the same name, so existing deployments must rebuild the index
manually (see PR description for per-database DDL).

Co-authored-by: wuyupeng <wuyupeng@floatmiracle.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:54:02 +08:00
CaIon 1588027084 feat: add subscription balance redemption toggle (#3071) 2026-05-29 12:54:00 +08:00
zengwei 38bf2d8daa feat(keys/cc-switch-dialog): 修复自定义cc-switch名称失焦后重置问题 (#5170) 2026-05-29 12:18:52 +08:00
55 changed files with 1692 additions and 3003 deletions
+18 -12
View File
@@ -33,16 +33,18 @@ jobs:
env:
CI: ""
run: |
cd web/default
bun install
cd web
bun install --frozen-lockfile
cd default
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../..
- name: Build Frontend (classic)
env:
CI: ""
run: |
cd web/classic
bun install
cd web
bun install --frozen-lockfile
cd classic
VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../..
- name: Set up Go
@@ -91,16 +93,18 @@ jobs:
CI: ""
NODE_OPTIONS: "--max-old-space-size=4096"
run: |
cd web/default
bun install
cd web
bun install --frozen-lockfile
cd default
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../..
- name: Build Frontend (classic)
env:
CI: ""
run: |
cd web/classic
bun install
cd web
bun install --frozen-lockfile
cd classic
VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../..
- name: Set up Go
@@ -146,16 +150,18 @@ jobs:
env:
CI: ""
run: |
cd web/default
bun install
cd web
bun install --frozen-lockfile
cd default
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../..
- name: Build Frontend (classic)
env:
CI: ""
run: |
cd web/classic
bun install
cd web
bun install --frozen-lockfile
cd classic
VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../..
- name: Set up Go
+18 -16
View File
@@ -1,22 +1,24 @@
FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder
WORKDIR /build
COPY web/default/package.json .
COPY web/default/bun.lock .
RUN bun install
COPY ./web/default .
COPY ./VERSION .
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
WORKDIR /build/web
COPY web/package.json web/bun.lock ./
COPY web/default/package.json ./default/package.json
COPY web/classic/package.json ./classic/package.json
RUN bun install --frozen-lockfile
COPY ./web/default ./default
COPY ./VERSION /build/VERSION
RUN cd default && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build
FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder-classic
WORKDIR /build
COPY web/classic/package.json .
COPY web/classic/bun.lock .
RUN bun install
COPY ./web/classic .
COPY ./VERSION .
RUN VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
WORKDIR /build/web
COPY web/package.json web/bun.lock ./
COPY web/default/package.json ./default/package.json
COPY web/classic/package.json ./classic/package.json
RUN bun install --frozen-lockfile
COPY ./web/classic ./classic
COPY ./VERSION /build/VERSION
RUN cd classic && VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build
FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2
ENV GO111MODULE=on CGO_ENABLED=0
@@ -32,8 +34,8 @@ ADD go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=builder /build/dist ./web/default/dist
COPY --from=builder-classic /build/dist ./web/classic/dist
COPY --from=builder /build/web/default/dist ./web/default/dist
COPY --from=builder-classic /build/web/classic/dist ./web/classic/dist
RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
+8
View File
@@ -41,6 +41,7 @@ func GetSubscriptionPlans(c *gin.Context) {
}
result := make([]SubscriptionPlanDTO, 0, len(plans))
for _, p := range plans {
p.NormalizeDefaults()
result = append(result, SubscriptionPlanDTO{
Plan: p,
})
@@ -125,6 +126,7 @@ func AdminListSubscriptionPlans(c *gin.Context) {
}
result := make([]SubscriptionPlanDTO, 0, len(plans))
for _, p := range plans {
p.NormalizeDefaults()
result = append(result, SubscriptionPlanDTO{
Plan: p,
})
@@ -163,6 +165,9 @@ func AdminCreateSubscriptionPlan(c *gin.Context) {
req.Plan.Currency = "USD"
}
req.Plan.Currency = "USD"
if req.Plan.AllowBalancePay == nil {
req.Plan.AllowBalancePay = common.GetPointer(true)
}
if req.Plan.DurationUnit == "" {
req.Plan.DurationUnit = model.SubscriptionDurationMonth
}
@@ -279,6 +284,9 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) {
"quota_reset_custom_seconds": req.Plan.QuotaResetCustomSeconds,
"updated_at": common.GetTimestamp(),
}
if req.Plan.AllowBalancePay != nil {
updateMap["allow_balance_pay"] = *req.Plan.AllowBalancePay
}
if err := tx.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Updates(updateMap).Error; err != nil {
return err
}
+32 -5
View File
@@ -1,6 +1,8 @@
FRONTEND_DIR = ./web/default
FRONTEND_CLASSIC_DIR = ./web/classic
BACKEND_DIR = .
DEV_FRONTEND_DEFAULT_PORT ?= 5173
DEV_FRONTEND_CLASSIC_PORT ?= 5174
DEV_COMPOSE_FILE = docker-compose.dev.yml
DEV_POSTGRES_SERVICE = postgres
DEV_BACKEND_SERVICE = new-api
@@ -14,11 +16,13 @@ all: build-all-frontends start-backend
build-frontend:
@echo "Building default frontend..."
@cd $(FRONTEND_DIR) && bun install && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat ../../VERSION) bun run build
@cd ./web && bun install --frozen-lockfile
@cd $(FRONTEND_DIR) && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat ../../VERSION) bun run build
build-frontend-classic:
@echo "Building classic frontend..."
@cd $(FRONTEND_CLASSIC_DIR) && bun install && VITE_REACT_APP_VERSION=$(cat ../../VERSION) bun run build
@cd ./web && bun install --frozen-lockfile
@cd $(FRONTEND_CLASSIC_DIR) && VITE_REACT_APP_VERSION=$(cat ../../VERSION) bun run build
build-all-frontends: build-frontend build-frontend-classic
@@ -35,12 +39,35 @@ dev-api-rebuild:
@docker compose -f $(DEV_COMPOSE_FILE) up -d --build $(DEV_BACKEND_SERVICE)
dev-web:
@echo "Starting frontend dev server..."
@cd $(FRONTEND_DIR) && bun install && bun run dev
@echo "Starting both frontend dev servers..."
@echo "Default frontend: http://localhost:$(DEV_FRONTEND_DEFAULT_PORT)"
@echo "Classic frontend: http://localhost:$(DEV_FRONTEND_CLASSIC_PORT)"
@cd ./web && bun install
@(cd $(FRONTEND_DIR) && bun run dev -- --host 0.0.0.0 --port $(DEV_FRONTEND_DEFAULT_PORT)) & \
default_pid=$$!; \
(cd $(FRONTEND_CLASSIC_DIR) && bun run dev -- --host 0.0.0.0 --port $(DEV_FRONTEND_CLASSIC_PORT)) & \
classic_pid=$$!; \
trap 'kill $$default_pid $$classic_pid 2>/dev/null; wait $$default_pid $$classic_pid 2>/dev/null; exit 130' INT TERM; \
while kill -0 $$default_pid 2>/dev/null && kill -0 $$classic_pid 2>/dev/null; do \
sleep 1; \
done; \
if ! kill -0 $$default_pid 2>/dev/null; then \
wait $$default_pid; \
status=$$?; \
kill $$classic_pid 2>/dev/null; \
wait $$classic_pid 2>/dev/null; \
exit $$status; \
fi; \
wait $$classic_pid; \
status=$$?; \
kill $$default_pid 2>/dev/null; \
wait $$default_pid 2>/dev/null; \
exit $$status
dev-web-classic:
@echo "Starting classic frontend dev server..."
@cd $(FRONTEND_CLASSIC_DIR) && bun install && bun run dev
@cd ./web && bun install
@cd $(FRONTEND_CLASSIC_DIR) && bun run dev -- --host 0.0.0.0 --port $(DEV_FRONTEND_CLASSIC_PORT)
dev: dev-api dev-web
+3 -3
View File
@@ -32,9 +32,9 @@ func applyExplicitLogTextFilter(tx *gorm.DB, column string, value string) (*gorm
}
type Log struct {
Id int `json:"id" gorm:"index:idx_created_at_id,priority:1;index:idx_user_id_id,priority:2"`
Id int `json:"id" gorm:"index:idx_created_at_id,priority:2;index:idx_user_id_id,priority:2"`
UserId int `json:"user_id" gorm:"index;index:idx_user_id_id,priority:1"`
CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_created_at_id,priority:2;index:idx_created_at_type"`
CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_created_at_id,priority:1;index:idx_created_at_type"`
Type int `json:"type" gorm:"index:idx_created_at_type"`
Content string `json:"content"`
Username string `json:"username" gorm:"index;index:index_username_model_name,priority:2;default:''"`
@@ -354,7 +354,7 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
if err != nil {
return nil, 0, err
}
err = tx.Order("logs.id desc").Limit(num).Offset(startIdx).Find(&logs).Error
err = tx.Order("logs.created_at desc, logs.id desc").Limit(num).Offset(startIdx).Find(&logs).Error
if err != nil {
return nil, 0, err
}
+2
View File
@@ -397,6 +397,7 @@ func ensureSubscriptionPlanTableSQLite() error {
` + "`custom_seconds`" + ` bigint NOT NULL DEFAULT 0,
` + "`enabled`" + ` numeric DEFAULT 1,
` + "`sort_order`" + ` integer DEFAULT 0,
` + "`allow_balance_pay`" + ` numeric DEFAULT 1,
` + "`stripe_price_id`" + ` varchar(128) DEFAULT '',
` + "`creem_product_id`" + ` varchar(128) DEFAULT '',
` + "`waffo_pancake_product_id`" + ` varchar(128) DEFAULT '',
@@ -431,6 +432,7 @@ PRIMARY KEY (` + "`id`" + `)
{Name: "custom_seconds", DDL: "`custom_seconds` bigint NOT NULL DEFAULT 0"},
{Name: "enabled", DDL: "`enabled` numeric DEFAULT 1"},
{Name: "sort_order", DDL: "`sort_order` integer DEFAULT 0"},
{Name: "allow_balance_pay", DDL: "`allow_balance_pay` numeric DEFAULT 1"},
{Name: "stripe_price_id", DDL: "`stripe_price_id` varchar(128) DEFAULT ''"},
{Name: "creem_product_id", DDL: "`creem_product_id` varchar(128) DEFAULT ''"},
{Name: "waffo_pancake_product_id", DDL: "`waffo_pancake_product_id` varchar(128) DEFAULT ''"},
+13
View File
@@ -160,6 +160,8 @@ type SubscriptionPlan struct {
Enabled bool `json:"enabled" gorm:"default:true"`
SortOrder int `json:"sort_order" gorm:"type:int;default:0"`
AllowBalancePay *bool `json:"allow_balance_pay" gorm:"default:true"`
StripePriceId string `json:"stripe_price_id" gorm:"type:varchar(128);default:''"`
CreemProductId string `json:"creem_product_id" gorm:"type:varchar(128);default:''"`
WaffoPancakeProductId string `json:"waffo_pancake_product_id" gorm:"type:varchar(128);default:''"`
@@ -193,6 +195,12 @@ func (p *SubscriptionPlan) BeforeUpdate(tx *gorm.DB) error {
return nil
}
func (p *SubscriptionPlan) NormalizeDefaults() {
if p.AllowBalancePay == nil {
p.AllowBalancePay = common.GetPointer(true)
}
}
// Subscription order (payment -> webhook -> create UserSubscription)
type SubscriptionOrder struct {
Id int `json:"id"`
@@ -360,6 +368,7 @@ func getSubscriptionPlanByIdTx(tx *gorm.DB, id int) (*SubscriptionPlan, error) {
key := subscriptionPlanCacheKey(id)
if key != "" {
if cached, found, err := getSubscriptionPlanCache().Get(key); err == nil && found {
cached.NormalizeDefaults()
return &cached, nil
}
}
@@ -371,6 +380,7 @@ func getSubscriptionPlanByIdTx(tx *gorm.DB, id int) (*SubscriptionPlan, error) {
if err := query.Where("id = ?", id).First(&plan).Error; err != nil {
return nil, err
}
plan.NormalizeDefaults()
_ = getSubscriptionPlanCache().SetWithTTL(key, plan, subscriptionPlanCacheTTL())
return &plan, nil
}
@@ -701,6 +711,9 @@ func PurchaseSubscriptionWithBalance(userId int, planId int) error {
if plan.PriceAmount < 0 {
return errors.New("套餐价格不能为负数")
}
if plan.AllowBalancePay != nil && !*plan.AllowBalancePay {
return errors.New("该套餐不允许使用余额兑换")
}
requiredQuota, err := calcSubscriptionBalanceQuota(plan.PriceAmount)
if err != nil {
+6
View File
@@ -19,6 +19,7 @@ var awsModelIDMap = map[string]string{
"claude-opus-4-5-20251101": "anthropic.claude-opus-4-5-20251101-v1:0",
"claude-opus-4-6": "anthropic.claude-opus-4-6-v1",
"claude-opus-4-7": "anthropic.claude-opus-4-7",
"claude-opus-4-8": "anthropic.claude-opus-4-8",
// Nova models
"nova-micro-v1:0": "amazon.nova-micro-v1:0",
"nova-lite-v1:0": "amazon.nova-lite-v1:0",
@@ -97,6 +98,11 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{
"ap": true,
"eu": true,
},
"anthropic.claude-opus-4-8": {
"us": true,
"ap": true,
"eu": true,
},
"anthropic.claude-haiku-4-5-20251001-v1:0": {
"us": true,
"ap": true,
+7
View File
@@ -33,6 +33,13 @@ var ModelList = []string{
"claude-opus-4-7-medium",
"claude-opus-4-7-low",
"claude-opus-4-7-thinking",
"claude-opus-4-8",
"claude-opus-4-8-max",
"claude-opus-4-8-xhigh",
"claude-opus-4-8-high",
"claude-opus-4-8-medium",
"claude-opus-4-8-low",
"claude-opus-4-8-thinking",
}
var ChannelName = "claude"
+9 -5
View File
@@ -154,14 +154,17 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
}
if baseModel, effortLevel, ok := reasoning.TrimEffortSuffix(textRequest.Model); ok && effortLevel != "" &&
(strings.HasPrefix(textRequest.Model, "claude-opus-4-6") || strings.HasPrefix(textRequest.Model, "claude-opus-4-7")) {
(strings.HasPrefix(textRequest.Model, "claude-opus-4-6") ||
strings.HasPrefix(textRequest.Model, "claude-opus-4-7") ||
strings.HasPrefix(textRequest.Model, "claude-opus-4-8")) {
claudeRequest.Model = baseModel
claudeRequest.Thinking = &dto.Thinking{
Type: "adaptive",
}
claudeRequest.OutputConfig = json.RawMessage(fmt.Sprintf(`{"effort":"%s"}`, effortLevel))
if strings.HasPrefix(baseModel, "claude-opus-4-7") {
// Opus 4.7 rejects non-default temperature/top_p/top_k with 400
if strings.HasPrefix(baseModel, "claude-opus-4-7") ||
strings.HasPrefix(baseModel, "claude-opus-4-8") {
// Opus 4.7/4.8 reject non-default temperature/top_p/top_k with 400
// and defaults display to "omitted"; restore the 4.6 visible summary.
claudeRequest.Thinking.Display = "summarized"
claudeRequest.Temperature = nil
@@ -175,8 +178,9 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
strings.HasSuffix(textRequest.Model, "-thinking") {
trimmedModel := strings.TrimSuffix(textRequest.Model, "-thinking")
if strings.HasPrefix(trimmedModel, "claude-opus-4-7") {
// Opus 4.7 rejects thinking.type="enabled"; use adaptive at high effort.
if strings.HasPrefix(trimmedModel, "claude-opus-4-7") ||
strings.HasPrefix(trimmedModel, "claude-opus-4-8") {
// Opus 4.7/4.8 reject thinking.type="enabled"; use adaptive at high effort.
claudeRequest.Thinking = &dto.Thinking{Type: "adaptive", Display: "summarized"}
claudeRequest.OutputConfig = json.RawMessage(`{"effort":"high"}`)
claudeRequest.Temperature = nil
+56
View File
@@ -9,6 +9,10 @@ import (
"github.com/stretchr/testify/require"
)
func commonPointer[T any](value T) *T {
return &value
}
func TestFormatClaudeResponseInfo_MessageStart(t *testing.T) {
claudeInfo := &ClaudeResponseInfo{
Usage: &dto.Usage{},
@@ -310,6 +314,58 @@ func TestRequestOpenAI2ClaudeMessage_IgnoresUnsupportedFileContent(t *testing.T)
require.Equal(t, "see attachment", *content[0].Text)
}
func TestRequestOpenAI2ClaudeMessage_ClaudeOpus48HighUsesAdaptiveThinking(t *testing.T) {
request := dto.GeneralOpenAIRequest{
Model: "claude-opus-4-8-high",
Temperature: commonPointer(0.7),
TopP: commonPointer(0.9),
TopK: commonPointer(40),
Messages: []dto.Message{
{
Role: "user",
Content: "hello",
},
},
}
claudeRequest, err := RequestOpenAI2ClaudeMessage(nil, request)
require.NoError(t, err)
require.Equal(t, "claude-opus-4-8", claudeRequest.Model)
require.NotNil(t, claudeRequest.Thinking)
require.Equal(t, "adaptive", claudeRequest.Thinking.Type)
require.Equal(t, "summarized", claudeRequest.Thinking.Display)
require.JSONEq(t, `{"effort":"high"}`, string(claudeRequest.OutputConfig))
require.Nil(t, claudeRequest.Temperature)
require.Nil(t, claudeRequest.TopP)
require.Nil(t, claudeRequest.TopK)
}
func TestRequestOpenAI2ClaudeMessage_ClaudeOpus48ThinkingUsesAdaptiveHighEffort(t *testing.T) {
request := dto.GeneralOpenAIRequest{
Model: "claude-opus-4-8-thinking",
Temperature: commonPointer(0.7),
TopP: commonPointer(0.9),
TopK: commonPointer(40),
Messages: []dto.Message{
{
Role: "user",
Content: "hello",
},
},
}
claudeRequest, err := RequestOpenAI2ClaudeMessage(nil, request)
require.NoError(t, err)
require.Equal(t, "claude-opus-4-8", claudeRequest.Model)
require.NotNil(t, claudeRequest.Thinking)
require.Equal(t, "adaptive", claudeRequest.Thinking.Type)
require.Equal(t, "summarized", claudeRequest.Thinking.Display)
require.JSONEq(t, `{"effort":"high"}`, string(claudeRequest.OutputConfig))
require.Nil(t, claudeRequest.Temperature)
require.Nil(t, claudeRequest.TopP)
require.Nil(t, claudeRequest.TopK)
}
func TestRequestOpenAI2ClaudeMessage_SupportsPDFFileContent(t *testing.T) {
request := dto.GeneralOpenAIRequest{
Model: "claude-3-5-sonnet",
+1
View File
@@ -45,6 +45,7 @@ var claudeModelMap = map[string]string{
"claude-opus-4-5-20251101": "claude-opus-4-5@20251101",
"claude-opus-4-6": "claude-opus-4-6",
"claude-opus-4-7": "claude-opus-4-7",
"claude-opus-4-8": "claude-opus-4-8",
}
const anthropicVersion = "vertex-2023-10-16"
+9 -5
View File
@@ -53,14 +53,17 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
}
if baseModel, effortLevel, ok := reasoning.TrimEffortSuffix(request.Model); ok && effortLevel != "" &&
(strings.HasPrefix(request.Model, "claude-opus-4-6") || strings.HasPrefix(request.Model, "claude-opus-4-7")) {
(strings.HasPrefix(request.Model, "claude-opus-4-6") ||
strings.HasPrefix(request.Model, "claude-opus-4-7") ||
strings.HasPrefix(request.Model, "claude-opus-4-8")) {
request.Model = baseModel
request.Thinking = &dto.Thinking{
Type: "adaptive",
}
request.OutputConfig = json.RawMessage(fmt.Sprintf(`{"effort":"%s"}`, effortLevel))
if strings.HasPrefix(request.Model, "claude-opus-4-7") {
// Opus 4.7 rejects non-default temperature/top_p/top_k with 400
if strings.HasPrefix(request.Model, "claude-opus-4-7") ||
strings.HasPrefix(request.Model, "claude-opus-4-8") {
// Opus 4.7/4.8 reject non-default temperature/top_p/top_k with 400
// and defaults display to "omitted"; restore the 4.6 visible summary.
request.Thinking.Display = "summarized"
request.Temperature = nil
@@ -74,8 +77,9 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
strings.HasSuffix(request.Model, "-thinking") {
if request.Thinking == nil {
baseModel := strings.TrimSuffix(request.Model, "-thinking")
if strings.HasPrefix(baseModel, "claude-opus-4-7") {
// Opus 4.7 rejects thinking.type="enabled"; use adaptive at high effort.
if strings.HasPrefix(baseModel, "claude-opus-4-7") ||
strings.HasPrefix(baseModel, "claude-opus-4-8") {
// Opus 4.7/4.8 reject thinking.type="enabled"; use adaptive at high effort.
request.Thinking = &dto.Thinking{Type: "adaptive", Display: "summarized"}
request.OutputConfig = json.RawMessage(`{"effort":"high"}`)
request.Temperature = nil
+14
View File
@@ -71,6 +71,13 @@ var defaultCacheRatio = map[string]float64{
"claude-opus-4-7-high": 0.1,
"claude-opus-4-7-medium": 0.1,
"claude-opus-4-7-low": 0.1,
"claude-opus-4-8": 0.1,
"claude-opus-4-8-thinking": 0.1,
"claude-opus-4-8-max": 0.1,
"claude-opus-4-8-xhigh": 0.1,
"claude-opus-4-8-high": 0.1,
"claude-opus-4-8-medium": 0.1,
"claude-opus-4-8-low": 0.1,
}
var defaultCreateCacheRatio = map[string]float64{
@@ -106,6 +113,13 @@ var defaultCreateCacheRatio = map[string]float64{
"claude-opus-4-7-high": 1.25,
"claude-opus-4-7-medium": 1.25,
"claude-opus-4-7-low": 1.25,
"claude-opus-4-8": 1.25,
"claude-opus-4-8-thinking": 1.25,
"claude-opus-4-8-max": 1.25,
"claude-opus-4-8-xhigh": 1.25,
"claude-opus-4-8-high": 1.25,
"claude-opus-4-8-medium": 1.25,
"claude-opus-4-8-low": 1.25,
}
//var defaultCreateCacheRatio = map[string]float64{}
+6
View File
@@ -152,6 +152,12 @@ var defaultModelRatio = map[string]float64{
"claude-opus-4-7-high": 2.5,
"claude-opus-4-7-medium": 2.5,
"claude-opus-4-7-low": 2.5,
"claude-opus-4-8": 2.5,
"claude-opus-4-8-max": 2.5,
"claude-opus-4-8-xhigh": 2.5,
"claude-opus-4-8-high": 2.5,
"claude-opus-4-8-medium": 2.5,
"claude-opus-4-8-low": 2.5,
"claude-3-opus-20240229": 7.5, // $15 / 1M tokens
"claude-opus-4-20250514": 7.5,
"claude-opus-4-1-20250805": 7.5,
+1136 -371
View File
File diff suppressed because it is too large Load Diff
-2379
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -24,6 +24,5 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
+21 -20
View File
@@ -4,30 +4,32 @@
"private": true,
"type": "module",
"dependencies": {
"@douyinfe/semi-illustrations": "^2.69.1",
"@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-ui": "^2.69.1",
"@lobehub/icons": "^2.0.0",
"@lobehub/icons": "catalog:",
"@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8",
"axios": "1.15.2",
"clsx": "^2.1.1",
"dayjs": "^1.11.11",
"axios": "catalog:",
"clsx": "catalog:",
"dayjs": "catalog:",
"history": "^5.3.0",
"highlight.js": "^11.11.1",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^7.2.0",
"katex": "^0.16.22",
"lucide-react": "^0.511.0",
"marked": "^4.1.1",
"mermaid": "^11.6.0",
"qrcode.react": "^4.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"qrcode.react": "catalog:",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-dropzone": "^14.2.3",
"react-fireworks": "^1.0.4",
"react-i18next": "^13.0.0",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-icons": "catalog:",
"react-markdown": "catalog:",
"react-router-dom": "^6.3.0",
"react-telegram-login": "^1.1.2",
"react-toastify": "^9.0.8",
@@ -35,20 +37,20 @@
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-gfm": "catalog:",
"remark-math": "^6.0.0",
"sse.js": "^2.6.0",
"sse.js": "catalog:",
"unist-util-visit": "^5.0.0",
"use-debounce": "^10.0.4"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"dev": "rsbuild dev",
"build": "rsbuild build",
"lint": "prettier . --check",
"lint:fix": "prettier . --write",
"eslint": "bunx eslint \"**/*.{js,jsx}\" --cache",
"eslint:fix": "bunx eslint \"**/*.{js,jsx}\" --fix --cache",
"preview": "vite preview",
"preview": "rsbuild preview",
"i18n:extract": "bunx i18next-cli extract",
"i18n:status": "bunx i18next-cli status",
"i18n:sync": "bunx i18next-cli sync",
@@ -73,20 +75,19 @@
]
},
"devDependencies": {
"@douyinfe/vite-plugin-semi": "^2.74.0-alpha.6",
"@rsbuild/core": "^2.0.7",
"@rsbuild/plugin-react": "^2.0.0",
"@so1ve/prettier-config": "^3.1.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.21",
"code-inspector-plugin": "^1.3.3",
"eslint": "8.57.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-react-hooks": "^5.2.0",
"i18next-cli": "^1.10.3",
"postcss": "^8.5.3",
"prettier": "^3.0.0",
"prop-types": "^15.8.1",
"prettier": "catalog:",
"tailwindcss": "^3",
"typescript": "4.4.2",
"vite": "^5.2.0"
"typescript": "4.4.2"
},
"prettier": {
"singleQuote": true,
+106
View File
@@ -0,0 +1,106 @@
import path from 'path'
import { createRequire } from 'module'
import { fileURLToPath } from 'url'
import { defineConfig, loadEnv } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)
const semiUiDir = path.resolve(
path.dirname(require.resolve('@douyinfe/semi-ui')),
'../..',
)
export default defineConfig(({ envMode }) => {
const env = loadEnv({ mode: envMode, prefixes: ['VITE_'] })
const clientServerUrl =
process.env.VITE_REACT_APP_SERVER_URL ||
env.rawPublicVars.VITE_REACT_APP_SERVER_URL ||
''
const proxyServerUrl =
clientServerUrl ||
'http://localhost:3000'
const isProd = envMode === 'production'
const devProxy = Object.fromEntries(
(['/api', '/mj', '/pg'] as const).map((key) => [
key,
{ target: proxyServerUrl, changeOrigin: true },
]),
) as Record<string, { target: string; changeOrigin: boolean }>
return {
plugins: [pluginReact()],
source: {
entry: {
index: './src/index.jsx',
},
define: {
'import.meta.env.VITE_REACT_APP_SERVER_URL': JSON.stringify(
clientServerUrl,
),
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@douyinfe/semi-ui/dist/css/semi.css': path.resolve(
semiUiDir,
'dist/css/semi.css',
),
},
},
html: {
template: './index.html',
},
server: {
host: '0.0.0.0',
strictPort: true,
proxy: devProxy,
},
output: {
minify: isProd,
target: 'web',
distPath: {
root: 'dist',
},
},
performance: {
removeConsole: isProd ? ['log'] : false,
buildCache: {
cacheDigest: [process.env.VITE_REACT_APP_VERSION],
},
},
tools: {
rspack: {
module: {
rules: [
{
test: /src[\\/].*\.js$/,
type: 'javascript/auto',
use: [
{
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'ecmascript',
jsx: true,
},
transform: {
react: {
runtime: 'automatic',
development: !isProd,
refresh: !isProd,
},
},
},
},
},
],
},
],
},
},
},
}
})
+1 -1
View File
@@ -947,7 +947,7 @@ const LoginForm = () => {
};
return (
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
<div className='classic-page-fill relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */}
<div
className='blur-ball blur-ball-indigo'
+1 -1
View File
@@ -104,7 +104,7 @@ const PasswordResetConfirm = () => {
}
return (
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
<div className='classic-page-fill relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */}
<div
className='blur-ball blur-ball-indigo'
+1 -1
View File
@@ -104,7 +104,7 @@ const PasswordResetForm = () => {
}
return (
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
<div className='classic-page-fill relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */}
<div
className='blur-ball blur-ball-indigo'
+1 -1
View File
@@ -770,7 +770,7 @@ const RegisterForm = () => {
};
return (
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
<div className='classic-page-fill relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */}
<div
className='blur-ball blur-ball-indigo'
@@ -141,7 +141,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 显示加载状态
if (loading) {
return (
<div className='flex justify-center items-center min-h-screen'>
<div className='classic-page-fill flex justify-center items-center'>
<Spin size='large' />
</div>
);
@@ -150,7 +150,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 如果没有内容,显示空状态
if (!content || content.trim() === '') {
return (
<div className='flex justify-center items-center min-h-screen bg-gray-50'>
<div className='classic-page-fill flex justify-center items-center bg-gray-50'>
<Empty
title={t('管理员未设置' + title + '内容')}
image={
@@ -168,7 +168,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 如果是 URL,显示链接卡片
if (isUrl(content)) {
return (
<div className='flex justify-center items-center min-h-screen bg-gray-50 p-4'>
<div className='classic-page-fill flex justify-center items-center bg-gray-50 p-4'>
<Card className='max-w-md w-full'>
<div className='text-center'>
<Title heading={4} className='mb-4'>
@@ -196,7 +196,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 如果是 HTML 内容,直接渲染
if (isHtmlContent(content)) {
return (
<div className='min-h-screen bg-gray-50'>
<div className='classic-page-fill bg-gray-50'>
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
<div className='bg-white rounded-lg shadow-sm p-8'>
<Title heading={2} className='text-center mb-8'>
@@ -214,7 +214,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 其他内容统一使用 Markdown 渲染器
return (
<div className='min-h-screen bg-gray-50'>
<div className='classic-page-fill bg-gray-50'>
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
<div className='bg-white rounded-lg shadow-sm p-8'>
<Title heading={2} className='text-center mb-8'>
+10 -5
View File
@@ -71,6 +71,7 @@ const PageLayout = () => {
const isConsoleRoute = location.pathname.startsWith('/console');
const showSider = isConsoleRoute && (!isMobile || drawerOpen);
const isFixedLayout = isConsoleRoute || location.pathname === '/pricing';
useEffect(() => {
if (isMobile && drawerOpen && collapsed) {
@@ -146,11 +147,11 @@ const PageLayout = () => {
return (
<Layout
className='app-layout'
className={`app-layout${isFixedLayout ? ' app-layout-fixed' : ''}`}
style={{
display: 'flex',
flexDirection: 'column',
overflow: isMobile ? 'visible' : 'hidden',
overflow: isFixedLayout && !isMobile ? 'hidden' : 'visible',
}}
>
<Header
@@ -171,9 +172,10 @@ const PageLayout = () => {
</Header>
<Layout
style={{
overflow: isMobile ? 'visible' : 'auto',
overflow: isFixedLayout && !isMobile ? 'auto' : 'visible',
display: 'flex',
flexDirection: 'column',
flex: '1 1 auto',
}}
>
{showSider && (
@@ -206,15 +208,18 @@ const PageLayout = () => {
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column',
minHeight: 0,
}}
>
<Content
className={isFixedLayout ? undefined : 'public-page-content'}
style={{
flex: '1 0 auto',
overflowY: isMobile ? 'visible' : 'hidden',
flex: isFixedLayout ? '1 0 auto' : '1 1 auto',
overflowY: isFixedLayout && !isMobile ? 'hidden' : 'visible',
WebkitOverflowScrolling: 'touch',
padding: shouldInnerPadding ? (isMobile ? '5px' : '24px') : '0',
position: 'relative',
minHeight: 0,
}}
>
<ErrorBoundary>
+43 -9
View File
@@ -17,12 +17,46 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export * from './channel.constants';
export * from './user.constants';
export * from './toast.constants';
export * from './common.constant';
export * from './dashboard.constants';
export * from './playground.constants';
export * from './redemption.constants';
export * from './channel-affinity-template.constants';
export * from './billing.constants';
export {
CHANNEL_OPTIONS,
MODEL_FETCHABLE_CHANNEL_TYPES,
MODEL_TABLE_PAGE_SIZE,
} from './channel.constants';
export { userConstants } from './user.constants';
export { toastConstants } from './toast.constants';
export {
ITEMS_PER_PAGE,
DEFAULT_ENDPOINT,
TABLE_COMPACT_MODES_KEY,
API_ENDPOINTS,
TASK_ACTION_GENERATE,
TASK_ACTION_TEXT_GENERATE,
TASK_ACTION_FIRST_TAIL_GENERATE,
TASK_ACTION_REFERENCE_GENERATE,
TASK_ACTION_REMIX_GENERATE,
} from './common.constant';
export {
REDEMPTION_STATUS,
REDEMPTION_STATUS_MAP,
REDEMPTION_ACTIONS,
} from './redemption.constants';
export {
CODEX_CLI_HEADER_PASSTHROUGH_HEADERS,
CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS,
CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE,
CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE,
CHANNEL_AFFINITY_RULE_TEMPLATES,
cloneChannelAffinityTemplate,
} from './channel-affinity-template.constants';
export {
BILLING_VARS,
BILLING_VAR_KEYS,
BILLING_PRICING_VARS,
BILLING_EXTRA_VARS,
BILLING_VAR_KEY_TO_FIELD,
BILLING_VAR_FIELD_TO_LABEL,
BILLING_VAR_FIELD_TO_SHORT_LABEL,
BILLING_CACHE_VAR_MAP,
BILLING_VAR_REGEX,
BILLING_CONDITION_VARS,
} from './billing.constants';
+2 -2
View File
@@ -94,7 +94,6 @@ import {
SiGitlab,
SiGoogle,
SiKeycloak,
SiLinkedin,
SiNextcloud,
SiNotion,
SiOkta,
@@ -106,6 +105,7 @@ import {
SiWechat,
SiX,
} from 'react-icons/si';
import { FaLinkedin } from 'react-icons/fa';
// 获取侧边栏Lucide图标组件
export function getLucideIcon(key, selected = false) {
@@ -509,7 +509,7 @@ const oauthProviderIconMap = {
google: SiGoogle,
discord: SiDiscord,
facebook: SiFacebook,
linkedin: SiLinkedin,
linkedin: FaLinkedin,
x: SiX,
twitter: SiX,
slack: SiSlack,
+1 -1
View File
@@ -123,7 +123,7 @@ export function showError(error) {
console.error(error);
if (error.message) {
if (error.name === 'AxiosError') {
switch (error.response.status) {
switch (error.response?.status) {
case 401:
// 清除用户状态
localStorage.removeItem('user');
+28 -6
View File
@@ -31,18 +31,40 @@ body {
background-color: var(--semi-color-bg-0);
}
/* 桌面端禁止 body 纵向滚动 - 防止 VChart tooltip 触发页面滚动条 */
@media (min-width: 768px) {
body {
overflow-y: hidden;
}
.app-layout {
min-height: 100vh;
min-height: 100dvh;
}
.app-layout {
.app-layout-fixed {
height: 100vh;
height: 100dvh;
}
.public-page-content {
display: flex;
flex-direction: column;
}
.classic-page-fill {
flex: 1 0 auto;
min-height: 100%;
}
.classic-home-page,
.classic-home-default {
display: flex;
flex-direction: column;
}
.classic-home-default {
flex: 1 0 auto;
}
.classic-home-hero {
flex: 1 0 auto;
}
.app-sider {
height: calc(100vh - 64px);
height: calc(100dvh - 64px);
+2
View File
@@ -1,3 +1,5 @@
import '@douyinfe/semi-ui/react19-adapter';
/*
Copyright (C) 2025 QuantumNous
+8 -3
View File
@@ -133,9 +133,9 @@ const About = () => {
);
return (
<div className='mt-[60px] px-2'>
<div className='classic-page-fill flex flex-col pt-[60px] px-2'>
{aboutLoaded && about === '' ? (
<div className='flex justify-center items-center h-screen p-8'>
<div className='flex flex-1 justify-center items-center p-8'>
<Empty
image={
<IllustrationConstruction style={{ width: 150, height: 150 }} />
@@ -156,7 +156,12 @@ const About = () => {
{about.startsWith('https://') ? (
<iframe
src={about}
style={{ width: '100%', height: '100vh', border: 'none' }}
style={{
width: '100%',
flex: '1 1 auto',
minHeight: 0,
border: 'none',
}}
/>
) : (
<div
+1 -1
View File
@@ -28,7 +28,7 @@ import { useTranslation } from 'react-i18next';
const Forbidden = () => {
const { t } = useTranslation();
return (
<div className='flex justify-center items-center h-screen p-8'>
<div className='classic-page-fill flex justify-center items-center p-8'>
<Empty
image={<IllustrationNoAccess style={{ width: 250, height: 250 }} />}
darkModeImage={
+6 -6
View File
@@ -149,20 +149,20 @@ const Home = () => {
}, [endpointItems.length]);
return (
<div className='w-full overflow-x-hidden'>
<div className='classic-page-fill classic-home-page w-full overflow-x-hidden'>
<NoticeModal
visible={noticeVisible}
onClose={() => setNoticeVisible(false)}
isMobile={isMobile}
/>
{homePageContentLoaded && homePageContent === '' ? (
<div className='w-full overflow-x-hidden'>
<div className='classic-home-default w-full overflow-x-hidden'>
{/* Banner 部分 */}
<div className='w-full border-b border-semi-color-border min-h-[500px] md:min-h-[600px] lg:min-h-[700px] relative overflow-x-hidden'>
<div className='classic-home-hero w-full border-b border-semi-color-border relative overflow-x-hidden'>
{/* 背景模糊晕染球 */}
<div className='blur-ball blur-ball-indigo' />
<div className='blur-ball blur-ball-teal' />
<div className='flex items-center justify-center h-full px-4 py-20 md:py-24 lg:py-32 mt-10'>
<div className='flex items-center justify-center px-4 pt-24 pb-8'>
{/* 居中内容区 */}
<div className='flex flex-col items-center justify-center text-center max-w-4xl mx-auto'>
<div className='flex flex-col items-center justify-center mb-6 md:mb-8'>
@@ -335,11 +335,11 @@ const Home = () => {
</div>
</div>
) : (
<div className='overflow-x-hidden w-full'>
<div className='classic-page-fill overflow-x-hidden w-full'>
{homePageContent.startsWith('https://') ? (
<iframe
src={homePageContent}
className='w-full h-screen border-none'
className='w-full h-full border-none'
/>
) : (
<div
+1 -1
View File
@@ -28,7 +28,7 @@ import { useTranslation } from 'react-i18next';
const NotFound = () => {
const { t } = useTranslation();
return (
<div className='flex justify-center items-center h-screen p-8'>
<div className='classic-page-fill flex justify-center items-center p-8'>
<Empty
image={<IllustrationNotFound style={{ width: 250, height: 250 }} />}
darkModeImage={
-107
View File
@@ -1,107 +0,0 @@
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import react from '@vitejs/plugin-react';
import { defineConfig, transformWithEsbuild } from 'vite';
import pkg from '@douyinfe/vite-plugin-semi';
import path from 'path';
import { codeInspectorPlugin } from 'code-inspector-plugin';
const { vitePluginSemi } = pkg;
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
plugins: [
codeInspectorPlugin({
bundler: 'vite',
}),
{
name: 'treat-js-files-as-jsx',
async transform(code, id) {
if (!/src\/.*\.js$/.test(id)) {
return null;
}
// Use the exposed transform from vite, instead of directly
// transforming with esbuild
return transformWithEsbuild(code, id, {
loader: 'jsx',
jsx: 'automatic',
});
},
},
react(),
vitePluginSemi({
cssLayer: true,
}),
],
optimizeDeps: {
force: true,
esbuildOptions: {
loader: {
'.js': 'jsx',
'.json': 'json',
},
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
'react-core': ['react', 'react-dom', 'react-router-dom'],
'semi-ui': ['@douyinfe/semi-icons', '@douyinfe/semi-ui'],
tools: ['axios', 'history', 'marked'],
'react-components': [
'react-dropzone',
'react-fireworks',
'react-telegram-login',
'react-toastify',
'react-turnstile',
],
i18n: [
'i18next',
'react-i18next',
'i18next-browser-languagedetector',
],
},
},
},
},
server: {
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/mj': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/pg': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});
+10 -10
View File
@@ -24,7 +24,7 @@
"@hookform/resolvers": "^5.4.0",
"@hugeicons/core-free-icons": "^4.1.4",
"@hugeicons/react": "^1.1.6",
"@lobehub/icons": "^5.8.0",
"@lobehub/icons": "catalog:",
"@tailwindcss/postcss": "^4.3.0",
"@tanstack/react-query": "^5.100.14",
"@tanstack/react-router": "^1.170.8",
@@ -34,12 +34,12 @@
"@visactor/vchart": "^2.0.22",
"ai": "^6.0.191",
"auto-skeleton-react": "^1.0.5",
"axios": "^1.16.1",
"axios": "catalog:",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"clsx": "catalog:",
"cmdk": "^1.1.1",
"date-fns": "^4.3.0",
"dayjs": "^1.11.20",
"dayjs": "catalog:",
"i18next": "^26.2.0",
"i18next-browser-languagedetector": "^8.2.1",
"input-otp": "^1.4.2",
@@ -47,22 +47,22 @@
"motion": "^12.40.0",
"nanoid": "^5.1.11",
"next-themes": "^0.4.6",
"qrcode.react": "^4.2.0",
"qrcode.react": "catalog:",
"react": "^19.2.6",
"react-day-picker": "^10.0.1",
"react-dom": "^19.2.6",
"react-hook-form": "^7.76.1",
"react-i18next": "^17.0.8",
"react-icons": "^5.6.0",
"react-markdown": "^10.1.0",
"react-icons": "catalog:",
"react-markdown": "catalog:",
"react-resizable-panels": "^4.11.2",
"react-top-loading-bar": "^3.0.2",
"recharts": "3.8.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"remark-gfm": "catalog:",
"shiki": "^4.1.0",
"sonner": "^2.0.7",
"sse.js": "^2.8.0",
"sse.js": "catalog:",
"streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
@@ -92,7 +92,7 @@
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"knip": "^6.14.2",
"prettier": "^3.8.3",
"prettier": "catalog:",
"prettier-plugin-tailwindcss": "^0.8.0",
"shadcn": "^4.8.0",
"typescript": "~6.0.3",
+1
View File
@@ -65,6 +65,7 @@ export default defineConfig(({ envMode }) => {
},
server: {
host: '0.0.0.0',
strictPort: true,
proxy: devProxy,
},
output: {
@@ -189,6 +189,7 @@ export function CCSwitchDialog(props: Props) {
onValueChange={setName}
placeholder={currentConfig.defaultName}
emptyText=''
allowCustomValue={true}
/>
</div>
+4 -3
View File
@@ -56,8 +56,9 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
const tags = parseTags(props.model.tags)
const groups = props.model.enable_groups || []
const endpoints = props.model.supported_endpoint_types || []
const vendorIcon = props.model.vendor_icon
? getLobeIcon(props.model.vendor_icon, 28)
const modelIconKey = props.model.icon || props.model.vendor_icon
const modelIcon = modelIconKey
? getLobeIcon(modelIconKey, 28)
: null
const initial = props.model.model_name?.charAt(0).toUpperCase() || '?'
const isDynamicPricing =
@@ -97,7 +98,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
<div className='flex items-start justify-between gap-2.5 sm:gap-3'>
<div className='flex min-w-0 items-start gap-2.5 sm:gap-3'>
<div className='bg-muted/40 flex size-9 shrink-0 items-center justify-center rounded-lg sm:size-10 sm:rounded-xl'>
{vendorIcon || (
{modelIcon || (
<span className='text-muted-foreground text-sm font-bold'>
{initial}
</span>
@@ -268,8 +268,9 @@ function OverviewSummaryGrid(props: { model: PricingModel }) {
function ModelHeader(props: { model: PricingModel }) {
const { t } = useTranslation()
const model = props.model
const vendorIcon = model.vendor_icon
? getLobeIcon(model.vendor_icon, 20)
const modelIconKey = model.icon || model.vendor_icon
const modelIcon = modelIconKey
? getLobeIcon(modelIconKey, 20)
: null
const description = model.description || model.vendor_description || null
const tags = parseTags(model.tags)
@@ -281,7 +282,7 @@ function ModelHeader(props: { model: PricingModel }) {
return (
<header className='pb-4'>
<div className='flex items-center gap-2.5'>
{vendorIcon}
{modelIcon}
<h1 className='font-mono text-xl font-bold tracking-tight sm:text-2xl'>
{model.model_name}
</h1>
@@ -106,13 +106,14 @@ export function usePricingColumns(
),
cell: ({ row }) => {
const model = row.original
const vendorIcon = model.vendor_icon
? getLobeIcon(model.vendor_icon, 14)
const modelIconKey = model.icon || model.vendor_icon
const modelIcon = modelIconKey
? getLobeIcon(modelIconKey, 14)
: null
return (
<div className='flex min-w-[200px] items-center gap-2'>
{vendorIcon}
{modelIcon}
<span className='truncate font-mono text-sm font-medium'>
{model.model_name}
</span>
+1
View File
@@ -31,6 +31,7 @@ export type PricingModel = {
id: number
model_name: string
description?: string
icon?: string
vendor_id?: number
vendor_name?: string
vendor_icon?: string
@@ -111,6 +111,7 @@ export function SubscriptionPurchaseDialog(props: Props) {
Math.ceil(Number(plan.price_amount || 0) * quotaPerUnit)
)
const userQuota = Math.max(0, Number(props.userQuota || 0))
const allowBalancePay = plan.allow_balance_pay !== false
const insufficientBalance = userQuota < balanceCost
const limitReached =
(props.purchaseLimit || 0) > 0 &&
@@ -232,6 +233,10 @@ export function SubscriptionPurchaseDialog(props: Props) {
}
const handlePayBalance = async () => {
if (!allowBalancePay) {
toast.error(t('This plan does not allow balance redemption'))
return
}
setPaying(true)
try {
const res = await paySubscriptionBalance({ plan_id: plan.id })
@@ -332,15 +337,27 @@ export function SubscriptionPurchaseDialog(props: Props) {
<span className='text-muted-foreground'>{t('Available')}</span>
<span>{formatQuota(userQuota)}</span>
</div>
{insufficientBalance && (
{!allowBalancePay ? (
<Alert variant='destructive'>
<AlertDescription>{t('Insufficient balance')}</AlertDescription>
<AlertDescription>
{t('This plan does not allow balance redemption')}
</AlertDescription>
</Alert>
) : (
insufficientBalance && (
<Alert variant='destructive'>
<AlertDescription>
{t('Insufficient balance')}
</AlertDescription>
</Alert>
)
)}
<Button
variant='outline'
onClick={handlePayBalance}
disabled={paying || limitReached || insufficientBalance}
disabled={
paying || limitReached || !allowBalancePay || insufficientBalance
}
>
{t('Pay with Balance')}
</Button>
@@ -461,6 +461,24 @@ export function SubscriptionsMutateDrawer({
</FormItem>
)}
/>
<FormField
control={form.control}
name='allow_balance_pay'
render={({ field }) => (
<FormItem className={sideDrawerSwitchItemClassName()}>
<FormLabel className='!mt-0'>
{t('Allow balance redemption')}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</SideDrawerSection>
@@ -39,6 +39,7 @@ export function getPlanFormSchema(t: TFunction) {
quota_reset_custom_seconds: z.coerce.number().min(0).optional(),
enabled: z.boolean(),
sort_order: z.coerce.number(),
allow_balance_pay: z.boolean(),
max_purchase_per_user: z.coerce.number().min(0),
total_amount: z.coerce.number().min(0),
upgrade_group: z.string().optional(),
@@ -61,6 +62,7 @@ export const PLAN_FORM_DEFAULTS: PlanFormValues = {
quota_reset_custom_seconds: 0,
enabled: true,
sort_order: 0,
allow_balance_pay: true,
max_purchase_per_user: 0,
total_amount: 0,
upgrade_group: '',
@@ -81,6 +83,7 @@ export function planToFormValues(plan: SubscriptionPlan): PlanFormValues {
quota_reset_custom_seconds: Number(plan.quota_reset_custom_seconds || 0),
enabled: plan.enabled !== false,
sort_order: Number(plan.sort_order || 0),
allow_balance_pay: plan.allow_balance_pay !== false,
max_purchase_per_user: Number(plan.max_purchase_per_user || 0),
total_amount: quotaUnitsToDollars(Number(plan.total_amount || 0)),
upgrade_group: plan.upgrade_group || '',
+1
View File
@@ -35,6 +35,7 @@ export const subscriptionPlanSchema = z.object({
quota_reset_custom_seconds: z.number().optional(),
enabled: z.boolean(),
sort_order: z.number(),
allow_balance_pay: z.boolean().optional().default(true),
max_purchase_per_user: z.number(),
total_amount: z.number(),
upgrade_group: z.string().optional(),
@@ -183,6 +183,9 @@ function CommonLogsCard<TData>({
const modelCell = cells.get('model_name')
const quotaCell = cells.get('quota')
const original = cells.get('created_at')?.row.original as
| Record<string, unknown>
| undefined
return (
<div className='space-y-2.5'>
@@ -200,8 +203,8 @@ function CommonLogsCard<TData>({
{t('Time')}
</div>
<MobileLogTimeStatus
createdAt={cells.get('created_at')?.row.original?.created_at}
type={cells.get('created_at')?.row.original?.type}
createdAt={original?.created_at}
type={original?.type}
/>
</div>
<SummaryField
+4 -2
View File
@@ -268,6 +268,7 @@
"All-time": "All-time",
"Allocated Memory": "Allocated Memory",
"Allow accountFilter parameter": "Allow accountFilter parameter",
"Allow balance redemption": "Allow balance redemption",
"Allow Claude beta query passthrough": "Allow Claude beta query passthrough",
"Allow clients to query configured ratios via `/api/ratio`.": "Allow clients to query configured ratios via `/api/ratio`.",
"Allow HTTP image requests": "Allow HTTP image requests",
@@ -2853,8 +2854,8 @@
"Path Regex (one per line)": "Path Regex (one per line)",
"Path:": "Path:",
"Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "Pay-as-you-go with real-time usage monitoring",
"Pay with Balance": "Pay with Balance",
"Pay-as-you-go with real-time usage monitoring": "Pay-as-you-go with real-time usage monitoring",
"Payment": "Payment",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.",
"Payment Channel": "Payment Channel",
@@ -3767,10 +3768,10 @@
"Subscription First": "Subscription First",
"Subscription Management": "Subscription Management",
"Subscription Only": "Subscription Only",
"Subscription purchased successfully": "Subscription purchased successfully",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.",
"Subscription Plans": "Subscription Plans",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).",
"Subscription purchased successfully": "Subscription purchased successfully",
"Subtract": "Subtract",
"Success": "Success",
"Success rate": "Success rate",
@@ -3951,6 +3952,7 @@
"This model is not available in any group, or no group pricing information is configured.": "This model is not available in any group, or no group pricing information is configured.",
"This month": "This month",
"This page has not been created yet.": "This page has not been created yet.",
"This plan does not allow balance redemption": "This plan does not allow balance redemption",
"This project must be used in compliance with the": "This project must be used in compliance with the",
"This record was written by a pre-upgrade instance and lacks audit info. Upgrade the instance to record server IP, callback IP, payment method and system version.": "This record was written by a pre-upgrade instance and lacks audit info. Upgrade the instance to record server IP, callback IP, payment method and system version.",
"This site currently has {{count}} models enabled": "This site currently has {{count}} models enabled",
+4 -2
View File
@@ -268,6 +268,7 @@
"All-time": "Tous temps",
"Allocated Memory": "Mémoire allouée",
"Allow accountFilter parameter": "Autoriser le paramètre accountFilter",
"Allow balance redemption": "Autoriser le paiement avec le solde",
"Allow Claude beta query passthrough": "Autoriser le passage des requêtes bêta Claude",
"Allow clients to query configured ratios via `/api/ratio`.": "Autoriser les clients à interroger les ratios configurés via `/api/ratio`.",
"Allow HTTP image requests": "Autoriser les requêtes d'images HTTP",
@@ -2853,8 +2854,8 @@
"Path Regex (one per line)": "Regex du chemin (un par ligne)",
"Path:": "Chemin :",
"Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "Paiement à l'usage avec suivi de la consommation en temps réel",
"Pay with Balance": "Payer avec le solde",
"Pay-as-you-go with real-time usage monitoring": "Paiement à l'usage avec suivi de la consommation en temps réel",
"Payment": "Paiement",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Mode agrégateur de paiement — embarquez avec votre propre société enregistrée (entité offshore). Conçu pour les entreprises.",
"Payment Channel": "Canal de paiement",
@@ -3767,10 +3768,10 @@
"Subscription First": "Abonnement en priorité",
"Subscription Management": "Gestion des abonnements",
"Subscription Only": "Abonnement uniquement",
"Subscription purchased successfully": "Abonnement acheté avec succès",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "La création et la modification des forfaits dabonnement sont verrouillées jusqu’à ce que ladministrateur confirme les conditions de conformité dans les paramètres de la passerelle de paiement.",
"Subscription Plans": "Plans d'abonnement",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Les forfaits dabonnement nutilisent PAS le produit associé : chaque forfait dispose de son propre produit Pancake dédié, défini dans ladministration des abonnements (ou créé automatiquement via le bouton « + Créer »).",
"Subscription purchased successfully": "Abonnement acheté avec succès",
"Subtract": "Soustraire",
"Success": "Succès",
"Success rate": "Taux de réussite",
@@ -3951,6 +3952,7 @@
"This model is not available in any group, or no group pricing information is configured.": "Ce modèle n'est disponible dans aucun groupe, ou aucune information de tarification de groupe n'est configurée.",
"This month": "Ce mois-ci",
"This page has not been created yet.": "Cette page n'a pas encore été créée.",
"This plan does not allow balance redemption": "Ce forfait ne permet pas le paiement avec le solde",
"This project must be used in compliance with the": "Ce projet doit être utilisé conformément aux",
"This record was written by a pre-upgrade instance and lacks audit info. Upgrade the instance to record server IP, callback IP, payment method and system version.": "Cet enregistrement provient dune instance avant la mise à niveau et ninclut pas daudits. Mettez à jour linstance pour enregistrer lIP du serveur, lIP de callback, le moyen de paiement et la version du système.",
"This site currently has {{count}} models enabled": "Ce site compte actuellement {{count}} modèles activés",
+4 -2
View File
@@ -268,6 +268,7 @@
"All-time": "全期間",
"Allocated Memory": "割り当て済みメモリ",
"Allow accountFilter parameter": "accountFilter パラメータを許可",
"Allow balance redemption": "残高での交換を許可",
"Allow Claude beta query passthrough": "Claude ベータクエリのパススルーを許可",
"Allow clients to query configured ratios via `/api/ratio`.": "クライアントが `/api/ratio` 経由で設定された比率を照会できるようにします。",
"Allow HTTP image requests": "HTTP画像リクエストを許可",
@@ -2853,8 +2854,8 @@
"Path Regex (one per line)": "パス正規表現(1行に1つ)",
"Path:": "パス:",
"Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "リアルタイム使用量監視付き従量課金制",
"Pay with Balance": "残高で支払う",
"Pay-as-you-go with real-time usage monitoring": "リアルタイム使用量監視付き従量課金制",
"Payment": "支払い",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "決済アグリゲーターモード — 自社の登録済み法人(オフショア法人)でオンボーディングします。エンタープライズ向けです。",
"Payment Channel": "決済チャネル",
@@ -3767,10 +3768,10 @@
"Subscription First": "サブスクリプション優先",
"Subscription Management": "サブスクリプション管理",
"Subscription Only": "サブスクリプションのみ",
"Subscription purchased successfully": "サブスクリプションを購入しました",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "管理者が支払いゲートウェイ設定でコンプライアンス条件を確認するまで、サブスクリプションプランの作成と変更はロックされます。",
"Subscription Plans": "サブスクリプションプラン",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "サブスクリプションプランは紐付け済み商品を使用しません。各プランには専用の Pancake 商品があり、サブスクリプション管理画面で設定します(または「+ 作成」ボタンで自動作成します)。",
"Subscription purchased successfully": "サブスクリプションを購入しました",
"Subtract": "減算",
"Success": "成功",
"Success rate": "成功率",
@@ -3951,6 +3952,7 @@
"This model is not available in any group, or no group pricing information is configured.": "このモデルはどのグループでも利用できないか、グループの料金情報が設定されていません。",
"This month": "今月",
"This page has not been created yet.": "このページはまだ作成されていません。",
"This plan does not allow balance redemption": "このプランでは残高での交換は許可されていません",
"This project must be used in compliance with the": "このプロジェクトは、以下を遵守して使用する必要があります",
"This record was written by a pre-upgrade instance and lacks audit info. Upgrade the instance to record server IP, callback IP, payment method and system version.": "古いバージョンのインスタンスがこの記録を書き込み、監査情報がありません。最新に更新し、サーバーIP・コールバックIP・支払方法・OSバージョンの記録を有効にしてください。",
"This site currently has {{count}} models enabled": "このサイトでは現在 {{count}} 個のモデルが有効です",
+4 -2
View File
@@ -268,6 +268,7 @@
"All-time": "За всё время",
"Allocated Memory": "Выделенная память",
"Allow accountFilter parameter": "Разрешить параметр accountFilter",
"Allow balance redemption": "Разрешить оплату балансом",
"Allow Claude beta query passthrough": "Разрешить проброс бета-запросов Claude",
"Allow clients to query configured ratios via `/api/ratio`.": "Разрешить клиентам запрашивать настроенные соотношения через `/api/ratio`.",
"Allow HTTP image requests": "Разрешить HTTP-запросы изображений",
@@ -2853,8 +2854,8 @@
"Path Regex (one per line)": "Регулярное выражение пути (по одному на строку)",
"Path:": "Путь:",
"Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "Оплата по мере использования с мониторингом в реальном времени",
"Pay with Balance": "Оплатить балансом",
"Pay-as-you-go with real-time usage monitoring": "Оплата по мере использования с мониторингом в реальном времени",
"Payment": "Платеж",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Режим платежного агрегатора — подключение через вашу зарегистрированную компанию (офшорное юрлицо). Создано для Enterprise.",
"Payment Channel": "Платёжный канал",
@@ -3767,10 +3768,10 @@
"Subscription First": "Подписка в приоритете",
"Subscription Management": "Управление подписками",
"Subscription Only": "Только подписка",
"Subscription purchased successfully": "Подписка успешно приобретена",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Создание и изменение планов подписки заблокированы, пока администратор не подтвердит условия соответствия в настройках платежного шлюза.",
"Subscription Plans": "Планы подписки",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Планы подписки НЕ используют привязанный продукт — у каждого плана есть собственный продукт Pancake, задаваемый в администрировании подписок (или автоматически создаваемый кнопкой «+ Создать»).",
"Subscription purchased successfully": "Подписка успешно приобретена",
"Subtract": "Вычесть",
"Success": "Успешно",
"Success rate": "Доля успешных запросов",
@@ -3951,6 +3952,7 @@
"This model is not available in any group, or no group pricing information is configured.": "Эта модель недоступна ни в одной группе, или информация о ценах для групп не настроена.",
"This month": "В этом месяце",
"This page has not been created yet.": "Эта страница еще не создана.",
"This plan does not allow balance redemption": "Этот план не разрешает оплату балансом",
"This project must be used in compliance with the": "Этот проект должен использоваться в соответствии с",
"This record was written by a pre-upgrade instance and lacks audit info. Upgrade the instance to record server IP, callback IP, payment method and system version.": "Запись создана экземпляром до обновления и не содержит сведений аудита. Обновите экземпляр, чтобы фиксировать IP сервера, IP callback, способ оплаты и версию ОС.",
"This site currently has {{count}} models enabled": "На этом сайте сейчас включено моделей: {{count}}",
+4 -2
View File
@@ -268,6 +268,7 @@
"All-time": "Mọi thời điểm",
"Allocated Memory": "Bộ nhớ đã cấp phát",
"Allow accountFilter parameter": "Cho phép tham số accountFilter",
"Allow balance redemption": "Cho phép thanh toán bằng số dư",
"Allow Claude beta query passthrough": "Cho phép chuyển tiếp truy vấn beta Claude",
"Allow clients to query configured ratios via `/api/ratio`.": "Cho phép khách hàng truy vấn các tỷ lệ đã cấu hình thông qua `/api/ratio`.",
"Allow HTTP image requests": "Cho phép yêu cầu hình ảnh HTTP",
@@ -2853,8 +2854,8 @@
"Path Regex (one per line)": "Regex đường dẫn (mỗi dòng một mục)",
"Path:": "Đường dẫn:",
"Pay": "Pay",
"Pay-as-you-go with real-time usage monitoring": "Thanh toán theo mức sử dụng với theo dõi mức sử dụng theo thời gian thực",
"Pay with Balance": "Thanh toán bằng số dư",
"Pay-as-you-go with real-time usage monitoring": "Thanh toán theo mức sử dụng với theo dõi mức sử dụng theo thời gian thực",
"Payment": "Thanh toán",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Chế độ tổng hợp thanh toán — đăng ký bằng công ty đã đăng ký của bạn (pháp nhân offshore). Dành cho doanh nghiệp.",
"Payment Channel": "Kênh thanh toán",
@@ -3767,10 +3768,10 @@
"Subscription First": "Ưu tiên đăng ký",
"Subscription Management": "Quản lý đăng ký",
"Subscription Only": "Chỉ đăng ký",
"Subscription purchased successfully": "Đã mua gói đăng ký thành công",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Việc tạo và thay đổi gói đăng ký bị khóa cho đến khi quản trị viên xác nhận điều khoản tuân thủ trong cài đặt Cổng thanh toán.",
"Subscription Plans": "Gói đăng ký",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Gói đăng ký KHÔNG dùng Sản phẩm đã liên kết — mỗi gói có một sản phẩm Pancake riêng, được đặt trong quản trị Đăng ký (hoặc tự động tạo bằng nút \"+ Create\" tại đó).",
"Subscription purchased successfully": "Đã mua gói đăng ký thành công",
"Subtract": "Trừ",
"Success": "Thành công",
"Success rate": "Tỷ lệ thành công",
@@ -3951,6 +3952,7 @@
"This model is not available in any group, or no group pricing information is configured.": "Mô hình này không khả dụng trong bất kỳ nhóm nào, hoặc thông tin giá nhóm chưa được cấu hình.",
"This month": "Tháng này",
"This page has not been created yet.": "Trang này chưa được tạo.",
"This plan does not allow balance redemption": "Gói này không cho phép thanh toán bằng số dư",
"This project must be used in compliance with the": "Dự án này phải được sử dụng tuân thủ theo",
"This record was written by a pre-upgrade instance and lacks audit info. Upgrade the instance to record server IP, callback IP, payment method and system version.": "Bản ghi này do bản cũ tạo và thiếu thông tin audit. Nâng cấp bản cài để lưu IP máy chủ, IP callback, hình thức thanh toán và phiên bản hệ thống.",
"This site currently has {{count}} models enabled": "Trang này hiện đã bật {{count}} mô hình",
+4 -2
View File
@@ -268,6 +268,7 @@
"All-time": "全部时间",
"Allocated Memory": "已分配内存",
"Allow accountFilter parameter": "允许 accountFilter 参数",
"Allow balance redemption": "允许余额兑换",
"Allow Claude beta query passthrough": "允许 Claude beta 查询透传",
"Allow clients to query configured ratios via `/api/ratio`.": "允许客户端通过 `/api/ratio` 查询配置的比例。",
"Allow HTTP image requests": "允许 HTTP 图像请求",
@@ -2853,8 +2854,8 @@
"Path Regex (one per line)": "路径正则(每行一个)",
"Path:": "路径:",
"Pay": "支付",
"Pay-as-you-go with real-time usage monitoring": "按量付费,实时监控使用情况",
"Pay with Balance": "使用余额支付",
"Pay-as-you-go with real-time usage monitoring": "按量付费,实时监控使用情况",
"Payment": "支付",
"Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "支付聚合模式:使用你自己的注册公司(离岸实体)入驻。面向企业场景构建。",
"Payment Channel": "支付渠道",
@@ -3767,10 +3768,10 @@
"Subscription First": "优先订阅",
"Subscription Management": "订阅管理",
"Subscription Only": "仅用订阅",
"Subscription purchased successfully": "订阅购买成功",
"Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "管理员在支付网关设置中确认合规条款之前,订阅套餐的创建和修改会被锁定。",
"Subscription Plans": "订阅套餐",
"Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "订阅套餐不会使用已绑定的产品。每个套餐都有独立的 Pancake 产品,可在订阅管理中设置,或通过其中的“+ 创建”按钮自动生成。",
"Subscription purchased successfully": "订阅购买成功",
"Subtract": "减少",
"Success": "成功",
"Success rate": "成功率",
@@ -3951,6 +3952,7 @@
"This model is not available in any group, or no group pricing information is configured.": "此模型在任何分组中均不可用,或未配置分组定价信息。",
"This month": "本月获得",
"This page has not been created yet.": "此页面尚未创建。",
"This plan does not allow balance redemption": "该套餐不允许使用余额兑换",
"This project must be used in compliance with the": "此项目的使用必须遵守",
"This record was written by a pre-upgrade instance and lacks audit info. Upgrade the instance to record server IP, callback IP, payment method and system version.": "该记录由旧版本实例写入,缺少审计信息,建议将实例升级至最新版本以便记录服务器 IP、回调 IP、支付方式与系统版本等审计字段。",
"This site currently has {{count}} models enabled": "本站当前已启用模型,总计 {{count}} 个",
+20
View File
@@ -0,0 +1,20 @@
{
"name": "new-api-web-workspace",
"private": true,
"workspaces": [
"default",
"classic"
],
"catalog": {
"@lobehub/icons": "^5.10.0",
"axios": "^1.16.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.20",
"prettier": "^3.8.3",
"qrcode.react": "^4.2.0",
"react-icons": "^5.6.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"sse.js": "^2.8.0"
}
}