Commit Graph
6194 Commits
Author SHA1 Message Date
LucasandClaude Fable 5 d6b5ce99de fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset (#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>
2026-08-06 15:56:38 +08:00
Calcium-Ion 0ab0202060 Feat/auto group (#6590)
* 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
2026-08-01 23:19:01 +08:00
Calcium-Ion bd585d78ef fix(aws): cancel Bedrock requests on client disconnect (#6589)
* fix(aws): cancel Bedrock requests on client disconnect

* fix(billing): log effective usage billing path
2026-08-01 22:39:54 +08:00
Calcium-Ion cfaba1dd67 fix(billing): harden tiered retry group-switch billing (#6570)
Follow-up to #6518 (issue #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.
2026-08-01 09:35:51 +08:00
吴天一 df43f80153 fix(billing): settle tiered retries with final group (#6518) 2026-07-31 19:16:46 +08:00
Seefs 9724ef1b24 feat: deepseek responses api (#6562) 2026-07-31 15:23:13 +08:00
feitianbubu aa7d0d39a4 style: use text-sm for public header nav links to match other nav components (#6557) 2026-07-31 14:54:23 +08:00
Neimar Avila e78e1db1e4 fix(oauth): stop treating a foreign window.opener as a bind flow (#6425)
* fix(oauth): stop treating a foreign window.opener as a bind flow

The /oauth/:provider callback decided between an account bind and a plain
login with `window.opener ? 'bind' : 'login'`. Any tab opened from an
external link (target="_blank", Slack, mail clients, another site) carries
a live opener, and that opener survives the cross-origin round trip to the
identity provider. Such a login callback was therefore misread as a bind:
it posted a handshake to a window that speaks no such protocol, showed the
"binding your account" screen, and hung until the 30s deadline fired with
"OAuth binding timed out" — while the backend was never called at all.

Reproduced against a real Keycloak round trip: a tab opened via window.open
still reports window.opener !== null on the callback, so mode resolved to
'bind' for an ordinary OIDC login.

A bind now requires positive proof: the popup we open for it is same-origin
(about:blank) before being sent to the provider, so we stamp its own
sessionStorage. The stamp rides through the provider round trip and is
scoped to that popup alone, so a login tab can never carry it. Ambiguity
resolves to 'login', which is the recoverable direction.

Affects every provider sharing this callback (OIDC, GitHub, Discord,
LinuxDO, custom).

* fix(oauth): harden bind popup detection
2026-07-31 14:53:59 +08:00
Seefs 8461e5339d fix(relay): preserve multipart image edits for New API channels (#6559) 2026-07-31 13:46:13 +08:00
Seefs 84834eee85 feat(logs): expose stream status to log owners (#6558) 2026-07-31 13:29:07 +08:00
Seefs 0f9f668c60 feat: support zstd request decompression (#6545) 2026-07-31 11:17:55 +08:00
Scott 66ee6b8f98 fix: preserve Qwen thinking_budget passthrough (#5836)
* fix: preserve qwen thinking budget

* test: address qwen thinking budget review comments

* chore: remove unreachable adaptor code

* test: cover zero Qwen thinking budgets
2026-07-29 17:45:14 +08:00
June Chi cb4c8c02f8 feat(oidc): 支持自定义 OIDC 登录显示名称 (#6012)
* docs: add design spec for OIDC custom display name

Mirrors the existing Custom OAuth Provider name pattern so admins can
show a meaningful label instead of the hardcoded "OIDC" on the login
page and in related copy.

* feat(oidc): add configurable display name with OIDC fallback

* feat(oidc): use configured display name in provider name and status API

* feat(oidc): add display name field to default-theme OIDC settings

Claude-Session: https://claude.ai/code/session_01FDkWJqigJi9yE3HG5pjZP5

* feat(oidc): show configured display name on default-theme login button

* feat(oidc): add display name field to classic-theme OIDC settings

* feat(oidc): show configured display name on classic-theme login button

* fix(oidc): trim whitespace before applying display name fallback

* chore: remove internal design doc from PR

Design/planning docs are working artifacts for this session and
shouldn't be submitted to the upstream project.

* fix(oidc): lead with example in classic-theme display name placeholder

Reorders the combined placeholder to show the example first, then
the fallback note, matching the Custom OAuth Provider Name field's
placeholder convention (example-only) that this feature mirrors.

* fix(i18n): improve Russian grammar in OIDC display-name placeholder translation

Leads each clause with its condition/subject and adds the missing
verb, per PR review feedback.

* test(web): remove redundant OIDC harness tests
2026-07-29 16:27:15 +08:00
CaIon c27d1ef651 Update .gitattributes 2026-07-29 13:45:10 +08:00
CaIon afe16c64cd feat: add README.md for RelayKit module with capabilities and usage examples 2026-07-28 15:24:37 +08:00
CaIon 1db6ae1957 feat: enhance CI workflow to include vetting for Go modules 2026-07-28 15:09:49 +08:00
CaIon 8e2bfe278b refactor: remove mutex from CustomEvent and update TCP connection handling in tests 2026-07-28 15:09:49 +08:00
feitianbubu c3db41407d fix: 慢查询/错误 SQL 日志参数化 (#6493)
* fix: parameterize slow/error SQL logs to avoid leaking credentials

* fix: validate SQL_SLOW_THRESHOLD_MS range

* fix: sanitize database driver error messages in SQL logs

* refactor: sanitize at gorm log writer seam to keep caller attribution
2026-07-27 22:21:31 +08:00
CaIon f01c13b086 feat: add CI workflow for backend and frontend build, typecheck, and test 2026-07-27 22:07:09 +08:00
CaIon 2cf3c8d71e docs: update AGENTS.md 2026-07-27 21:41:31 +08:00
CaIon e99a9bd86f feat: add per-channel HTTP transport controls 2026-07-27 21:41:13 +08:00
CaIon b27b2b1d6f fix(web): detect iPad login sessions correctly 2026-07-27 21:19:42 +08:00
CaIon a043eef559 feat: implement Gemini to OpenAI chat stream conversion with state management and terminal handling 2026-07-27 20:17:20 +08:00
CaIon f3ab2cff36 feat: enhance GitCode release workflow to handle bootstrap and matrix assets 2026-07-27 18:24:43 +08:00
CaIon 6d57d250f8 feat: enhance GitCode release workflow with bootstrap asset and asset matrix 2026-07-27 18:20:01 +08:00
CaIon 2ec6171faa fix: replace ASCII apostrophes in release notes to prevent script breakage 2026-07-27 17:17:01 +08:00
CaIon 8a7a49072a feat: add workflow to sync GitHub releases to GitCode 2026-07-27 17:09:27 +08:00
CaIon 8aa5e754a8 refactor: rename trusted_proxies package to middleware and update function calls 2026-07-27 16:48:23 +08:00
CaIon b8bb3f40ac refactor: update import paths to use new types package 2026-07-27 16:45:02 +08:00
CaIon 60a1acb703 refactor: update import paths to use relaykit module 2026-07-27 16:00:25 +08:00
Calcium-Ion 86ac0f7745 refactor: extract protocol conversion layer into standalone relaykit module (#6369)
* test(relayconvert): add golden snapshot matrix and relaykit boundary guard

Phase 0 of the relaykit extraction plan: pin byte-level output of every
registered (from,to) request/response/stream conversion route, and
forbid kit-bound packages from growing host-only imports.

* wip(relayconvert): drop gin.Context from converter signatures; add convmeta draft

Phase 1 in progress: relayconvert now takes context.Context; host media
resolver adapts gin.Context back at the service boundary.

* refactor(relayconvert): decouple converters from RelayInfo, gin, and settings

Phase 1 of the relaykit extraction plan:
- converters now depend on convmeta.Meta (implemented by RelayInfo) instead
  of *relaycommon.RelayInfo; ClaudeConvertInfo and the format guesser move
  to convmeta with aliases left behind
- host settings reach converters via a convmeta.Options snapshot built in
  RelayInfo.ConvOptions; no more model_setting/reasoning global reads inside
  the conversion layer
- effort-suffix helpers move to service/relayconvert/reasoning (old package
  forwards); chat-to-responses upgrade policy moves to service (host routing
  logic, not conversion)
- golden conversion matrix unchanged

* test(relayconvert): tighten boundary — kit packages now free of gin/setting imports

* refactor(dto): drop gin and logger dependencies

Phase 2 (part 1): dto.Request.IsStream now takes *http.Request instead of
*gin.Context (Gemini's impl reads query/path off the std request); dto's
three logger calls become common.SysError. Boundary test allowlist is now
empty — kit-bound packages import no gin/setting/logger/model.

* refactor(kit): extract dependency-free kitutil; dto/types/relayconvert stop importing common

Phase 2 of the relaykit extraction plan:
- new service/relayconvert/kitutil holds the pure helpers the kit needs
  (JSON wrappers, pointer/string/uuid/timestamp utils, MaskSensitiveInfo,
  pluggable LogInfo/LogError hooks, Debug flag)
- dto, types, and all relayconvert packages now use kitutil; their only
  remaining internal deps are dto/types/constant
- common keeps every original symbol (MaskSensitiveInfo delegates to
  kitutil) so host code is untouched; main.go routes kit logging into
  common.SysLog/SysError and mirrors DebugEnabled
- golden conversion matrix unchanged

* refactor(kit): move EndpointType/FinishReason to types; OpenRouter dialect via Options

Kit packages (dto/types/relayconvert/reasonmap) no longer import constant:
- EndpointType and finish-reason values live in types; constant re-exports
- the OpenRouter special-case in claude->openai request conversion reads
  Options.OpenRouterDialect, set by the host from the channel type;
  InitChannelMeta invalidates the cached snapshot on channel switch

* refactor: extract relaykit submodule (dto/types/relayconvert/reasonmap)

Phase 3 of the relaykit extraction plan:
- new go module github.com/QuantumNous/new-api/relaykit containing dto
  (minus task family), types, relayconvert (with convmeta/kitutil/reasoning),
  and reasonmap; host consumes it via require + replace, go.work for dev
- task-family dto (task/suno/midjourney/video) stays in the host dto
  package; dual-consumer host files alias it as taskdto
- relaykit builds and tests standalone (GOWORK=off): no host imports,
  no gin, no DB, no settings
- golden conversion matrix unchanged

* build(docker): copy relaykit/go.mod before go mod download

The local-replace submodule's go.mod must exist inside the build context
for the main module graph to resolve.

* fix: address relaykit extraction regressions

* fix: address relaykit review regressions

* docs: document Meta nil receiver contract

* fix(relaykit): fail OpenAI→Claude conversion without max_tokens; reject negative default_max_tokens

The Claude Messages API requires max_tokens (omitting it is a 400
"Field required"), but with a nil Options.Claude.DefaultMaxTokens hook
the converters silently emitted a request the upstream is guaranteed to
reject. Both OpenAI Chat and Responses → Claude conversions now return
sharedclaude.ErrMissingMaxTokens when no path (client value, default
hook, thinking-adapter floor) supplied one. Unreachable in the host,
which always configures the hook.

Host side, claude.default_max_tokens now rejects negative values at the
option API before persisting — they would wrap into huge unsigned values
during conversion. Zero stays allowed: the current API treats
max_tokens: 0 as cache pre-warming.

* fix: make Gemini safety settings read path race-free
2026-07-27 15:56:21 +08:00
CaIon f51dd4d808 fix: allow Advanced Custom Responses Compact
close #6461
2026-07-27 15:21:30 +08:00
CaIon 398cdafecf feat: add New API channel support 2026-07-27 15:20:19 +08:00
CaIon bc14c18f60 refactor: update task refund logic and remove legacy handling 2026-07-26 20:47:52 +08:00
CaIon 2d23cdf291 feat: configurable tool pricing, Sub2API channel, and alpha search billing
Add admin-configurable tool-call prices with cross-provider surcharge
settlement, Sub2API channel support, /v1/alpha/search relay, and usage-log
surcharge UI.
2026-07-26 20:05:15 +08:00
feitianbubu 3e1e728279 perf(web): debounce users page search input (#6474) 2026-07-26 14:46:19 +08:00
feitianbubu ab65d2582f fix: allow topup amount input to be fully cleared (#6473) 2026-07-26 14:46:05 +08:00
feitianbubu 08f88d25e5 feat: support Tencent TokenHub API key via OpenAI-compatible protocol (#6232)
* feat: support tencent tokenhub api key via openai-compatible protocol

* fix: use tokenhub base url for tencent api key channels

* test: cover tencent key-format dispatch and add TokenHub key prompt locales
2026-07-25 22:05:09 +08:00
feitianbubu 8b41defbe0 feat: add gemini-3-pro-image and gemini-3.1-flash-image GA models (#6371) 2026-07-25 20:28:08 +08:00
CaIon ae17f2749d fix(json-code-editor): remove opaque line-number layer background hiding editor content
The line-number layer spans the full editor, so giving it an opaque
background and raised z-index covered the highlighted content. Reverts
the gutter background added in 88868002f; the horizontal-scroll
gutter overlap it targeted returns as a known cosmetic issue.
2026-07-25 19:23:18 +08:00
CaIon 5ede832d80 chore(README): update AtomGit badge and remove Product Hunt badge 2026-07-25 19:17:47 +08:00
CaIon 257223be26 chore(README): update AtomGit badge and remove Product Hunt badge 2026-07-25 19:17:43 +08:00
QuentinHsu eb4a1bd193 perf(json-editor): unify admin JSON editing experience (#6421)
* perf(json-editor): improve JSON editing experience

- integrate Yace for syntax highlighting, history, indentation, auto-closing, and smart line breaks.
- add copy support, cursor location feedback, and synchronized content and line-number scrolling.
- extract JSON editor utilities and cover key interactions with unit tests.

* perf(system-settings): improve JSON configuration editing

- replace raw JSON textareas with the shared editor for highlighting, validation, copy, and formatting.
- preserve field-specific examples and make placeholders visible through the transparent editor layer.
- remove duplicate formatting controls while keeping existing form validation and save behavior.

* perf(json-editor): standardize JSON inputs across admin settings

- replace pure JSON textareas with the shared editor across system settings and channel workflows.
- preserve form focus, validation, placeholders, and visual or JSON editing modes.
- add happy-dom component coverage for form bindings, controlled updates, and formatting.

* fix(json-code-editor): address accessibility review findings

- Drop the unconditional aria-label that overrode every field's
  label-derived accessible name; add an optional ariaLabel prop and
  set it at call sites without an associated label
- Associate standalone Labels via htmlFor/id in channel-affinity views
- Hide the highlight mirror and line-number layers from the
  accessibility tree (aria-hidden)
- Give the line-number gutter an opaque background so horizontally
  scrolled code no longer slides under it
- Degrade to no scroll sync instead of destroying the editor when the
  line-number layer is not found
2026-07-25 19:10:28 +08:00
CaIon 18b0b7631a refactor: rename channel priority update to channel field update 2026-07-25 16:59:05 +08:00
RedwindA a0d0e5049e fix(web/channel): stabilize inline priority updates (#6415)
* fix(channel): debounce inline priority updates

* fix(channel): commit spinner edits only on Enter or focus leave

Blurring the inline edit input to the +/- buttons flushed the pending
priority update immediately, bypassing the container focus-containment
check and defeating the debounce. Commit now happens via the container
blur handler or explicitly on Enter. Add unit tests for the priority
update scheduler.

* fix(channel): preserve row identity when priority updates reorder channels
2026-07-25 15:59:59 +08:00
CaIon bf8cfcc512 fix(model-mutate-drawer): prevent form reset on modelSettings refetch 2026-07-25 14:51:23 +08:00
feitianbubu 27235a277a fix: prevent model create from wiping existing pricing for same name (#6365)
* fix: prevent model create from wiping existing pricing for same name

* fix(models): keep create from wiping pricing for any existing name

Prefilling from the drawer's open-time model name only covered the
missing-models entry point. Submit deletes the pricing entries for the
name in the form, which is editable and starts empty from the toolbar
"Create model" button, so both a hand-typed existing name and a renamed
prefill still dropped the configured pricing.

Track the name whose pricing was actually read into the form and scope
the delete-then-readd to it, or to a name the user explicitly priced.
Editing still clears pricing by emptying the fields, a prefilled create
does too, and a name the form never loaded is left alone -- which also
stops an edit that renames onto an existing name from destroying that
name's pricing.

Read the prefill through one shared readPricingConfig instead of
duplicating the seven-map parse in both branches, and open the advanced
section for ratios that are configured as 0 rather than only truthy ones.
2026-07-25 14:15:48 +08:00
bigsong 84a79b6807 fix: log response body when parsed upstream error message is empty
When an upstream error response parses as valid JSON but yields no usable
error message (e.g. an aggregator gateway returning {"error":{"message":""}}),
RelayErrorHandler previously produced a bare "bad response status code N"
error with no trace of the original body, making the failure undiagnosable.
Log the body preview in that case, mirroring the existing behavior for
unparseable bodies.
2026-07-24 14:10:32 +08:00
Seefs cbd9b30aa4 fix(github): restore issue form visibility (#6454) 2026-07-24 13:53:56 +08:00
Seefs cb96ab0208 chore(github): migrate issue templates to required forms (#6452) 2026-07-24 13:46:54 +08:00