Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3a9f41e
fix: temp disable /messages/count_tokens
Calcium-Ion Sep 4, 2026
7c044d7
feat(relay): explicit @ model modifiers and canonical billing identity
Calcium-Ion Sep 4, 2026
6b659fd
fix(relay): preserve reasoning effort without implicit remapping
Calcium-Ion Sep 5, 2026
d580353
docs: require expression pricing and consolidated tests
Calcium-Ion Sep 5, 2026
eb99ab1
feat(billing): add built-in expression pricing for gpt-6-astra
Calcium-Ion Sep 5, 2026
2cf177a
perf(common): 批量复制 RawMessage,优化请求深拷贝 (#7221)
mingisrookie Sep 6, 2026
49ec469
fix(relay): apply model-specific OpenAI chat capabilities (#7211)
pdmaker Sep 6, 2026
d8cb177
feat(security): add access token management and audit logs
Calcium-Ion Sep 5, 2026
9a86744
fix(db): avoid redundant schema migrations on restart
Calcium-Ion Sep 5, 2026
45c3fbe
fix(security): bind verification proofs to sessions and actions
Calcium-Ion Sep 5, 2026
3e84ec0
feat(auth): migrate Telegram to unified OAuth
Calcium-Ion Sep 6, 2026
a8729b5
feat(security): require verification for access token management
Calcium-Ion Sep 6, 2026
0973dc2
feat(security): harden account binding and password changes
Calcium-Ion Sep 6, 2026
3f8a50c
feat(audit): complete token and quota operation records
Calcium-Ion Sep 6, 2026
521cebf
fix(dashboard): simplify completed setup guide
Calcium-Ion Sep 6, 2026
6f23339
feat(auth): unify login verification and secure account deletion
Calcium-Ion Sep 6, 2026
0c76e4d
feat(models): rework model/vendor management and pricing
Calcium-Ion Sep 6, 2026
3b46522
feat(ali): support wan3.0 all-in-one video models
Calcium-Ion Sep 7, 2026
6e10f9b
fix(relay): preserve Kimi K3 dynamic tool loading messages
Calcium-Ion Sep 7, 2026
7bbe85b
refactor(json): route JSON helpers through a host-injectable codec
Calcium-Ion Sep 7, 2026
387a409
fix(web): keep drawer popups interactive and shim storage in tests
Calcium-Ion Sep 7, 2026
5c7cca0
fix(perf): return hourly success-rate series for model health bar
Calcium-Ion Sep 7, 2026
6298b0f
fix(plugin): suppress factory layer when disabling an overridden task…
Calcium-Ion Sep 7, 2026
92bc7ff
fix(plugins): make sunoapi alias-safe and lock alias echo across buil…
Calcium-Ion Sep 7, 2026
210734b
refactor(task): remove the custom-plugin layer switch
Calcium-Ion Sep 7, 2026
bee45b5
fix(web): switch the pricing card grid to three columns at xl
Calcium-Ion Sep 7, 2026
75e5332
feat(pricing): support site currency in pricing editors
Calcium-Ion Sep 8, 2026
99974a8
feat(plugins): extend plugin metadata and icon support
Calcium-Ion Sep 8, 2026
9843309
feat(web): improve plugin management and marketplace
Calcium-Ion Sep 8, 2026
eb76b13
feat(channels): improve plugin channel setup and icons
Calcium-Ion Sep 8, 2026
0e0ba15
feat(pricing): improve pricing editors and log display
Calcium-Ion Sep 8, 2026
71c1fd7
feat(models): improve model listing, pricing and visibility filters
Calcium-Ion Sep 8, 2026
a5e41a8
feat(usage-logs): refine mobile layout and keep quick actions visible
Calcium-Ion Sep 8, 2026
2bec370
feat(web): refine API key and user quota displays
Calcium-Ion Sep 8, 2026
8f72ecb
feat(usage-logs): add searchable group filter
Calcium-Ion Sep 8, 2026
551bb63
fix(keys): show desktop quota amounts side by side
Calcium-Ion Sep 8, 2026
bd22e45
style(keys): widen the desktop quota column
Calcium-Ion Sep 8, 2026
950644c
fix(keys): preserve spacing after desktop quota content
Calcium-Ion Sep 8, 2026
524455f
feat(redemptions): add batch deletion and optional file exports
Calcium-Ion Sep 8, 2026
fff0635
docs: update project architecture and Go conventions
Calcium-Ion Sep 8, 2026
ebe4c36
refactor: modernize Go code conventions
Calcium-Ion Sep 8, 2026
ea7cb0b
refactor(web): unify table cells and quota details
Calcium-Ion Sep 8, 2026
79b758f
Merge upstream QuantumNous/new-api into main
mrdjango Sep 8, 2026
c51cbae
test(dashboard): re-query the setup guide toggle while the overview s…
mrdjango Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@
# SESSION_SECRET=random_string
# 登录密码请求体 RSA-OAEP 加密;默认关闭,且不能替代 HTTPS
# PASSWORD_LOGIN_ENCRYPTION_ENABLED=true

# Account password storage. For a rolling upgrade, deploy every node with bcrypt
# first (dual-format verification), then switch all nodes to argon2id before
# enabling long passwords. A rollback must retain Argon2id verification and
# v2 login-password envelope support (when login encryption is enabled).
# Existing bcrypt hashes and MFA backup codes are not rewritten.
# ACCOUNT_PASSWORD_HASH_ALGORITHM=argon2id
# false/未配置:本地 HTTP 模式,关闭 refresh/logout OriginGuard,且不得设置 TRUSTED_URL;兼容本地开发代理。
# true:启用 Secure Refresh Cookie 和严格 OriginGuard,必须同时列出全部可信 HTTPS Origin。
# SESSION_COOKIE_TRUSTED_URL 多项用英文逗号分隔;不支持通配符、路径或域名后缀匹配。
Expand Down
70 changes: 44 additions & 26 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,20 @@ This is an AI API gateway/proxy built with Go. It aggregates 40+ upstream AI pro

## Tech Stack

- **Backend**: Go 1.22+, Gin web framework, GORM v2 ORM
- **Frontend**: React 19, TypeScript, Rsbuild, Base UI, Tailwind CSS
- **Databases**: SQLite, MySQL, PostgreSQL (all three must be supported)
- **Backend**: Go 1.25.1 (see each module’s `go.mod`), Gin web framework, GORM v2 ORM
- **Frontend**: React 19, TypeScript, Rsbuild 2, TanStack Router/Query/Table, Zustand, Base UI, Tailwind CSS 4
- **Databases**: SQLite, MySQL, PostgreSQL for the primary database (all three must be supported); a separately configured log database also supports ClickHouse
- **Cache**: Redis (go-redis) + in-memory cache
- **Auth**: JWT, WebAuthn/Passkeys, OAuth (GitHub, Discord, OIDC, etc.)
- **Auth**: Browser sessions, API tokens and personal access tokens, JWT, WebAuthn/Passkeys, TOTP, OAuth/OIDC; Casbin authorization in `service/authz/`
- **Extensions**: JavaScript task plugins executed by Sobek; Electron desktop wrapper
- **Frontend package manager**: Bun (preferred over npm/yarn/pnpm)

## Architecture

Layered architecture: Router -> Controller -> Service -> Model

```
router/ — HTTP routing (API, relay, dashboard, web)
controller/ — Request handlers
service/ — Business logic
model/ — Data models and DB access (GORM)
relay/ — AI API relay/proxy with provider adapters
relay/channel/ — Provider-specific adapters (openai/, claude/, gemini/, aws/, etc.)
middleware/ — Auth, rate limiting, CORS, logging, distribution
setting/ — Configuration management (ratio, model, operation, system, performance)
common/ — Shared utilities (JSON, crypto, Redis, env, rate-limit, etc.)
dto/ — Data transfer objects (request/response structs)
constant/ — Constants (API types, channel types, context keys)
types/ — Type definitions (relay formats, file sources, errors)
i18n/ — Backend internationalization (go-i18n, en/zh)
oauth/ — OAuth provider implementations
pkg/ — Internal packages (cachex, ionet)
web/ — Frontend (React 19, Rsbuild, Base UI, Tailwind)
src/i18n/ — Frontend internationalization (i18next, en/zh/zh-TW/fr/ru/ja/vi)
```
- The Go gateway handles management APIs, upstream relay, billing, and background tasks across `router/`, `middleware/`, `controller/`, `service/`, `model/`, and `relay/`.
- `relaykit/` is an independent Go module for protocol DTOs and conversions; transport, authentication, database access, and billing stay in the host.
- JavaScript task plugins live in `plugins/tasks/`, run through `pkg/jsplugin/`, and integrate with host task polling and settlement.
- `web/` is the React frontend (see `web/AGENTS.md`); `electron/` is the desktop wrapper.

## Internationalization (i18n)

Expand All @@ -62,14 +46,38 @@ web/ — Frontend (React 19, Rsbuild, Base UI, Tailwind)
- A separate function is appropriate when it represents reusable behavior, a required interface/framework callback, an exported API, a test fixture, or complex business logic that deserves direct tests.
- If a single-use helper is kept, its name must describe a durable domain concept rather than a mechanical step extracted only to shorten the caller.

### Authentication Security (OWASP Mandatory)

- Any implementation, modification, or review involving authentication-related flows MUST comply with the applicable requirements of the latest stable [OWASP Application Security Verification Standard (ASVS)](https://owasp.org/www-project-application-security-verification-standard/) and the relevant [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/). This applies to both backend and frontend changes, including registration, login/logout, password changes and recovery, email verification, MFA, WebAuthn/Passkeys, OAuth/OIDC, account linking/unlinking, sessions, JWTs, API credentials, and re-authentication for sensitive actions.
- Before changing these flows, read the applicable OWASP guidance, starting with the [Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) and [Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html). Consult the password storage, forgot password, MFA, OAuth, and CSRF guidance when those mechanisms are involved. Identify the applicable controls before implementation; existing code is not a justification for retaining or introducing an insecure pattern.
- Enforce security controls on the server. Apply the relevant requirements for credential storage and transport, resistance to account enumeration and brute force, CSRF and replay protection, token/challenge expiry and single use where required, protocol-specific verification, session rotation and invalidation, and re-authentication for sensitive account changes. Frontend checks MUST NOT substitute for server-side enforcement, and recovery or alternative login paths MUST NOT bypass the required authentication assurance.
- Authentication audit events MUST exclude passwords, verification codes, recovery codes, private keys, and usable session or authentication tokens. Record enough non-secret context to investigate authentication failures and sensitive account changes.
- Verify affected security controls with focused regression tests, including applicable failure, expiry, replay, and bypass cases, following the existing backend/frontend test conventions. Record the OWASP references (including the ASVS version and requirement IDs when used), validation performed, and any unresolved gaps in the change summary or PR description. Do not claim compliance or completion while an applicable security requirement remains unmet or unverified.

### Backend Rules

**Modern Go conventions:** Apply these conventions to new or modified Go code, including tests and `relaykit/`, when they preserve behavior and improve readability. Use the Go version declared in the relevant module's `go.mod` as the compatibility baseline.

- Use `any` instead of `interface{}`, including map values, slice elements, parameters, and return types.
- For fixed-count loops, prefer `for i := range n`, or `for range n` when the index is unused. For slice indices, prefer `for i := range items`. Keep conventional loops when the bound changes during iteration or the loop needs a different start or step.
- When split results are only traversed once without indexing or reuse, prefer `strings.SplitSeq` or `bytes.SplitSeq` over allocating a slice with `Split`.
- Use `strings.Cut` when splitting at the first separator, and `strings.CutPrefix` / `strings.CutSuffix` when checking and removing a prefix or suffix. Avoid separate searches and manual slicing for the same operation.
- Use `slices.Contains` / `slices.ContainsFunc` for membership checks and `slices.Sort` for natural ordering of ordered element types instead of equivalent hand-written loops or sort callbacks.
- Use `maps.Copy` for shallow map copies and merges. Initialize the destination as needed, and preserve nil-versus-empty behavior and the order in which later values overwrite earlier ones. It does not replace a deep copy.
- Use built-in `min` / `max` for simple bounds instead of equivalent conditional assignments. Preserve numeric semantics; these functions do not prevent overflow in their arguments or replace billing validation and safe quota conversion.
- Use `strings.Builder` for repeated string concatenation in loops; retain direct concatenation for simple fixed expressions.
- Use `reflect.TypeFor[T]()` when the type is known statically, and `reflect.Pointer` instead of `reflect.Ptr`. Keep `reflect.TypeOf` when the dynamic type of a value is required.
- Prefer `sync.WaitGroup.Go` for the standard `Add(1)` / goroutine / deferred `Done()` pattern when its lifecycle and panic contract apply. Preserve existing recovery behavior; the function passed to `Go` must not panic.
- Remove redundant loop-variable copies such as `tc := tc` when they exist only for pre-Go-1.22 closure capture. Retain copies needed for actual snapshot semantics or variables assigned outside the loop.
- Remove ineffective `omitempty` tags on non-pointer struct fields only after confirming the active JSON encoder preserves the same output. Do not change field types or omission behavior as part of a style cleanup; optional relay scalar fields must still follow the pointer rules below.
- Format modified Go files with `gofmt` and remove unused imports after these changes.

**relaykit module independence:** The `relaykit/` Go module MUST remain independently buildable.

- Code under `relaykit/` MUST NOT import or depend on packages from the root `new-api` module, or rely on root-only configuration, generated files, or workspace wiring.
- Any change affecting `relaykit/` or its public APIs MUST be verified with `cd relaykit && GOWORK=off go build ./...`; a successful root-module build is not sufficient.

**JSON package:** All JSON marshal/unmarshal operations MUST use the wrapper functions in `common/json.go`:
**JSON package:** In the root Go module, all JSON marshal/unmarshal operations MUST use the wrapper functions in `common/json.go`:

- `common.Marshal(v any) ([]byte, error)`
- `common.Unmarshal(data []byte, v any) error`
Expand All @@ -79,6 +87,8 @@ web/ — Frontend (React 19, Rsbuild, Base UI, Tailwind)

Do NOT directly import or call `encoding/json` in business code. `json.RawMessage`, `json.Number`, and other type definitions from `encoding/json` may still be referenced as types, but actual marshal/unmarshal calls must go through `common.*`.

Inside `relaykit/`, use `kitutil.*` from `relaykit/relayconvert/kitutil/json.go`, never host `common`. Direct encoder calls belong only in codec implementations.

**Database compatibility:** All database code MUST work with SQLite, MySQL >= 5.7.8, and PostgreSQL >= 9.6 simultaneously.

- Any change that can affect database behavior MUST be verified before the work is considered complete. This includes ORM/database-driver dependency changes, connection/DSN/protocol or prepared-statement configuration, models and GORM tags, migrations and `AutoMigrate`, constraints and indexes, `Scanner`/`Valuer`/serializer behavior, raw SQL, transactions, and row locking.
Expand Down Expand Up @@ -107,6 +117,8 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag

**Billing expression system:** When working on tiered/dynamic billing (expression-based pricing), MUST read `pkg/billingexpr/expr.md` first. It documents the design philosophy, expression language, full architecture, token normalization rules, quota conversion, and expression versioning. All billing expression changes must follow that document.

**Built-in model pricing:** New built-in model prices MUST be defined as self-contained billing expressions in `setting/billing_setting/builtin_billing.go`, using real USD per million tokens. Do not add new built-in prices to the legacy model/completion/cache ratio tables. Preserve explicit administrator pricing overrides. Existing legacy prices are migrated only when explicitly requested. Verify published prices and cover applicable context-length thresholds and cache categories.

**Billing safety invariants:** Quota/billing code MUST never produce a negative charge (a credit) from arithmetic overflow or unvalidated input. Apply defense in depth:

- Every user-controlled quantity that becomes a billing multiplier (image `n`, video `seconds`/`duration`, resolution/quality ratios, batch counts) MUST be bounded before it reaches quota calculation. Reject out-of-range values at request validation with a 400. Existing bounds: `dto.MaxImageN` for image generation count, `relaycommon.MaxTaskDurationSeconds` for task video duration, `maxTokensLimit` (`relay/helper/valid_request.go`) for `max_tokens`-family fields on every relay format (OpenAI, Claude, Gemini, Responses). Reuse these constants instead of introducing new ad hoc limits for the same concepts. When adding a new relay format or request DTO, bound its max-tokens and count fields in its validator from day one.
Expand All @@ -121,6 +133,7 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag

**Backend test quality:** Backend tests must protect real behavior, API contracts, billing/accounting invariants, data compatibility, or regression paths.

- **Do not scatter tests for a small change:** For a focused feature or fix, extend an existing suitable test file first. If a new test file is necessary, add at most one and consolidate the key regression cases there. MUST NOT create separate test files for the same small feature across `controller/`, `service/`, `setting/`, or other layers merely because its call chain crosses those layers. Do not repeat fixtures and assertions at each layer. Keep the cases compact and focused on observable behavior; the number of production files touched is not a reason to add more test files.
- Do not add tests that only improve coverage numbers, prove that code happens to run, or lock in implementation details without a user-visible or cross-module contract.
- Avoid fake fuzz/stress/smoke/performance tests built from random inputs, large loop counts, sleeps, timing comparisons, or log-only assertions.
- Avoid duplicate tests that exercise the same branch with different names but no new invariant.
Expand All @@ -132,8 +145,13 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag
- Avoid hand-written assertion helpers unless they encode a reusable project-specific invariant.
- When cleaning tests, preserve meaningful regression coverage. If a deleted test covered a real contract indirectly, replace it with a smaller test that asserts that contract directly.

**Documentation files:** Do NOT add new files under `docs/` or any of its subdirectories unless the user explicitly requests it.

### Frontend Rules

- **Reuse existing UI components first (mandatory):** Before implementing or changing frontend UI, read `web/AGENTS.md` and the project `shadcn-ui` skill, search `web/src/components/` and the relevant feature for existing components, and read matching implementations and call sites. Do not start from custom markup or registry installation without checking the repository first.
- Prefer the project's shared business components over lower-level UI primitives when they cover the use case. Evaluate existing props, composition, and a compatible extension before introducing a replacement. Importing `Button` or `AlertDialog` does not satisfy this rule if the same behavior is already provided by a shared component such as `CopyButton` or `ConfirmDialog`.
- New implementations of common UI behavior require a concrete capability gap: identify the existing candidates and explain why reuse, composition, or a compatible extension is unsuitable in the change summary or PR description. Different text, dimensions, colors, or feature location alone do not justify duplication. Feature components may compose shared components with business data and actions. Follow the reuse workflow and component entry points in `web/AGENTS.md`; generic library or registry guidance does not override this project-specific priority.
- Use `bun` as the preferred package manager and script runner for the frontend (`web/`):
- `bun install` for dependency installation
- `bun run dev` for development server
Expand Down
86 changes: 86 additions & 0 deletions common/account_password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package common

import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"os"
"strings"
"unicode/utf8"

"golang.org/x/crypto/argon2"
)

const (
MinAccountPasswordLength = 8
MaxAccountPasswordLength = 128
accountPasswordMemory = 19 * 1024
accountPasswordTime = 2
accountPasswordSaltBytes = 16
accountPasswordKeyBytes = 32
)

var (
ErrAccountPasswordLength = errors.New("Password must contain between 8 and 128 characters.")
ErrAccountPasswordSame = errors.New("New password must be different from current password")
ErrPasswordLegacyLimit = errors.New("Long passwords are unavailable until the password storage upgrade is complete.")
)

// ValidateNewAccountPassword applies only when a user chooses a new password.
// Authentication must continue to accept historical passwords without applying
// the new policy. Do not normalize passwords, including surrounding whitespace.
func ValidateNewAccountPassword(password string) error {
if !utf8.ValidString(password) || utf8.RuneCountInString(password) < MinAccountPasswordLength || utf8.RuneCountInString(password) > MaxAccountPasswordLength {
return ErrAccountPasswordLength
}
return nil
}

// HashAccountPassword is for account passwords, not MFA backup codes. The
// temporary bcrypt mode permits rolling out dual-format readers to all nodes
// before enabling Argon2id writes. Existing hashes are never rewritten in bulk.
func HashAccountPassword(password string) (string, error) {
if err := ValidateNewAccountPassword(password); err != nil {
return "", err
}
switch os.Getenv("ACCOUNT_PASSWORD_HASH_ALGORITHM") {
case "bcrypt":
if len(password) > 72 {
return "", ErrPasswordLegacyLimit
}
return Password2Hash(password)
case "", "argon2id":
default:
return "", errors.New("Unsupported account password hashing configuration.")
}
salt := make([]byte, accountPasswordSaltBytes)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("generate account password salt: %w", err)
}
key := argon2.IDKey([]byte(password), salt, accountPasswordTime, accountPasswordMemory, 1, accountPasswordKeyBytes)
return fmt.Sprintf("$argon2id$v=19$m=19456,t=2,p=1$%s$%s", base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(key)), nil
}

func validateArgon2AccountPassword(password, encoded string) bool {
// Bound both plaintext and parameters before invoking a memory-hard KDF.
// Only the version/parameters emitted by this application are accepted.
if len(password) > MaxAccountPasswordLength*utf8.UTFMax || len(encoded) > 256 {
return false
}
parts := strings.Split(encoded, "$")
if len(parts) != 6 || parts[0] != "" || parts[1] != "argon2id" || parts[2] != "v=19" || parts[3] != "m=19456,t=2,p=1" {
return false
}
salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4])
if err != nil || len(salt) != accountPasswordSaltBytes {
return false
}
expected, err := base64.RawStdEncoding.Strict().DecodeString(parts[5])
if err != nil || len(expected) != accountPasswordKeyBytes {
return false
}
actual := argon2.IDKey([]byte(password), salt, accountPasswordTime, accountPasswordMemory, 1, accountPasswordKeyBytes)
return subtle.ConstantTimeCompare(actual, expected) == 1
}
Loading
Loading