Skip to content

sync: merge QuantumNous/new-api main (gpt-6-astra billing + relay) - #9

Merged
3metaJun merged 100 commits into
mainfrom
sync/upstream-main-2026-09-05
Sep 5, 2026
Merged

sync: merge QuantumNous/new-api main (gpt-6-astra billing + relay)#9
3metaJun merged 100 commits into
mainfrom
sync/upstream-main-2026-09-05

Conversation

@3metaJun

@3metaJun 3metaJun commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Merge entire upstream QuantumNous/new-api main (~99 commits) into this fork via git merge (not rebase).
  • Primary reason: pick up gpt-6-astra built-in expression pricing / billing and the broader relay stack updates.
  • Preserves fork-only CI: .github/workflows/metaproxy-image.yml (unique tip 6d158f4).

Risk

  • Large sync: fork was 1 commit ahead / 99 behind upstream tip eb99ab1.
  • Broad surface: billing, relay channels, workflows, web/task-plugins, docs.

Notes / follow-ups

  • claude-fable-5-1 still needs catalog / ModelList follow-up after this sync.
  • Upstream fix(relay): apply model-specific OpenAI chat capabilities QuantumNous/new-api#7211 remains open — track separately.
  • Local quick checks (Windows): go build OK (with stub web/dist for embed); common + model tests OK; relay/channel had 2 failing HTTP/2 GetBody tests that look environment/upstream-local rather than merge-induced.

Conflicts

  • None — clean merge (ort strategy). Metaproxy workflow retained.

Merge policy

  • Do not merge this PR to fork main until CI is green / explicit approval.

Calcium-Ion and others added 30 commits August 1, 2026 09:35
)

Follow-up to QuantumNous#6518 (issue QuantumNous#6480) addressing three review findings:

- Document and lock in arrears semantics for the wallet Reserve top-up:
  when an auto-group retry lands on a more expensive group, the full
  reservation delta is deducted unconditionally (balance may go
  negative), mirroring settlement, so the logged pre-consumed quota
  always reconciles with the actual balance movement. Genuine DB
  errors still fail the attempt with update_data_error. Subscription
  funding keeps its insufficient-quota behavior: subscriptions enforce
  a hard used<=total cap and do not support arrears.
- PriceData.FreeModel is cleared when a retry switches from a free
  group to a paid one, keeping it consistent with the billing session
  created at that point.
- getChannel refreshes GroupRatioInfo only after channel selection
  succeeds, and the retry loop records the channel in use_channel
  before PrepareTieredBillingForSelectedGroup can fail.
)

* fix(aws): cancel Bedrock requests on client disconnect

* fix(billing): log effective usage billing path
* feat(token): support custom auto group order

* feat(keys): enhance auto group presentation

* fix(keys): rework Auto flow border and compact inherited order

The Auto group highlight previously tinted the whole control surface
with a gradient and animated only a 1px top sweep, which read as a
background color rather than a flowing border. Replace it with a
border-only effect: an aria-hidden, pointer-events-none overlay whose
conic gradient is masked down to a thin ring hugging the rounded
perimeter, so the highlight travels around all four edges and corners
every 3.2s. The interior stays neutral with a restrained static
primary border and glow; prefers-reduced-motion hides the moving
layer while keeping the static emphasis.

The inherited global Auto order also rendered as spacious two-line
rows with circular sequence markers, wasting drawer space. Render it
as a compact wrapping strip of one-line chips (index, name, ratio
badge) with descriptions kept accessible via title and sr-only text,
scrolling only past a much smaller max height.

Custom add/remove/reorder editing, empty-array inheritance semantics,
and the submit payload are unchanged.

* fix(keys): preserve Auto inheritance and unify effects

