Reuse the channel base host and plugin allowedHosts for redirected query
credentials. Keep anonymous redirects working, prevent credentials from
returning after an untrusted hop, and restrict body replay to trusted hosts.
Preserve SSRF and redirect limits, reject HTTPS downgrades, and redact signed
URLs from redirect-policy errors. Document the existing allowedHosts contract.
Validation: go test ./relay/channel/task/jsplugin -count=1
Passed against clean 6347fb0aa plus this patch, without an overlay.
Covers single/batch polling, 301/302/303/307/308, host and port boundaries,
multi-hop credentials, SSRF, downgrade rejection, and real HTTP receivers.
Related OWASP ASVS v5.0.0 controls: 14.2.3, 1.3.6, 12.3.1, 16.2.5.
https://github.com/OWASP/ASVS/tree/v5.0.0/5.0/en
The email binding audit test failed now and then because it searched the
whole audit JSON for the six-digit code. Audit event and request IDs start
with a long run of time digits, so a random code sometimes appeared inside
one of them. Treat only a code that no other digit touches as a leak.
A task retry that moved to another plugin or channel kept the previous
attempt's usage expression. A retry from an expression-priced plugin to
one priced per request charged, and later settled, at the first plugin's
price. Each attempt now prices itself from scratch.
A retry to a more expensive channel was topped up only after the
upstream had accepted the task. A user who could not cover the
difference went into arrears, or the task was dropped while the upstream
was already running it. The retry now raises the reservation before the
request is sent, with the same balance check as the first pre-consume,
and a channel the balance cannot cover is never called.
A wallet top-up taken before submission replaced the amount held by the
first pre-consume instead of adding to it, so a request that then failed
refunded only the top-up. Image requests whose quantity was raised
before submission were affected too. The full amount is now refunded.
DeepSeek /user/balance returns both USD and CNY wallets. An account
funded in CNY reports USD 0.00, and getDeepSeekBalanceUSD returned that
zero before ever reading the CNY entry.
Only short-circuit on a positive USD balance. When the USD wallet is 0
and CNY holds funds, convert CNY with USDExchangeRate; a lone zero USD
wallet still returns 0 instead of an error.
* fix(relaykit): keep tool calls when merging adjacent assistant messages
Adjacent assistant messages with string content were merged keeping only
the second message's tool calls, so tool results for the first message's
calls had no matching tool_use and Claude rejected the request.
- concatenate both messages' tool calls in input order
- skip the "..." placeholder of a tool-call message in the merged text
* fix(relaykit): preserve literal ellipses in assistant message merges
---------
Co-authored-by: seefs001 <[email protected]>
* feat: add ali MultimodalEmbedding qwen3-vl-embedding
* fix: report image/video tokens in qwen3-vl-embedding usage for billing
* fix(ali): serve every Bailian multimodal embedding model natively
Route qwen2.5-vl-embedding, the tongyi-embedding-vision series and
multimodal-embedding-v1, dated snapshots included, to the native
multimodal-embedding endpoint like qwen3-vl-embedding. Text embedding
models stay in compatible mode.
Take tongyi-embedding-vision's text and image tokens from
input_tokens_details so the image ratio applies to its image tokens,
and set total_tokens to the billed input tokens: tongyi counts its
unpriced output_tokens in total_tokens. A response without usage bills
the request estimate, as compatible mode does. Return an error code
carried in a 200 body as a 500, close the response body when reading
it fails, and fold the input conversion into one function.
Count the text, image and video parts of embedding input in the request
estimate the way chat content is counted, so image calls are pre-charged
and a response without usage is not free. With the default settings no
image is downloaded for the estimate.
* fix(gemini): reject embedding input that is not plain text
Gemini embeds text only, so the converter kept the strings and dropped
image, video and text parts. The embedding request estimate now counts
those parts, and Gemini embeddings bill that estimate, so a dropped part
would be charged without ever reaching Gemini. A part now fails the
request with a 400 that skips retries, before anything is sent or charged.
---------
Co-authored-by: CaIon <[email protected]>
* fix(task): honor channel header overrides for submission and polling
Apply the existing Request Header Override configuration after task plugin
headers, including per-task, batch and realtime queries. Resolve api_key
from the credential used for that request and skip client-only placeholders
when polling has no original client request. Reuse the HTTP header resolver
without changing plugin contracts or adding provider-specific settings.
Keep polling redirects visible, matching submission and utils.fetch, so a
custom credential header cannot be forwarded to a redirect destination.
Document configuration and cover precedence, saved keys, absent client
context, invalid values, redirects and all polling initialization paths.
Validation with Go 1.26.4 and GOWORK=off:
- make test: root, relaykit and tokenkit passed before the realtime follow-up
- go test ./relay ./relay/channel ./relay/channel/task/jsplugin ./service -count=1: passed after the follow-up
- go vet ./... and go build ./...: passed after the follow-up
- Independent read-only review: passed
Scoped security review used OWASP ASVS 5.0.0 and the Authentication,
Session Management and REST Security cheat sheets. Credentials remain in
headers, client headers are not persisted, and redirects are not followed.
No live provider calls or production deployment were performed.
https://owasp.org/www-project-application-security-verification-standard/https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.htmlhttps://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.htmlhttps://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
* fix(task): follow poll redirects and override content request headers
Polling follows redirects again through the shared HTTP client, which
checks every hop against the SSRF protection settings. Refusing them
turned each poll of a redirecting upstream into a failure, and after
enough failures the task was failed and refunded.
Credentialed content requests now carry the channel header overrides,
as submission and polling do. Credentialless links, utils.fetch and the
Host header stay as the plugin builds them.
* fix(task): settle tasks that a realtime query sees finish
Fetching a Gemini or Vertex task queries the upstream directly. When
that query saw the task finish, it wrote the final status itself, so
the poller skipped the task and never settled or refunded it: a failed
task kept its charge. Hand that transition to the poller's single-task
update, which records the status and settles exactly once.
---------
Co-authored-by: CaIon <[email protected]>
* feat(email): add Resend delivery provider
Add Resend as an alternative to SMTP with separately editable sender address
and name, credential-safe configuration status, and validation before activation.
Preserve the target repository's email notification and balance reminder settings.
Only Resend changes are transplanted from hicoderai/new-api; no landing,
legal-document, model-group, pricing, branding, or dependency changes are included.
Validation: Go common/controller email regression tests; 19 frontend email and
notification tests; frontend production build; tsgo typecheck; touched-file lint.
Frontend validation uses the target lockfile's Axios 1.18.1 with local cached tools.
Security references: OWASP Authentication, Session Management, and Forgot Password
Cheat Sheets. Tests cover credential non-disclosure, invalid activation, sanitized
transport errors, and no automatic retry or fallback; token and session flows
are unchanged.
https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.htmlhttps://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.htmlhttps://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
* fix(email): log Resend rejections and translate Resend errors
Resend's reason for a rejected or failed send now goes to the server log.
People who asked for the email still see the short error.
Resend and email method errors are translatable messages in all seven
languages.
The email settings hints are shorter and reuse the existing key hint.
The new frontend files carry the license header, the sender name check
passes lint, and test fixtures use neutral names.
* fix(email): check Resend settings while Resend is active
Saving the sender address or API key now checks the whole Resend setup
while Resend sends the mail. A typo the form accepts, such as
[email protected], is rejected instead of breaking every send.
Resend's error text and network errors are quoted in the server log.
---------
Co-authored-by: CaIon <[email protected]>
* fix(relay): keep Claude streams alive during preparation and retries
* fix(relay): retain failed outcomes for keepalive stream errors
* fix(relay): wait one interval before sending keepalive
* fix(relay): synchronize request keepalive handoff before headers
Stop and join the request keepalive before the stream scanner copies upstream
headers. The scanner then owns subsequent ping and data writes through its
shared write mutex.
Add regression coverage for an immediate stream and for the first keepalive
committing response headers. Before the fix, the latter case reports a data
race between Header.Clone and copyCodexSSEHeaders.
Validation: GOWORK=off GOCACHE=/private/tmp/new-api-pr7857-review-go-cache go test -race -p 2 -parallel 1 ./controller ./relay/channel ./relay/helper -run 'TestRelayKeepAlive|TestRelayStreamErrorAfterKeepAlive|TestStreamKeepAlive|TestDoRequest|TestNewTaskAPIRequest|TestProcessHeaderOverride|TestStreamScannerHandler_' -count=1
Result: all three packages passed on Go 1.26.6, darwin/arm64.
git diff --check passed.
---------
Co-authored-by: Calcium-Ion <[email protected]>
* fix(notify): keep notification limit window from resetting every hour
The rate-limit key included the current clock hour, so the count reset on
every hour boundary and NOTIFICATION_LIMIT_DURATION_MINUTE above 60 had no
effect. Drop the hour suffix so the window runs for the configured duration
from the first notification, and make the Redis counter atomic with
SETNX+INCR in one transaction.
* fix(notify): fall back to default for nonpositive notification window
A zero or negative NOTIFICATION_LIMIT_DURATION_MINUTE left the Redis
counter without an expiry (or failed the SET), which blocked quota
notifications for good once the counter filled.
* test(notify): cover the Redis notification limit window
* 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.
SMTP settings now only decide whether mail can be sent. Verification
codes, password reset, email binding and account security mails depend
on SMTP alone, and no notification switch affects them.
Admins get a site-wide low balance reminder switch, on by default, next
to the SMTP settings. The default threshold is entered as an amount.
Users choose which notifications they receive. Low balance reminders are
on for everyone by default. Admins can also turn channel notifications
and upstream model update notices on or off. The root user receives
channel notifications until it turns them off. Other admins receive them
after they turn them on.
A low balance reminder is sent once, when a charge takes the balance
below the threshold. A top-up above the threshold arms it again.
Channel notifications reach every admin who has them on, in that admin's
language. Email binding codes and account security mails follow the
account's language.
Saving notification settings no longer clears the language, the sidebar
layout or the billing preference.
Existing settings keep their behavior, and old clients that do not send
the new fields keep the stored switches. Webhook, Bark and Gotify
payloads are unchanged.
Keep table drop targets stable, use compact previews, and prevent
accidental mode switches and leftover row transforms.
Add first-use guides for card and table views with automatic playback,
pause and replay controls, and translations for all seven locales.
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.
A task plugin can offer two billing modes, such as per task for some
models and per second for others. An administrator who prices a model
per second leaves its per-task price at zero. The model card, the list
view and the detail page then showed that unused zero price next to
the real one, and it read as a free item.
A usage price that is zero in every tier is now hidden when another
usage price is set. A model priced at zero throughout still shows its
zero prices. The card also drops the tier name when a model has a
single tier, since that name was always the editor's default "base".
Dark Reader checks html.dark / color-scheme only once during page load,
but the theme class was added by ThemeProvider after React mounted.
Resolve the saved theme in an inline head script and declare
color-scheme: dark on .dark.
The usage-log model badge resolves its icon from MODEL_PROVIDER_RULES by model
name only. clef (Cloudflare) and the NVIDIA families without an nvidia prefix
(nv-embed, nv-rerank, riva-, parakeet-, canary-, nemoretriever) matched no rule
and fell back to the plain dot.
Co-authored-by: xuu1998 <[email protected]>
* fix(relayconvert): preserve input_image detail in Chat/Claude -> Responses conversion
* fix(relayconvert): preserve original image detail in Responses
Preserve all supported image detail levels during Chat to Responses conversion instead of downgrading original to auto.
Use bogus for unknown-detail fallback coverage and verify auto defaults for Claude message and tool-result images.
---------
Co-authored-by: seefs001 <[email protected]>
Add an opt-in Mantle interface with top-level cache_control, API key and
SigV4 authentication, and native Claude response handling. Keep InvokeModel
as the default for existing AWS channels.
Strip hop-by-hop headers before signing and map the dated Haiku alias to
Mantle's model ID. Cover signatures over HTTP/1.1 and HTTP/2, cache usage,
legacy routing, and channel configuration with regression tests.
Refs #4791
The Responses schema requires summary on every reasoning item, but the
chat-to-Responses stream announces a reasoning item with an empty
summary that omitempty drops. Strict clients reject the item and then
see every summary event that follows as having no active item.
Items that already carry summary text keep their existing encoding.
Group rate limits only picked the limit; every request was still
counted against the user as a whole. A user with 200 successful
requests in a group allowed 500 per minute got a 429 on the first
request in a group allowed 50, and in Redis one request in the smaller
group cut the larger group's remaining burst down to the smaller one.
A group that has its own limit now counts each user's requests on its
own, for both the total and the success limit. Groups without their own
limit still share the user's global limit, so tokens spread over
several groups do not multiply it.
The Sankey labels were hard-coded to #475569, which is nearly invisible
on the dark theme. Read the app's --foreground token at runtime instead,
so labels and the active node outline match the surrounding text in
light/dark mode and every theme preset, and follow preset switches live.
The search box in the console header listed every page, so regular
users saw the admin pages and only found out on click that they had
no access. It now offers the same pages as the sidebar, which already
filters them by role and by the sidebar settings.
The layout now renders the search dialog instead of the search
provider, which removes the import cycle between the two.
* fix(relay): mask only the upstream address in error messages
MaskSensitiveInfo masked every URL, domain and IPv4 in upstream error
messages, which garbled documentation links, email domains and dotted
identifiers that clients need to read.
It now takes the upstream addresses of the request: the channel base URL,
the origin the gateway actually dialed (recorded in doRequest, DoWssRequest,
the AWS SDK client and the Xunfei and Volcengine TTS dials, which covers
absolute Advanced Custom routes), the channel proxy and the environment
proxy. URLs and bare hostnames on those hosts and all IPv4 addresses are
masked; other URLs and domains stay readable. With no upstream address no
address is masked. Credentials are masked on every URL regardless of host:
api_key values, query values and userinfo.
The URL matcher also keeps quotes, colons and semicolons that follow a URL,
keeps parentheses and apostrophes inside it, masks an IPv4 URL host whole and
keeps a country-code TLD when the host has a port.
* fix(relay): keep preset official endpoints readable in masked errors
A channel on the base URL new-api presets for its type reaches a public
official endpoint, so its address is not masked. Only addresses the admin
configured are: a custom base URL, the origin of an Advanced Custom route,
and the channel and environment proxies. Preset hosts are compared with
their port so a configured localhost upstream stays apart from the Ollama
preset.
The origins recorded for the AWS SDK, Xunfei and Volcengine TTS dials and the
plugin allowedHosts masking are dropped: they are all official endpoints.
* fix(relay): mask IPv4 addresses and plugin allowed hosts in every error
IPv4 addresses in network errors include the gateway's own address, so they
are masked even when the channel uses a preset base URL. Task plugin build
failures mask the plugin's allowedHosts again: an installed plugin may point
them at private hosts.
* fix(relay): mask bracketed IPv6 addresses in errors
Go network errors write IPv6 addresses in brackets, as in
"dial tcp [2001:db8::1]:443". Like IPv4, they are the resolved upstream or
the gateway's own address, so they are masked on every error once the
bracketed content parses as an IP.
Vertex AI serves the us and eu multi-regions from
aiplatform.us.rep.googleapis.com and aiplatform.eu.rep.googleapis.com.
The URL builder turned every non-global location into
<location>-aiplatform.googleapis.com, so a channel whose deployment region
was us or eu sent requests to us-aiplatform.googleapis.com and
eu-aiplatform.googleapis.com. Single regions and global are unchanged,
and a custom base URL still decides the host.
The hailuo, vidu, google and vertex-ai plugins now declare the new_api
upstream, so a New API (type 60) channel can bind them, and each serves its
vendor wire format on prefixed native routes that a downstream gateway
running the same plugin addresses:
- hailuo: /hailuo/v1/video_generation and /hailuo/v2/video_generation, with
path-parameter queries. Through a gateway, v1 videos download from the
upstream's task artifact, since only the upstream can resolve the MiniMax
file link.
- vidu: /vidu/ent/v2/{text2video,img2video,start-end2video,reference2video}
and the creations query; the route names the action, so one or two
reference images stay a reference request.
- google: models/{model}:predictLongRunning and operation polling. The
operation is renamed after the gateway task, and videos download from the
upstream's task artifact, since the Gemini file needs the upstream key.
- vertex-ai: one dynamic route for predictLongRunning and
fetchPredictOperation. Operation names echo the caller's project and
location and never carry the upstream project.
Status presenters render a task that is not polled yet as pending and a
task the poller failed while its snapshot was still running as failed, so
the downstream gateway reads the upstream state and settles on the same
usage.
Versions: hailuo 1.3.0; vidu, google and vertex-ai 1.1.0.
buildSubmitRequest spread metadata over the request after the billed
fields were computed, so a model, duration or resolution inside metadata
reached Vidu while the charge used the top-level values: a request billed
as vidu1.5 for 4 seconds at 360p could generate viduq2 for 8 seconds at
1080p. The billed fields are now applied last; metadata still carries
every other Vidu field.
The table had drifted from go.mod, web/bun.lock and electron/package-lock.json. Versions now match what the builds ship, missing direct dependencies are listed, testify moves to test scope, and go-sql-driver/mysql's MPL-2.0 license gets a section.
go-epay is listed under MIT, the license it ships with, so the Proprietary/Internal section has no dependency left and is removed.
alpha.7 is a performance release on the JSON and call path: JSON.stringify
and AppendJSON keep their stringifier on the stack, JSON.parse clears its
working stack once per parse, ReleaseCallData clears only the registers a
finished call used, and arithmetic and bitwise results reach registers
without a write barrier. The interpreter's register stack now grows in
place within its capacity.
The host only calls ReleaseCallData, so no adaptor changes are needed.
new-api builds and its plugin tests pass unchanged.
* fix(claude): preserve explicit zero usage during stream finalization
* test: use synthetic values for Claude usage regressions
* fix(claude): estimate stream usage only when the counts are missing
Trusting every explicit zero let an upstream that reports zeros stream
output for free, and a zero in message_delta erased the input counted
at message_start. Decide from the counts instead: input is missing only
when input, cache reads and cache writes are all zero, and output only
when it is zero after streamed content. A broken stream still takes the
larger of the reported and estimated output. A fully cached prompt and
a refusal before any output keep their zeros, so relaykit usage parsing
goes back to its previous form.
---------
Co-authored-by: CaIon <[email protected]>
* fix(vertex): pass project_id in API Key mode to avoid 404 on official endpoints
Closes#6250.
Vertex AI channel in vertex_key_type=api_key mode: getRequestUrl passed
an empty string as project_id to BuildGoogleModelURL /
BuildAnthropicModelURL, leaving the URL without the
/projects/{PROJECT_ID}/locations/{REGION}/ segment, so the official
Vertex endpoints returned 404.
In JSON mode project_id is parsed from the service account, so the bug
is specific to api_key mode.
Fix:
- Add VertexProjectID to ChannelOtherSettings (used only in api_key
mode; JSON mode keeps parsing from the service account, unchanged).
- In adaptor.go's api_key branch, use ChannelOtherSettings.VertexProjectID
instead of the hardcoded empty string. When unset, fall back to the
legacy behavior so existing api_key channels are not broken.
- Frontend (web/default): show a Project ID input when
VertexKeyType=api_key, persist settings.vertex_project_id, and require
it via superRefine so users cannot save an api_key channel that would
still 404. Clear the residual field in non-api_key mode or non-Vertex
channels.
- Add translations across 7 locales (en/zh/zh-TW/ja/fr/ru/vi).
Regression tests (relay/channel/vertex/adaptor_api_key_url_test.go)
cover 4 scenarios:
- api_key mode + configured project_id -> URL contains /projects/.../locations/
- api_key mode + stream suffix -> key joined with &, path contains project_id
- api_key mode + Claude mode -> anthropic publisher path
- api_key mode + empty project_id -> falls back to legacy behavior
* fix(vertex): make the API Key project ID optional
Express-mode API keys are documented without a project, so a required
project ID would block saving those channels. Without one the URL keeps
the express-mode form as before; with one, the URL carries the project
and the deployment region, so a global region is honored.
- Drop the required check and describe the field in plain words.
- Treat vertex_project_id as a sensitive field, like the key format.
- Assert exact request URLs in a table test, including the global case.
- Cover saving, clearing and loading the project ID in the channel form.
Add a per-token "Allow querying user quota" switch. When it is on,
GET /api/usage/token/ also returns user_quota with the account's
remaining and used quota in quota units. New tokens start with the
switch on: the console form, POST /api/token/ without the field, and the
default token created at registration. Tokens created before this change
stay off. PUT /api/token/ keeps the stored value when the field is
absent, and the token audit lists the field when it changes.
The key usage and key log queries now check the key's IP whitelist the
same way API requests do, and answer 403 from an address outside it.
Expired and exhausted keys can still use both queries.
New API channels now query their balance from the upstream's
/api/usage/token/ with the channel key. When the upstream also returns
the owner's quota, the smaller of the key's and the owner's remaining
quota is the balance, and an unlimited key uses the owner's quota alone.
Without it, a limited key reports its own remaining quota, and an
unlimited key asks for the switch to be turned on upstream. Quota
converts to USD with the upstream's quota_per_unit from /api/status.
Refs #3122
Refs #6828
A failed read consumes and closes the request body. Endpoints that read
it a second time then answered "http: invalid Read on closed Body"
instead of the cause: /v1/video/generations, and audio transcriptions
and translations. The first failure is now kept for the rest of the
request, so every later reader reports it.
A body over MAX_REQUEST_BODY_MB now gets 413 instead of 400 on the
endpoints that still answered 400: ordinary relay endpoints such as
/v1/chat/completions, Midjourney, task plugin routes and /v1/tasks.
Task plugin routes also write the cause to the log, and Midjourney
errors no longer repeat "Invalid request:".
POST /v1/responses, /v1/videos and /v1/images/* pass through the task
plugin endpoint check before any model is matched. A body that failed to
read or parse there got 400 "Invalid task protocol request" even when no
plugin claims the model, and the log carried only that sentence, so the
real cause, such as a body over MAX_REQUEST_BODY_MB or a connection cut
mid-upload, was lost.
These requests now get the cause in the response and in the log, and a
body over the size limit gets 413 with the limit in the message. Other
endpoints such as /v1/chat/completions no longer repeat the prefix, as in
"Invalid request: Invalid request: invalid JSON request body".
* 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.