Fresh Docker installs created the admin as admin@sub2api.local and accepted
any ADMIN_PASSWORD (e.g. "123456"), making them easy brute-force targets.
- Generate a random admin-<12 hex>@sub2api.local login email when
ADMIN_EMAIL is empty; drop the fixed default from .env.example and the
compose files, and have docker-deploy.sh write a random ADMIN_EMAIL.
- Validate user-supplied ADMIN_PASSWORD (8-72 bytes) and ADMIN_EMAIL on the
auto-setup path, only when an admin is actually about to be created, so
existing deployments are never blocked at startup.
- Align validateEmail with the login endpoint's email binding so setup can no
longer create an admin that can never log in (e.g. "a@b", "Name <a@b.com>").
- Cap validatePassword at bcrypt's 72-byte limit; the CLI reuses it and now
defaults to a random admin email instead of admin@example.com.
- Update READMEs/deploy docs to stop suggesting guessable credentials.
- Route qwen3.8-max to Chat Completions on OpenCode Zen mode instead of Messages
- Fast-reject unsupported model families (gemini-*, jev-*) with HTTP 400 on OpenCode gateway to avoid upstream 404 failovers and strike accumulation
- Sync frontend DEFAULT_OPENCODE_ZEN_PROTOCOL_RULES with backend
An order-creation signature (visible to the payer in popup submit.php
URLs) could be replayed as a payment-success notification, with no
merchant key required. Two compounding defects:
- CanonicalizeReturnURL kept client-supplied query parameters, so a
return_url ending in '&trade_status=TRADE_SUCCESS' entered the signed
order string verbatim;
- the sign base string concatenates values unescaped, so a callback that
decodes return_url only up to the smuggled pair re-sorts to a
byte-identical sign base and passes verification.
Fixes:
- CanonicalizeReturnURL now drops client query parameters entirely; the
server builds its own signed set and the client query has no
legitimate use;
- VerifyNotification rejects any parameter outside the genuine async
notify set (pid/trade_no/out_trade_no/type/name/money/trade_status/
param/sign/sign_type), failing closed against the whole smuggling
class regardless of the injection point.
Regression tests cover the PoC payload, full order-URL replay, genuine
callback acceptance, unknown-parameter rejection (including empty
values), and the return_url smuggling strip. Genuinely paid orders that
an exotic upstream variant might reject are still recovered through the
existing upstream QueryOrder reconcile path.
The zhipu adapter always appended the official /api/paas/v4/chat/completions
path, so probes broke whenever the endpoint was not the official root:
- OpenAI-compatible relay (e.g. https://relay.example.com): requested
/api/paas/v4/chat/completions, which relays do not serve -> 404
- Coding Plan base (https://open.bigmodel.cn/api/coding/paas/v4): produced
.../coding/paas/v4/api/paas/v4/chat/completions -> 404
Resolve the path from the endpoint instead: append /chat/completions when it
already carries /paas/v4, keep the official path for bigmodel.cn / z.ai
roots, and fall back to /v1/chat/completions (same as kimi/deepseek/minimax)
for everything else.
Fixes#7067
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AnthropicContentBlock / antigravity ClaudeContentItem serialized `signature`
with omitempty, so thinking blocks without an upstream signature (Chat
Completions reasoning_content, Responses reasoning without encrypted_content,
every streamed thinking content_block_start) went out with no `signature` key.
Anthropic always sends the key (`""` on content_block_start). Strict clients
such as Grok Build deserialize it as a required field and abort the response
or SSE stream with "missing field `signature`".
- apicompat: thinking MarshalJSON always emits `thinking` and `signature`
- antigravity: ClaudeContentItem MarshalJSON does the same; streamed thinking
content_block_start carries `signature: ""`
- tests: wire-shape regression tests; update TestStreamingReasoning expectation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Anthropic processes cache breakpoints in tools, system, messages order and
rejects a ttl=1h breakpoint after a 5m one. The OAuth mimic path adds 5m
breakpoints to the injected system block and the last tool, so a third-party
client that marks a single message block with ttl=1h received a 400.
After the block limit, raise ephemeral breakpoints that precede the last 1h
breakpoint to 1h. Valid requests are unchanged.
The embedded frontend middleware runs before the gateway routes and only
let a few root aliases through. Bare aliases such as /chat/completions,
/embeddings, /messages/count_tokens and /models/:model were answered with
index.html and HTTP 200 instead of reaching the API handlers.
Add every root alias registered in routes/gateway.go to the bypass list.
This also covers POST /videos, which the /videos/ prefix did not match.
Fixes#7670
When an API key with quota or rate limits was deleted while a request was
still in flight, the key counter update matched no rows and returned
ErrAPIKeyNotFound, rolling back the whole billing transaction including the
balance/subscription charge. Skip the key-scoped counters in that case and
keep settling the user and account side as usual.
The create form shows the upstream billing auto-probe toggle for every
API-key platform and enables it by default. TypeSafe was added as an
API-key platform but not to IsUpstreamBillingProbeIdentity, so creating a
TypeSafe account with the default form state fails with
400 UPSTREAM_BILLING_PROBE_ACCOUNT_INVALID ("account is not an API key
account").
TypeSafe accounts store credentials.api_key/base_url like every other
API-key platform, which is all the probe reads. Add TypeSafe to the probe
identity set, and add typesafe.ai to the official API domains so accounts
on the default https://api.typesafe.ai base URL record "unsupported"
without sending the key to a path that cannot exist, matching the other
official providers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
intersectUpstreamModelMetadata falls back to null for every Codex tool
capability field whose value differs across peer accounts. Since
e6d191a83 added service_tiers to codexToolCapabilityFields, a model served
by accounts with different (or partially declared) tier lists ends up with
"service_tiers": null in the generated Codex manifest. Codex decodes
service_tiers as a required array and rejects the whole manifest, so the
client loses model metadata for every model.
Fall back to [] for conflicting service_tiers (do not advertise any tier
that is not shared by all peers), and normalize an explicit null to []
when applying capabilities.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WS v2 passthrough relay only read output_tokens_details.image_tokens,
so image generation over WebSocket billed image input tokens at the text
input rate and ignored the hosted response.tool_usage.image_gen breakdown
that the HTTP Responses path already merges (extractOpenAIUsageFromJSONBytes
+ mergeHostedImageGenToolUsage).
The relay now reads image input tokens from input_tokens_details or
prompt_tokens_details, fills missing image input/output counters from
response.tool_usage.image_gen (usage values keep precedence), and carries
ImageInputTokens per turn and in the session aggregate into the
OpenAIForwardResult used for billing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Responses refusal arrives as a {"type":"refusal","refusal":...} content
part (streamed as response.refusal.delta), not as output_text. The
Responses → Chat Completions converters only read output_text, and
ResponsesContentPart had no refusal field, so Chat clients received an
empty assistant message when the model refused.
Map refusal parts to choices[0].message.refusal, response.refusal.delta
to delta.refusal, and let BufferedResponseAccumulator keep refusal deltas
so non-streaming replies rebuilt from deltas keep them too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The buffered Anthropic aggregators index finalResp.Content with the
upstream content_block_delta index after checking only idx < len. A
malformed or third-party Anthropic-compatible upstream that sends a
negative index makes the request goroutine panic, so the client gets no
response and the forward never returns a result to record usage.
Add the lower bound in all four buffered aggregators (Chat Completions
and Responses, on both the Anthropic gateway and the OpenAI native
Anthropic paths); the out-of-range delta is skipped like an index past
the end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>