When a task plugin claimed a model on a shared host endpoint
(/v1/images/generations, /v1/images/edits, /v1/responses, /v1/videos),
every request for that model went to the plugin, even when an ordinary
OpenAI-compatible channel served the model too. Ordinary channels and
the matching plugin channels now compete for the request under the usual
group, priority, weight, affinity and channel-pin rules. When an
ordinary channel wins, the original request reaches the ordinary relay
unchanged: no plugin decoder, mode check or model rewrite runs, and
retries stay on ordinary channels. When a plugin channel wins, the
plugin handles the request as before and retries stay on its accepted
plugin candidates. Native plugin routes still belong to their plugins.
Ordinary and plugin prices are kept apart. Task expressions return USD,
while ordinary expressions keep their USD-per-million result, so an
ordinary request now fails with 400 model_price_error before any quota
is reserved when the model's expression reads task usage (u()) or is
still inherited by a task plugin on that endpoint. The model pricing
editor can set ordinary OpenAI prices, per returned image with
fixed(...) * image_count, while each plugin keeps its price in its own
override. A model served by a single plugin now shows that plugin's
pricing editor as well.
* fix(model): find channels for @-modified model names without the memory cache
With the channel memory cache off (no Redis and MEMORY_CACHE_ENABLED
unset), channel selection matched abilities by the exact requested name,
so a name such as deepseek-flash@effort:max found no channel. Retry with
the routing name when the exact name has no channel, as the cached path
already does.
* feat(deepseek): support V4.1 Flash, the Search and Fetch APIs, and @ reasoning modifiers
Follows the DeepSeek API update of 2026-10-17.
- Default models: deepseek-flash and deepseek-v4-pro, each with -none,
-low and -max, plus deepseek-search and deepseek-fetch. The retired
deepseek-chat, deepseek-reasoner and deepseek-v4-flash names are no
longer listed.
- Thinking: the -none/-low/-max tails also work on deepseek-flash.
@thinking and @effort now reach DeepSeek's own fields on Chat
(thinking, reasoning_effort), Anthropic (thinking, output_config.effort)
and Responses (reasoning.effort), and take precedence over the tails.
- Built-in prices: peak/off-peak expressions for deepseek-flash (also the
deepseek-v4-flash aliases) and deepseek-v4-pro, and $0.75 per 1,000
requests for deepseek-search and deepseek-fetch.
- Server-side search: searches and page fetches DeepSeek runs for Claude
Code and Codex bill at $0.75 per 1,000 under deepseek_search and
deepseek_fetch, whatever model name the client sent. Searches whose
results already arrived stay billed when a stream ends before its final
usage; this also applies to Alibaba's Anthropic endpoint.
- New endpoints POST /v1/tools/search and /v1/tools/fetch relay the
DeepSeek APIs like /v1/alpha/search. A call is charged only when
DeepSeek returns results or page content.
* fix(deepseek): price /v1/tools/search and /v1/tools/fetch by endpoint
A model named in the request body chose the billing model, so a body such
as {"query":"...","model":"deepseek-flash"} was priced as a chat model with
no tokens and the search or fetch cost nothing. Both endpoints now always
use their routing name, deepseek-search or deepseek-fetch. DeepSeek ignores
the body field, so the body is still forwarded unchanged.
* fix(deepseek): bill Claude-format page fetches at the DeepSeek Fetch price
usage.server_tool_use.web_fetch_requests always went to the web_fetch
tool price, which has no built-in value, while only searches used the
DeepSeek key. A channel can now name its own fetch key next to the search
key, and DeepSeek channels bill reported fetches under deepseek_fetch. A
channel retry clears it like the search key.
Support priority ordering in table and card views with automatic priority calculation, same-tier and between-tier drop targets, live previews, conflict detection, and undo.
Keep drag mode off by default and place its toggle before Batch Operations. Consolidate sorting controls and preserve the shared visibility and view controls.
* feat(logs): export usage logs from the common log page
The common log page has an Export button next to Search. It asks for a
time range and downloads the logs of the current view, with the page's
other filters, as a CSV file in the viewer's language. Users export their
own logs; administrators in the all-users view export every user's logs.
Each user runs one export at a time. A second export started while one is
running, from another tab or another node, is refused until the first one
ends. With Redis the reservation holds across nodes and lapses within a
minute if its node stops.
Log Maintenance settings add two options: whether users may export (on by
default) and the longest log time range in days (30 by default). Regular
users can search their logs within that range at once, and every export
is limited to it. A user's log search had no limit before.
* fix(web): split the log maintenance page into four modules
The log maintenance page ran its settings, the history log cleanup and
the server log files together. It now shows four modules, each under its
own heading and separated by a divider, like the performance page: log
recording, log search and export, history log cleanup, and server log
management. The history log cleanup no longer sits in a box of its own.
The history log cleanup and the server log files are now components of
their own; what they do is unchanged.
* feat(logs): default the log time range to 365 days and apply it to the dashboard
The longest log time range now defaults to 365 days instead of 30.
Regular users had no limit on log searches before this setting, and 30
days could not cover a 31-day month.
A user's own usage and traffic charts on the dashboard were held to a
fixed one-month range. They now follow the same setting, so the
dashboard, the log search and the export share one limit, and a range
that is too long gets the same "cannot exceed N days" message.
* feat(i18n): translate web console messages in the frontend and AI-client errors in the backend
Chinese text was hard-coded in API messages, stored log content, emails and
server logs, so it reached users of every interface language.
Text is now translated where its reader is:
- Web console APIs return the English source text as message_key with
message_params, and the web console translates it in all seven languages.
message keeps the English text for other API callers.
- Errors returned to AI clients, emails and notifications are translated by
the backend (en, zh-CN, zh-TW) in the language saved on the account, then
the language of the request's Accept-Language header.
- A reader who states no language still gets the original Chinese text of the
messages that were Chinese before, word for word. Gateways that use another
new-api as their upstream match that text, for example to disable a channel
whose account ran out of quota.
- Usage, top-up and system log content is stored as content_parts (key and
params) and rendered in the viewer's language. Task fail reasons and refund
reasons written by the gateway are English keys. Rows written earlier are
shown as before.
- Server logs are English.
Also:
- The saved interface language zhTW, and zh-HK, zh-MO and zh-Hant, now select
Traditional Chinese in the backend instead of Simplified Chinese.
- The built-in group descriptions are translated; descriptions an
administrator saved are shown as written.
- .agents/rules/i18n.md records the conventions. Tests in i18n/ fail on a
non-English string literal, on a web console message without translations
and on a locale key without a constant.
* feat(i18n): print server logs in Chinese when LOG_LANGUAGE asks for it
Server log lines are English by default. With LOG_LANGUAGE=zh-CN, the lines
that were Chinese before print their original Chinese text.
common.LogText formats a log line from its English format, which is also the
key of its Chinese text in common/log_text.zh-CN.json. A line without an entry
prints in English. go vet checks the arguments of every call, and
TestServerLogTexts checks that each entry still matches a call and has the
same verbs as its key.
* docs: list LOG_LANGUAGE in the docker compose example
* feat(i18n): translate every server log line and add DEFAULT_LANGUAGE
Every line the gateway prints goes through common.LogText. With
DEFAULT_LANGUAGE=zh-CN, the lines that have a Chinese text in
common/log_text.zh-CN.json print it and the others print English.
DEFAULT_LANGUAGE replaces LOG_LANGUAGE. It also sets the language of
backend messages, emails and notifications for a reader who states no
language: en, zh-CN or zh-TW, English when unset. The list of messages
that stayed Chinese for such readers is removed.
Review fixes:
- Channel test logs store the token name "Model test"; the console shows
it in the interface language.
- Task fail reasons written by the gateway are fixed sentences the console
translates. The HTTP status and the upstream detail go to the server log
when a polled task is failed.
- The auto-disable reason of a slow channel is written in DEFAULT_LANGUAGE.
Also:
- Drop the repository-scanning lint tests, the i18n rules file and its
read gate. AGENTS.md keeps the conventions in four lines.
- Payment error toasts keep their previous text and only add the
translated message key.
- Remove guards and helpers that no caller needs.
* fix(i18n): address review on language lookup, notices and error responses
- Relay requests no longer load the user a second time to find the
language: the settings TokenAuth put into the request are used as they
are, also when they hold no language.
- Upstream model update notices are rendered in each recipient's saved
language, DEFAULT_LANGUAGE when none.
- /api/usage/token and /api/log/token answer with backend-translated
messages again, with the keys and status codes used before this PR.
Their callers are scripts, not the web console.
- Webhook notifications fill {{value}} placeholders like the other
senders. Running the rendered text through Sprintf appended
%!(EXTRA ...) and broke %2F in links.
- RootUserLanguage reads the setting of the root user it already loaded.
- zh_TW and zh_HK are read as Traditional Chinese.
- Add common.ApiErrorStatus for error responses under another HTTP status
and use it where those responses were built by hand.
- Remove middleware.GetLanguage, which had no callers.
GLOBAL_WEB_RATE_LIMIT (120 requests per 180 s per IP) counted every file
served from the frontend build. Since provider icons became lazy chunks,
one cold session sends about 90 static requests, so a second user behind
the same IP gets 429 on route chunks and the page fails to load.
Files that exist in the build now count against a separate static
limiter (GLOBAL_STATIC_RATE_LIMIT_ENABLE, GLOBAL_STATIC_RATE_LIMIT,
GLOBAL_STATIC_RATE_LIMIT_DURATION), disabled by default. Pages and
unknown paths still count against the web limit.
The gzip middleware also recompressed every static response, about
120 ms of CPU per request for the 4.8 MB main bundle. Build files are
embedded and never change, so each one is now served from a gzip
encoding computed once per process. Pages and unknown API paths still
go through the gzip middleware.
The token menu now opens an edit dialog instead of a rename prompt.
Renaming still needs no verification. Changing the permissions requires
step-up verification bound to the token id and the new grant, so a
proof cannot be replayed for another token or another set of
permissions; the grant is checked against what the owner can do, the
same as at creation. PATCH /api/user/access_tokens/:id takes an
optional scopes list and is rate limited like the other token routes.
A token that holds permissions the catalog no longer offers can still
be renamed and keeps them; a new grant only contains offered
permissions. Edit stays disabled until the permission catalog has
loaded.
Grant changes are audited as access_token.update with the previous and
new scopes, and audit details show both as permission names.
The permission picker for tokens and admin users now uses toggle
buttons. Token permissions are grouped by personal, administration and
system, each group collapsible with a selected count and select-all or
clear.
Each user can now hold up to 20 named access tokens in the
user_access_tokens table instead of a single token on the user row.
Tokens are shown once at creation (nap_ prefix) and stored as a SHA-256
digest. They expire after 30 days by default; a token without expiry is
allowed with a warning.
A token only reaches the routes its scopes allow. Scopes are picked from
the same permission matrix as admin permissions and are limited to what
the owner can do; every management route declares the scope it needs,
and routes without a declaration reject tokens. Token management itself
always requires a browser session.
Tokens can complete step-up verification, so verified actions such as
changing a password or 2FA settings work with a token; such changes sign
out all browser sessions. Disabling a user keeps their tokens, deleting
the account removes them.
The existing dashboard token keeps working for 30 days after the
upgrade. It can be revoked but no longer regenerated, and afterwards it
is rejected with ACCESS_TOKEN_LEGACY_RETIRED. The security page lists
tokens with their access records, and audit details show scope names.
Add the synchronous openai_image host protocol so a task plugin can claim
POST /v1/images/generations and /v1/images/edits; the host submits the
task, polls it inside the request and lets the plugin render the OpenAI
image response. Unclaimed models still fall back to the HTTP image relay.
- Migrate Ali (Bailian) image models to the alibaba plugin and remove the
ali channel image/wan adaptors and the qwen model setting
- Add Doubao Seedream image generation to the doubao plugin
- Support retainResult on routes and indexed multipart file references
- Settle image counts from upstream usage or image payloads; document the
rule in the billing conventions and plugin API docs
- Show billing source and task artifacts in usage logs; rename the
DoubaoVideo channel display name to Doubao and refresh the favicon
List system tasks by type, status and scope with paging, show a history
table beside the active task panel, and add DELETE /api/system-task/history
to purge finished entries.
Group sensitive-word checks, channel selection, retries and channel health
under one "Request policies" settings area with a session rules editor,
per-rule session modes (inherit/off/prefer/strict) and a global default.
Record routing decisions per attempt into log admin info, expose
GET/PATCH /api/option/request_policy with whole-policy validation, and
redirect the legacy settings URLs to the new sections.
Preserve legacy credentials across primary RP ID changes and bind successful
assertions to their verified RP ID. Share domain selection across sign-in,
login verification and sensitive operations without replaying ceremonies.
Save domain settings atomically, preview known and unknown credential impact,
and require fresh confirmation before removing domains still in use. Recheck
trusted RP IDs when persisting credentials and record safe operation audits.
Use read-only configuration snapshots, accept trusted HTTPS intranet hosts,
check registration origins early, and preserve historical capitalization.
Update settings guidance and all supported translations.
Validation: SQLite 3.50.4, MySQL 8.0.46 and PostgreSQL 16.15, including fresh
and released-schema upgrade checks; Go regression and race checks; 79 frontend
tests; TypeScript, lint, frontend build and root Go build.
Security references: OWASP ASVS 5.0.0 V6.3.4 and V6.3.8, Authentication and
Session Management Cheat Sheets, and the WebAuthn RP ID/origin rules.
Add read-only conversion and preview APIs using shared effective price resolution, provider metadata, and legacy DALL-E multipliers. Standardize preview payloads under data.effective with billing metadata.
Validated conversion paths on real SQLite 3.50.4, MySQL 8.0.46, and PostgreSQL 16.15 instances.
Add confirmed multi-select deletion through one batch API request and one
soft-delete statement. Record the affected count and requested IDs in a
separate batch audit event, and identify legacy events with missing counts.
After creation, offer an unchecked Save as a file option with TXT/Markdown
formats and optional name/quota fields. Keep Done as the default completion
action. Include translations for all seven frontend locales.
Validation:
- Redemption and audit frontend regression tests, typecheck, and scoped lint.
- go build ./...
- go test ./controller -run '^TestDeleteRedemptionBatch$' -count=1 -v
with TEST_MYSQL_DSN, TEST_MYSQL_LOG_DSN, TEST_POSTGRES_DSN,
and TEST_POSTGRES_LOG_DSN set to isolated primary and log databases.
- Real SQLite 3.50.4, MySQL 8.4.11, and PostgreSQL 16.15 passed, including
deletion of 15 records, duplicate/missing IDs, zero-row retries, audit
deduplication, invalid input, and exclusion of credentials from logs.
Introduce a unified model management experience: catalog metadata
validation, vendor management, batch delete with channel/pricing
cleanup, model pricing snapshot editing with optimistic concurrency,
and an upstream ratio-sync flow with price cells. Move configuration
into dedicated pricing config/metadata-sync/vendor-management backend
services and add audit records for model/vendor/pricing mutations.
Rework the models page around vendors and model connections, add
model-pricing and vendor-management dialogs, and replace the shared
Select usages with the Combobox component across subscriptions,
plugins, OAuth presets, audit filters, and settings. Add the model
pricing panel and verify behavior with focused tests.
Treat TOTP and Passkey as alternative enrolled factors across login and
sensitive account operations. Gate every primary login transport before
issuing a session, require WebAuthn user verification, and consume login
challenges atomically with session creation.
Reuse the shared verification UI for login, 2FA management, and account
deletion. Require scoped, single-use deletion proof; recheck the session
inside the deletion transaction and revoke all sessions afterward.
Validation: controller/service/model/middleware tests; real SQLite 3.50.4,
MySQL 8.4.11, and PostgreSQL 16.15 security regressions; frontend tests,
TypeScript, targeted lint, formatting, and production build.
Deploy the frontend and all backend nodes together. No schema changes.
Record successful and failed API token operations with safe target metadata. Capture quota adjustments in a transaction, synchronize committed cache differences, and correlate audit and top-up records.
Show operation targets, changes, quota balances, and failure details consistently across audit and usage logs, with translations for all seven locales.
Validated controller, middleware, and model tests; 78 frontend tests; typecheck and lint; real SQLite 3.50.4, MySQL 8.4.11, and PostgreSQL 16.15 with shared and separate log databases.
Require scoped, single-use verification for account bindings and password
operations. Bind OAuth authorization and email confirmations to the initiating
session; preserve the last usable login method and audit operation outcomes.
Apply Unicode-aware password length limits, Argon2id writes with bcrypt
compatibility, and long-password encryption.
Return has_password with the existing profile SELECT without extra queries.
Reuse the existing security dialogs and add all seven locale translations.
Validation:
- Go: go test ./common ./model ./service ./middleware ./controller ./router -count=1
- DB: SQLite 3.50.4, MySQL 8.4.11, PostgreSQL 16.15; separate main/log databases
- MySQL/PostgreSQL: TEST_SECURITY_DIALECT=<dialect> with TEST_<DIALECT>_DSN,
go test ./controller -run '^(TestSecurityAccount|TestSecurityEnrollment|TestGenerateOAuthCode|TestOAuthBind|TestTelegramOAuth)' -count=1 -v
- Web: relevant Vitest suites, bun run typecheck, targeted oxlint/format,
bun run i18n:sync, and bun run build
Roll out dual-format readers to every instance with
ACCOUNT_PASSWORD_HASH_ALGORITHM=bcrypt before enabling Argon2id writes
and the new UI. Rollbacks must retain Argon2id and v2 envelope readers.
Relevant controls: ASVS 5.0.0 6.2.1-6.2.3, 6.2.5-6.2.9, 6.3.7, 7.4.3, 7.5.1;
this change does not assert application-wide ASVS certification.
Use authorization code flow with PKCE and verified ID tokens for Telegram login, binding, and security verification. Preserve existing bindings and require administrator OAuth configuration.
Keep the restricted WeChat first-enrollment session proof, fix missing-target authentication errors, and preserve callback requests after OAuth popups close.
Require single-use operation proofs for passkey enrollment, two-factor setup, and channel key access. Add password and OAuth verification flows, enforce session-bound enrollment, and redact OAuth callback secrets from logs.
Validation: affected Go packages pass; frontend typecheck, changed-file lint, and 111 tests pass. Security enrollment regressions pass on SQLite 3.50.4, MySQL 8.0.46, and PostgreSQL 16.15. Full frontend lint has pre-existing errors outside the changed files.
Move account security settings into a dedicated page and add token status, rotation, revocation, and access history.
Store audit events with role snapshots and JSON metadata, add audit.read authorization and an independent audit page, and upgrade the ClickHouse driver to v2.46.0.
* feat(relaykit): preserve hosted tools across conversions
- add protocol-neutral hosted-tool DTOs, conversion metadata, and loss policies
- bridge citations, grounding metadata, and hosted-tool stream lifecycles
- document the public conversion behavior and channel policy controls
* refactor(relaykit): normalize reasoning and thinking intent
- centralize provider-neutral reasoning intent, effort, and budget mappings
- parse model suffixes at the host entry boundary while preserving provider-owned tails
- keep adaptive Claude thinking and explicit zero-token compatibility consistent
* fix(billing): preserve authoritative usage across relay hops
- carry native BillingUsage sidecars through direct and streamed protocol bridges
- merge partial and terminal usage monotonically with safe fallback settlement
- retain cache metadata, penultimate usage, and per-call Gemini tool surcharges
* feat(relay): bridge Responses with Claude and Gemini protocols
- add direct request, response, and stream converters across supported relay formats
- expose Claude count_tokens and Chat-to-Responses compatibility endpoints
- carry conversion diagnostics through the host while retaining the curated public goldens
* fix(relay): wire relaykit conversions into host channels
- connect handlers, adaptors, and channel settings to the standalone conversion layer
- keep model mapping, pricing identity, retries, and provider-specific suffix behavior aligned
- ignore local audit artifacts and retain focused public regression coverage
* feat(token): support custom auto group order
* feat(keys): enhance auto group presentation
* fix(keys): rework Auto flow border and compact inherited order
The Auto group highlight previously tinted the whole control surface
with a gradient and animated only a 1px top sweep, which read as a
background color rather than a flowing border. Replace it with a
border-only effect: an aria-hidden, pointer-events-none overlay whose
conic gradient is masked down to a thin ring hugging the rounded
perimeter, so the highlight travels around all four edges and corners
every 3.2s. The interior stays neutral with a restrained static
primary border and glow; prefers-reduced-motion hides the moving
layer while keeping the static emphasis.
The inherited global Auto order also rendered as spacious two-line
rows with circular sequence markers, wasting drawer space. Render it
as a compact wrapping strip of one-line chips (index, name, ratio
badge) with descriptions kept accessible via title and sr-only text,
scrolling only past a much smaller max height.
Custom add/remove/reorder editing, empty-array inheritance semantics,
and the submit payload are unchanged.
* fix(keys): preserve Auto inheritance and unify effects
* refactor(keys): temporarily disable AutoGroupBadge in api-key-group-cell
* test(relayconvert): add golden snapshot matrix and relaykit boundary guard
Phase 0 of the relaykit extraction plan: pin byte-level output of every
registered (from,to) request/response/stream conversion route, and
forbid kit-bound packages from growing host-only imports.
* wip(relayconvert): drop gin.Context from converter signatures; add convmeta draft
Phase 1 in progress: relayconvert now takes context.Context; host media
resolver adapts gin.Context back at the service boundary.
* refactor(relayconvert): decouple converters from RelayInfo, gin, and settings
Phase 1 of the relaykit extraction plan:
- converters now depend on convmeta.Meta (implemented by RelayInfo) instead
of *relaycommon.RelayInfo; ClaudeConvertInfo and the format guesser move
to convmeta with aliases left behind
- host settings reach converters via a convmeta.Options snapshot built in
RelayInfo.ConvOptions; no more model_setting/reasoning global reads inside
the conversion layer
- effort-suffix helpers move to service/relayconvert/reasoning (old package
forwards); chat-to-responses upgrade policy moves to service (host routing
logic, not conversion)
- golden conversion matrix unchanged
* test(relayconvert): tighten boundary — kit packages now free of gin/setting imports
* refactor(dto): drop gin and logger dependencies
Phase 2 (part 1): dto.Request.IsStream now takes *http.Request instead of
*gin.Context (Gemini's impl reads query/path off the std request); dto's
three logger calls become common.SysError. Boundary test allowlist is now
empty — kit-bound packages import no gin/setting/logger/model.
* refactor(kit): extract dependency-free kitutil; dto/types/relayconvert stop importing common
Phase 2 of the relaykit extraction plan:
- new service/relayconvert/kitutil holds the pure helpers the kit needs
(JSON wrappers, pointer/string/uuid/timestamp utils, MaskSensitiveInfo,
pluggable LogInfo/LogError hooks, Debug flag)
- dto, types, and all relayconvert packages now use kitutil; their only
remaining internal deps are dto/types/constant
- common keeps every original symbol (MaskSensitiveInfo delegates to
kitutil) so host code is untouched; main.go routes kit logging into
common.SysLog/SysError and mirrors DebugEnabled
- golden conversion matrix unchanged
* refactor(kit): move EndpointType/FinishReason to types; OpenRouter dialect via Options
Kit packages (dto/types/relayconvert/reasonmap) no longer import constant:
- EndpointType and finish-reason values live in types; constant re-exports
- the OpenRouter special-case in claude->openai request conversion reads
Options.OpenRouterDialect, set by the host from the channel type;
InitChannelMeta invalidates the cached snapshot on channel switch
* refactor: extract relaykit submodule (dto/types/relayconvert/reasonmap)
Phase 3 of the relaykit extraction plan:
- new go module github.com/QuantumNous/new-api/relaykit containing dto
(minus task family), types, relayconvert (with convmeta/kitutil/reasoning),
and reasonmap; host consumes it via require + replace, go.work for dev
- task-family dto (task/suno/midjourney/video) stays in the host dto
package; dual-consumer host files alias it as taskdto
- relaykit builds and tests standalone (GOWORK=off): no host imports,
no gin, no DB, no settings
- golden conversion matrix unchanged
* build(docker): copy relaykit/go.mod before go mod download
The local-replace submodule's go.mod must exist inside the build context
for the main module graph to resolve.
* fix: address relaykit extraction regressions
* fix: address relaykit review regressions
* docs: document Meta nil receiver contract
* fix(relaykit): fail OpenAI→Claude conversion without max_tokens; reject negative default_max_tokens
The Claude Messages API requires max_tokens (omitting it is a 400
"Field required"), but with a nil Options.Claude.DefaultMaxTokens hook
the converters silently emitted a request the upstream is guaranteed to
reject. Both OpenAI Chat and Responses → Claude conversions now return
sharedclaude.ErrMissingMaxTokens when no path (client value, default
hook, thinking-adapter floor) supplied one. Unreachable in the host,
which always configures the hook.
Host side, claude.default_max_tokens now rejects negative values at the
option API before persisting — they would wrap into huge unsigned values
during conversion. Zero stays allowed: the current API treats
max_tokens: 0 as cache pre-warming.
* fix: make Gemini safety settings read path race-free
* feat(subscription): add admin quota reset actions
* fix(subscription): keep quota reset in plan row actions
* refactor(subscription): move user subscription actions into menu
* feat: add system instance reporting
* feat: show system instance resources
* fix: update translations for heartbeat messages in Russian and Vietnamese
* feat(dashboard): add traffic flow sankey chart
Add dashboard flow APIs and a Sankey-based flow view with user, optional API key, model, and channel layers.\n\nReuse the dashboard VChart palette, add precise link/node tooltips and interactions, and cover filtering, layer ordering, color stability, and error states with tests.
* feat: build flow chart from quota data
---------
Co-authored-by: CaIon <i@caion.me>