* refactor(keys): temporarily disable AutoGroupBadge in api-key-group-cell
…ntly retry after an upstream stream reset (QuantumNous#6249)

* fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset

The outbound request body is a type-erased io.Reader over BodyStorage, so
net/http cannot derive Request.GetBody (it only does so for *bytes.Reader,
*bytes.Buffer and *strings.Reader). With GetBody nil, the HTTP/2 transport
cannot transparently retry a request once the body has been written and the
upstream resets the stream with a retryable error (REFUSED_STREAM, or a
connection-level GOAWAY); the relay request then fails with:

    http2: Transport: cannot retry err [...] after Request.Body was written;
    define Request.GetBody to avoid this error

This affects every relay path that goes through DoApiRequest (chat, claude,
gemini, responses, embedding, image, rerank).

BodyStorage (memory and disk) already implements io.Seeker, so replay support
only needed wiring:

- NewOutboundJSONBody additionally returns a getBody that rewinds the storage
  and hands out a fresh non-closing reader. The transport only calls GetBody
  after the previous attempt's body has been abandoned, so the rewind cannot
  race an in-flight read.
- RelayInfo carries it in the new UpstreamRequestGetBody field, set alongside
  UpstreamRequestBodySize by the handlers that build storage-backed bodies.
- applyUpstreamGetBody (symmetric with applyUpstreamContentLength) wires it
  into DoApiRequest/DoFormRequest/DoTaskApiRequest, only when req.GetBody is
  still nil.

Also remove the hand-rolled GetBody override in DoTaskApiRequest: it returned
the same already-consumed reader, so any transport-level replay would have
silently sent an empty body, and it clobbered the correct snapshot-based
GetBody that net/http derives from the *bytes.Reader bodies the task adaptors
pass in. For non-replayable bodies GetBody now stays nil, so a retry fails
loudly instead of corrupting the request.

Covered by unit tests plus an end-to-end raw-frame HTTP/2 test that resets
the first stream with REFUSED_STREAM after the body is written and asserts
the transport transparently retries with the complete body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): hand out independent readers from GetBody (address review)

Per the http.Request.GetBody contract ("returns a new copy of Body"),
each call must yield a reader with its own cursor. The previous
implementation rewound and reused the shared BodyStorage, so two
consecutive GetBody readers would interfere with each other, and a
replay could disturb the primary body's offset under extreme transport
timing (e.g. attempt N's body write not yet fully abandoned when the
transport builds attempt N+1).

Instead of snapshotting the payload (an extra copy), add
BodyStorage.NewReader, which returns an independent zero-copy reader:

- memory mode: a fresh bytes.Reader over the same immutable backing
  array;
- disk mode: a separate file descriptor over the cache file, so the
  transport closing a replayed body only closes that descriptor.

NewOutboundJSONBody's getBody now simply hands out storage.NewReader,
and once the handler releases the storage, GetBody fails with
ErrStorageClosed instead of replaying stale data.

Tests: interleaved reads across two replay readers and the primary
body each observe exactly their own byte stream, for both the memory
and the disk-backed storage; the existing GetBody and HTTP/2 retry
suites still pass (h2 e2e tests flake-free with -count=20).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(relay): bind replayable metadata on pass-through requests

* fix(relay): reset upstream body metadata between channels

* test(relay): cover replay across retries and channel attempts

* fix(relay): stop following upstream redirects

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(channels): refine fetched model categorization

* fix: channel category

