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.
* 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".
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
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 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.
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.
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.
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
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
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.
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
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
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
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
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
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
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
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.
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.
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.
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.
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.
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
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.
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
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.
* 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.
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.
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.
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.
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.
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.
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.
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.
* 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.
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.
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.
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.
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.
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.
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.
* 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.
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.
* 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]>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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#7407Closes#7495
Co-Authored-By: MOON-DREAM-STARS <[email protected]>
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]>
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
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.
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.
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.
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.
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