100 Commits
Author SHA1 Message Date
CaIon 206e0fa5c6 fix(kling): bump plugin to 1.1.1 2026-10-20 22:43:11 +08:00
CaIon 5900426821 fix(task): restrict query redirect credentials to allowed hosts
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
2026-10-20 22:43:11 +08:00
CaIon 120dbeafb1 test(security): match the email code only as a whole number
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.
2026-10-20 22:43:11 +08:00
CaIon f9a0634f84 test(audit): read PostgreSQL index columns in key order 2026-10-20 22:43:11 +08:00
CaIon 55e2ae54d1 fix(task): reprice each retry and reserve it before submission
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.
2026-10-20 22:43:11 +08:00
Calcium-Ion 6cf0d90b5f feat(deepseek): support V4.1 Flash, the Search and Fetch APIs, and @ reasoning modifiers (#7867)
* 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.
2026-10-17 23:24:33 +08:00
Calcium-Ion 6461f084ff feat(notify): turn each kind of notification on or off (#7868)
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.
2026-10-17 22:17:59 +08:00
CaIon 5c31c1fa9e feat(channels): improve priority dragging and add onboarding guides
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.
2026-10-17 21:52:39 +08:00
CaIon 5e9595190e feat(channels): add drag mode for channel priority ordering
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.
2026-10-17 20:19:41 +08:00
CaIon 5a59d69153 fix(pricing): hide task prices left at zero when another price is set
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".
2026-10-17 20:19:40 +08:00
CaIon d032204733 feat(aws): add Mantle Messages API support
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
2026-10-16 14:45:17 +08:00
CaIon 45fbb149f0 fix(rate-limit): count each group with its own limit separately
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.
2026-10-15 22:38:36 +08:00
CaIon 908f94d65d fix(web): hide pages the user cannot open from console search
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.
2026-10-15 17:57:30 +08:00
CaIon 718a4e7f97 fix(vertex): use the multi-region hostnames for us and eu
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.
2026-10-14 23:20:54 +08:00
CaIon f196934ce6 feat(plugins): bind Hailuo, Vidu and Veo plugins to New API channels
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.
2026-10-14 23:20:54 +08:00
CaIon bfdad293cd fix(vidu): bill the model, duration and resolution Vidu generates
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.
2026-10-14 23:20:54 +08:00
CaIon 150ab3cd43 chore(licenses): refresh the third-party dependency inventory
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.
2026-10-14 23:20:54 +08:00
CaIon 76cd5114ea build: upgrade moejs to v0.1.0-alpha.7
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.
2026-10-14 23:20:54 +08:00
CaIon 7de10f2c60 feat(token): let an API key read its owner's quota
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
2026-10-13 21:39:52 +08:00
CaIon ef60ef27f3 fix(relay): keep the cause of a failed body read for every later reader
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:".
2026-10-13 19:48:01 +08:00
CaIon d10eafb57e fix(relay): report the real cause when a request body cannot be read
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".
2026-10-13 19:48:01 +08:00
Calcium-Ion 633b4c5b19 feat(logs): export usage logs from the common log page (#7774)
* 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.
2026-10-12 21:33:53 +08:00
CaIon c10780e054 fix(relayconvert): keep JSON Schema constraints in tools and structured output sent to Gemini
Requests converted for Gemini had their tool parameters and structured
output schemas forced into Gemini's typed Schema, and anything it cannot
hold was dropped silently: const, oneOf, $ref and $defs lost their
meaning, non-string enum values were removed from tool parameters, and
structured output with type arrays or non-string enum values got a 400
from Gemini.

A schema the typed Schema holds is sent exactly as before. Any other
schema now goes unchanged in parametersJsonSchema or responseJsonSchema,
which Vertex AI and the Gemini API v1beta accept. When a model is set to
Gemini API v1, which has no parametersJsonSchema, the schema is cleaned
as before and a conversion warning names what was dropped.
additionalProperties and $schema alone still do not switch fields, so
Claude Code, Codex and MCP tools and OpenAI strict structured output are
sent unchanged. Responses custom tools keep their typed parameters.

Fixes #7455
Refs #7704
2026-10-11 17:16:17 +08:00
CaIon 801456a061 fix(relay): send a close frame when a Responses WebSocket session ends
When a Responses WebSocket session ended on the server side, for example
because the upstream closed or reset its connection, a request timed out
or failed, or a token was rejected, new-api dropped the client socket
without a close frame, so clients such as Codex reported a connection
reset without a closing handshake.

Every way the session ends now sends a close frame first: 1008 for a
rejected token or a disabled channel, the upstream's own 1000 or 1001
when an idle upstream closes normally, 1011 when a request is cut off or
fails, and 1000 to the upstream when the client closes. Each frame waits
at most one second and never waits for a busy writer. Completed turns
still reuse the connection, and billing is unchanged.

Refs #7642
2026-10-11 17:10:53 +08:00
CaIon 93ea55f3c3 feat(web): pick dates and times down to the second
The date-time picker (expiry times, announcements, log cleanup and the
dashboard filters) and the date range filter on the log pages stopped
at the minute: editing a time reset its seconds to zero, and confirming
a range again dropped the seconds it had. Both now show and keep
seconds, and the range filter button shows them too.

Emptying the time field, for example with Backspace on one part of it,
sent out an invalid date. The field now goes back to its current time
instead.
2026-10-11 14:19:10 +08:00
CaIon 96275d224b fix(web): show date pickers and relative times in the interface language
The date pickers looked up their calendar language under "zh", but the
interface uses zhCN and zhTW, so both Chinese interfaces showed English
month and weekday names. They now use the Simplified and Traditional
Chinese calendars.

Relative times on the passkey card, the login session list and the
announcement list were always English, so a Chinese page showed
"2 hours ago". They now follow the interface language through the
shared formatter; English announcements read "5 minutes ago" instead of
"5m ago".

Refs #7654
2026-10-11 13:57:00 +08:00
CaIon 06dd76a9d3 fix(playground): keep each account's Playground data separate
Playground settings and messages were stored under fixed browser keys,
so after switching accounts in the same browser the next user saw the
previous user's chat history.

They are now stored per user ID. Data saved under the old shared keys
has no known owner and is deleted, so existing Playground history is
cleared once.

Fixes #7713
2026-10-11 13:57:00 +08:00
CaIon 1fb6d07fee perf(web): stop loading the whole Simple Icons pack on four pages
The wallet, security and OAuth callback pages and the user binding
dialog imported a few brand icons from react-icons/si, which pulled the
whole pack (about 3,400 icons, 5 MB, 2 MB gzipped) into their routes.
They now use the brand icons already in the project, plus a new Alipay
icon, and Linux DO bindings show the Linux DO logo instead of the Linux
penguin. The wallet page now loads 535 KB of scripts instead of 5.6 MB.

Payment method icons configured by name still load their pack on
demand.

Refs #7634
2026-10-11 13:56:59 +08:00
CaIon b892a6b2e4 fix(web): reload once when a page's code fails to load
When a route's script or style chunk failed to load, for example on a
flaky connection, the console showed the error page with a large "500"
although no request had returned 500, and it stayed there until the
user reloaded by hand.

The error page now reloads the page once after a chunk load failure; if
the chunk still fails, the error page stays. It no longer shows a status
code for errors that have none. Rate-limit errors still show 429.

Fixes #7634
2026-10-11 13:56:59 +08:00
CaIon 977450a3e8 fix(rate-limit): explain model rate-limit rejections without Redis
Without Redis, the in-memory model request limiter rejected requests
with a bare 429: no error body and no log line. Clients could not tell
it from an upstream 429, and operators could not see which user hit the
limit.

It now returns the same error message and writes the same log line as
the Redis limiter, for both the total and the success limit.

Fixes #7721
2026-10-11 13:56:49 +08:00
CaIon 5ac7562840 fix(relayconvert): keep answer text that shares a stream chunk with reasoning
Some OpenAI-compatible upstreams put the last reasoning token and the
first answer text in one stream chunk. The Chat to Claude Messages
stream converter emitted only the thinking delta for such a chunk and
dropped the text, so Claude clients lost the start of the answer.

Such a chunk now emits the thinking delta, closes the thinking block,
opens a text block and emits the text delta. Usage, stop reason and the
closing events are unchanged.

Refs #7664
2026-10-11 13:56:49 +08:00
CaIon 13c8e90b8f fix(relayconvert): send Responses image URLs to Chat upstreams as objects
A Responses input_image carries image_url as a string, while Chat
Completions defines image_url as an object with a url field. The
converter forwarded the string unchanged, so Chat upstreams that follow
the documented shape, such as vLLM and strict gateways, rejected
requests with images, for example Codex screenshots and images returned
by tools.

A string image_url is now wrapped as {"url": ...}. The detail beside it
is still left out, because Chat upstreams that validate it with older
OpenAI SDKs reject newer values such as "original". Object values and
file_id parts are unchanged.

Fixes #7578
2026-10-11 13:56:49 +08:00
CaIon 036ba376da fix(relaykit): match Claude effort and Gemini 2.5 budgets to each model
RelayKit sent some Claude effort levels that the target model does not
accept. A request with an effort but no thinking reached Opus 4.5 with
max or xhigh. Sonnet 4.5, Haiku 4.5 and other models without effort
support received an effort anyway. When a model-name suffix or modifier
turned thinking off, the client's output_config.effort stayed in the
request unchecked. Opus 5 and Haiku 5.5 then got disabled thinking at
xhigh or max, and Sonnet 5.5 got between_tools at those levels. The API
rejects these combinations with a 400.

Known Claude models now get the nearest effort level they accept, and no
effort when they accept none. Effort works without thinking, so a
thinking-off request keeps the client's effort within those limits. On
Opus 5, Haiku 5.5 and Sonnet 5.5 it is capped at high. Fable, Mythos
and Mythos Preview are now marked as taking effort. These models cannot
turn thinking off, so a thinking-off request runs them at low effort,
as Opus 5.5 already does. The new reasoning.ClaudeEffort returns the
effort a model accepts for a requested level. Native requests without a
suffix or modifier still pass through unchanged, and models RelayKit
does not know keep the effort they were sent.

Claude Haiku 5.5 joins the capability table: adaptive thinking only, on
by default, all five effort levels, and medium as the default effort.
Converted requests no longer send it manual thinking with budget_tokens,
which it rejects. They also drop temperature, top_p and top_k, which it
accepts only at their defaults.

Gemini 2.5 Pro takes thinking budgets up to 32768, but xhigh and max
were rendered as 24576. They now get the model's largest budget: 32768
on 2.5 Pro, and 24576 on 2.5 Flash and Flash-Lite as before. high stays
at 24576, as in Google's OpenAI-compatible mapping.
2026-10-11 12:42:17 +08:00
CaIon 0f38a14a5e fix(web): restore browser sans-serif for Auto font 2026-10-10 20:29:04 +08:00
CaIon 1ca79d57e7 fix(pricing): show recent status samples without gaps
Fetch the latest 24 nonempty hourly samples within 7 days for the visible models, while preserving the 24-hour summary metrics. Render only available samples without gray placeholders.

Validated perf-metrics tests with SQLite 3.50.4, MySQL 5.7.44, and PostgreSQL 9.6.24; Go build; 36 frontend tests; frontend typecheck, lint, formatting, and production build.
2026-10-10 19:24:32 +08:00
CaIon c6741c36a9 feat(channel): add a per-channel switch to skip upstream TLS verification
Until now only the global TLS_INSECURE_SKIP_VERIFY setting could accept a
self-signed upstream certificate. A channel's tls_insecure_skip_verify
setting now does it for that channel's API requests: relay and the Responses
and Realtime WebSocket dialer, task submission and polling, plugin fetches,
balance, usage and model-list queries, and media proxied through the channel
proxy. Credential exchanges (Codex OAuth refresh, Vertex and plugin
service-account tokens) keep verifying certificates. The channel drawer shows
the switch under the HTTP protocol settings.
2026-10-10 14:02:06 +08:00
CaIon c8d234baa1 fix(xai): bill video edits and extensions by their output resolution
The "source" resolution tier was not an xAI price. xAI bills edits and
extensions at the output resolution, which keeps the input video's
resolution capped at 720p and is reported nowhere. xai 1.1.0 reserves 720p,
then reads the output MP4's track header with ranged utils.fetch calls to
vidgen.x.ai and settles at 480p when the shorter side is at most 480 pixels.
If the header cannot be read, the task settles at 720p.
2026-10-10 13:40:04 +08:00
CaIon ba3b2c015a refactor(jsplugin): stop requiring json-clone@1 and submit-sse-delta@1
utils.json.clone is available to every plugin, and a plugin that exports
parseSubmitEventDelta without parseSubmitEvent now gets the delta hook
without declaring anything. Both capability strings stay accepted for
plugins written for earlier hosts, and a plugin exporting both hooks keeps
the snapshot hook unless it declares submit-sse-delta@1. alibaba drops both
declarations.
2026-10-10 13:40:04 +08:00
CaIon 3f02244783 feat(web): show the request domains a plugin declares when uploading it 2026-10-10 13:39:55 +08:00
CaIon 66c3e14282 fix(hailuo): download MiniMax v1 videos through the file retrieve link
The plugin pointed content requests at /v1/files/download, which MiniMax
does not serve, so v1 video downloads returned 410. hailuo 1.2.1 looks the
file up with /v1/files/retrieve through utils.fetch and proxies the signed
download_url without the channel key, using GET for HEAD clients because the
link is signed for GET.

Fixes #7709
2026-10-10 13:39:55 +08:00
CaIon 29509776ef feat(jsplugin): let driver hooks read upstream data with utils.fetch
utils.fetch is a synchronous, read-only GET or HEAD to the channel Base URL
host or the plugin's allowedHosts, through the channel proxy used for
polling. It is available in buildSubmitRequest, the query and batch build
and parse hooks, and buildContentRequest; every other hook throws. Each hook
call gets at most 4 fetches, 15 s per fetch, 30 s in total and 1 MiB per
body. Redirects are returned, not followed. responseType "bytes" returns an
ArrayBuffer for binary data such as a media file header.

While a fetch waits, the runtime gives up its execution slot and the
watchdog pauses. BuildContentRequest now receives the request context, and
the media proxy shares the plugin request header rules.
2026-10-10 13:39:55 +08:00
CaIon b66674ac23 feat(task-plugin): let Seedance and wan3.0 pick the video duration with -1
A plugin that declares duration-auto@1 may receive a duration of -1 (the
vendor picks the length) anywhere in the request; other negative values and
values above the limit are still rejected, and usage facts stay
non-negative. A declaring plugin reserves the model's longest duration and
settles on the usage the vendor reports.

- doubao 1.3.0 declares it, forwards -1 to Ark and reserves each model's
  longest duration (12 s for Seedance 1.0 and 1.5 pro, 15 s for 2.0, 30 s for
  2.5). A JSON duration on /v1/videos now reaches Ark.
- alibaba 1.4.2 declares it, so wan3.0 smart duration also works on
  /v1/video/generations.

Fixes #7700
2026-10-10 13:39:44 +08:00
CaIon 1d4328e974 docs(relaykit): rewrite README for the current conversion API
Describe the conversion result diagnostics and loss policy, the
reasoning-state handoff and thinking suffix parsing, hosted web search
and Responses custom tool handling, the usage and cache token fields,
the Responses stream failure and sequence_number options, the JSON
codec and logging hooks, and the full option list. Plain product-page
wording.
2026-10-09 22:59:20 +08:00
Calcium-Ion 128089fa7d feat(i18n): translate web console messages in the frontend and AI-client errors in the backend (#7712)
* 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.
2026-10-09 21:39:51 +08:00
CaIon e0483122c7 fix(relayconvert): drop trailing assistant text from Responses input on Claude and Gemini
In Responses, assistant output at the end of input is history and the
model answers with a new item. Claude and Gemini continue a final
assistant turn as a prefill instead, and Claude 4.6 and later and Gemini
3.6 Flash, 3.5 Flash-Lite and later reject it with a 400. Codex sends
such input when it retries a stream that failed after an assistant
message completed: the retry is rebuilt from history, so the received
reasoning and answer end the input.

The Responses converters for Claude and Gemini now drop the assistant
turns without tool calls that follow the last user message or tool
result, and record a trailing_assistant_omitted warning. Input that ends
in a tool call still waiting for its result, or holds only assistant
turns, is sent unchanged. Chat and Messages requests keep their prefill.
2026-10-09 19:53:38 +08:00
CaIon 6370b29424 feat(jsplugin): keep JSON member order for plugins that declare json-order@1
The host decoded plugin request bodies into Go maps, so hooks saw their
members sorted and the upstream body was written sorted. TypeSafe Jev
reads the state object as written, and the reordering changed its scores
against calling TypeSafe directly.

A plugin that declares the json-order@1 capability now gets the client's
order end to end: its decoders receive the JSON body as text, its decoded
requestBody is kept as JSON.stringify text for the driver hooks, and its
request descriptors are decoded with Unmarshal so the body reaches the
upstream as that text. Other plugins keep the Go-map path unchanged, and
billing bounds still validate the decoded requestBody.

RawJSON hook arguments, top-level or nested, are now parsed in place with
ParseJSONString instead of being copied first; every caller passes bytes
it never changes afterwards.
2026-10-08 18:04:56 +08:00
CaIon 192802ee06 build: build release binaries with moejs's PGO profile
moejs ships a default.pgo recorded from plugin workloads. Go only uses a
profile found in the main package directory, so the Docker image, the
release workflow and the Electron builds now pass it with -pgo.
2026-10-08 18:04:56 +08:00
CaIon b23471112d build: upgrade moejs to v0.1.0-alpha.6
alpha.6 adds json.RawMessage arguments and targets, ParseJSONString and
a faster interpreter. new-api builds and its plugin tests pass on it
without host changes. On moejs's new-api gate the BenchmarkTaskPlugin
rows are 4.3 % faster on geomean (HookCall -13 %, Submit 8 MiB -8.6 %)
with allocations unchanged.
2026-10-08 18:04:56 +08:00
CaIon bc82357929 test(relaykit): keep format-conversion tests out of the repository
Format-conversion tests for relaykit are maintained outside the
repository. Remove the conversion tests added in 78bd5b1cb (Gemini
tool-output media and the Opus 5.5 default effort) and c6bac617d
(thinking suffix aliases), and restore the tests those commits extended
to their earlier form. Usage tests such as dto/usage_merge_test.go
stay.
2026-10-08 18:04:56 +08:00
CaIon 0cce21db02 perf(jsplugin): stop rebuilding the usage-key replacer and request body on every hook call
canonicalUsageLimit built a new strings.Replacer for every usage key it
normalized, which took about a third of the CPU and most allocations on
passthrough task submissions; the replacer is now built once. The task
adaptor also normalized the same request body again on every hook call
of a request. Hooks never write into the body, so the normalized value
is now cached per request body and reused. In the task plugin
benchmarks, passthrough submissions run 42-60% faster with 85% fewer
bytes allocated, and large-body submissions about 1.5% faster.
2026-10-08 18:04:56 +08:00
CaIon d10f11ffff feat(gemini): bill Google Search grounding per query or per grounded prompt
Gemini responses grounded with Google Search billed one google_search
call per response on every model. Google bills Gemini 3 per unique
search query ($14 per 1,000) and Gemini 2.5 and older per grounded
prompt ($35 per 1,000). A counter now collects the unique non-empty web
and image search queries across every stream frame and candidate of a
response. It records google_search as that query count and
google_search_grounded_prompt as one grounded prompt, and the tool price
table decides which of the two costs money for a model. The native
generateContent handler now counts grounding too. Google reports no
billed count in the response, so the unique query set is the best
available evidence, not a guaranteed match.

Built-in prices make queries free and grounded prompts $35 per 1,000 on
gemini-2.5*, gemini-2.0*, and gemini-1.5*; other models keep
google_search at $14 per query. Because built-in model-prefix prices
take precedence over a tool default, an operator's own google_search
price no longer applies to those older models; to keep search free
there, set google_search_grounded_prompt:gemini-2.5* (and the 2.0* and
1.5* keys) to 0.
2026-10-08 18:04:55 +08:00
CaIon 45094bdf41 fix(web): 修复 OriginOS 6 设备渠道页白屏
用户环境:vivo X300 Ultra,OriginOS 6(Android 16),Chrome 和
Via 浏览器均出现渠道页白屏,控制台无错误。

保留页面模糊过渡,将入场终点由 blur(0px) 改为 blur(0.01px),
动画结束后设置 filter: none,避免零半径 blur 在受影响设备上
触发白屏。补充页面入场和路由切换的回归测试。

验证:受影响测试 2 项通过,typecheck、相关文件 lint 和格式检查
通过;测试实例已由用户在故障设备上复测,确认页面正常。
2026-10-07 23:40:17 +08:00
Calcium-Ion acb347a197 feat(relay): web search encoding per channel and vendor-reported search billing (#7690)
* feat(channels): convert Claude and Gemini requests to OpenAI Responses on Advanced Custom channels

Advanced Custom channels could convert Claude Messages and Gemini
generateContent requests only to OpenAI Chat. Two converters,
claude_messages_to_openai_responses and
gemini_generate_content_to_openai_responses, now send them to an OpenAI
Responses upstream and convert the response back to the client's
format. The channel form offers them with the /v1/responses upstream
path and bearer auth by default, and the backend validates their
incoming paths like the existing converters. The IDs keep the values the
relay already uses for these conversions.

* feat(moonshot): forward Responses requests to Kimi unchanged

Moonshot channels answered /v1/responses with "not implemented". They
now forward the request body unchanged to {base}/v1/responses. Kimi
serves only kimi-k3 on that endpoint today; other models are rejected
by the upstream, not by the gateway.

* fix(relay): bill completed image_generation calls when a Responses request ends failed or incomplete

An image_generation call that completed before a response.failed,
response.incomplete, or cancelled terminal had already reached the
client, but the stream accumulator reset the image count to zero on
those terminals, and the non-stream handler skipped image outputs when
the response status was failed, cancelled, or incomplete. Completed
images now bill on every terminal, as they already did when the client
disconnected; image items that never completed still bill nothing.

* feat(relay): let channels encode hosted web search and bill vendor-reported search counts

Converted requests always encoded hosted web search the OpenAI way:
web_search_options on Chat and the native hosted tool on Responses,
Claude, and Gemini. Zhipu and DashScope compatible-mode Chat take their
own search fields, and the search counts that Azure, xAI, and DashScope
report in usage were ignored; only web_search_call items were billed.

relaykit gains a WebSearchEncoder option that a channel adaptor sets in
Init; without one, the default encodings stay. Zhipu sends the GLM
web_search tool with search_std, and Gemini clients on Zhipu now convert
through Chat. DashScope compatible-mode Chat sends enable_search and
search_options with the default turbo strategy and maps allowed domains
to assigned_site_list (up to 25 sites). DashScope Anthropic-compatible
requests that carry a web search tool get the cc_entrypoint system
marker DashScope requires before it searches. Zhipu and DashScope Chat
responses report no search usage, so a forced search bills one call,
which is dropped again if the request retries on another channel; a
Zhipu non-stream response that returns search results also bills one.
Several hosted web search definitions on a Chat target now drop the
extras with a diagnostic instead of failing the request.

Vendor-reported counts replace the counted web_search_call items under
the vendor's price key: Azure tool_usage.web_search.num_requests as
bing_web_search, xAI server_side_tool_usage_details (web_search_calls as
web_search, priced for grok models, plus x_search posts and profiles),
and DashScope usage.x_tools on Responses and server_tool_use on its
Anthropic endpoint as search_strategy_agent. Claude server_tool_use also
counts web_fetch, code_execution, and tool_search, which bill only once
an operator prices them. Tool names with a built-in price and these
Claude server tools are reserved, so a client function of the same name
is never billed under them. The Claude and Gemini search counts move
from gin context keys into the per-request tool counts, and every tool
count is capped at 10,000 at settlement.

Built-in prices are seeded for these keys in each vendor's list currency
(Zhipu and DashScope in CNY, Azure and xAI in USD) and listed on the
tool price settings page. DashScope Responses requests move to
/compatible-mode/v1/responses, because the old /api/v2/apps/protocols
endpoint is no longer maintained.
2026-10-07 23:29:24 +08:00
CaIon 0a03088e0e fix(relayconvert): fix lossy conversions and converted-stream failure billing
A review of the cross-protocol conversions found requests that the
upstream rejected or misread after conversion. Gemini function schemas
in its OpenAPI subset (uppercase types, nullable, propertyOrdering)
reached OpenAI, Claude, and Responses targets unchanged; they are now
rewritten as JSON Schema, and keywords without an equivalent are dropped
with a diagnostic. Media is mapped by MIME type instead of being sent as
an image: Gemini inline and file data, Claude image and document
sources, Responses input_file, and Chat file parts reach each target as
the image, PDF, text, or audio part it accepts. Media a target cannot
carry is omitted with an unsupported_media_type diagnostic instead of
reaching the upstream in a shape it rejects. Chat image_url objects no
longer carry a stray MimeType key, and Responses tool outputs that hold
content parts become Claude tool_result blocks instead of a JSON string
with base64 inside.

Codex declares tools in namespaces and in additional_tools input items.
They are now flattened to functions named <namespace>__<name> (tools in
the default functions namespace keep their names), and calls get their
namespace back on the Responses side; only names that collide are
dropped. When no tool reaches the target, tool_choice and
parallel_tool_calls are omitted. A forced tool_choice is sent as auto
when manual extended thinking is enabled on Claude, which rejects that
combination. A disable intent renders as adaptive thinking at low effort
on Claude Opus 5.5, which cannot turn thinking off, and as between_tools
on Claude Sonnet 5.5.

Converted streams (Chat via Responses and Responses via Chat) now follow
the native Responses billing rule. A client that disconnects mid-stream
settles the output it received instead of failing the request. An
upstream failure before any output reached the client is not charged;
after output it bills the upstream usage, or the estimate of the
delivered text, reasoning, and tool arguments. Stopping a stream with a
typed nil error no longer panics.
2026-10-07 22:56:12 +08:00
CaIon 0a532f7f7d feat(plugins): add an xAI Grok Imagine video plugin
xAI-type channels could only serve chat. The new built-in xai plugin
serves grok-imagine-video, grok-imagine-video-1.5, and
grok-imagine-video-1.5-lite through the native /xai/v1/videos routes
(generations, edits, extensions, and status), /v1/videos, and
/v1/responses, on xAI channels and on New API channels that bind it.

Usage bills output seconds by model and resolution, input images, and
the input video seconds of edits and extensions on grok-imagine-video.
Edits and extensions keep the input video's resolution, which xAI does
not report, so they bill under a separate "same as input video"
resolution. xAI still charges for a video its moderation filters after
generation and for a failed request that reports a cost, so those tasks
settle as charged with the moderated fact set instead of being refunded.
The native status route presents such a failure as a moderated result,
so a chained New API gateway charges it too. Other failures are refunded
as before.
2026-10-07 22:55:39 +08:00
CaIon bb28f2ccad feat(tokenkit): move token counting into a module calibrated on agent traffic
Token estimates badly undercounted agent requests. Claude tool
definitions, Responses function calls and their outputs, Chat assistant
tool calls, and Gemini system instructions, function calls, function
responses, and tools never reached the counter; Chat tool schemas were
counted as Go map syntax and Claude tool results as escaped JSON.
Measured against upstream usage on 54 real Claude Code and Codex
conversation windows, native prompt estimates were 31-93% low, and the
Claude and Gemini completion fallbacks about 90% low because streamed
tool calls were not counted.

All of these now reach the counter. The completion fallbacks count tool
names and arguments, plus 40 tokens per Claude tool_use block. The
estimator weights are refitted per tokenizer: Claude before Opus 4.7,
Claude 4.7 and later (about 30% more tokens for the same text), GPT, and
Gemini. Prompt estimates now land within 1% bias and 3-4% mean error for
each family.

Token counting moves into tokenkit, an independent module like
relaykit, so other gateways can use it. It also replaces the flat
520-token image estimate with each provider's sizing rules, checked
against upstream usage on sizes outside the test table: OpenAI patch and
tile rules per model and detail level (gpt-5.5 and later default to
original detail, and their high detail is billed as measured, not as
documented), Claude's two resolution tiers, and Gemini 3 media
resolution and Gemini 2.x tiles. Chat image_url parts without detail
no longer default to high in ParseContent, so they estimate as the
upstream default auto; no converter sent that value upstream.

tiktoken-go moves to v0.8.1, about twice as fast on ordinary text. It
requires Go 1.26, so the root module and tokenkit move to Go 1.26;
relaykit stays on 1.25.1. In every version BPE cost grows with the
square of a pre-tokenized piece, so 100,000 Chinese characters without
punctuation took 21.6 s to count during pre-consume. Count now cuts
single-class runs longer than 1024 bytes, which leaves ordinary text
unchanged and brings that input down to 106 ms. The encoder cache no
longer grows with client-supplied model names.
2026-10-07 21:26:35 +08:00
CaIon 0908d9e7b9 fix(epay): upgrade go-epay to v0.0.5 2026-10-07 21:26:35 +08:00
CaIon c6bac617d4 fix(relaykit): make the thinking suffix adapter toggle cover effort suffixes
With the Claude or Gemini thinking suffix adapter switched off, the parser
still stripped effort tails such as -high from claude-* and gemini-* model
names and injected thinking parameters upstream; only the -thinking family
honoured the toggle. Gate every alias on the same flag so a disabled adapter
sends the model name verbatim, rename the parameter to allowSuffixAlias,
correct the ThinkingAdapterEnabled doc comments, and spell out the covered
suffixes and the off behaviour in both settings cards.
2026-10-07 21:26:34 +08:00
CaIon 956019109b feat(web): filter the user list by group
The users table already sent a group parameter to /api/user/search, but
the toolbar had no control for it. Add a single-select User Group filter
next to Status and Role, with options from /api/group/. The group search
parameter is now an array, like status and role.

The options are built on every render: DataTableFacetedFilter is
memoized and reads the selection from the column, so a memoized options
array would keep the chip from showing the selected group.
2026-10-07 21:26:34 +08:00
CaIon 8ca26d81df fix(web): stop frontend build files from exhausting the web rate limit
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.
2026-10-07 21:26:34 +08:00
Calcium-Ion 78bd5b1cbf fix(relaykit): Gemini stream token details, Responses tool-output media on Gemini, Opus 5.5 default effort (#7687)
* fix(relaykit): stop summing cumulative Gemini stream token details

Gemini usageMetadata on every stream frame is a cumulative snapshot, but
mergeGeminiTokenDetails added each frame's per-modality counts to the
previous ones. Prompt, tool-use prompt and candidates details were
multiplied by the number of frames carrying usage, and converted streams
multiplied them again because the same sidecar is remembered at several
pipeline steps. A streamed image prompt was billed as 3 x 1089 image
tokens.

A modality reported by the incoming frame now replaces all earlier
entries for it, matching how the scalar counts are merged; modalities
the frame omits keep their earlier value, and duplicate entries inside
one snapshot are still summed at settlement.

* fix(relayconvert): send Responses tool-output media to Gemini as inline data

A function_call_output or custom_tool_call_output holding a Responses
content-part array was stringified into functionResponse.response, so
Gemini tokenized the base64 media as text (an image tool output cost
24,366 prompt tokens and the model could not see it).

Text parts now stay in response and images and documents become
inlineData resolved through the SSRF-protected media resolver. Gemini 3
documents multimodal function responses, so the media go into
functionResponse.parts there and for any model id not known to be older.
Gemini 1.x and 2.x get them as ordinary inlineData parts after every
functionResponse of the same turn, which keeps parallel calls grouped.
Media a target cannot carry (audio or video inside
functionResponse.parts, unsupported MIME types, failed loads) is dropped
with an unsupported_media_type diagnostic for the tool-loss policy.

The inline-data MIME allowlist now follows the Blob.mimeType list in the
Gemini API reference, adding image/gif, image/avif, audio/*, video/* and
the listed text and application types, so ordinary Chat and Responses
messages with those files are no longer rejected locally.

* fix(relayconvert): use medium as the Opus 5.5 default effort

Claude Opus 5.5 runs at medium effort when a request sets none; every
other model, including Opus 5, Sonnet 5.5 and Fable 5.1, runs at high
(platform.claude.com/docs/en/build-with-claude/effort). The converter
assumed high everywhere, so Opus 5.5 requests without an effort were
logged as high, and a Claude client on claude-opus-5-5 routed to an
OpenAI upstream sent reasoning effort high instead of the model's
default.

Claude capabilities now carry a per-model default effort, used when the
client gave none: the no-intent and include_thoughts-only paths,
adaptive or enabled thinking without a strength, the native passthrough
label, and ResolveClaudeDefault for Claude-to-OpenAI conversions.
Explicit efforts and budget mappings are unchanged. The value feeds the
consume-log reasoning_effort field, OpenAI parameter filtering and the
RelayInfo debug string; billing does not read it.
2026-10-07 15:45:08 +08:00
CaIon b48b74ab77 feat(jsplugin): decode hook results with moejs ToGoInto
Update moejs to v0.1.0-alpha.5. Engine.CallInto and Engine.CallPathInto
decode a hook result straight into a Go value through ToGoInto, which
gives the same value and error as encoding the result and decoding it
again with the host codec, without that round trip for plain results.
A result the codec cannot encode or the target rejects is a ResultError
carrying the codec's message, and the runtime stays reusable.

The request-building hooks (buildSubmitRequest, buildQueryRequest,
buildBatchQueryRequest, buildContentRequest), parseBatchResult and the
openai_video renderer use them, replacing decodeRequestDescriptor.
parseSubmitResponse and parseTaskResult still read the raw result,
because they check exact keys that a case-insensitive struct decode
would not.
2026-10-05 22:01:40 +08:00
Calcium-IonandKabuda 1a4166d8e8 fix(relayconvert): keep Responses custom tools on Claude and Gemini upstreams (#7636)
* fix(relayconvert): drop unnamed function calls from Responses output

A Chat Completions tool call without a function name cannot become a
valid Responses function_call item, so the non-stream conversion now
skips it instead of emitting an item with an empty name.

* fix(relayconvert): hold streamed tool calls until a name arrives

A streamed tool call is no longer announced while it has no function
name, whether or not custom tools are in play. A call whose name never
arrives is left out of the stream and the completed response. A call
ID that arrives after the first fragment becomes the item ID as long
as the item has not been announced yet.

* test(relayconvert): fold unnamed stream tool cases into the hold test

The two standalone stream tests repeated what the nameless-tool hold
test already covers. Their distinct checks now live in its subtests:
a held function call without custom tools, its late call ID becoming
the item ID, and calls whose name never arrives leaving no event,
output index or sequence number behind, with and without custom tools.

* fix(relayconvert): keep Responses custom tools on Claude and Gemini upstreams

Responses requests sent to Claude Messages dropped Codex custom tools
such as exec and apply_patch as unsupported hosted tools, and Gemini
omitted their definitions and stripped custom_tool_call history before
conversion. A forced custom tool_choice was rejected on both targets,
and Claude parsed JSON-looking custom tool input from history.

Claude and Gemini now get the same treatment as Chat Completions: each
custom tool is sent as a function that takes one string input, with
the grammar kept in the description, and a forced custom tool_choice
becomes the matching named tool choice when that tool was sent. Gemini
declarations go through the usual parameter cleanup.

custom_tool_call history replays the raw input as {"input": ...} on
both targets, and its outputs pair with the call as tool results. The
custom tool selection is shared by all three targets and recorded for
each of them, so the existing Chat to Responses step restores
custom_tool_call items from Claude and Gemini responses as well.

* test(relayconvert): cover custom tool restore from Claude and Gemini responses

Claude and Gemini responses reach the Responses format through the
Chat to Responses step, but only the Chat source had a restore test.
The test is now table-driven over all three sources and checks the
non-stream output item and the exact streamed custom tool events.

* test(gemini): assert custom tools survive the Responses to Gemini adaptor path

---------

Co-authored-by: Kabuda <[email protected]>
2026-10-01 19:50:53 +08:00
CaIon 2035a82aeb feat(web): add a refresh button to the channels table
A Refresh button next to the sensitive-data toggle reloads the channel
list with the current search, filters, sorting and page. It is marked
busy and its icon spins while the list is loading.
2026-09-30 20:52:57 +08:00
CaIon ae73ef8e25 feat(plugins): add Seedream 5.0 flash and price each Seedream model by its own tiers
The doubao plugin now serves doubao-seedream-5-0-flash-260915 with 1K,
1.5K and 2K sizes, layer decomposition, output_format and the same
background rules as 5.0 pro.

Seedream models shared one usage schema, so the pricing matrix offered
an up-to-1.5K price and a layer decomposition price for every model.
Each model now gets a profile from its capability table: Seedream 5.0,
5.0 lite and 4.5 start at 2K and show a single image price, and layer
decomposition is listed only for 5.0 pro and flash. Seedance profiles
are built by the same helper. On completion a single-tier model counts
every delivered image at the upper tier, even when the result has no
parseable size.

Usage facts keep all four released image keys, and every Seedance model
still reports video_input, so expressions saved before the per-model
profiles charge as before.
2026-09-30 20:52:51 +08:00
CaIon 4924361aef feat(auth): edit the name and permissions of an access token
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.
2026-09-30 20:52:36 +08:00
CaIon 2506e1b980 fix(auth): reject non-standard roles when creating a user
CreateUser accepted any integer role below the operator's own, so an
admin could create a user with role 5 or -1, and a root user's request
for role 99 went on to step-up verification before failing. Only the
guest, common, admin and root roles are accepted now; any other value
fails with an invalid-parameter error before verification and nothing
is written.
2026-09-30 20:52:10 +08:00
CaIon c71eefbcd1 docs: update readme 2026-09-30 17:24:20 +08:00
CaIon 2ffc595901 feat(jsplugin): run task plugins on moejs
Task plugins now run on github.com/Calcium-Ion/moejs v0.1.0-alpha.3
instead of Sobek, and the host utils are ported to it. The plugin API is
unchanged. Code compiled from strings is no longer allowed: eval and the
Function constructors throw an EvalError.

Hooks receive host values without defensive copies. The engine converts
an object or array when the hook first reads it and never writes
JavaScript changes back to the host value. Stored protocol state and task
views are passed as raw JSON and parsed inside the runtime instead of
being decoded into Go maps first. An idle runtime drops its references to
the last call's arguments.

Whether an optional hook is exported is decided once when the plugin
loads, so checking for it no longer takes an execution slot.

Usage validation returns the normalized facts in a new map instead of
writing into the hook result, so a hook that returns the request body no
longer changes it. The request descriptor is copied instead of being
round-tripped through JSON.
2026-09-30 17:24:19 +08:00
CaIon 335ebcff71 fix(relayconvert): keep Responses custom tools on Chat Completions upstreams
Responses to Chat conversion kept only function tools, so Codex custom
tools such as exec and apply_patch were dropped while the request still
succeeded. The model then called the remaining tools with made-up
arguments and Codex failed with "exec cell noop not found".

Custom tools are now sent as Chat functions that take a single string
input, with the grammar kept in the description. A forced custom
tool_choice becomes the matching function choice, and custom_tool_call
history items round-trip as function calls and tool messages.

On the way back, function calls to those tools are restored as
custom_tool_call output items, and streams emit the
custom_tool_call_input delta and done events.
2026-09-30 17:24:19 +08:00
CaIon caca52f8de feat(auth): replace the dashboard access token with scoped access tokens
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.
2026-09-30 17:24:19 +08:00
CaIon 6cbb1c7edb feat(auth): require step-up verification for admin user management
Deleting a user, changing their status or role, setting a password or
admin permissions, creating an admin, resetting a passkey, disabling 2FA
and clearing a binding now need a single-use verification proof. The
proof context names the managed user (and the action or binding where
relevant), so it cannot be reused for another user or operation. Quota
adjustments and edits to display name, group or remark stay unchanged.

The admin.user.* scopes are issued only to admins. An admin with 2FA or
a passkey must use one of them; otherwise password or a bound OAuth
account is accepted. The users page asks for verification before sending
each gated request.
2026-09-30 17:24:19 +08:00
CaIon 3509ae7c19 fix(pricing): keep prices saved before a usage profile narrows editable
An unchanged model expression or provider override of a single-plugin
model stays saveable after the plugin drops a fact from its profile, so
a full pricing save is no longer blocked by existing prices. Shared
models still follow each provider's profile.

The task pricing table reads a dropped fact at the value a plugin keeps
reporting: price terms on dropped counts are omitted, tiers requiring a
dropped boolean to be true are skipped, and other conditions on dropped
facts are removed with the first matching tier kept. Existing prices
open in the table with only the remaining fields.
2026-09-30 17:24:19 +08:00
CaIon 87bb71e7fc feat(plugins): serve Seedream images through the OpenAI Images protocol in doubao
Claim openai_image for the Seedream image models so /v1/images/generations and
/v1/images/edits run through the task plugin instead of the HTTP image relay,
which has no usage facts for u() expressions.

Requests map onto the same Ark body as /doubao/api/v3/images/generations, so
validation and usage facts are identical: images_up_to_1_5k, images_above_1_5k,
input_images and layer_decomposition. n only accepts 1 (group generation covers
multiple images), stream and mask are rejected, response_format is left to the
host, and multipart uploads are inlined as data:image Base64 URLs.
2026-09-30 17:24:19 +08:00
CaIon f5e54ccdb2 fix(web): revalidate marketplace fetches so stale caches do not fail integrity checks
Marketplace hosts serve versioned plugin sources as immutable. When a file
is republished in place, a browser that cached the earlier bytes keeps
reading them, while the index is fetched fresh and declares the new hash,
so the install dialog reports a sha256 mismatch for a correct source.

Fetch the plugin source, the sidecar icon and the index with
cache: 'no-cache' so each request revalidates with the host (a 304 when
unchanged).
2026-09-30 17:24:18 +08:00
CaIonandByebyDoggy 0aec08fee8 chore: credit #7444 author for the Claude tool_result fix landed in #7512
Co-Authored-By: ByebyDoggy <[email protected]>
2026-09-21 23:02:57 +08:00
CaIon b6809a52df fix(web): resolve safe constants in plugin metadata previews 2026-09-21 23:02:37 +08:00
Calcium-IonandMOON-DREAM-STARS 4eb3b91605 fix(relayconvert): hoist Claude tool_result images and segment Responses reasoning items (#7512)
Claude Messages -> Chat: a tool_result whose content is a block array was
stringified into the Chat tool message, so image base64 travelled upstream
as text instead of vision input. Text blocks now stay on the tool message
(joined with newlines) and image blocks become image_url parts on the user
message the same Claude message produces, which lands after the tool batch
and keeps parallel tool messages contiguous. Image-only results carry an
"[image]" placeholder; arrays with other block types keep the stringified
form.

Chat stream -> Responses stream: reasoning items now emit
response.reasoning_summary_part.added and .done around the summary text,
and deltas that arrive after a mid-stream finish_reason open a new
reasoning or message item instead of appending to the closed one. Closed
items are snapshotted so the completed response lists every segment.

Closes #7407
Closes #7495

Co-Authored-By: MOON-DREAM-STARS <[email protected]>
2026-09-21 22:50:27 +08:00
Calcium-IonandMOON-DREAM-STARS 6e9de44a7d fix(relayconvert): hoist Responses tool-output media into a user message (#7510)
A function_call_output whose output is a Responses content-part array was
stringified into the Chat tool message, so image data URLs travelled
upstream as base64 text instead of vision input.

Text parts now stay on the tool message (joined with newlines) and media
parts are mapped like regular content parts, then emitted as one user
message once the contiguous tool batch ends so parallel tool outputs stay
adjacent to their assistant tool_calls message. Media-only outputs carry
an "[image]"-style placeholder because upstreams reject empty tool
content; payloads that are not content-part arrays keep the stringified
form.

Closes #7498

Co-Authored-By: MOON-DREAM-STARS <[email protected]>
2026-09-21 22:11:21 +08:00
CaIon 54eee488be fix(web): isolate theme preferences in local storage 2026-09-21 21:01:16 +08:00
CaIon d61d6be759 fix(task): accept any 2xx upstream status on task submission
The submit relay treated every status other than 200 as an upstream
failure, so a task API that answers 201 Created or 202 Accepted (for
example Tencent's wand-vega-video-1.0-lite) returned fail_to_fetch_task
and parseSubmitResponse never ran. The check predates the plugin system,
when every built-in adaptor answered 200; the plugin adaptor already
passes the exact statusCode to parseSubmitResponse and polling has
always classified 2xx as a successful response.

Accept any 2xx before parsing and keep failing other statuses with the
upstream status and body. Add a relay regression that drives a plugin
submission against a real upstream returning 200/201/202 and 400/502,
and document the submit-side 2xx contract in the plugin API.

Fixes #7478
2026-09-21 21:01:16 +08:00
CaIon 2c175190c4 perf(plugins): sync task plugins from hashes and load source only for changed rows
Every node polls the active override set every 30 seconds, and the
snapshot query selected whole rows, so each poll pulled every plugin's
full source and icon from the database only to compare source_hash and
discard them. With sources allowed up to 8 MiB that is real traffic.

Skip the source and icon columns in the sync snapshot and fetch source by
row id only for plugins whose hash changed. A row never changes source in
place, so the id from the snapshot identifies exactly the text its hash
describes. A failed source load is handled like a compile failure: the
running plugin stays and the next poll retries.
2026-09-21 21:01:16 +08:00
CaIon c0cff23a3a fix(plugins): store task plugin source as longtext on MySQL and raise the upload cap to 8 MiB
A bare TEXT column holds 64 KiB on MySQL, and the shipped alibaba plugin
is already 73 KB, so uploading an override of it failed with "Data too
long for column 'source'" or was silently truncated. The 1 MiB upload
cap also blocks the larger plugins planned next.

Add model.LongText, a string type that picks longtext on MySQL and text
on PostgreSQL and SQLite through GormDBDataType, and use it for source
and icon. Neither type carries a length, so AutoMigrate leaves an
up-to-date column alone and widens the shipped MySQL text column once
without data loss. Raise the source cap to 8 MiB on the backend and in
the frontend picker, importers and locale strings.

Verified fresh and rc.39-upgrade startups twice each on SQLite, MySQL
5.7.44 and 8.4.11, PostgreSQL 9.6.24 and 17.11; the second start issues
no DDL and a 73 KB source round-trips on every engine.
2026-09-21 21:01:16 +08:00
CaIon e537dc380b fix(web): expose plugin activation after upload 2026-09-21 21:01:15 +08:00
CaIon 00e8a00cb4 fix(web): include provider icons in Windows builds 2026-09-21 21:01:15 +08:00
CaIon 9a0be8750a fix(web): keep task pricing editable after a plugin narrows its enum values
When a task plugin stops declaring an enum value (Seedance 2.0 mini now
offers 480p and 720p only), an expression saved with 1080p and 4k
branches no longer opened in the visual price table: the parser rejected
the whole tier chain on the first undeclared value, the editor fell back
to expression mode with a "cannot be represented" notice, and switching
to the visual editor reset every price to zero.

Treat a branch on an undeclared enum value as unreachable and skip it,
so the remaining tiers stay editable with their prices preserved. Nested
conditions in the pricing display treat such a comparison as never
matching. Editing a price regenerates the expression without the
unreachable branches; opening the editor alone leaves the stored
expression unchanged.
2026-09-20 23:27:57 +08:00
CaIon 47713bcb17 feat: update plugins 2026-09-20 23:23:48 +08:00
CaIon 9978ee1e25 feat(billing): configure trust threshold and input pre-consume multiplier 2026-09-20 22:17:20 +08:00
CaIon 65d3a2171f fix(plugins): price only the resolutions each Seedance and Wan model offers
The doubao plugin shared one usage schema across every Seedance model, so
the pricing matrix listed 1080p and 4k rows for Seedance 2.0 mini and fast
and a 4k row for Seedance 2.5, none of which Ark offers, and priced 1.0
and 1.5 pro by a reference-video dimension they do not accept. Describe
each video model with its resolutions, reference-video and audio
capabilities from the Ark model list and derive the usage schema,
examples and profiles from that table; reject unsupported resolutions at
submission, reserve the model's highest tier when the resolution is left
to Ark and ignore an out-of-profile resolution on completion. Seedance
1.5 pro gains the generate_audio fact its audio and silent prices need.

The alibaba plugin likewise derives one Wan video profile per resolution
set from the model capability table instead of listing 480P/720P/1080P
for every model, and adds the audio fact for wan2.6-i2v-flash.
2026-09-20 22:17:20 +08:00
CaIon 2fddb863f2 feat(models): add bulk field selection to metadata sync 2026-09-20 19:27:37 +08:00
CaIon a5bf940220 fix(web): correct dialog viewport fallbacks and editor gutter alignment 2026-09-20 18:24:39 +08:00
CaIon 03563a4a79 feat(plugins): serve OpenAI Images API through task plugins
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
2026-09-20 17:58:45 +08:00
CaIon c49972441e fix(web): keep mobile dialog actions within visible viewport 2026-09-20 17:51:53 +08:00
CaIon 585c4610e5 feat(keys): show and copy configured API addresses 2026-09-20 17:51:53 +08:00
CaIon d904b97000 feat(settings): improve group configuration UX and drag sorting 2026-09-20 17:51:53 +08:00
CaIon da2540dda1 fix(channels): align card metrics and remove badge spacing 2026-09-20 17:51:53 +08:00
CaIon 3fdf9083dc fix(settings): move documentation link to site settings 2026-09-20 17:51:52 +08:00
CaIon 58a9eff4c8 style: refresh logo 2026-09-20 17:51:52 +08:00
CaIon db536ec8bc fix(channels): simplify model redirects and clarify panel controls 2026-09-20 17:51:52 +08:00
CaIon 6b638788c6 fix(channels): preview upstream model changes before applying 2026-09-20 17:51:52 +08:00