* fix: hy3 category
* fix: 修复兑换码额度精度损失(QuantumNous#6680)

* fix(redemption): guard update data integrity
…ntumNous#6561)

* feat(billing): highlight matched conditional multipliers in usage logs

* fix(billing): make request rule tracing stable and type-safe
* fix: mobile sidebar

* fix(web): prevent iOS sidebar taps from being swallowed
Co-authored-by: multica-agent <github@multica.ai>
* fix: support Gemini model listing on v1 route

* test: cover Gemini query key model listing
* fix(billing): 异步任务退款时同步减少 used_quota
退款时仅恢复了 quota(剩余额度),但未同步减少 used_quota(已用额度),
导致"总额度"(quota + used_quota)随退款次数持续虚增,超出用户实际充值金额。

修复三处退款路径:
- RefundTaskQuota:任务失败完整退款
- RecalculateTaskQuota:差额结算退款分支
- controller/midjourney.go:Midjourney 任务失败退款

新增 model.UpdateUserUsedQuota 公开函数,仅调整 used_quota 不影响 request_count。

* fix(billing): 任务退款时同步扣减渠道 used_quota

* fix(billing): complete async task refund accounting

* style(model): group internal Midjourney fields

---------

Co-authored-by: CaIon <i@caion.me>
LinineTy and others added 29 commits August 30, 2026 20:46
… stop concurrent write lockouts (QuantumNous#7030)

* fix(sqlite): enable WAL + working busy timeout + _txlock=immediate to stop concurrent write lockouts
…ocol

With PrepareStmt disabled, PostgreSQL queries run over pgx's simple
protocol, which encodes every []byte parameter as a bytea hex literal
('\x...'). driver.Valuer implementations returning []byte from
json.Marshal therefore fail json-column writes with SQLSTATE 22P02
(reported on the channels UPDATE path via ChannelInfo).

Reproduced against a live PostgreSQL 16: []byte Valuer into a json
column fails under simple protocol, string succeeds; []byte into a
text column silently stores the hex literal (no such path exists in
the repo today — audited all Valuers, json.RawMessage fields, and raw
SQL call sites).

- ChannelInfo, Properties, TaskPrivateData, JSONValue Value() now
  return string; zero-value nil semantics unchanged. Task.Data
  (bare json.RawMessage) is unaffected — database/sql's default
  converter already passes it as expected.
- Their Scan() counterparts now accept both []byte and string via a
  shared jsonScanBytes helper: SQLite returns string for these columns
  once Value() emits string, and the old []byte-only assertions
  silently zeroed the field (caught by the model test suite).
- Add regression tests locking both contracts: json-column Valuers
  must return string (or nil for zero values), Scanners must accept
  []byte and string.

Verified end-to-end against PostgreSQL 16 with the real model types:
Channel create/update/read-back, Task json fields, PrefillGroup items.
…nded heap growth → OOM) (QuantumNous#6949)

* fix(relay): bound the wait for upstream response headers (fixes unbounded heap growth)

The relay transport sets a dial timeout, a TLS handshake timeout and an expect-continue
timeout, but nothing bounds how long it waits for the upstream *response headers* after
the request has been written. An upstream that accepts the connection and then never
answers -- without sending FIN/RST, which is what happens when a NAT/firewall silently
drops the flow or the provider hangs -- parks the goroutine in
net/http.(*persistConn).roundTrip forever.

That goroutine keeps the whole request alive, which in practice means three copies of the
request body stay reachable for the lifetime of the process: the raw bytes from
io.ReadAll in CreateBodyStorageFromReader, the decoded messages held as json.RawMessage,
and the re-marshalled upstream body from common.Marshal. BodyStorageCleanup cannot help
here: it runs after c.Next() returns, and for these requests c.Next() never returns.

Measured on v1.0.0-rc.23 in production (see QuantumNous#6947 for the full evidence):

  - 23 goroutines stuck in persistConn.roundTrip on a single 40h-old instance,
    blocked between 353 and 1894 minutes (5.9h to 31.5h)
  - 96.9% of the live heap, sampled after a forced GC, attributable to those three
    body copies (HeapAlloc 892 MiB surviving three GC cycles; HeapObjects dropping
    30x while bytes dropped only 25%)
  - the live floor grows with uptime: 33.7 MiB at 0.1h, 89.2 at 13.8h, 510.0 at 40.1h,
    955.2 at 146.8h, OOMKilled at 172.9h -- same image, same config, same load

Doubling the memory limit and adding GOMEMLIMIT only moved the OOM from 132h to 172.9h.

RELAY_TIMEOUT (http.Client.Timeout) cannot be used for this: it covers the whole response
read and would cut legitimate long streaming calls, which is why it defaults to 0.
ResponseHeaderTimeout only bounds the wait for the headers; streaming after they arrive is
unaffected.

The default is deliberately generous. Non-streaming upstreams usually send the response
headers only once generation has finished, so the value has to leave room for a long
completion. 1800s is 12x shorter than the shortest hang observed here while leaving
several times the headroom a normal non-streaming request needs; 0 restores the previous
unbounded behaviour.

The assignment goes next to the other transport.* lines rather than inside the else
branch: newRelayHTTPTransport() normally takes the http.DefaultTransport.Clone() path,
and DefaultTransport does not set ResponseHeaderTimeout either.

This repo already sets ResponseHeaderTimeout on its other outbound transports
(controller/model_sync.go, controller/ratio_sync.go); the relay path appears to have
been missed.

Refs QuantumNous#6947. Likely also the root cause of QuantumNous#6731, which reported the same symptom
(production OOM on /v1/responses after ~64h) but was closed for template reasons.

* review: clamp overflowing timeout values and switch the test to testify

Addresses the two CodeRabbit findings on this PR.

Overflow (common/init.go:113): a RELAY_RESPONSE_HEADER_TIMEOUT beyond ~9.2e9 seconds
overflows time.Duration and can wrap into a *tiny positive* timeout, which would cut
every relay request instead of only the stuck ones. The value is now clamped before the
conversion, with regression tests for both the negative and the overflowing input.

I did not add fail-on-startup validation for negative values, for two reasons: the
existing `if seconds > 0` guard already treats them as "disabled", and the neighbouring
env-driven timeouts in this file are less strict still -- RelayIdleConnTimeout is
converted with no guard at all. Failing startup on a bad value would be a behaviour
change out of step with the rest of the file; happy to add it if you'd prefer that
direction repo-wide.

Test style: switched to testify (require.Equal / require.Zero / require.Positive), which
is what every other test under service/ uses.

go build, go vet and go test ./common/... ./service/... pass.
(`go build ./...` fails on the `web/dist` embed both with and without this change -- the
frontend bundle is not checked in.)
* fix(log): preserve quota in usage statistics
…, and billing usage integrity (QuantumNous#7137)

* feat(relaykit): preserve hosted tools across conversions

- add protocol-neutral hosted-tool DTOs, conversion metadata, and loss policies
- bridge citations, grounding metadata, and hosted-tool stream lifecycles
- document the public conversion behavior and channel policy controls

* refactor(relaykit): normalize reasoning and thinking intent

- centralize provider-neutral reasoning intent, effort, and budget mappings
- parse model suffixes at the host entry boundary while preserving provider-owned tails
- keep adaptive Claude thinking and explicit zero-token compatibility consistent

* fix(billing): preserve authoritative usage across relay hops

- carry native BillingUsage sidecars through direct and streamed protocol bridges
- merge partial and terminal usage monotonically with safe fallback settlement
- retain cache metadata, penultimate usage, and per-call Gemini tool surcharges

* feat(relay): bridge Responses with Claude and Gemini protocols

- add direct request, response, and stream converters across supported relay formats
- expose Claude count_tokens and Chat-to-Responses compatibility endpoints
- carry conversion diagnostics through the host while retaining the curated public goldens

* fix(relay): wire relaykit conversions into host channels

- connect handlers, adaptors, and channel settings to the standalone conversion layer
- keep model mapping, pricing identity, retries, and provider-specific suffix behavior aligned
- ignore local audit artifacts and retain focused public regression coverage
…uantumNous#7170)

Deferred follow-ups from the relaykit-tools review cycle, verified by
live end-to-end billing tests:

- billing: normalize Gemini modality keys consistently between stream
  merge and settlement (case/whitespace variants no longer drop
  independent audio/image pricing) and sum duplicate modality entries
  on both paths
- billing: sync legacy flat Claude cache-creation fields from the
  CacheCreation sub-object (including zeroing) and fall back to flat
  fields only when the snapshot never carried a sub-object, closing a
  stale 1h-cache overcharge path in cascaded deployments
- relay: move Chat-to-Claude and Chat-to-Gemini stream conversion state
  from gin.Context onto RelayInfo and reset it with SendResponseCount in
  InitChannelMeta, so channel retries start clean while per-request
  state (stream error collection, conversion diagnostics, channel
  chain, billing accumulators) survives
- relay: Claude channel now serves Gemini-format clients (request via
  registry conversion, response and stream composed through the Chat
  pivot), removing the last unimplemented conversion direction
- relaykit: recognize legacy pseudo tool names (googleSearch,
  codeExecution, urlContext) in the toolconv decode stage and drop the
  string-matching bypass in the Chat-to-Gemini converter; native Gemini
  tool output is restored and non-Gemini targets follow standard loss
  diagnostics
- relaykit: attach upstream Gemini usage (with billing_usage sidecar)
  to intermediate stream chunks so converted Claude streams report
  upstream truth from message_start, and preserve the sidecar through
  Claude stream usage merges; billing settlement unchanged
- billing: clamp negative Total-Prompt completion derivation, OR the
  Estimated flag across cross-dialect snapshot replacement, and fill
  canonical OpenAI prompt details via field-wise merge
QuantumNous#7168)

* feat(plugin): add MiniMax-H3 /v2 video generation to the hailuo task plugin

MiniMax-H3 speaks a different contract from the other Hailuo models, so the
hailuo task plugin now branches on the upstream model instead of adding a Go
adaptor:

- submit builds /v2/video_generation with a multimodal `content` array
  (text, first/last frame images, reference video/audio, or a full
  `metadata.content` passthrough), an explicit `ratio`, and 768P/2K
  resolutions; `metadata.callback_url` and `metadata.aigc_watermark` pass
  through
- query uses /v2/query/video_generation/{task_id} and parses the
  `{"task": {...}}` envelope, falling back to the /v1 shapes for every other
  model
- the /v2 result is a public CDN URL, so its artifact is proxied
  credentialless instead of through /v1/files/download
- request bounds (duration 4-15, resolution 768P/2K, ratio whitelist, at most
  2 frame images and 9/3/3 reference images/videos/audios) are enforced while
  the request body is built, which the host runs during validation, so an
  out-of-range duration is rejected with a 400 before it can become a billing
  multiplier
- duration and resolution are reported as usage facts only. Like the rest of
  this plugin, extractUsage returns no billing ratios, so per-call pricing is
  flat and 2K/duration pricing is expressed through the model's tiered billing
  expression over those facts.

Query hooks are driver hooks and are documented to receive `ctx.model` and
`ctx.upstreamModel`, but polling has no relay info and never populated them.
The polling and realtime-fetch call sites now carry the persisted task model
properties and the plugin adaptor maps them onto the query context, with
`upstreamModel` falling back to the origin name for tasks submitted without a
channel mapping.

* fix(plugin): validate Hailuo H3 requests and errors
* fix: reduce public bootstrap requests and revalidate content

* fix(controller): use a weak ETag for revalidated public JSON

/api is gzip-compressed by middleware that runs after the handler returns,
and the validator is computed over the uncompressed body. The compressed and
identity forms of one payload therefore share a validator, which a strong ETag
must not do -- it asserts byte-for-byte equality across representations
(RFC 9110 8.8.1). Serve W/ instead.

Weak comparison ignores W/ on both operands, so etagMatches now strips it from
the served validator as well as from each candidate. Stripping only the
candidate would make a weak served validator match nothing and silently
disable every 304.

Vary: Accept-Encoding stays. Weakening the validator makes revalidation
correct, but it does not separate the two encodings in a shared cache.

* fix(test): align response cookie helper name

* fix(auth): revalidate stale route sessions

* Update web/src/features/about/api.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* test: remove newly added PR tests

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Drop the unreachable user-visibility branch in LogOther.toMap and the
  receiver-mutating normalizeLegacyAdminFields; JSONString/Snapshot/
  MarshalJSON now share one full serialization
- Define legacySensitiveLogOtherKeys once and reference it from both
  SetPublic rejection and the user read-side projection
- Return the original JSON for every role when formatLogOtherJSON
  removed nothing, avoiding a re-marshal on the user log list path
- Log rejected OtherRatios keys in taskBillingOther instead of dropping
  them silently
- Use Snapshot() with typed assertions in service tests
…fication, and bounded poll failures

Plugin polling hooks previously ran against a hollow context: parseTaskResult
and parseBatchResult received {} / nil, buildQueryRequest received a
{task_id, action} map under the misleading name requestBody, and batch hooks
saw only bare task ids. The per-task poller also never looked at the upstream
HTTP status, and every built-in plugin papered over unrecognized bodies with
`|| "IN_PROGRESS"`, so a 404, a revoked key, or a shape the plugin did not
know would sit in IN_PROGRESS for the full 24h TASK_TIMEOUT_MINUTES while
holding the user's pre-charged quota.

Contract (docs/plugin-api v1.d.ts, v1.md, v1.schema.json):
- TaskQueryContext is declared separately from DriverContext and rebuilt from
  the persisted Task row: taskId, publicTaskId, action, model, upstreamModel,
  baseUrl, apiKey, authHeader, auth, data, state. Query-side requestBody is
  removed; the original request is not persisted and hooks that need a
  request-derived value must save it into state at submit time.
- parseTaskResult / parseBatchResult receive a third {status, headers}
  argument. Batch hooks receive tasks[] with one TaskQueryContext per task.
- NormalizedTaskResult accepts status "UNKNOWN" meaning "I do not recognize
  this body". Falling back to IN_PROGRESS for unknown shapes is forbidden;
  `plugin lint` warns on the literal.
- parseSubmitResponse / parseTaskResult / parseBatchResult may return `state`.
  Task.Data remains a per-round snapshot overwritten on every valid parse;
  state is plugin-owned, persisted in TaskPrivateData.PluginState, preserved
  when a hook omits it, byte-capped like taskData, and never exposed through
  presenter views.

Host (service/task_polling.go, relay/channel/task/jsplugin/adaptor.go):
- TaskPollingAdaptor / BatchTaskPollingAdaptor take *model.Task and the
  *http.Response so the adaptor can build the full context; jsplugin is the
  only implementation.
- HTTP classification before the plugin sees the body: 2xx -> plugin;
  404/410 -> FAILURE and refund; 401/403 -> poll failure plus a channel-scoped
  warning, no auto-disable; 429/5xx/transport -> poll failure; other 4xx ->
  plugin with the status visible, counted as unrecognized if the plugin still
  reports a non-terminal state.
- TaskPrivateData.PollFailures counts consecutive poll failures (transient
  HTTP, auth, transport, hook error, UNKNOWN). It is persisted through the
  existing UpdateWithStatus CAS so a concurrent terminal transition on another
  instance is never clobbered, and reset on any valid 2xx non-terminal parse.
  Reaching TASK_POLL_MAX_FAILURES (default 20, <= 0 disables) fails the task
  with the last classification and HTTP code in fail_reason and runs the
  existing settle/refund chain exactly once. sweepTimedOutTasks and its
  1440-minute default are unchanged as the outer backstop.
- Unrecognized bodies are logged at WARN with a bounded redacted copy since
  Task.Data is intentionally not overwritten on that path.

Plugins (all ten bumped one patch version):
- jimeng persists the outbound req_key in state and reads it back in
  buildQueryRequest, replacing dead reads of ctx.data / ctx.requestBody that
  never resolved.
- sunoapi batch hooks read tasks[] instead of the removed requestBody.
- hailuo treats base_resp.status_code != 0 as FAILURE before the status table.
- kling, vidu, sora, alibaba, doubao, hailuo, jimeng return UNKNOWN with the
  raw upstream status in reason on table miss.
- google and vertex-ai treat a missing `done` as in-progress: Google
  long-running operations omit proto3 default fields, so a running Veo
  operation has no `done` key at all. Only a body without an operation name is
  UNKNOWN. plugins/veo_poll_test.go locks this so the poll-failure cutoff can
  never fail a rendering Veo task.

Tests cover the classification table end to end against a real DB (404
immediate refund, 429xN refund, 401 increments without status change, 2xx
reset, UNKNOWN increments, state preserved vs replaced, PollFailures survives
the CAS write), the query-context shape, UNKNOWN on unrecognized bodies, and
the absence of PluginState/PollFailures from TaskView. Controller tests derive
the kling factory version from the embedded manifest instead of hardcoding it.
A model declared by a task plugin is served only by that plugin's channels.
When the claiming plugin has no enabled channel in the request group, the
distributor answered with the generic "no available channel" text, which hides
the actual cause and led operators to expect channel model_mapping on another
plugin's channel to take over (QuantumNous#7185). That expectation is not supported:
plugin declarations own model names statically, and channel availability must
not silently reassign ownership at request time. The supported fixes are the
existing operator tools, disabling the factory plugin per key or overriding it.

Both no-channel 503 sites in the distributor now route through
noAvailableChannelMessage. When the request is pinned to a task plugin, the
message names the claiming plugin and points to disabling or overriding it;
non-plugin requests keep the generic message. Added in en, zh-CN, zh-TW.
Model-name post-processing is rebuilt around an explicit trailing
@key:value modifier syntax (thinking/effort/temperature/topp) that
overrides request fields, survives model mapping, and records
conversion diagnostics on the consume log.

- Legacy naked aliases (-thinking, -nothinking, -thinking-<budget>,
  effort tails) now parse only for positively matched families
  (gpt-*/o-series, claude-*, gemini-*, incl. vendor/ namespaces);
  names like qwen-max stay opaque. EffortTailModelIDs remains the
  escape hatch for real in-family IDs such as gpt-5.1-codex-max.
- Billing identity resolves once in ModelPriceHelper via a ladder:
  configured request name first (legacy wildcard entries intact), then
  canonical billing names rebuilt from parsed intent
  (base@effort:E@thinking:S, then base@thinking:S; order, duplicates,
  and budget values are irrelevant; temperature/topp never priced),
  then base. Routing and token limits fall back through
  RoutingMatchModelName; pricing lookups stay wildcard-only.
- Pass-through stays byte-identical: modifiers and aliases are neither
  parsed nor validated there and forward verbatim for the upstream
  (or a chained gateway) to interpret.
- Unknown modifier keys and invalid known-key values are rejected with
  400; models whose real names contain @tag:value are exempted via the
  thinking-suffix blacklist, which now supports re:-prefixed Go regex
  entries.
- Claude reasoning render coerces unsupported combinations (disable,
  adaptive, budgets) with warning diagnostics instead of erroring;
  native-protocol requests without host syntax pass through untouched.

BREAKING(openrouter): drop the host-invented "-thinking" model-name
alias (added in 4f6d16e) that trimmed any *-thinking model on
OpenRouter channels and injected reasoning.enabled. It matched too
broadly and mangled real model IDs such as kimi-k2-thinking.
Migration: use some-model@thinking:on, or keep the old public name via
a channel model mapping {"some-model-thinking": "some-model@thinking:on"}.
Claude/Gemini family aliases (incl. anthropic/claude-*-thinking) keep
working via the family whitelist.
Sync ~99 upstream commits (gpt-6-astra billing + relay stack) while
preserving fork metaproxy-image.yml workflow.
@3metaJun
3metaJun merged commit 6141bd8 into main Sep 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.