Composite model routes support a match_type: prefix mode with an optional
upstream_model. Leaving upstream_model empty is meant to pass the concrete
requested model through to the upstream, but every request matching the prefix
collapsed to the route's public_model instead. A route with
public_model=deepseek-v4, match_type=prefix and an empty upstream_model
forwarded both deepseek-v4-flash and deepseek-v4-pro upstream as deepseek-v4.
Root cause: normalizeCompositeRouteInput backfilled an empty upstream_model with
public_model at save time, so CompositeRouteResolver.Resolve never reached its
"upstreamModel == '' -> concrete requested model" fallback.
- Skip the backfill for prefix routes only; an empty upstream_model stays empty
and Resolve passes the concrete requested model through.
- exact routes keep the backfill (model == public_model, so persisted/display
contract is unchanged).
- Explicit upstream_model still forwards that fixed model in every mode.
- Frontend: add an upstream_model hint (zh/en) documenting empty = passthrough.
Avoid recording the generic account-model transient cooldown for pool-mode statuses explicitly configured for same-account retry. This lets the bounded request-local retry budget complete while preserving cooldowns for other statuses and non-pool accounts.
Composite public aliases (e.g. all/claude) reach the Anthropic/Gemini
billing core via OriginalModel/ChannelMappedModel source overrides.
Unknown aliases resolved to no pricing and silently recorded $0 cost,
while family-word aliases were mispriced by the fallback family match
(Opus traffic billed at the Sonnet fallback rate). The OpenAI path
already guards this via usageBillingModelCandidates; the shared
recordUsageCore had neither the guard nor a fallback.
- composite groups: unless the admin explicitly configured channel
pricing for the alias (OpenRouter-style custom pricing), bill by the
concrete forwarded model
- general safety net: when the selected billing model has no resolvable
pricing at all, fall back to the concrete forwarded model instead of
silently recording $0
- grok media usage records now attribute OriginalModel to the client
requested public alias, consistent with every other endpoint
(billing unaffected: empty BillingModelSource never triggers source
overrides)
Priced traffic and non-composite groups are unaffected.
The composite-groups PR (#3581) replaced the raw ent SubscriptionPlan
response of GET /admin/payment/plans with a projection struct but
dropped the currency field added by #4323. PlanEditDialog then read an
undefined currency, sent an empty string on save, and silently wiped
the stored plan currency. Restore currency plus created_at/updated_at
so the projection preserves the full original response shape.