Compare commits

...
1124 Commits
Author SHA1 Message Date
ZyyandCodex 7cee430947 chore(release): v3.42.0
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 01:59:00 +08:00
ZyyandCodex ac98a764ff fix(update): discover channels from verified release assets
Publish an immutable channel snapshot with each release and advance a fixed release pointer only after public assets pass verification. Check the exact platform package before notifying clients.

Co-authored-by: Codex <noreply@openai.com>
2026-11-07 01:59:00 +08:00
ZyyandCodex 56ef1bffa2 test(openai): align service tier fixture with OAuth normalization
Use client_metadata to verify nested service tier fields survive fast-mode handling while preserving outbound tier and billing assertions.

Co-authored-by: Codex <noreply@openai.com>
2026-11-07 01:46:54 +08:00
ZyyandCodex e275a6dada chore(release): v3.42.0
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 01:21:29 +08:00
555269739c fix(openai): normalize unsupported Codex OAuth request options (#630)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 01:16:25 +08:00
ZyyandCodex d03d8408fc docs: clarify current configuration and update behavior
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 00:55:33 +08:00
ZyyandCodex 228eea90c1 chore(db): consolidate unreleased migrations
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 00:48:32 +08:00
ZyyandCodex 0d25061ef0 docs: bound UI verification and generalize AI attribution
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 00:37:46 +08:00
ZyyandZyy e97b1faf0d feat(openai): derive installation identities from key seeds (#629)
* feat(openai): derive installation identities from key seeds

* docs: explain installation seed migration effects

* docs: fix the key configuration reference

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-11-07 00:33:17 +08:00
ZyyandZyy 90ecac978a feat(accounts): make quota credit use opt-in per account (#628)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-11-07 00:20:43 +08:00
ZyyandZyy 4326eb1e4b fix(update): bound GitHub lookups with a published channel index (#627)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-11-07 00:20:19 +08:00
ZyyandCodex 2a8d5d8643 ci(release): publish the channel index after verifying release assets
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 00:13:47 +08:00
ZyyandCodex b7fa5872a3 refactor(ui): reuse shared button components
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 23:41:36 +08:00
ZyyandCodex e907f419ab fix(ui): show account group names in popovers
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 23:20:13 +08:00
ZyyandZyy ee7e837d1f feat(scheduling): split queue timeouts and allow account fallback (#625)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-11-06 23:09:29 +08:00
ZyyandCodex 1240f22580 fix(openai): honor affinity mode for sessionless requests
Use the configured affinity mode for content and cache-key bindings, including strict waiting and binding store failures. Cover queue rejection, cancellation, migration, and preferred spillover, and document the scheduling boundaries.

Refs #623

Co-authored-by: Codex <noreply@openai.com>
2026-11-06 22:05:15 +08:00
ZyyandCodex e390417821 fix(openai): prefer the same account for sessionless requests
Use the client cache key or the opening messages as a tenant-scoped scheduling preference. Keep explicit sessions and continuation ownership separate, and allow spillover when the preferred account is busy.

Refs #622

Co-authored-by: Codex <noreply@openai.com>
2026-11-06 20:01:01 +08:00
ZyyandCodex e3f1299a0c fix(ui): retain custom model choices until save
Keep deselected model options in the editing session and remove the nested model list scrollbar.

Co-authored-by: Codex <noreply@openai.com>
2026-11-06 15:53:42 +08:00
Zyy 4e4cd96478 fix(openai): recheck cross-account capacity while queued 2026-11-06 15:02:16 +08:00
ZyyandZyy 6de1beb1df perf(openai): reduce scheduling reads and stabilize session affinity (#618)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-11-06 14:44:42 +08:00
ZyyandCodex ba3baddbda chore(release): v3.41.2
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 12:21:27 +08:00
ZyyandCodex 09b22a9550 fix(openai): honor account allowlists for unlisted models
Let OpenAI accounts send explicitly allowlisted model IDs upstream while retaining catalog checks for other accounts. Update the account editor and connection test to support custom IDs.

Co-authored-by: Codex <noreply@openai.com>
2026-11-06 12:18:08 +08:00
ZyyandCodex 04a48e848d chore(deps): update mio to 1.2.4
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 11:25:06 +08:00
ZyyandCodex 1655ecc972 style(usage): clarify tooltip success rate formatting
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 11:06:22 +08:00
ZyyandCodex 4527db7b5d feat(usage): show cancelled and incomplete requests separately
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 11:04:07 +08:00
ZyyandCodex 2b7a64dae6 docs: preserve task evidence and clarify completion rules
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 22:06:09 +08:00
ZyyandCodex 0c07aac1d6 chore(release): v3.41.1
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 22:01:26 +08:00
ZyyandCodex 3e33e84a45 fix(observability): unify request error statistics
Count incomplete requests as errors, exclude recovered and cancelled requests from success rates, and preserve gaps for missing samples.

Co-authored-by: Codex <noreply@openai.com>
2026-11-05 21:56:12 +08:00
ZyyandCodex c73e7126a2 fix(openai): clarify OAuth connection failures and extend timeouts
Distinguish connection failures from uncertain authorization results and retain redacted diagnostic causes. Increase token connection timeout to 30 seconds and total request timeout to 60 seconds.

Co-authored-by: Codex <noreply@openai.com>
2026-11-05 21:50:08 +08:00
ZyyandCodex 1d88c9a4a5 fix(websocket): close idle clients when upstream connections end
Notify idle clients with Close 1000 after upstream loss while preserving settlement and queued requests. Align idle transport termination detection with Codex and document the reconnect contract.

Co-authored-by: Codex <noreply@openai.com>
2026-11-05 18:48:38 +08:00
蓝芷怡andCodex 26dbc12010 fix(accounts): show full model names in the account catalog (#616)
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 15:17:55 +08:00
dependabot[bot] 2dc6bf0150 chore(deps): bump the minor-and-patch group in /backend with 4 updates (#614)
Bumps the minor-and-patch group in /backend with 4 updates: [zip](https://github.com/zip-rs/zip2), [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust), [redis](https://github.com/redis-rs/redis-rs) and [crc32fast](https://github.com/srijs/rust-crc32fast).


Updates `zip` from 9.1.3 to 9.2.2
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v9.1.3...v9.2.2)

Updates `aws-sdk-s3` from 1.156.0 to 1.157.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `redis` from 1.8.0 to 1.8.1
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-1.8.0...redis-1.8.1)

Updates `crc32fast` from 1.5.3 to 1.5.4
- [Commits](https://github.com/srijs/rust-crc32fast/compare/v1.5.3...v1.5.4)

---
updated-dependencies:
- dependency-name: zip
  dependency-version: 9.2.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: aws-sdk-s3
  dependency-version: 1.157.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: redis
  dependency-version: 1.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: crc32fast
  dependency-version: 1.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-05 15:11:46 +08:00
dependabot[bot] f7f64ead3b chore(deps): bump the minor-and-patch group in /frontend with 6 updates (#613)
Bumps the minor-and-patch group in /frontend with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.66.0` | `1.67.0` |
| [gsap](https://github.com/greensock/GSAP) | `3.15.0` | `3.15.1` |
| [semver](https://github.com/npm/node-semver) | `7.8.5` | `7.8.6` |
| [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) | `11.4.16` | `11.4.18` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.3` | `4.3.4` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.3` | `4.3.4` |


Updates `@lucide/vue` from 1.66.0 to 1.67.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.67.0/packages/vue)

Updates `gsap` from 3.15.0 to 3.15.1
- [Commits](https://github.com/greensock/GSAP/compare/3.15.0...3.15.1)

Updates `semver` from 7.8.5 to 7.8.6
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.5...v7.8.6)

Updates `vue-i18n` from 11.4.16 to 11.4.18
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/v11.4.18/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v11.4.18/packages/vue-i18n)

Updates `@tailwindcss/vite` from 4.3.3 to 4.3.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.4/packages/@tailwindcss-vite)

Updates `tailwindcss` from 4.3.3 to 4.3.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.4/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.67.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: gsap
  dependency-version: 3.15.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: semver
  dependency-version: 7.8.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: vue-i18n
  dependency-version: 11.4.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: tailwindcss
  dependency-version: 4.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-05 15:11:02 +08:00
dependabot[bot] 37749001db chore(deps): bump taiki-e/install-action (#612)
Bumps the minor-and-patch group with 1 update: [taiki-e/install-action](https://github.com/taiki-e/install-action).


Updates `taiki-e/install-action` from 2.88.1 to 2.89.1
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/68a389c788b2cd04016ce8c80ee23b80d2ab3d68...682ce6c3c696a4cd5168a05f98e51aa02b877941)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.89.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-05 15:10:31 +08:00
ZyyandCodex 272534a3b6 perf(host): reduce startup and restart latency
Read the local running version during bootstrap without waiting for a
remote release check. Reuse the restart handoff pipe on Unix so the
replacement starts as soon as the old service releases its resources.

Keep shutdown cleanup budgets and bounded listener retries. Update the
handoff regression coverage and lifecycle documentation.

Validation: Rustfmt, strict workspace Clippy, 252 Host and bootstrap
tests, and isolated Linux startup and consecutive restart measurements.

Co-authored-by: Codex <noreply@openai.com>
2026-11-05 14:15:58 +08:00
ZyyandCodex b9bcb4e086 ci: streamline builds and release validation
Share binary builds and dependency caches, run Windows platform checks in parallel, and reuse verified CI evidence for version-only releases. Reuse prebuilt CI image inputs and add read-only release preflight.

Co-authored-by: Codex <noreply@openai.com>
2026-11-05 13:03:59 +08:00
ZyyandCodex 9039cef3d8 chore(release): v3.41.0
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 12:18:54 +08:00
a9a77f5f9d fix(accounts): calculate model success rates from request outcomes (#611)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 12:14:27 +08:00
ZyyandCodex 6f7d92594e fix(frontend): defer incomplete configuration errors until submit
Avoid validating model access and plugin forms when switching modes.
Keep error protection drafts quiet until input or submission, and retain
condition identity when editing or removing entries.

Co-authored-by: Codex <noreply@openai.com>
2026-11-05 12:11:52 +08:00
6859520969 fix(api): broaden Chat compatibility and capture ingress failures (#610)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 11:53:21 +08:00
ZyyandCodex 21d84af2d8 fix: simplify user-facing messages and error feedback
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 00:48:19 +08:00
ZyyandCodex 311e4d228f docs(contributing): prioritize concise text in development and feedback
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 00:47:45 +08:00
ZyyandCodex d604adfbeb fix(login): center the sign-in form on mobile
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 00:47:45 +08:00
ZyyandCodex 46b4665c5b feat(openai): allow API keys to override the installation ID
Co-authored-by: Codex <noreply@openai.com>
2026-11-05 00:31:37 +08:00
ZyyandCodex 1daa0623a0 fix(openai): preserve client turn state without local ownership records
Preserve client-supplied turn state on full requests with unknown ownership
and remove the process-local proof cache. Keep confirmed account-switch
cleanup and existing continuation authorization boundaries.

Cover HTTP and WebSocket retries, provider recreation, expired affinity,
and conflicting account ownership; update the API and architecture docs.

Co-authored-by: Codex <noreply@openai.com>
2026-11-05 00:14:53 +08:00
ZyyandCodex 860fb39622 fix(frontend): satisfy account formatting rules
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 22:49:25 +08:00
f5c5a6e5b7 feat(proxies): test account egress and assign accounts in bulk (#607)
* feat(proxies): test account egress and assign accounts in bulk

Co-authored-by: Codex <noreply@openai.com>

* fix(accounts): refine proxy controls and quota display

Keep egress results with their proxy options and refresh stale proxy revisions before retrying. Compact account assignment and settings controls, and hide missing quota refresh timestamps.

Co-authored-by: Codex <noreply@openai.com>

* fix(proxies): reset account selection without closing dialog

Co-authored-by: Codex <noreply@openai.com>

* fix(proxies): clarify assignment reset and unavailable accounts

Use a red reset button and disabled checkboxes for accounts already using the target proxy.

Co-authored-by: Codex <noreply@openai.com>

* feat(proxies): show account details in assignment dialog

Add platform, subscription, and group columns, remove inline provider icons, and use the existing subtle danger style for reset.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-11-04 22:42:16 +08:00
ZyyandCodex bdfc201795 fix(peer-link): update account identity on definitive sharing results
Clear the peer badge when the upstream explicitly disables sharing or does not support CPR Link. Preserve identity on transient failures and allow manual rediscovery from the quota panel.

Co-authored-by: Codex <noreply@openai.com>
2026-11-04 19:58:55 +08:00
ZyyandCodex 789609dd53 chore(release): v3.40.0
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 19:36:41 +08:00
10e25ce25f feat(api): extend Chat Completions compatibility with Responses (#606)
Map tool-result names, inline PDF inputs, and hosted web search to Responses. Project URL citations into Chat messages and preserve the requested output token limit in the OpenAI encoder.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 19:32:42 +08:00
6b362a5029 fix(accounts): keep failed recovery probes from thawing accounts (#605)
Carry the freeze recovery target into provider account selection so probe responses cannot update credentials or clear the active protection. Let the recovery worker postpone or release the same freeze after observing the upstream result.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 19:30:22 +08:00
13374e2a70 fix(api): separate traffic readiness from worker health (#604)
Expose /readyz for the runtime snapshot, PostgreSQL, and Redis. Keep worker and logging diagnostics on /healthz so a maintenance failure can alert without removing a serving instance from service discovery.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 19:29:50 +08:00
ZyyandCodex 19ef484969 feat(peer-link)!: share upstream key concurrency and RPM limits
Use a fresh upstream key concurrency limit as the account default for scheduling, leases, and capacity reporting. Show disclosed limits alongside the upstream pool summary.

BREAKING CHANGE: Both peers must support CPR Link schema v3.

Refs: #587

Co-authored-by: Codex <noreply@openai.com>
2026-11-04 18:32:42 +08:00
242ffb2324 feat(accounts): add a separate model allowlist cleanup button (#600)
* feat(accounts): add manual allowlist cleanup

Co-authored-by: Codex <noreply@openai.com>

* fix(accounts): refine model allowlist cleanup interactions

Co-authored-by: Codex <noreply@openai.com>

* chore(docs): remove PR screenshot assets

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-11-04 17:15:04 +08:00
ZyyandCodex 53a09bbfe9 docs(skills): require English pull request titles
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 17:03:08 +08:00
239def9a5b fix(store): preserve client key ledgers after deletion (#602)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 17:00:44 +08:00
ZyyandCodex 5eaca14a64 fix(openai): clear client headers when switching accounts
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 15:28:48 +08:00
78e943f406 feat(usage): add snapshot scans and complete grouped statistics (#599)
Expose bounded usage readers through Admin, Store, Runtime, and SDK. Preserve snapshot consistency, coverage metadata, cancellation, and parent-call cleanup.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 15:22:44 +08:00
ZyyandCodex 0663df14df fix(responses): reconnect idle WebSockets for replay regardless of client identity
Refs #596

Co-authored-by: Codex <noreply@openai.com>
2026-11-04 14:14:29 +08:00
39aa20a13a fix(plugins): prefer account-scoped profiles for adapted models (#588)
* fix(plugins): prefer account-scoped model profiles in catalogs

Co-authored-by: Codex <noreply@openai.com>

* fix(models): honor scoped plugin profiles in fallback catalogs

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-11-04 13:20:08 +08:00
Ke WangandCodex 621b1faceb fix(store): keep usage queries compatible with PostgreSQL 16 (#597)
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 13:17:09 +08:00
28eb4708b5 fix(update): keep read-only official plugin files during extract (#595)
* fix(update): keep read-only official plugin files during extract

Docker Desktop returns EPERM when chmod runs again on an official plugin file that tar already unpacked as mode 0444.

Fixes #594

* test(update): cover writable official plugin archives

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: le-czs <243511553+le-czs@users.noreply.github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-04 13:09:57 +08:00
Ke Wang aa003f7d90 fix(build): keep frozen migrations in LF (#589) 2026-11-04 13:02:04 +08:00
ZyyandCodex 4c57a0405d fix(xai): preserve quota snapshots and restore daily usage
Preserve account profiles and quota observations during OAuth refresh and retry deferral. Project rolling daily usage independently of upstream snapshots and expose its period through the existing account usage API.

Keep quota capacity forecasts scoped to weekly and monthly windows. Cover refresh persistence, daily statistics, API projection, and PostgreSQL CAS behavior.

Co-authored-by: Codex <noreply@openai.com>
2026-11-04 12:39:10 +08:00
ZyyandCodex 49cee2397f fix(openai): avoid repeated replay errors after large SSE requests
Keep oversized Codex threads on SSE until a full request falls below 13.5 MiB. After delivering a response that requires full replay, close an idle Codex socket so the next step can send its history directly.

Co-authored-by: Codex <noreply@openai.com>
2026-11-04 12:17:26 +08:00
ZyyandCodex 1915806bd1 perf(theme): load preview content after the canvas
Co-authored-by: Codex <noreply@openai.com>
2026-11-03 21:18:02 +08:00
ZyyandCodex 5100f6333e fix(usage): improve error diagnostics and request inspection
Co-authored-by: Codex <noreply@openai.com>
2026-11-03 21:18:02 +08:00
ZyyandCodex 187af36306 fix(accounts): preserve known peer identity after sync failures
Keep undiscovered upstreams displayed as ordinary API keys and retain known peer errors without revoked shared data. Reuse quota refresh for retries and reset recognition when the connection changes.

Co-authored-by: Codex <noreply@openai.com>
2026-11-03 19:33:24 +08:00
ZyyandCodex be35f8a6ee fix(accounts): refine upstream quota display and layout
Show quota availability consistently for unlimited and stale observations, keep pool summaries at the bottom, and compact plan counts with an inline overflow popover.

Co-authored-by: Codex <noreply@openai.com>
2026-11-03 17:48:47 +08:00
ZyyandCodex 317c05c74a fix(accounts): show OAuth results in notifications
Co-authored-by: Codex <noreply@openai.com>
2026-11-03 15:55:56 +08:00
ZyyandCodex 12c32fcdec chore(release): v3.39.1
Co-authored-by: Codex <noreply@openai.com>
2026-11-03 11:47:32 +08:00
Ke WangandCodex 26de671226 fix(runtime): reload full snapshot on config revision races (#584)
* fix(runtime): reload raced snapshot facts before suspending publication

Co-authored-by: Codex <noreply@openai.com>

* fix(runtime): retry extension preparation when its revision becomes stale

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
2026-11-03 11:38:47 +08:00
Ke WangandCodex a92b95c48c fix(proxies): measure latency for the selected successful address family (#585)
Co-authored-by: Codex <noreply@openai.com>
2026-11-03 11:34:40 +08:00
ZyyandCodex e7c24781fb fix(openai): preserve deployment account routing policy
Co-authored-by: Codex <noreply@openai.com>
2026-11-03 11:33:53 +08:00
ZyyandCodex 3b4b547e9c fix(store): release proxy locks before returning delete conflicts
Co-authored-by: Codex <noreply@openai.com>
2026-11-03 11:23:28 +08:00
ZyyandCodex 2016bda031 fix(openai): preserve client headers and send account installation id
Co-authored-by: Codex <noreply@openai.com>
2026-11-03 11:23:16 +08:00
ZyyandCodex 790f3147e0 fix(openai): preserve verified turn state on full requests
Track bounded ownership proofs for upstream turn-state tokens, including failed responses, so same-turn full requests can echo them without trusting unrelated account state.

Co-authored-by: Codex <noreply@openai.com>
2026-11-03 10:19:53 +08:00
ZyyandCodex 96e8443ede chore(release): v3.39.0
Co-authored-by: Codex <noreply@openai.com>
2026-11-02 19:40:58 +08:00
ZyyandCodex 42badf64ca docs(deploy): document low-memory database tuning
Co-authored-by: Codex <noreply@openai.com>
2026-11-02 16:30:04 +08:00
35afe1123f feat(plugins): edit sensitive configuration per field (#582)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-02 15:32:32 +08:00
ZyyandCodex 2ac4265832 feat(windows): add builds and online updates
Build x64 and ARM64 release archives with a static MSVC runtime.
Support executable replacement, restart handoff and rollback on Windows.

Co-authored-by: Codex <noreply@openai.com>
2026-11-02 14:45:33 +08:00
ZyyandCodex d22ec29824 fix(usage): show CPR badges for linked accounts
Use current peer summaries for request records and account diagnostics without changing historical snapshots or database schema.

Co-authored-by: Codex <noreply@openai.com>
2026-11-02 14:39:44 +08:00
ZyyandCodex 7a32c6cec2 chore(release): v3.38.2
Co-authored-by: Codex <noreply@openai.com>
2026-11-02 13:44:36 +08:00
0ff1a72919 fix(retry): limit upstream Retry-After waits (#578)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-02 13:07:20 +08:00
79083a5632 fix(openai): reconnect before websocket search activation (#577)
Track hosted search initialization on pooled Codex sockets and retire incompatible connections before sending. Preserve existing client full replay recovery for connection-local deltas.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-02 12:28:14 +08:00
ZyyandCodex 90f7456db8 fix(ui): align CPR badge text with its icon
Keep the compact line height and offset the uppercase label by one pixel to align its visible center with the brand glyph across badge sizes.

Co-authored-by: Codex <noreply@openai.com>
2026-11-02 12:27:51 +08:00
ZyyandCodex 6f71b27c3c fix(peer): include quota-backed plans in pool summaries
Reuse account plan resolution when exporting peer pool summaries. Read saved quota observations for missing plans even when quota sharing is disabled, without exposing quota data or mutating account records.

Cover unknown and missing plans, subtype preservation, plan-only limits, and quota refresh from an unknown account plan.

Co-authored-by: Codex <noreply@openai.com>
2026-11-02 12:24:57 +08:00
ZyyandCodex 7b05b14a2b fix(openai): narrow model rejection cooldown detection
Require the official model_not_found code and supported status codes for
account-model cooldowns. Treat unsupported_value as a terminal request
error and preserve account eligibility across HTTP, SSE, and WebSocket.

Co-authored-by: Codex <noreply@openai.com>
2026-11-02 11:36:58 +08:00
f9b5b5ff77 chore(deps): bump node from 26.12.1-bookworm-slim to 26.13.0-bookworm-slim in /deploy in the minor-and-patch group across 1 directory (#573)
* chore(deps): bump node

Bumps the minor-and-patch group with 1 update in the /deploy directory: node.


Updates `node` from 26.12.1-bookworm-slim to 26.13.0-bookworm-slim

---
updated-dependencies:
- dependency-name: node
  dependency-version: 26.13.0-bookworm-slim
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs(development): align Node.js version with Docker

Co-authored-by: Codex <noreply@openai.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-11-02 10:33:44 +08:00
dependabot[bot] 4d5778a84f chore(deps): bump rust from 48b7f43 to 9181b56 in /deploy (#575)
Bumps rust from `48b7f43` to `9181b56`.

---
updated-dependencies:
- dependency-name: rust
  dependency-version: 1.99.1-bookworm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 10:32:20 +08:00
dependabot[bot] 6d22d73506 chore(deps): bump debian from 7ff6ba3 to 99c0b25 in /deploy (#574)
Bumps debian from `7ff6ba3` to `99c0b25`.

---
updated-dependencies:
- dependency-name: debian
  dependency-version: bookworm-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 10:32:10 +08:00
dependabot[bot] d8ab207282 chore(deps): bump the minor-and-patch group in /frontend with 7 updates (#572)
Bumps the minor-and-patch group in /frontend with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.61.0` | `1.66.0` |
| [axios](https://github.com/axios/axios) | `1.20.0` | `1.21.0` |
| [es-toolkit](https://github.com/toss/es-toolkit) | `1.53.0` | `1.53.1` |
| [@intlify/unplugin-vue-i18n](https://github.com/intlify/bundle-tools/tree/HEAD/packages/unplugin-vue-i18n) | `11.2.7` | `11.2.8` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.6.5` | `26.6.6` |
| [eslint](https://github.com/eslint/eslint) | `10.13.0` | `10.14.0` |
| [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) | `3.3.13` | `3.3.14` |


Updates `@lucide/vue` from 1.61.0 to 1.66.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.66.0/packages/vue)

Updates `axios` from 1.20.0 to 1.21.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.20.0...v1.21.0)

Updates `es-toolkit` from 1.53.0 to 1.53.1
- [Release notes](https://github.com/toss/es-toolkit/releases)
- [Changelog](https://github.com/toss/es-toolkit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/toss/es-toolkit/compare/v1.53.0...v1.53.1)

Updates `@intlify/unplugin-vue-i18n` from 11.2.7 to 11.2.8
- [Release notes](https://github.com/intlify/bundle-tools/releases)
- [Changelog](https://github.com/intlify/bundle-tools/blob/v11.2.8/CHANGELOG.md)
- [Commits](https://github.com/intlify/bundle-tools/commits/v11.2.8/packages/unplugin-vue-i18n)

Updates `@types/node` from 26.6.5 to 26.6.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.13.0 to 10.14.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.13.0...v10.14.0)

Updates `vue-tsc` from 3.3.13 to 3.3.14
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.3.14/packages/tsc)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.66.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: axios
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: es-toolkit
  dependency-version: 1.53.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@intlify/unplugin-vue-i18n"
  dependency-version: 11.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@types/node"
  dependency-version: 26.6.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: eslint
  dependency-version: 10.14.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: vue-tsc
  dependency-version: 3.3.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 10:31:59 +08:00
dependabot[bot] 3816b46ba1 chore(deps): bump the minor-and-patch group in /backend with 8 updates (#571)
Bumps the minor-and-patch group in /backend with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [uuid](https://github.com/uuid-rs/uuid) | `1.28.0` | `1.29.0` |
| [zip](https://github.com/zip-rs/zip2) | `9.0.5` | `9.1.3` |
| [jsonschema](https://github.com/Stranger6667/jsonschema) | `0.60.0` | `0.60.2` |
| [roxmltree](https://github.com/RazrFalcon/roxmltree) | `0.21.1` | `0.21.2` |
| [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust) | `1.155.0` | `1.156.0` |
| [aws-smithy-runtime-api](https://github.com/smithy-lang/smithy-rs) | `1.19.1` | `1.19.2` |
| [aws-smithy-types](https://github.com/smithy-lang/smithy-rs) | `1.9.0` | `1.9.1` |
| [redis](https://github.com/redis-rs/redis-rs) | `1.7.1` | `1.8.0` |


Updates `uuid` from 1.28.0 to 1.29.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.28.0...v1.29.0)

Updates `zip` from 9.0.5 to 9.1.3
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v9.0.5...v9.1.3)

Updates `jsonschema` from 0.60.0 to 0.60.2
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/ruby-v0.60.0...ruby-v0.60.2)

Updates `roxmltree` from 0.21.1 to 0.21.2
- [Changelog](https://github.com/RazrFalcon/roxmltree/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RazrFalcon/roxmltree/compare/v0.21.1...v0.21.2)

Updates `aws-sdk-s3` from 1.155.0 to 1.156.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `aws-smithy-runtime-api` from 1.19.1 to 1.19.2
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `aws-smithy-types` from 1.9.0 to 1.9.1
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `redis` from 1.7.1 to 1.8.0
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-1.7.1...redis-1.8.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: zip
  dependency-version: 9.1.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: jsonschema
  dependency-version: 0.60.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: roxmltree
  dependency-version: 0.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-sdk-s3
  dependency-version: 1.156.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-runtime-api
  dependency-version: 1.19.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-types
  dependency-version: 1.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: redis
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 10:31:48 +08:00
dependabot[bot] 4a7c06f895 chore(deps): bump taiki-e/install-action (#569)
Bumps the minor-and-patch group with 1 update: [taiki-e/install-action](https://github.com/taiki-e/install-action).


Updates `taiki-e/install-action` from 2.87.31 to 2.88.1
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/1e29709c0ec63babbe8f266397a465f29a825f91...68a389c788b2cd04016ce8c80ee23b80d2ab3d68)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.88.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 10:31:13 +08:00
ZyyandCodex e0b7a841ca chore(release): v3.38.1
Co-authored-by: Codex <noreply@openai.com>
2026-11-02 00:08:30 +08:00
ZyyandCodex 96a292dd0f fix(providers): align catalog-based account eligibility
Apply valid catalog membership alongside account model access for native OAuth, API key upstreams, and xAI. Preserve upstream validation for missing catalogs and unknown plans.

Co-authored-by: Codex <noreply@openai.com>
2026-11-02 00:02:08 +08:00
ZyyandCodex 6f138c8256 chore(release): v3.38.0
Co-authored-by: Codex <noreply@openai.com>
2026-11-01 23:32:37 +08:00
ZyyandCodex c8ffb1a8c8 perf(store): batch account lease runtime reads
Read up to 128 accounts per Redis script while preserving input order, pool isolation, shared scheduling timestamps, expiry cleanup, and error propagation. Extend real Redis coverage for mixed lease states and invalid batch data.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 23:23:49 +08:00
ZyyandCodex 79df82a4f0 fix(openai): respect model capabilities in account selection
Filter known OAuth models by fresh plan catalogs and retain account-scoped model rejections. Preserve credential, transport, adapter, and continuation boundaries, and combine cooldown expiry without dropping protection semantics.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 22:39:21 +08:00
ZyyandCodex 62516d9769 fix(ci): reduce Cargo cache snapshot disk usage
Keep Cargo snapshots compressed during transfer, extract directly into BuildKit cache mounts, and release restored runner snapshots. Export archives without an extra copy layer and use a new cache format key.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 22:05:10 +08:00
ZyyandCodex 892cba68ae test: streamline backend tests and speed up validation
Remove obsolete and redundant assertions while preserving current behavior.
Optimize zlib-rs in dev/test builds and use eight concurrent CI test workers.
Use explicit time bases and synchronization for integration fixtures.

Validated 4322 tests, 72 Redis ACL tests, and ten stress iterations of 102
concurrency-related tests. Rustfmt, strict Clippy, and actionlint passed.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 21:31:07 +08:00
ZyyandCodex fada9bc0bd refactor: clarify module boundaries and naming
Organize backend modules by responsibility, simplify runtime layers, and
keep shared contracts with their owning modules. Split frontend account
APIs and separate shared types from request implementations.

Strengthen architecture checks for module dependencies and test layout.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 20:35:59 +08:00
ZyyandCodex 27b9bbe994 fix: persist peer discovery and quiet expected cookie conflicts
Persist unsupported CPR Link detection for each connection and skip periodic
probes across restarts. Retain manual rediscovery, connection invalidation,
retry throttling, and existing quota access protections.

Log stale response-cookie revision conflicts at debug while retaining
warnings for actual storage failures.

Validation: 29 peer-related tests, 14 cookie tests, 16 architecture checks,
strict Clippy, Rustfmt, and diff checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 20:20:45 +08:00
ZyyandCodex db849c1251 feat(peer-link)!: simplify the CPR Link protocol
Make capability blocks optional and represent key budgets as named windows. Centralize validation and preserve unknown states for missing observations.

BREAKING CHANGE: CPR Link requires schema_version 2; v1 snapshots are no longer accepted.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 19:24:17 +08:00
ZyyandCodex c57af9b0dd fix(openai): strip sentry-trace and baggage from outbound requests
Remove tracing headers and their WebSocket metadata copies through the existing correlation cleanup. Cover cache identity stability and WebSocket reuse after privacy header changes.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 14:21:01 +08:00
ZyyandCodex 641f43b303 chore(release): v3.37.2
Co-authored-by: Codex <noreply@openai.com>
2026-11-01 03:41:24 +08:00
ZyyandCodex 6ce723ce93 fix(accounts): show account names in delete confirmation
Co-authored-by: Codex <noreply@openai.com>
2026-11-01 03:41:16 +08:00
ZyyandCodex 2ff43c73da fix(accounts): bound discovery refreshes and align import settings
Share a four-request limit across CPR discovery, manual refreshes, and
periodic refreshes, and avoid loading runtime settings for initial reads.

Reuse account name and notes fields for import and editing, apply custom
names only to single-account imports, and match API usage bars to quota
progress styling.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 03:35:29 +08:00
ZyyandCodex cb79f4d5a5 fix(accounts): start peer discovery after credential imports
Reuse quota routing for initial peer observation and preserve discovery state across ordinary account edits. Hide pending subscription badges and remove redundant provider quota labels without adding frontend polling.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 03:10:18 +08:00
ZyyandCodex 1e57805598 chore(release): v3.37.1
Co-authored-by: Codex <noreply@openai.com>
2026-11-01 02:20:01 +08:00
ZyyandCodex aca75490cb fix(accounts): preserve migrated identities and restore cumulative usage
Co-authored-by: Codex <noreply@openai.com>
2026-11-01 02:19:53 +08:00
ZyyandCodex 6a95869a72 docs: fix README links and add UI playground
Co-authored-by: Codex <noreply@openai.com>
2026-11-01 01:59:55 +08:00
ZyyandCodex dfc29d4709 chore(release): v3.37.0
Co-authored-by: Codex <noreply@openai.com>
2026-11-01 01:49:30 +08:00
ZyyandCodex 3cbebb5f9b feat(frontend): adopt UI v0.8.0 and refine form feedback
Unify theme namespaces and plugin theme delivery, improve validation and empty states, and pin UI v0.8.0 plus plugin workbench v0.9.0.

Co-authored-by: Codex <noreply@openai.com>
2026-11-01 01:40:09 +08:00
ZyyandCodex fbdfe22757 feat(settings): update default concurrency policy
Co-authored-by: Codex <noreply@openai.com>
2026-10-31 21:29:50 +08:00
7cf99769b1 feat(accounts): support editing optional account names (#568)
Allow local account names to be changed or cleared without replacing credentials or account settings. Show the name with notes and fall back to email when the name is empty.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-31 21:19:07 +08:00
ZyyandCodex 839054cdaf docs: refine GitHub reply tone
Co-authored-by: Codex <noreply@openai.com>
2026-10-31 21:09:49 +08:00
ZyyandCodex bcf0582814 refactor(peer): reuse account quota storage for CPR Link
Store peer snapshots in existing account quota fields and remove dedicated node and synchronization tables. Keep CPR protocol handling in Admin and native quota handling in Providers, sharing persistence and the quota refresh endpoint.

Use runtime refresh coordination and preserve credential, connection, and observation-time guards. Update the unreleased migration, API contracts, documentation, and regression coverage.

Validated with 354 selected tests, strict workspace Clippy, Rustfmt, frontend lint and build, and migration checksum verification. Browser interaction remains unverified.

Co-authored-by: Codex <noreply@openai.com>
2026-10-31 20:43:15 +08:00
ZyyandCodex a5d3d9f12d fix(peer): recover quota from explicit upstream budget state
Export authoritative budget access flags and keep monetary fields display-only
for peer consumers. Use the official usage_limit_reached error type for budget
exhaustion while preserving ordinary rate-limit cooldowns.

Apply fresh quota facts atomically with peer snapshots through the shared quota
writer, retaining connection, claim, and observation-time guards. Reuse the
existing sync worker and document the recovery contract.

Validated with 234 selected tests, strict workspace Clippy, and Rustfmt.

Co-authored-by: Codex <noreply@openai.com>
2026-10-31 15:58:02 +08:00
58a1f7541e feat(peer): add CPR Link discovery and scoped quota sharing (#566)
* feat(peer): add automatic discovery and scalable quota sharing

Export authorized direct-account pools and budgets through the CPR peer
protocol, with durable upstream discovery and immutable scheduling views.

Show shared-pool node quotas through common quota popovers and lazy account
details. Bound snapshot reads and export concurrency, reduce Redis and SQL
work, and keep only the latest observation for each upstream connection.

Co-authored-by: Codex <noreply@openai.com>

* refactor(peer): centralize upstream discovery and quota projections

Move peer synchronization into Admin, HTTP transport into Host, and expose
neutral Core scheduling signals through explicitly wired provider capabilities.

Preserve provider-visible model catalogs and independent quota disclosure,
with stale windows following the existing OAuth quota layout.

Cover cross-provider discovery, lifecycle fencing, and database leases, and
update the architecture and usage documentation.

Co-authored-by: Codex <noreply@openai.com>

* fix(ui): center usage card loading and empty states

Co-authored-by: Codex <noreply@openai.com>

* fix(accounts): unify peer subscription badges across account views

Share CPR identity rendering through AccountPlanBadge, project upstream metadata into dashboard and proxy account views, and align quota empty states and API badges.

Co-authored-by: Codex <noreply@openai.com>

* fix(peer): bound snapshot work and preserve quota windows

Keep summary reads off full snapshots, reject oversized model expansion early, and yield export work. Preserve daily and weekly unlimited budgets, hide empty token counters, and unify quota empty states.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-31 15:00:12 +08:00
ZyyandCodex 35e8b73075 fix(ui): center usage card loading and empty states
Co-authored-by: Codex <noreply@openai.com>
2026-10-31 13:03:30 +08:00
ZyyandCodex 73f0706f99 fix(deps): update follow-redirects to 1.16.2
Update the transitive Axios dependency to address GHSA-mrwp-f3q8-3g9p.

Co-authored-by: Codex <noreply@openai.com>
2026-10-31 12:11:14 +08:00
ZyyandCodex 76eecd771e fix(ui): correct account subscription and quota displays
Preserve authentication snapshots for diagnostic account badges and align
account note typography. Hide unavailable dashboard quotas consistently
and show available quota rates independently of local request counts.

Co-authored-by: Codex <noreply@openai.com>
2026-10-31 12:06:16 +08:00
Ke WangandCodex 7293d03d70 fix(models): separate reasoning summary capability from effort (#565)
Co-authored-by: Codex <noreply@openai.com>
2026-10-31 11:53:26 +08:00
ZyyandCodex 5ae38dc0c5 chore(release): v3.36.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-31 01:17:36 +08:00
ZyyandCodex 503e47fac2 fix(openai): restore upstream websocket keepalive pings
Send upstream Ping frames every 25 seconds without a Pong deadline.
Keep downstream cancellation, business idle timeout and pool eviction
independent from heartbeat traffic, with bounded heartbeat writes.

Co-authored-by: Codex <noreply@openai.com>
2026-10-31 01:09:05 +08:00
b4aea0ea09 fix(accounts): add missing model catalog fetch timestamps and capability hints (#560)
* fix(admin): clarify upstream model catalog evidence

* test(admin): cover model catalog timestamps over HTTP

Co-authored-by: Codex <noreply@openai.com>

* docs(api): define model catalog observation semantics

Co-authored-by: Codex <noreply@openai.com>

* fix(accounts): refine model catalog help and transport labels

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-10-30 23:15:05 +08:00
Ke WangandCodex 63036a3694 fix(accounts): preserve scheduling when batch editing (#563)
Co-authored-by: Codex <noreply@openai.com>
2026-10-30 22:52:55 +08:00
Mason MaandCodex a3e24198cd fix(openai): back off failed model catalog ETag refreshes (#561)
Return catalog refresh failures to the host supervisor so queued ETags use the existing restart backoff. Preserve failure causes and report recovery only after a successful refresh.

Co-authored-by: Codex <noreply@openai.com>
2026-10-30 22:45:28 +08:00
Yuqi Xiong fdd124e72f fix(backup): clean up probes after response body failures (#559) 2026-10-30 17:07:18 +08:00
3a2ccbc5e6 perf(usage): query insights by card and selected metric (#556)
* fix(usage): keep optional concurrency scans within a bounded budget

Skip unused concurrency work for Key usage and Dashboard trends. Preserve
other insights when exact concurrency exceeds its row, time, or slot budget.

Constraint: Preserve exact overlap semantics and one snapshot for usage metrics
Rejected: Add hourly peaks or truncate intervals | Neither preserves exact concurrency
Confidence: medium
Scope-risk: moderate
Directive: Keep unavailable concurrency distinct from a measured zero
Tested: Rust 1.99.1 rustfmt on changed Rust files; git diff --check; independent source review
Not-tested: PostgreSQL integration, Clippy, and frontend checks pending isolated CI
Co-authored-by: Codex <noreply@openai.com>

* refactor(analytics): limit concurrency changes to unused results

Keep Key usage and Dashboard skip behavior while retaining the existing concurrency calculation and query budget.

Co-authored-by: Codex <noreply@openai.com>

* perf(usage): query insight cards by selected metric

Split health, performance and cost reads by card and selected metric, with independent frontend requests and metric-aware caching. Only the load metric reads concurrency.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-10-30 15:09:24 +08:00
ZyyandCodex aa0ce52034 fix(openai): support Codex 166 catalogs and Guardian routing
Remove the per-response model catalog byte cap while keeping native
catalog cache residency bounded to 32 MiB.

Carry parent WebSocket routing cookies into Guardian reviews within the
same client key and OAuth account. Isolate reviewer sockets by routing
snapshot and preserve cookie expiry across pooled parent connections.

Co-authored-by: Codex <noreply@openai.com>
2026-10-30 12:28:54 +08:00
7e7acad6a3 fix(responses): reject interrupts without aborting HTTP fallback (#558)
Return response.interrupt.failed for supported interrupts when no upstream control channel is bound, so clients can drain the genuine response and continue. Preserve existing errors for unsupported controls and ambiguous sends.

Cover active, settling, and idle HTTP responses and queued request admission. Document the rejection contract while retaining the oversized request protection.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-30 12:17:09 +08:00
Yuqi XiongandCodex b3dd9b5337 fix(settings): preserve drafts after rejected saves (#551)
Keep unsaved settings and their revision when the management API rejects a
request with a structured 4xx response. Continue reconciling settings for
uncertain write outcomes, including transport failures and server errors.

Co-authored-by: Codex <noreply@openai.com>
2026-10-30 10:54:05 +08:00
Yuqi XiongandCodex 0e1c609cd9 fix(backup): paginate scheduled retention cleanup (#552)
Scan completed scheduled backups in bounded pages and evaluate retention
against their global positions. Collect a deletion batch before deleting
records so cleanup does not shift its own later page offsets.

Cover count and age thresholds beyond 1000 records and PostgreSQL page
ordering with tied completion times.

Co-authored-by: Codex <noreply@openai.com>
2026-10-30 10:43:11 +08:00
ZyyandCodex 12771aca87 fix(ui): align help icons, account notes, and settings navigation
Use transparent help triggers and keep note text styling stable.
Show notes in account dialogs and simplify upstream labels.
Keep settings navigation segmented on narrow screens.

Co-authored-by: Codex <noreply@openai.com>
2026-10-29 20:45:32 +08:00
ZyyandCodex 23b85172b5 refactor(openai): centralize compatibility and drop Grok overrides
Move request, header, and search-history compatibility into a private compat
module. Remove Grok-specific header filtering and prompt rewriting, with
matching contract tests and documentation.

Cover hosted web-search error transparency over HTTP and WebSocket without
introducing replay compensation.

Validation: 1,071 targeted tests passed; strict provider-openai Clippy,
Rustfmt, and diff checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-10-29 18:41:38 +08:00
ZyyandCodex d89db84a77 chore(release): v3.35.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-29 13:09:42 +08:00
ZyyandCodex 6504761ce4 fix(responses): keep slow websocket uploads alive
Track inbound byte progress before WebSocket decoding so active uploads do not trip the heartbeat timeout. Reuse Axum handshake validation and report actual receive idle time.

Cover successive slow uploads and stalled partial frames, and document the downstream liveness contract.

Validation: 652 API and gateway tests passed; production API check, strict workspace Clippy, and formatting passed.

Co-authored-by: Codex <noreply@openai.com>
2026-10-29 13:03:06 +08:00
ZyyandCodex 5877b0cb2f fix(openai): tolerate session affinity storage jitter
Raise the session affinity storage budget to 500ms while keeping cyber policy operations on a separate 100ms budget. Preserve fail-closed binding behavior and cover delayed admission and timeout rejection before upstream sending.

Refs #549

Co-authored-by: Codex <noreply@openai.com>
2026-10-29 12:04:09 +08:00
ZyyandCodex 958e13c370 fix(errors): standardize client messages and correct error mappings
Return non-retryable client version errors and correct provider error types.
Keep safe user messages separate from operational diagnostics.
Unify error wording and document the conventions in the development skill.

Co-authored-by: Codex <noreply@openai.com>
2026-10-29 01:57:31 +08:00
zyycnandCodex 80f7d53085 chore(release): v3.35.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-29 00:14:10 +08:00
2f2c1787be fix(responses): align heartbeat and websocket connection lifecycles (#548)
Use passive upstream heartbeats and independent downstream liveness checks.
Reclaim unbound sockets after five idle minutes and release client-owned
resources on disconnect, with safe continuation ownership transfer.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-29 00:08:40 +08:00
ZyyandCodex d679d5e708 fix(i18n): default to Simplified Chinese and simplify login labels
Co-authored-by: Codex <noreply@openai.com>
2026-10-28 21:50:52 +08:00
ZyyandCodex 0f214c3e25 docs: align guides and workflows with current behavior
Co-authored-by: Codex <noreply@openai.com>
2026-10-28 21:50:51 +08:00
736eca6593 feat(accounts): support configurable error protection rules (#547)
* feat(accounts): support configurable error protection rules

Replace capacity-specific freezing with bounded, configurable account error rules and shared attempt feedback.

Drop legacy settings without conversion and expose rule editing and freeze reasons in the admin UI.

Co-authored-by: Codex <noreply@openai.com>

* fix(accounts): preserve error protection scheduling boundaries

Keep xAI accounts excluded while recovery probes are pending, and cover
selection before and after recovery.

Separate scheduling cooldown reads from admin display details, share the
atomic Redis migration and selection logic, and use one policy revision.
Remove the unused adaptive concurrency reduction path and its tests.

Validation: 393 targeted backend tests, workspace Clippy with -D warnings,
Rustfmt, frontend type checking and build, and local service health checks.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-28 21:50:51 +08:00
ZyyandCodex be5b6d199e fix(api): enforce version gates for official Codex clients
Recognize official TUI, exec, IDE, SDK and App Server/MCP product names using the Core version in the User-Agent. Keep Desktop app version handling and unknown-client compatibility intact.

Compare SemVer precedence without build metadata, cover HTTP/SSE and real WebSocket admission, and align API documentation and localized settings help.

Validated 54 targeted and architecture tests, strict Clippy, Rust formatting, frontend lint/build, and localized component previews.

Co-authored-by: Codex <noreply@openai.com>
2026-10-28 19:31:20 +08:00
ZyyandCodex 53e1e9cfb8 fix(ui): show loading failures in notifications
Remove inline loading failure placeholders and dedicated retry actions from
settings, accounts, plugins and usage views. Route foreground failures through
existing notifications while preserving disabled controls and available data.
Clean up obsolete error props, state and translations.

Co-authored-by: Codex <noreply@openai.com>
2026-10-28 19:11:55 +08:00
蓝芷怡andCodex e03851e23f fix(dashboard): show notes below API key accounts (#545)
* fix(dashboard): show notes below API key accounts

Co-authored-by: Codex <noreply@openai.com>

* fix(frontend): complete dashboard theme preview account notes

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
2026-10-28 18:55:57 +08:00
7cd17aff42 feat(usage): add request concurrency trends (#537)
* feat(usage): add request concurrency trends

* perf(usage)!: unify load insights and optimize concurrency

Aggregate concurrency on a single event timeline and reuse lifecycle
indexes instead of joining every request against every time bucket.

Align load summaries and trend points with the four chart metrics,
combine concurrency and scheduling in one view, and right-align the
statistics card selectors. Update the API contract and regression tests.

BREAKING CHANGE: Usage insights concurrency and scheduling P95 fields
move under performance.load and performance.points[].load. Unused
scheduling P50, capacity, and performance coverage fields are removed.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-28 17:22:28 +08:00
005466bcb0 feat(api): support Chat Completions through Responses (#542)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-28 15:15:31 +08:00
ZyyandCodex a6031310f1 fix(openai): exclude free accounts from live selection
Centralize account authentication, plan and model eligibility for Images and Live. Reuse the same checks before leases, affinity and retries, while preserving unknown plans and Free text/search access.

Co-authored-by: Codex <noreply@openai.com>
2026-10-28 13:12:01 +08:00
niceSprite 1ac2a4eac3 fix(logging): preserve warnings after disabled target probes (#539)
* fix(logging): preserve warnings after disabled target probes

* docs: keep logging fix focused on runtime behavior
2026-10-28 12:52:25 +08:00
ZyyandCodex 790a643020 fix(frontend): hide general quota heading in account details
Co-authored-by: Codex <noreply@openai.com>
2026-10-28 11:22:30 +08:00
ZyyandCodex 1fa37ac92f fix(openai): observe reasoning effort updates across continuations
Resolve reasoning effort from configuration updates and preserve overrides in response-scoped session metadata without rewriting upstream requests. Cover HTTP and WebSocket continuations, new-chain resets, and baseline changes.

Co-authored-by: Codex <noreply@openai.com>
2026-10-28 11:04:29 +08:00
ZyyandCodex 0ceac908f4 fix(ui): route Codex voice control through configured gateway
Include the realtime WebSocket base URL in generated Codex configuration and document voice setup. Simplify configuration comments.

Track key reveal actions separately so opening client configuration and copying a key show loading only on the initiating button. Preserve duplicate request protection and stale request cancellation.

Co-authored-by: Codex <noreply@openai.com>
2026-10-28 00:19:13 +08:00
ZyyandCodex 86a27bed94 fix(ui): include model catalog URL in Codex config
Enable remote model discovery for custom Codex providers so upstream catalog updates appear in the model picker.

Co-authored-by: Codex <noreply@openai.com>
2026-10-27 18:22:53 +08:00
ZyyandCodex 60f8f1ee80 refactor(plugins)!: remove model catalog v1 support
Keep direct aliases in v2 and update the SDK builder, runtime compatibility declaration, tests and migration guidance.

BREAKING CHANGE: model_catalog plugins must declare version 2, move their alias list to aliases and return models for adapted models.

Co-authored-by: Codex <noreply@openai.com>
2026-10-27 17:40:13 +08:00
ZyyandCodex 7b6a204889 docs(release): restore account routing advisory
Co-authored-by: Codex <noreply@openai.com>
2026-10-27 17:13:54 +08:00
ZyyandCodex 47d30474af chore(release): v3.34.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-27 16:48:35 +08:00
ZyyandCodex f1d5b73255 fix(ui): refine admin filters, quota colors and batch actions
Simplify group filters and keep date and platform controls aligned. Apply quota warning thresholds independently to each window and move account batch actions into a compact menu.

Co-authored-by: Codex <noreply@openai.com>
2026-10-27 16:20:15 +08:00
ZyyandCodex 18c75132c3 fix(openai): add built-in GPT-6.1 Terra pricing
Include published standard, flex, priority, and long-context prices in the built-in catalog and classify Terra as a reasoning model for preview web search billing.

Co-authored-by: Codex <noreply@openai.com>
2026-10-27 16:20:15 +08:00
ZyyandCodex a48139b4c2 fix(store): support literal substring search in admin lists
Match account names and emails, client key names and labels, and observability search fields by literal substrings. Preserve wildcard escaping and existing case rules.

Co-authored-by: Codex <noreply@openai.com>
2026-10-27 16:20:15 +08:00
蓝芷怡andCodex dd529d07ec fix(store): support pg_dump staging files on Windows (#475)
Co-authored-by: Codex <noreply@openai.com>
2026-10-27 15:13:13 +08:00
19fbb5da75 feat(plugin): support account-scoped adapted model catalogs (#533)
* feat(plugin): publish account-scoped adapted model catalogs

* fix(core): preserve candidate account scope during execution

Apply the narrowed candidate scope before provider account selection, including affinity and account rotation. Cover adapted-only models and same-name models with incompatible tool capabilities through the real OpenAI selector and local HTTP fixtures.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-27 13:37:57 +08:00
ZyyandCodex 9296885cce fix(tests): align middleware fixture with startup compatibility
Co-authored-by: Codex <noreply@openai.com>
2026-10-27 13:17:38 +08:00
Ke WangandCodex af9790ac37 fix(plugin): prove replay safety for managed HTTP 429 rejections (#532)
Seal managed callback observations before Core retry decisions and synchronize RPC resource revocation with stream teardown.

Co-authored-by: Codex <noreply@openai.com>
2026-10-27 13:12:42 +08:00
Ke Wang c58e280739 fix(deps): update zstd safety wrappers (#534)
Preserve Zstd 1.5.7 and compression level 3 while updating the Rust wrappers to resolve security advisories.
2026-10-27 12:53:00 +08:00
ZyyandCodex e33f50ec09 fix(plugins): gate incompatible plugin startup
Keep incompatible plugins out of execution sets during automatic startup.
Require scoped confirmation for manual compatibility overrides and show
the stopped state and upgrade effects in the management UI.

Co-authored-by: Codex <noreply@openai.com>
2026-10-27 12:37:28 +08:00
ZyyandCodex 1db8b0e319 chore(deps): update plugins to v0.8.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-26 19:19:56 +08:00
ZyyandCodex 8c8618aa0f chore(release): v3.33.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-26 18:44:00 +08:00
c43a8872db fix(frontend): highlight selected models missing upstream (#528)
* fix(frontend): highlight unavailable selected models

* fix(frontend): refine model catalog warnings and refresh control

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-26 18:39:08 +08:00
ZyyandCodex 51c5caed0e fix(deploy): disable account routing override in example config
Co-authored-by: Codex <noreply@openai.com>
2026-10-26 18:00:53 +08:00
a7f95c3a78 fix(openai): preserve Retry-After when converting WS errors to SSE (#531)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-26 17:08:57 +08:00
ZyyandCodex 421e37896a fix(tests): avoid RPC admission callback deadlock
Drive the ordinary callback call to completion before consuming its notification so delayed suspension cannot strand an unpolled admission future. Bound next callback startup and resumption waits.

Co-authored-by: Codex <noreply@openai.com>
2026-10-26 17:06:45 +08:00
1d1ccda42a refactor(plugins): consolidate host lifecycle and diagnostics (#529)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-26 17:06:44 +08:00
e38cf31323 refactor(plugins): separate client identity from model execution ports (#527)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-26 13:21:31 +08:00
614c81c375 refactor(plugins)!: separate identity queries and cooperative scheduling (#525)
Resolve identity and frozen access facts without constructing model execution
contexts. Coordinate parent task suspension, resumption and retained callbacks
at the SDK and Runtime boundary, with bounded admission and cancellation.
Expose account HTTP, client verification, access facts and quota details through
the existing host owners.

BREAKING CHANGE: Plugin sessions require RPC v7; rebuild and repackage plugins
with the matching SDK. No legacy protocol negotiation or fallback is provided.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-26 13:05:08 +08:00
f0484c4e11 chore(frontend): upgrade ESLint config to v10 and migrate formatting (#522)
* chore(deps-dev): bump @antfu/eslint-config in /frontend

Bumps [@antfu/eslint-config](https://github.com/antfu/eslint-config) from 9.5.1 to 10.0.0.
- [Release notes](https://github.com/antfu/eslint-config/releases)
- [Commits](https://github.com/antfu/eslint-config/compare/v9.5.1...v10.0.0)

---
updated-dependencies:
- dependency-name: "@antfu/eslint-config"
  dependency-version: 10.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(frontend): migrate formatting to ESLint v10

Co-authored-by: Codex <noreply@openai.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-26 11:34:32 +08:00
dependabot[bot] dffc417b9f chore(deps): bump rust from e12566d to 48b7f43 in /deploy (#524)
Bumps rust from `e12566d` to `48b7f43`.

---
updated-dependencies:
- dependency-name: rust
  dependency-version: 1.99.1-bookworm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 11:04:28 +08:00
dependabot[bot] 2d4294c3c9 chore(deps): bump debian from 7c7b2c9 to 7ff6ba3 in /deploy (#523)
Bumps debian from `7c7b2c9` to `7ff6ba3`.

---
updated-dependencies:
- dependency-name: debian
  dependency-version: bookworm-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 11:04:23 +08:00
dependabot[bot] 3d9906facf chore(deps): bump the minor-and-patch group in /frontend with 5 updates (#520)
Bumps the minor-and-patch group in /frontend with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.59.0` | `1.61.0` |
| [es-toolkit](https://github.com/toss/es-toolkit) | `1.52.0` | `1.53.0` |
| [eslint-plugin-vuejs-accessibility](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility) | `2.6.2` | `2.6.3` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.3.4` | `8.3.5` |
| [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) | `3.3.11` | `3.3.13` |


Updates `@lucide/vue` from 1.59.0 to 1.61.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.61.0/packages/vue)

Updates `es-toolkit` from 1.52.0 to 1.53.0
- [Release notes](https://github.com/toss/es-toolkit/releases)
- [Changelog](https://github.com/toss/es-toolkit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/toss/es-toolkit/compare/v1.52.0...v1.53.0)

Updates `eslint-plugin-vuejs-accessibility` from 2.6.2 to 2.6.3
- [Release notes](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/releases)
- [Changelog](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/compare/v2.6.2...v2.6.3)

Updates `vite` from 8.3.4 to 8.3.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.3.5/packages/vite)

Updates `vue-tsc` from 3.3.11 to 3.3.13
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.3.13/packages/tsc)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.61.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: es-toolkit
  dependency-version: 1.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: eslint-plugin-vuejs-accessibility
  dependency-version: 2.6.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: vite
  dependency-version: 8.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: vue-tsc
  dependency-version: 3.3.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 11:04:18 +08:00
dependabot[bot] da9c2b92b0 chore(deps): bump jsonschema from 0.58.6 to 0.60.0 in /backend (#519)
Bumps [jsonschema](https://github.com/Stranger6667/jsonschema) from 0.58.6 to 0.60.0.
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/ruby-v0.58.6...ruby-v0.60.0)

---
updated-dependencies:
- dependency-name: jsonschema
  dependency-version: 0.60.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 11:02:43 +08:00
dependabot[bot] de5c10c4e4 chore(deps): bump scraper from 0.27.0 to 0.29.0 in /backend (#518)
Bumps [scraper](https://github.com/rust-scraper/scraper) from 0.27.0 to 0.29.0.
- [Release notes](https://github.com/rust-scraper/scraper/releases)
- [Commits](https://github.com/rust-scraper/scraper/compare/v0.27.0...v0.29.0)

---
updated-dependencies:
- dependency-name: scraper
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 11:02:33 +08:00
dependabot[bot] 939cb99985 chore(deps): bump the minor-and-patch group in /backend with 7 updates (#517)
Bumps the minor-and-patch group in /backend with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [zip](https://github.com/zip-rs/zip2) | `9.0.4` | `9.0.5` |
| [async-compression](https://github.com/Nullus157/async-compression) | `0.4.50` | `0.4.51` |
| [aws-credential-types](https://github.com/smithy-lang/smithy-rs) | `1.3.0` | `1.3.1` |
| [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust) | `1.152.0` | `1.155.0` |
| [aws-smithy-runtime-api](https://github.com/smithy-lang/smithy-rs) | `1.19.0` | `1.19.1` |
| [aws-smithy-types](https://github.com/smithy-lang/smithy-rs) | `1.8.1` | `1.9.0` |
| [crc32fast](https://github.com/srijs/rust-crc32fast) | `1.5.2` | `1.5.3` |


Updates `zip` from 9.0.4 to 9.0.5
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v9.0.4...v9.0.5)

Updates `async-compression` from 0.4.50 to 0.4.51
- [Release notes](https://github.com/Nullus157/async-compression/releases)
- [Commits](https://github.com/Nullus157/async-compression/compare/async-compression-v0.4.50...async-compression-v0.4.51)

Updates `aws-credential-types` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `aws-sdk-s3` from 1.152.0 to 1.155.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `aws-smithy-runtime-api` from 1.19.0 to 1.19.1
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `aws-smithy-types` from 1.8.1 to 1.9.0
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `crc32fast` from 1.5.2 to 1.5.3
- [Commits](https://github.com/srijs/rust-crc32fast/compare/v1.5.2...v1.5.3)

---
updated-dependencies:
- dependency-name: zip
  dependency-version: 9.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: async-compression
  dependency-version: 0.4.51
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-credential-types
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-sdk-s3
  dependency-version: 1.155.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-runtime-api
  dependency-version: 1.19.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-types
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: crc32fast
  dependency-version: 1.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 11:02:21 +08:00
dependabot[bot] 2bb8ebc01c chore(deps): bump the minor-and-patch group with 3 updates (#516)
Bumps the minor-and-patch group with 3 updates: [Swatinem/rust-cache](https://github.com/swatinem/rust-cache), [taiki-e/install-action](https://github.com/taiki-e/install-action) and [docker/login-action](https://github.com/docker/login-action).


Updates `Swatinem/rust-cache` from 2.9.2 to 2.9.3
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/swatinem/rust-cache/compare/6323deb102c322ba6fcbdcafc7e3dddab59af2b6...d1527c8f67ef9af357290475d4af4b0fab8f12e0)

Updates `taiki-e/install-action` from 2.87.29 to 2.87.31
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/8cca0918f8471c33515202b73f56ab27f2bed283...1e29709c0ec63babbe8f266397a465f29a825f91)

Updates `docker/login-action` from 4.6.0 to 4.7.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/dbcb813823bdd20940b903addbd779551569679f...81c6f93d4bc991e510b342a00b419b3bb4efc5c6)

---
updated-dependencies:
- dependency-name: Swatinem/rust-cache
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: taiki-e/install-action
  dependency-version: 2.87.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: docker/login-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 11:01:07 +08:00
ZyyandCodex 6a5300a7b5 chore(release): v3.32.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 23:34:39 +08:00
2c5b8b5c1a feat(openai): configure residency and account routing headers (#515)
Accept optional deployment strings for residency and account routing,
discard downstream values when unset, and isolate WebSocket connections
by the configured handshake headers.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 23:19:11 +08:00
0a87ac7f76 fix(plugins)!: honor declared stage call budgets (#513)
Resolve scenario defaults and plugin declarations consistently across the SDK
and runtime. Preserve maintenance across unrelated publications and cancel
long preparation when the runtime shuts down.

BREAKING CHANGE: Plugin RPC requires protocol v6. Rebuild plugins with the
updated SDK; previous protocol versions are not supported.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 22:39:23 +08:00
ZyyandCodex 3ac9d5f6f8 ci: keep repository checks independent
Remove automatic cross-repository source integration from host CI while
preserving host checks against locked dependencies. Keep source integration
available for explicit local version combinations and align the documentation.

Co-authored-by: Codex <noreply@openai.com>
2026-10-25 22:34:48 +08:00
ZyyandCodex e7e5b659cd chore(release): v3.31.3
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 17:31:19 +08:00
ZyyandCodex b225b3732d fix(accounts): unify Codex labels and tighten plan badge spacing
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 17:28:06 +08:00
ZyyandCodex 9dca763ea4 fix(ui): polish plugin management and API key labels
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 16:57:42 +08:00
ZyyandCodex d26bc36db5 chore(release): v3.31.2
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 15:59:42 +08:00
ZyyandCodex 96d5ab674a feat(ui): refine forms, navigation and theme loading
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 15:59:34 +08:00
ZyyandCodex 84c0db2613 fix(ui): simplify token label and center reasoning effort
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 04:58:06 +08:00
ZyyandCodex d819315d8b fix(ci): allow independent UI dependency versions
Keep frozen installs, source compatibility builds, and lockfile checks
without requiring every consumer to pin the UI submodule revision.

Co-authored-by: Codex <noreply@openai.com>
2026-10-25 04:48:28 +08:00
ZyyandCodex 06e1d8f0ac chore(release): v3.31.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 04:40:32 +08:00
ZyyandCodex bdc8274b36 chore(ui): upgrade component library to v0.6.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 04:38:49 +08:00
ZyyandCodex 3f6607209b refactor(usage): remove reported model tracking and polish UI
Remove upstream response model tracking across providers, storage, APIs,
and plugin facts without migrating historical records.

Refine sidebar hints, locale loading indicators, model columns, and usage
page labels and filters.

Co-authored-by: Codex <noreply@openai.com>
2026-10-25 04:03:27 +08:00
ZyyandCodex 3c05bc8f07 refactor(plugins): remove provider-specific request parameters
Use request, attempt, and outbound middleware for plugin-owned identity
selection and complete request rewriting. Preserve native account identity
and partition WebSocket connections through existing outbound opening changes.

Remove the construction parameter channel and its obsolete tests and docs.
Keep middleware at v6 without a compatibility shim.

Co-authored-by: Codex <noreply@openai.com>
2026-10-25 03:30:52 +08:00
ZyyandCodex 9ccd84f1a0 test(store): fix pg_dump lifecycle probe on Ubuntu 26.04
Redirect the mock process stdout to /dev/null explicitly so the wait-stage probe works when sleep reopens closed standard descriptors. Preserve cancellation, process reaping, and partial archive cleanup assertions.

Co-authored-by: Codex <noreply@openai.com>
2026-10-25 01:35:59 +08:00
ZyyandCodex a8f183707e chore(build): refresh base images and pin Ubuntu runners
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 01:02:46 +08:00
ZyyandCodex 3f20af61ff chore(plugins): update workbench to v0.7.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 00:23:55 +08:00
ZyyandCodex efaed4c3a6 fix(web): refine health loading and quota labels
Hide request health granularity until analytics are loaded and remove the placeholder daily granularity. Use compact uppercase quota badges and localized five-hour labels.

Co-authored-by: Codex <noreply@openai.com>
2026-10-25 00:08:42 +08:00
ZyyandCodex a36137bafb chore(release): v3.31.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-24 23:22:06 +08:00
2e7a39e1e8 feat(plugins): add provider request composition middleware (#511)
* feat(plugins): add provider request composition middleware

Expose per-attempt provider parameters and a final outbound preparation chain using middleware v6. Preserve native transport behavior when no outbound scope matches.

Co-authored-by: Codex <noreply@openai.com>

* fix(plugins): sync workbench with middleware v6

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-24 22:58:06 +08:00
ZyyandCodex 6648198df4 perf(responses): reduce WebSocket copies and trace completion latency
Move owned request bodies back into WebSocket requests and forward validated
UTF-8 frames without copying their payloads. Record quota, account feedback,
and cyber policy durations while preserving terminal delivery ordering.

Co-authored-by: Codex <noreply@openai.com>
2026-10-24 21:05:22 +08:00
ZyyandCodex 2eed9b20ed fix(web): persist table sorting preferences
Co-authored-by: Codex <noreply@openai.com>
2026-10-24 20:54:37 +08:00
16aa08091c feat(plugins): select upstream adapters by account (#509)
* feat(plugins): select upstream adapters within account scopes

* fix(plugins): defer adapter decisions until account selection

Keep scoped adapters pending until an account is known so unavailable
fallbacks cannot mask a healthy earlier match. Filter native xAI accounts
when only an adapter can accept the request, preserving normal selection
and cold-stream behavior.

Cover fallback precedence and mixed-pool prewarm eligibility regressions.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-24 20:20:47 +08:00
ZyyandCodex abae4a631b fix(skills): enforce required domain guidance
Co-authored-by: Codex <noreply@openai.com>
2026-10-24 19:53:37 +08:00
8983eb4ce9 fix(openai): preserve session affinity for managed upstream adapters (#508)
* fix(openai): preserve session affinity for managed upstream adapters

* test(openai): avoid coupling wire invariance to adapter lookup count

* fix(openai): preserve plugin control over adapter request identity

Keep affinity and turn aliases tied to the admitted request before attempt middleware. Pass plugin rewrites to the adapter without native identity revalidation.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-24 19:51:48 +08:00
ZyyandCodex cd605b850e chore(deps): pin UI dependency to v0.6.0 release tag
Reference the v0.6.0 release tag and synchronize the pnpm lockfile.
The verified remote tag resolves to the existing UI commit.

Co-authored-by: Codex <noreply@openai.com>
2026-10-24 19:02:45 +08:00
ZyyandCodex 7beebb2340 fix(deps): patch crypto-bigint and tokio security advisories
Update crypto-bigint to 0.7.6 for RUSTSEC-2026-0387 and Tokio to
1.53.3 for RUSTSEC-2026-0386. Preserve the exact Tokio constraint
and verify existing transport fingerprint and cancellation contracts.

Co-authored-by: Codex <noreply@openai.com>
2026-10-24 19:02:41 +08:00
ZyyandCodex 31de0bd83c perf(store): move client budget settlement off request path
Track pending charges in memory and settle them with a supervised worker.
Include pending costs in budget checks without retrying historical writes
at admission, and reject new admissions when the backlog reaches its limit.

Use idempotent per-key batches with write deadlines, adaptive batch sizes,
and bounded shutdown draining. Keep load tests outside the regular suite.

Co-authored-by: Codex <noreply@openai.com>
2026-10-24 18:45:12 +08:00
ZyyandCodex 5cfc6dee10 fix(openai): abort stalled websocket pumps during error cleanup
Stop the send pump before returning terminal errors or discarding an
exchange. Preserve socket cleanup ordering and cover stalled data and
control writes.

Co-authored-by: Codex <noreply@openai.com>
2026-10-24 18:45:04 +08:00
ZyyandCodex ef5b4d7dbd fix(openai): reuse reasoning content registrations
Honor reasoning summary indexes and reuse registered content parts while
preserving upstream wire events and type conflict diagnostics.

Co-authored-by: Codex <noreply@openai.com>
2026-10-24 18:45:00 +08:00
ZyyandCodex 31974bc289 fix(diagnostics): align trace snapshot persistence limits
Co-authored-by: Codex <noreply@openai.com>
2026-10-24 14:19:22 +08:00
ZyyandCodex e033e21994 fix(frontend): remove default concurrency input icon
Co-authored-by: Codex <noreply@openai.com>
2026-10-24 00:05:43 +08:00
ZyyandCodex 2818edb9fc perf(i18n): load language resources on demand
Load and cache the selected language with synchronized UI messages. Keep the latest selection when loads overlap and render missing messages as empty text without language fallback.

Co-authored-by: Codex <noreply@openai.com>
2026-10-24 00:05:38 +08:00
ZyyandCodex 1d1f34ba7e fix(frontend): refine admin copy and responsive layouts
Shorten contextual actions and loading messages while preserving explanatory text. Improve quota metadata, profile metrics, theme controls and usage layouts, and add collapsed navigation hints.

Use spaced lowercase time units and remove chart tooltip dividers.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 23:10:15 +08:00
ZyyandCodex 76291f448c chore(release): v3.30.2
Co-authored-by: Codex <noreply@openai.com>
2026-10-23 21:47:55 +08:00
Zyy c10efea12c perf(dashboard): deduplicate histogram samples and trim unused trend fields
Group identical positive samples before numeric logarithm evaluation while retaining histogram boundaries and weights. Remove nine unused trend fields and synchronize frontend types, preview data, and API documentation.

Validate against the 30-million-row Docker dataset, matching retained statistics and reducing uncached today-summary median latency from 623 ms to 319 ms. Run 133 related tests, Clippy, formatting, frontend checks, and browser tab checks.
2026-10-23 21:06:12 +08:00
ZyyandCodex 806f7ad9ee fix(i18n): preserve server-authoritative relative times
Keep dashboard and quota relative-time thresholds and future timestamps owned by the backend deployment timezone. Localize only the structured presentation in the frontend.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 21:06:12 +08:00
ZyyandCodex 632edee7dc fix(frontend): localize account metrics and polish responsive layouts
Move account metric and relative-time presentation to the frontend using raw API values. Tighten latency unit spacing and unify edit actions and page header sizing.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 21:06:12 +08:00
ZyyandCodex 22518d064c chore(deps): sync UI and plugin release references
Track UI v0.6.0 and Plugins v0.6.1 release commits.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 21:06:12 +08:00
ZyyandCodex 64c1ec2afd chore(release): v3.30.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-23 19:05:30 +08:00
ZyyandCodex a3f0c1b028 fix: correct localized UI and update hyper-util
Restore quota labels, custom window durations, account usage panel width,
and consistent USD symbols. Return stable provider identity attribute keys
for frontend localization and refine login controls, copy, and alignment.

Pin hyper-util to the 0.1.21 registry release and remove the Git patch.

Tests were not rerun for this combined commit, as requested.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 19:02:48 +08:00
ZyyandCodex 605354c861 chore(release): v3.30.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-23 17:33:23 +08:00
ZyyandCodex 60e6bc0125 fix(deps): update audited cryptographic dependencies
Upgrade ctutils to 0.4.5 for RUSTSEC-2026-0385 and move chacha20 and wnaf off yanked releases.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 17:26:00 +08:00
ZyyandCodex bfea3e1d6f feat(frontend): unify API key import dialog
Route API key imports through a shared CC Switch dialog with a GitHub link. Simplify key creation actions and narrow the action menu.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 17:25:56 +08:00
ZyyandCodex 6b4b8faaf4 fix(openai): preserve client turn identifiers
Preserve turn IDs and parent references across request metadata, input history, headers, Images and Desktop context. Update contract coverage and API documentation for unchanged turn identity across accounts.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 15:59:19 +08:00
ZyyandCodex f195de38f5 fix(openai): isolate Live correlation and preserve business headers
Forward Live business headers through shared identity and transport filters,
preserving HTTP header bytes and repeated values. Scope tracking identifiers
to the selected or pinned account, remove client tracing and account-bound
metadata, and retain Live call ownership.

Preserve x-session-id and voice-session-id as business session references,
consistent with Codex 0.164.0. Validate standard HTTP header names in Core
and document the distinction between tracking and business identifiers.

Validation: 36 targeted nextest checks passed. Real-account checks through
the production Provider and LiveGateway completed SDP creation (201), WebRTC
connection and session.started, sideband events, and hangup (200), including
header-only business identifiers. Bidirectional RTP traffic was observed.
The deployed HTTP/UI path and speech recognition or spoken replies were not
validated; the earlier Voice session access denied error was not reproduced.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 15:59:19 +08:00
ZyyandCodex 07bdd425b6 fix(openai): preserve historical dates during location overrides
Keep client context dates stable across midnight, continuation and replay
while retaining timezone and web search location overrides. Remove wall-clock
dependence from both request encoding and selected-account adaptation.

Cover appended day contexts, replay, and HTTP/WebSocket upstream payloads;
update the request-location contract. The regression fails before the fix.
Provider tests (1028), architecture checks (14), workspace strict Clippy,
Rustfmt and diff checks pass.

Refs #503; the reported repeated zero-cache requests are not yet confirmed
to share this cause.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 12:27:02 +08:00
ZyyandCodex d48982eb04 fix(frontend): refine dashboard copy and summary value size
Co-authored-by: Codex <noreply@openai.com>
2026-10-23 12:24:21 +08:00
39b35d46a0 feat(frontend): add reactive localization and refine admin layouts (#501)
* feat(frontend): add English and Traditional Chinese locales

* fix(frontend): capitalize the English login key label

* fix(frontend): capitalize standalone English labels

* feat(frontend): add reactive localization and refine admin layouts

Replace DOM text translation with domain-based Vue I18n resources for English, Simplified Chinese, and Traditional Chinese. Localize shared controls, notifications, charts, and key usage while refining table alignment, menus, and compact labels.

Expose semantic usage-window and trend-metric identifiers from the API. Pin the UI and workbench dependencies to published feature commits and preserve the original font defaults.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Jimbb <14995924+jimbb@users.noreply.github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-23 11:40:38 +08:00
8fc8c6228d fix(openai): avoid invalidating accounts on streaming 404 errors (#506)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-23 11:34:58 +08:00
Zyy 7da79b82fd fix(deps): align UI pin and update DOMPurify across frontend and workbench 2026-10-23 11:07:24 +08:00
ZyyandCodex a23afbf2f5 perf: streamline usage queries and batch account reads
Use literal contains filters for usage names and models, align Key Usage filtering, and remove unused usage query parameters. Optimize summary aggregation, record counting, quota reads, and credential cooldown lookups without adding indexes.

Co-authored-by: Codex <noreply@openai.com>
2026-10-23 01:08:57 +08:00
ZyyandCodex b4a5210e44 test: streamline behavior coverage and parallel execution
Remove redundant contract cases and express fixtures in terms of current behavior.
Reduce isolated database migration contention, run independent scenarios concurrently,
and replace fixed waits with clock and I/O synchronization. Check absolute Redis
session expiry and wait for PostgreSQL migration session locks to be released.
Document nextest for local test execution.

Validated 4060 tests, 70 Redis ACL tests, and five repeat runs of 26 focused tests.
The 10 plugin state tests also pass 50 repeat runs with four workers.
Rustfmt and strict Clippy pass. Local full-suite execution with 16 workers falls
from 162.760s to 89.072s, excluding compilation.

Co-authored-by: Codex <noreply@openai.com>
2026-10-22 23:30:38 +08:00
Zyy f7f59103a3 fix: harden request lifecycles and align repository documentation
Preserve request evidence and cold middleware boundaries across cancellation. Make backup auditing atomic, retain committed account-group results, and serialize restart operations.

Unify frontend stream authentication and session recovery, clear stale secrets, and remove redundant runtime paths. Correct CI permissions and database test URL handling.

Align documentation with current module responsibilities, APIs, and validation workflows, including both submodules.
2026-10-22 22:06:01 +08:00
ZyyandCodex 5b77a6b8de docs: make GitHub replies more considerate
Explain how to address the reported scenario, offer practical next steps,
and clarify limits with a cooperative tone. Keep shared guidance centralized
in CONTRIBUTING.md and review each reply from the questioner's perspective.

Co-authored-by: Codex <noreply@openai.com>
2026-10-22 19:35:34 +08:00
ZyyandCodex 3f95f8b0c4 feat(diagnostics): preserve evidence and export ZIP bundles
Capture bounded request fingerprints and terminal usage without rewriting payloads.
Reserve critical trace events and report capture gaps centrally.
Export sanitized ZIP bundles with summaries through native browser downloads.
Simplify account export filenames.

Co-authored-by: Codex <noreply@openai.com>
2026-10-22 19:35:30 +08:00
ZyyandCodex 28fa655c26 fix(openai): scope input turns and preserve executable paths
Map input turn references with the same account identity as request metadata.
Limit the workspace preset to metadata usernames and remote namespaces;
preserve project names and executable paths to avoid path failures and
same-parent workspace collisions.

Co-authored-by: Codex <noreply@openai.com>
2026-10-22 00:02:57 +08:00
ZyyandCodex 7a60bc6ded fix(openai): isolate account correlation identities
Scope outgoing session, turn, parent, window and cache identifiers to the selected account across HTTP, WebSocket, Desktop context and Search. Strip client trace propagation and forward account-scoped image turn metadata.

Co-authored-by: Codex <noreply@openai.com>
2026-10-21 18:29:52 +08:00
ZyyandCodex a5b767e69b chore(deps): pin plugins v0.6.0
Sync the workbench release using host v3.29.7 protocol v5.

Co-authored-by: Codex <noreply@openai.com>
2026-10-21 11:48:01 +08:00
ZyyandCodex 78bc57c5cf chore(release): v3.29.7
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 11:23:31 +08:00
ef11b72d8a fix(workers): recover daemon health after successful work (#499)
Report per-run daemon progress to reset health failures and restart backoff
after successful work, with stale-run isolation and recovery regression tests.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 11:08:15 +08:00
ZyyandCodex 52fb1e7180 fix(deps): update vulnerable redirect and TLS dependencies
Update follow-redirects, rustls-webpki, and webpki-roots to patched releases so frontend and backend security audits pass.

Co-authored-by: Codex <noreply@openai.com>
2026-10-21 10:47:57 +08:00
ZyyandCodex 7a7a7ed6ad fix(openai): avoid full MSIX scans during version checks
Use the known MSIX archive offset to keep Windows Desktop version checks within the range download budget. Cover both architectures with a bounded-read regression test.

Co-authored-by: Codex <noreply@openai.com>
2026-10-21 10:33:18 +08:00
b8479b59db fix(plugins): preserve request origin and raw upstream observations (#497)
* fix(plugins): preserve request origin and raw upstream observations

Co-authored-by: Codex <noreply@openai.com>

* perf(plugins): share immutable response observations

Avoid copying response metadata and header collections for every stream event. Update Workbench byte headers and virtual-time timeout coverage, and align the large-request fixture with RPC v5.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 01:09:58 +08:00
2480f07f2d fix(plugins): honor configured model and HTTP request body limits (#496)
* fix(plugins): honor configured request body limits

* test(plugin-sdk): mirror upstream adapter module layout

Co-authored-by: Codex <noreply@openai.com>

* fix(plugins): preserve frozen request body budgets across callbacks

Carry the execution body budget through policy and attempt callbacks, and validate explicit-key model requests against their bound snapshot. Add regression coverage for configuration publication and document budget ownership and lifetime.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-10-21 00:51:13 +08:00
niceSprite b2dc9f493d fix(plugins): retain request middleware resource streams (#492) 2026-10-20 23:09:06 +08:00
d92d310e45 fix(accounts): select the bound outbound proxy by default (#493)
Expose the stored proxy ID through account queries and use it to display
the bound proxy name without matching redacted endpoints. Preserve
unchanged bindings and keep the preserve option for batch editing.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 23:02:51 +08:00
ZyyandCodex 3caf83131a docs: refine GitHub reply tone
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 20:48:35 +08:00
ZyyandCodex af35cde374 chore(release): v3.29.6
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 18:44:26 +08:00
5c115fb38f fix(core): reserve rotation budget during connection recovery (#487)
* fix(retry): rotate accounts after connection failures

Reliable NotSent HTTP connection failures stay on the same account and
egress until the shared 30-second opening window is exhausted. When an
account's proxy does not respond, each opening waits for the full
15-second connect timeout on the same route, so requests that selected
that account fail after about 30 seconds while other accounts stay idle.

After a connection failure, exclude the account and reselect through
the existing rotation path, consuming routing and rotation budget and
sharing the same opening window. Rotation happens after the first
failure because a second same-account opening on an unresponsive
egress would leave too little of the window to rotate. If no other
account is eligible, restore the exclusion and continue the original
same-account HTTP recovery, so single-account deployments keep their
current behavior. Required accounts and continuation attempts are
unchanged.

Refs #485

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(retry): retain same-account connection recovery

Keep existing account rotation rules and cover rotation after a recovered
connection receives a replay-safe rate-limit rejection.

Co-authored-by: Codex <noreply@openai.com>

* fix(core): reserve connection budget for account rotation

Retry the original account before rotating on unsent connection failures.
Split the retry window and retain an opening for HTTP recovery on another
account, with bounded fallback when no alternative can be selected.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 17:42:42 +08:00
ed558e5396 fix(plugins): decouple upstream streams from initial deadlines (#489)
Transfer upstream stream lifetime to the model request after the initial RPC reply and give managed HTTP body reads an operation budget. Cover heartbeat continuity, cancellation cleanup, and per-read timeouts.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-20 17:17:28 +08:00
ZyyandCodex 929f781a63 perf(store): slim business backups and accelerate history cleanup
Export business data and published hourly usage from a shared snapshot as
bounded gzip SQL archives, excluding request history and diagnostic data.
Run bounded retention cycles every minute, deleting oldest unlocked rows
with per-batch lock and statement deadlines.

Add real PostgreSQL export/restore and lock-contention coverage, document
the backup scope, and move detailed UI guidance into a help popover.

Validation: 4011 backend tests passed in partitions; Rustfmt, Clippy,
frontend lint/type checks, and actionlint passed. Resource-limited load
checks covered current-adapter million-row backups and a read-only policy
check on the existing 31-million-row database. Final popover interaction
checks were stopped at the user's request. Online restore is deferred.

Co-authored-by: Codex <noreply@openai.com>
2026-10-20 14:42:29 +08:00
e0326ecba4 fix(openai): base pooled WS breaker on opening results (#484)
* fix(openai): base pooled WS breaker on opening results

Keep foreground fallback independent of pooled opening health and fence late results by circuit generation.

Refs #482

Co-authored-by: Codex <noreply@openai.com>

* fix(openai): preserve concurrent breaker results while closed

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-10-20 14:06:23 +08:00
ZyyandCodex 6057088300 chore(release): v3.29.5
Co-authored-by: Codex <noreply@openai.com>
2026-10-19 17:42:17 +08:00
e3a49a6217 fix(openai): supplement declarations for hosted search history (#478)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-19 17:07:27 +08:00
b32f87fa8a fix(store): bound extrema rebuilds to their analytics bucket (#477)
Group extrema rebuilds by collection and analytics bucket, with explicit started_at bounds so large fact histories can use the time-range index. Preserve per-group build prefixes and aggregate results.

Keep the scan regression test on a single PostgreSQL connection so forced statistics flushes include the worker's reads. The updated test passes with the bounded query and rejects the previous query after reading 6,000,040 rows.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-19 16:50:01 +08:00
ZyyandCodex 91e8105e5d fix(openai): measure first-token latency from first response data
Record first-token timing from nonempty HTTP SSE data or WebSocket messages before JSON and event classification. Preserve semantic output, reasoning and text timing boundaries.

Co-authored-by: Codex <noreply@openai.com>
2026-10-19 13:59:05 +08:00
dependabot[bot]andCodex c4d9576639 chore(deps): update GitHub Actions and composite action pins (#472)
Align pinned action versions across CI, release workflows and composite actions.

Co-authored-by: Codex <noreply@openai.com>
2026-10-19 13:52:12 +08:00
ZyyandCodex 015cd60be1 fix(ci): allow scoped Dependabot updates through rules guard
Co-authored-by: Codex <noreply@openai.com>
2026-10-19 13:46:36 +08:00
ZyyandCodex bad2a245ab chore(release): v3.29.4
Co-authored-by: Codex <noreply@openai.com>
2026-10-19 12:59:43 +08:00
ZyyandCodex b49b4528e7 fix(store): prevent analytics stalls during history cleanup
Batch empty-group deletion and extrema rebuilds, and reserve queue capacity
for live requests while historical contributions are removed.

Keep adaptive batch sizes across worker cycles so timed-out batches can
shrink and make progress without changing transaction or version ACK rules.

Add regression coverage for rollback, fair processing, and timeout recovery.
Validated with 452 Store tests, strict Clippy, and delayed PostgreSQL stress
with 30,000 historical requests plus 12,000 new requests at 200 per second.

Co-authored-by: Codex <noreply@openai.com>
2026-10-19 12:57:08 +08:00
dependabot[bot]andCodex d678f350c1 chore(deps): update frontend dependencies with compatible vue-tsc (#473)
Update nine frontend dependencies and pin vue-tsc to 3.3.11 to avoid the upstream imported-binding code generation regression.

Refs: https://github.com/vuejs/language-tools/issues/6237

Co-authored-by: Codex <noreply@openai.com>
2026-10-19 12:14:01 +08:00
dependabot[bot]andCodex 17b05d4671 chore(deps): update backend dependencies and sha2 build profile (#471)
Update serde_json, sha2, syn, tokio-util, and uuid. Keep the sha2 dev/test optimization profile aligned with version 0.11.1.

Co-authored-by: Codex <noreply@openai.com>
2026-10-19 12:13:39 +08:00
ZyyandCodex 7148303ad6 chore(deps): update plugins submodule to v0.5.0
Pin modules/plugins to the published v0.5.0 release at 7500732.

Co-authored-by: Codex <noreply@openai.com>
2026-10-19 11:36:13 +08:00
ZyyandCodex f6538d433d test: align backend coverage with current behavior
Consolidate overlapping scenarios and remove obsolete assertions and
migration-specific SQL tests. Keep persistence fixtures on the current schema
and document the test maintenance rules in the development skill.

Validation: 3,997 tests passed across 12 crates, plus rustfmt, strict Clippy,
and skill validation. All 33 migration SQL files passed temporary isolated
execution, and the opt-in xAI official network test passed.

Co-authored-by: Codex <noreply@openai.com>
2026-10-19 11:36:06 +08:00
ZyyandCodex 406256bcc0 fix(ci): check frontend dependencies in the correct directory
Co-authored-by: Codex <noreply@openai.com>
2026-10-19 10:43:30 +08:00
ZyyandCodex d912fa0c59 fix(ui): auto-hide key usage log scrollbars
Co-authored-by: Codex <noreply@openai.com>
2026-10-18 23:58:15 +08:00
ZyyandCodex a166580aed fix(ui): remove redundant failure messages and concurrency controls
Co-authored-by: Codex <noreply@openai.com>
2026-10-18 23:31:12 +08:00
ZyyandCodex d1aec7311c fix(proxies): refresh regions and hide stale detection results
Co-authored-by: Codex <noreply@openai.com>
2026-10-18 23:31:01 +08:00
ZyyandCodex 16d8a3f873 chore(release): v3.29.3
Co-authored-by: Codex <noreply@openai.com>
2026-10-18 23:14:48 +08:00
ZyyandCodex ac6033f6fa docs: refine GitHub reply wording
Co-authored-by: Codex <noreply@openai.com>
2026-10-18 23:10:58 +08:00
cef9f0b21e fix(openai): honor usable credits and recover quota after reset (#469)
Refs #465

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-18 22:45:15 +08:00
591f95cc18 fix(store): accept managed PostgreSQL and Redis passwords (#467)
* fix(store): accept managed PostgreSQL passwords

Co-authored-by: Codex <noreply@openai.com>

* fix(store): unify PostgreSQL and Redis password validation

Co-authored-by: Codex <noreply@openai.com>

* fix(store): preserve literal percent sequences in passwords

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-10-18 20:34:20 +08:00
ZyyandCodex 99ba5b1120 fix(openai): align request ordering and simplify timing metrics
Preserve response.create field order and align OAuth query ordering with
current official Codex sources.

Stop adding the timing-metrics request header by default and remove the
four unused engine TTFT and token interval fields throughout the backend,
plugin observation contracts, and frontend. Keep rate calculations unchanged.

Co-authored-by: Codex <noreply@openai.com>
2026-10-18 16:32:15 +08:00
ZyyandCodex 2221f400da chore(release): v3.29.2
Co-authored-by: Codex <noreply@openai.com>
2026-10-18 13:45:14 +08:00
ZyyandCodex 38bcfb9da1 fix(dashboard): correct yesterday trend comparisons
Read yesterday card counters independently using deployment calendar boundaries. Hide trends when today has no requests or cache rates are unavailable, and label arrows as comparisons with yesterday.

Co-authored-by: Codex <noreply@openai.com>
2026-10-18 13:35:05 +08:00
ZyyandCodex 10f122aac8 fix(frontend): align theme preview data and input hints
Match the supplied Codex Desktop identity reference and avoid undefined verification labels. Remove ellipses from input placeholders while preserving OAuth callback URL examples.

Co-authored-by: Codex <noreply@openai.com>
2026-10-18 13:32:51 +08:00
ZyyandCodex 598d56baed fix(frontend): stabilize async state and session lifecycles
Preserve plugin pages during session renewal, restore pagination after failed requests, and cancel plugin resources on reload or disposal. Reuse request state for quota forecasts and disable logout while authentication changes.

Validated with frontend lint and build, 23 temporary behavioral checks, and browser checks using simulated pagination and authentication responses.

Co-authored-by: Codex <noreply@openai.com>
2026-10-18 13:32:46 +08:00
ZyyandCodex 5f61309941 refactor(frontend): standardize API request contracts
Use named request and response types with consistent data arguments and request configuration. Update affected callers while retaining request options where needed.

Co-authored-by: Codex <noreply@openai.com>
2026-10-18 13:32:27 +08:00
ZyyandCodex ab8c61b118 chore(toolchain): upgrade Node to 26.12.0 and Rust to 1.99.1
Align Docker images, CI inputs, Rust requirements and development commands.
Adapt strict compiler checks and reuse owned buffers in UTF-8 conversion.

Validate 4,021 backend tests, SDK feature configurations, Clippy, frontend
checks, and the amd64 container build and startup.

Co-authored-by: Codex <noreply@openai.com>
2026-10-18 04:57:48 +08:00
ZyyandCodex c3146dc10a fix(usage): align request tables and detail dialogs
Share data columns and status filters between admin and key usage. Keep final error summaries in the error view and focus error details in a separate dialog.

Use logical request outcomes for key usage records and keep concurrency lists at a stable height without pagination.

Co-authored-by: Codex <noreply@openai.com>
2026-10-18 04:28:41 +08:00
ZyyandCodex 72bdc234ca docs(skills): streamline routing and clarify development boundaries
Co-authored-by: Codex <noreply@openai.com>
2026-10-18 04:14:22 +08:00
ZyyandCodex 733e9aa8a0 fix(release): guard publication and consolidate release tooling
Co-authored-by: Codex <noreply@openai.com>
2026-10-18 04:02:17 +08:00
ZyyandCodex 743a3aba70 chore(release): v3.29.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 22:01:49 +08:00
ZyyandCodex 349e983eb7 fix(ui): correct account labels, proxy actions and usage defaults
Reuse account identity labels in concurrency details and use a text button
for reset-credit lead time settings. Disable row tests during batch proxy
testing while retaining loading feedback for individual tests.

Default the usage date range to today.

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 22:00:16 +08:00
ZyyandCodex 45954899e5 chore(release): v3.29.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 21:31:14 +08:00
ZyyandCodex 78f533d9d3 chore(db): consolidate unreleased migrations for v3.29.0
Merge migrations 0033-0035 into one migration while preserving published migrations 0001-0032. Verified PostgreSQL 18 fresh installation, upgrade from v3.28.0, existing data defaults, and schema equivalence with the original migration sequence.

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 21:30:59 +08:00
ZyyandCodex 376305fed4 docs: align reference documentation with current behavior
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 21:20:33 +08:00
a82bcdb8f8 fix(providers): preserve upstream failures across account selection (#462)
* fix(openai): distinguish session policy exclusions from unavailable accounts

Co-authored-by: Codex <noreply@openai.com>

* fix(providers): preserve upstream failures after selection exhaustion

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: 2233224230 <111681546+2233224230@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-10-17 21:19:00 +08:00
ZyyandCodex 3d072fedcf fix(deploy): preserve runtime ownership on installer reruns
Limit application data and log ownership changes to their top-level directories. Add a container check that reapplies installer permissions and verifies application startup.

Validation: first-install and repeated-install filesystem permission checks, Bash syntax, ShellCheck, Actionlint, and diff checks passed. Full local container validation was blocked by Docker Desktop bind-mount permission mapping.

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 21:10:39 +08:00
ZyyandCodex a0e708071a perf(plugins)!: optimize RPC and simplify host boundaries
Move business JSON out of control metadata, encode request snapshot headers
compactly, and reuse encoded frames. Cache compiled configuration schemas
and prepare plugin instances with bounded concurrency.

Ignore unused plugin result fields and remove redundant business validation
while preserving host authorization, resource ownership, and capacity limits.
Keep response decoding failures local to their calls.

Seal callback side effects before completing or cancelling RPC calls, and
recover abandoned state migrations without allowing old owners to promote.

Validation: SDK 112 tests and its doctest, Runtime 203 tests, Core 542 tests,
40 PostgreSQL plugin tests, and 16 architecture checks passed. Strict Clippy,
formatting, and diff checks passed.

BREAKING CHANGE: RPC protocol v4 changes frame payload layout and request
snapshot header encoding. Plugins must update their SDK and protocol
manifest version and be rebuilt.

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 20:54:41 +08:00
ZyyandCodex e9ad1811dd feat(usage): add bounded date range picker
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 20:38:02 +08:00
ZyyandCodex 615e8d66f4 fix(openai): enforce account model access for standalone search
Apply model allowlists and denylists before selecting search accounts.
Use unrestricted accounts when the request model cannot be determined,
and preserve plugin body rewrites after selection.

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 20:36:58 +08:00
ZyyandCodex d79c76a105 feat(accounts): add optional weekly quota limit for reset credits
Skip new automatic redemptions above the configured remaining weekly
quota limit, leaving it unset by default and preserving pending retries.
Keep temporarily ineligible credits scheduled and expose the setting
beside the redemption lead time.

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 20:15:21 +08:00
ZyyandCodex 1bf47f71b6 chore(deps): align workbench with UI v0.5.0
Keep the host, UI submodule, and plugin workbench lockfile on the same UI release for source development.

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 20:09:14 +08:00
ZyyandCodex 2f3f2975ef feat(ui): upgrade UI to v0.5.0 and simplify health popovers
Use shared hover and dismissal handling for health timeline popovers, remove host workarounds and redundant footer copy, and pin the UI dependency and submodule to v0.5.0.

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 20:03:36 +08:00
759935b75d feat(accounts): add concurrency details and SDK request context (#461)
* feat(accounts): add concurrency details and SDK request context

Co-authored-by: Codex <noreply@openai.com>

* fix(plugins): version request-context frames and preserve failed snapshots

Co-authored-by: Codex <noreply@openai.com>

* fix(dev): sync workbench with request-context contracts

Co-authored-by: Codex <noreply@openai.com>

* fix(accounts): refine concurrency and request context handling

Preserve entry context and recursion scopes in retry and public management callbacks. Encode queued RPC frames once and use a dedicated account existence query for concurrency polling. Sync workbench packaging tooling and CI through the plugin submodule.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 19:45:49 +08:00
ZyyandCodex fcf4af0ddd fix(ui): let key usage page fill viewport width
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 19:27:59 +08:00
ZyyandCodex 8fb48dd6c3 fix(plugins): expose reset credit automation controls
Register typed account automation operations for server and CLI plugins, reusing the native account service. Preserve the initiating actor across service continuations and generate the SDK contract from host declarations.

Co-authored-by: Codex <noreply@openai.com>
2026-10-17 16:53:54 +08:00
5a81d61979 feat(accounts): automatically use expiring reset credits (#460)
* feat(accounts): automate expiring reset credits

Persist per-account plans and redemption keys, stagger daily discovery, and verify availability before consuming expiring credits.

Co-authored-by: Codex <noreply@openai.com>

* fix(ui): remove reset credit timing button background

Use the existing link variant to keep the control transparent in both idle and hover states.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 16:25:12 +08:00
ZyyandCodex 1fadc75ba2 fix(ui): refine table labels and status alignment
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 15:17:16 +08:00
83f5d71853 fix(openai): bound WebSocket connections per outbound proxy (#458)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 14:56:30 +08:00
f4eceb076b fix(openai): adapt multipart image edits to native JSON (#457)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 14:52:13 +08:00
ZyyandCodex 0b02d0d98a fix(deps): upgrade zip to resolve security advisories
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 13:27:38 +08:00
a014da6916 feat(proxies): add batch APIs and streamline proxy management (#450)
* feat(proxies): add batch creation and geolocated testing

Co-authored-by: Codex <noreply@openai.com>

* feat(proxies): unify batch APIs and simplify management UI

Accept arrays on the existing create and test routes with ordered per-item
results, partial success, bounded concurrency and no automatic replay.
Streamline selected-proxy testing and creation forms, combine location
presentation, and generate bundled licenses from the npm flag package.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-10-17 13:21:11 +08:00
ZyyandCodex 9d4c58f967 docs: prioritize official Codex behavior and clarify comments
Co-authored-by: Codex <noreply@openai.com>
2026-10-17 12:36:15 +08:00
ZyyandCodex c67b081ec1 feat(settings): detect request location from server egress
Reuse proxy egress detection for global request location drafts and preserve cached client identity previews.

Co-authored-by: Codex <noreply@openai.com>
2026-10-16 20:02:12 +08:00
ZyyandCodex 1ce9710afa chore(deps): update plugins submodule to v0.4.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-16 19:35:40 +08:00
ZyyandCodex 447869860a chore(release): v3.28.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-16 17:15:10 +08:00
ZyyandCodex 815c93a715 test(store): make group capacity fixtures independent of defaults
Set inherited concurrency explicitly so the aggregation test exercises
its intended 3 + 4 capacity without depending on installation defaults.

Co-authored-by: Codex <noreply@openai.com>
2026-10-16 17:15:05 +08:00
ZyyandCodex 5843091969 docs: align diagnostic export and troubleshooting guidance
Document server-side redaction of stored error details and timelines,
add the export route, and explain how to inspect diagnostic gaps.

Co-authored-by: Codex <noreply@openai.com>
2026-10-16 17:07:59 +08:00
ZyyandCodex 1280c437b4 fix(settings): raise initial account concurrency to five
Set the database default and untouched initial settings to five in the
unreleased 0032 migration, preserving saved concurrency settings.
Update the migration checksum and document the initial value.

Co-authored-by: Codex <noreply@openai.com>
2026-10-16 16:59:33 +08:00
ZyyandCodex d965bc9050 fix: harden diagnostics and consolidate unreleased migrations
Redact inline credential headers and token assignments in diagnostic exports.
Remove unnecessary usage query copies and the reverse analytics dependency.

Combine unreleased migrations 0032-0035 without changing published migrations.
Update existing migration fixtures and remove five oversized database stress
tests from the regular suite.

Validate with Store, Provider and diagnostics tests, strict Clippy, Rustfmt,
and frontend lint/build.

Co-authored-by: Codex <noreply@openai.com>
2026-10-16 16:55:42 +08:00
bd9b017ed3 feat(usage): unify request records with status filters (#447)
* feat(usage): unify request records with status filters

Co-authored-by: Codex <noreply@openai.com>

* fix(usage): wrap record filters on narrow screens

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-16 16:19:36 +08:00
338cde1505 feat(scheduling): add plugin filtering and remove automatic switchback (#446)
* feat(scheduling): add plugin filtering and remove automatic switchback

Add scheduler v2 filtered delegation across native selection, affinity,
capacity waits, and lease retries. Keep plugin configuration and business
rules owned by plugins without legacy scheduler compatibility.

Remove automatic switchback from the runtime, SDK, and settings UI.
Migrate the existing settings JSON without creating new tables.

Co-authored-by: Codex <noreply@openai.com>

* fix(dev): align plugin source checks with scheduler v2

Update the workbench submodule to the v2-capable SDK and scheduler,
and document the explicit scheduler manifest version.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-16 15:13:56 +08:00
b25f96559a feat(groups): control image generation by account group (#445)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-16 14:00:14 +08:00
ZyyandCodex a8a96d8919 feat(diagnostics): generate support bundles on the backend
Collect request, retry, error and recovery evidence with bounded reads and
credential redaction. Keep ordinary detail responses small and expose a
shared download action next to the close button.

Preserve export metadata when limits are reached and deduplicate selected
errors without discarding business context based on generic field names.

Co-authored-by: Codex <noreply@openai.com>
2026-10-16 12:49:18 +08:00
ZyyandCodex 7b8339ede3 fix(openai): preserve retries after response metadata
Keep response.metadata replayable while forwarding its turn and safety metadata unchanged. Cover delivered metadata before 503 on HTTP/SSE and WebSocket, preserve barriers after output or unknown events, and verify connection-limit recovery.

Co-authored-by: Codex <noreply@openai.com>
2026-10-16 11:16:49 +08:00
f439a30612 feat(usage): expose bounded data maintenance services (#436)
Expose generic detail CRUD, aggregate contributions, and native pricing evaluation through Admin and the plugin SDK. Reuse existing request and statistics storage without schema changes; keep workflow state and recovery with callers.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-16 00:49:51 +08:00
ZyyandCodex 8572873c5f refactor(store): standardize database table names
Rename inconsistent tables and their schema objects, update SQL references,
and preserve analytics trigger behavior through the upgrade.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 23:54:05 +08:00
57c8ef358f feat(settings): configure request history retention (#435)
Allow 1–31 day retention and keep published analytics readable during cleanup.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-15 22:38:53 +08:00
ZyyandCodex 70ab8ebc8d fix(ui): refine privacy controls and plugin empty state
Co-authored-by: Codex <noreply@openai.com>
2026-10-15 21:34:47 +08:00
ZyyandCodex 14ea5234ce chore(release): v3.27.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-15 18:12:46 +08:00
ZyyandCodex f8480f3bdc fix(privacy): preserve project state in identity presets
Keep repository, commit, branch and worktree data intact while normalizing personal paths. Remove the Desktop identity preset and version shortcuts from privacy settings.

Synchronize automatic Desktop Git context versions from the existing request-scoped client identity in HTTP and WebSocket requests. Reuse official release updates, fixed versions and key overrides, and share context parsing with privacy rules. Preserve unknown or incomplete context and leave installation identity to its existing account owner.

Verified version field semantics against the official Desktop 26.1007.71326 bundle and Codex app-server initialization source. Validated provider tests, temporary HTTP/WS identity tests, architecture boundaries, strict Clippy, frontend lint/build and the preset UI.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 16:45:04 +08:00
ZyyandCodex 52b8103bc9 chore(release): v3.27.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-15 15:37:04 +08:00
ZyyandCodex dacdd3f0e2 chore(db): consolidate unreleased proxy and privacy migration
Keep published migrations unchanged and apply proxy enablement and privacy rule conversion in one migration.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 15:36:43 +08:00
ac36f5bc13 fix(plugins): separate execution capacity from downstream waits (#434)
* fix(plugins): delegate HTTP requests on RPC admission exhaustion

Co-authored-by: Codex <noreply@openai.com>

* fix(plugins): align default RPC capacity at 64

Raise host and SDK call and callback limits together, exercise 64 in-flight HTTP calls, and document plugin rebuild requirements.

Co-authored-by: Codex <noreply@openai.com>

* fix(plugins): preserve middleware execution under RPC saturation

Remove capacity-triggered delegation, retain the 64-call defaults, and verify that saturation never bypasses a healthy HTTP plugin.

Co-authored-by: Codex <noreply@openai.com>

* fix(plugins): separate execution capacity from downstream waits

Bound initial admission and total in-flight resources while preserving middleware continuations. Keep queue waits independent of processing deadlines and report overload without bypassing plugins.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-15 15:17:00 +08:00
ZyyandCodex e0a022518c refactor(backend): simplify shared rules and redundant tests
Reuse Core error classification and token presentation, remove unused
operation APIs and redundant tests, and avoid unnecessary context cloning.
Fix expired reset timestamp underflow and SSE metadata frame boundaries.
Consolidate architecture checks and document the backend audit results.

Validation: backend tests, architecture tests, rustfmt and strict Clippy.
PostgreSQL/Redis integration was not verified without dedicated services.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 13:46:11 +08:00
ZyyandCodex 37f7a55427 feat(privacy): improve rule editor interactions and layout
Add draggable step cards with inline removal and a dedicated scrollbar. Keep the editor height stable while preview text areas fill the available space, and refine rule actions and preview outcomes.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 13:17:54 +08:00
ZyyandCodex f6085206eb fix(retry): suppress repeated return-error retries for failed turns
Track short-lived failures by client key, thread, and turn. Skip RetryThenReturnError retries on subsequent requests, clear markers on success with version checks, and preserve other retry policies.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 12:11:29 +08:00
ZyyandCodex 9813c86055 fix(ci): stream Cargo cache snapshots through tar
Export BuildKit cache snapshots as a tar stream and extract directly into the temporary directory to avoid local exporter stalls. Preserve snapshot validation and pipeline error propagation before replacing the saved cache.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 11:14:58 +08:00
ZyyandCodex 260f775039 fix(deps): patch rustls vulnerabilities after TLS fingerprint verification
Pin rustls to 0.23.46 while retaining the TLS features and Dependabot
ignore policy. Preserve the sampled Codex HTTP and WebSocket handshake
characteristics and document the manual security-patch policy.

Validation: Cargo audit, strict workspace Clippy, TLS and proxy transport
tests, and real-account HTTP and WebSocket handshakes.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 10:16:26 +08:00
ZyyandCodex 1424cea3a9 feat(proxies): add enablement and automatic account unbinding
Preserve proxy configuration and account bindings while disabled and route
subsequent account requests directly. Restore the proxy when enabled.

Automatically unbind accounts on deletion in one transaction. Add the
status column, action menu, and disable/delete confirmation messages.

Refs #416

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 10:16:26 +08:00
ZyyandCodex 787ad28424 fix(auth): show login when initial session restore fails
Redirect an initial navigation to login when the status request fails,
while preserving an already open page and its session on later failures.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 10:16:26 +08:00
ZyyandCodex db23560b0e feat(privacy): group normalization steps into generic rules
Execute each rule's steps in order with atomic rollback and migrate saved
single-action rules once to the canonical steps structure.

Consolidate workspace and Desktop presets, add drag ordering and step help,
and simplify the rule editor and preview controls.

Co-authored-by: Codex <noreply@openai.com>
2026-10-15 10:16:26 +08:00
ZyyandCodex d700df8a4f fix(openai): separate transient retry exhaustion policies
Retry ordinary 503 server errors up to five times on the same account with capped backoff and positive jitter. Preserve upstream Retry-After and return the final error without rotating accounts; retain rotation for explicit capacity rejections.

Co-authored-by: Codex <noreply@openai.com>
2026-10-14 18:12:18 +08:00
ZyyandCodex c9adfd9a2b fix(admin): exclude public plugin routes from compression
Keep public plugin resources and OAuth callbacks outside the admin compression layer while preserving cache and method error handling.

Co-authored-by: Codex <noreply@openai.com>
2026-10-14 17:54:43 +08:00
ZyyandCodex 654b6baf89 perf(admin): compress large JSON responses with gzip
Negotiate fast gzip for known-length admin JSON responses of at least
1 KiB while preserving streaming and model execution paths.

Cover encoding negotiation, body equivalence, the size threshold,
response headers, duplicate compression, and SSE first-event delivery.
Document the admin response compression contract.

Refs #432

Co-authored-by: Codex <noreply@openai.com>
2026-10-14 17:51:08 +08:00
ZyyandCodex ae5eaa46b4 chore(release): v3.26.2
Co-authored-by: Codex <noreply@openai.com>
2026-10-14 16:16:30 +08:00
ZyyandCodex 65d5b32eba fix(openai): keep turn rate limits scoped to the request
Preserve upstream turn_rate_limit_exceeded errors and retry advice without
cooling the account or replaying the request inside the proxy.

Cover HTTP and WebSocket rejections plus streamed failures, and document
client-owned waiting and retries.

Co-authored-by: Codex <noreply@openai.com>
2026-10-14 16:01:49 +08:00
ZyyandCodex 6c158447be fix: prevent cookie replay and improve Windows download fallback
Distinguish credential revision conflicts from account store failures and
persist response cookies once across streaming errors and WebSocket reuse.

Keep Microsoft Store downloads first with a 30-second timeout, read official
fallback sizes through bounded HEAD requests, and correct Store size units.

Co-authored-by: Codex <noreply@openai.com>
2026-10-14 15:54:47 +08:00
81d939bd03 fix(frontend): fix batch scheduling and sidebar focus (#430)
Default batch scheduling to disabled and always submit the displayed
switch value, allowing an untouched form to apply that setting.
Keep sidebar focus rings inside navigation buttons to prevent clipping
without changing their layout or keyboard behavior.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-14 14:30:58 +08:00
4a22479dff feat(openai): add bounded retries for explicit 503 server errors (#428)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-14 13:50:51 +08:00
ZyyandCodex 3ee454b750 feat(logging): support fractional request dump retention
Accept positive fractional days and expire request dump segments by elapsed time. Rotate small dump files by age, preserve archive timestamps, and document the retention window.

Co-authored-by: Codex <noreply@openai.com>
2026-10-14 13:03:55 +08:00
dudukee b5bd63b585 fix(openai): use a catalog-supported warmup reasoning effort (#425)
Scheduled account warmup sent reasoning.effort=none, but the Codex model
catalog lists low as the lowest supported level for every model an account
can see. Upstream answers HTTP 200 with an SSE error event (unsupported_value
on reasoning.effort), which the warmup terminal check records as
response_failed, so the cycle ends with warmed_up=0 and never starts the 5h
window.

Send low instead and cover the upstream payload with a regression test.
2026-10-14 11:42:48 +08:00
286031a441 fix(auth): separate access and refresh session credentials (#426)
Recover expired admin access credentials through a rotating refresh cookie,
preserve concurrent retry semantics, and keep normal requests read-only.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-14 11:42:33 +08:00
ZyyandCodex 5358de53dd chore(release): v3.26.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-14 01:52:02 +08:00
ZyyandCodex d814582c09 fix(openai): forward response preludes without buffering
Deliver HTTP/SSE and WebSocket events immediately while preserving bounded
recovery after replaceable preludes. Share prelude classification so response
metadata does not hide WebSocket connection-limit recovery.

Keep irreversible delivery, cancellation, middleware, and retry-budget guards.
Document the distinction between transport keepalive and event idle timeouts.

Refs #405

Co-authored-by: Codex <noreply@openai.com>
2026-10-14 01:51:53 +08:00
ZyyandCodex a9ce01f91f refactor(frontend): use a fixed API timeout
Co-authored-by: Codex <noreply@openai.com>
2026-10-14 00:00:53 +08:00
ZyyandCodex 0fc25bfce9 chore(release): v3.26.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-13 22:32:21 +08:00
ZyyandCodex eed8c93ae2 refactor(store): consolidate unreleased database migrations
Merge migrations 0030 through 0032 after the v3.25.0 release baseline. Queue analytics bucket rebuilds once and verify upgrade preservation and repeated startup.

Co-authored-by: Codex <noreply@openai.com>
2026-10-13 22:13:26 +08:00
ZyyandCodex d865a00768 style(pricing): simplify model search placeholder
Co-authored-by: Codex <noreply@openai.com>
2026-10-13 21:58:00 +08:00
ZyyandCodex a0671fb70b style(settings): refine privacy controls and pricing table
Co-authored-by: Codex <noreply@openai.com>
2026-10-13 21:50:53 +08:00
ZyyandCodex fce543d4ce feat(settings): remove account request interval limits
Co-authored-by: Codex <noreply@openai.com>
2026-10-13 21:50:53 +08:00
219888d5c2 perf(store): scale analytics and pagination for large request histories (#423)
Aggregate analytics in PostgreSQL, reconcile synchronous usage with pending source changes, and preserve exact pagination and forecast totals across large histories.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-13 21:29:25 +08:00
5b80340135 fix(privacy): avoid repeated full-request processing (#420)
Stage scoped body fragments and reuse context locations while preserving per-rule rollback. Account for encoded size incrementally and use bounded work plus cancellation instead of a fixed wall-clock cutoff.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-13 17:09:33 +08:00
b2dab66a80 fix(accounts): import account file entries independently (#411)
* fix(accounts): import valid account file entries independently

Co-authored-by: Codex <noreply@openai.com>

* test(openai): cover single-token account imports

* style(openai): format token import test assertion

* fix(accounts): preserve provider boundaries and bound import batches

Co-authored-by: Codex <noreply@openai.com>

* fix(ui): keep account dialogs within narrow viewports

Co-authored-by: Codex <noreply@openai.com>

* chore(deps): update UI v0.4.1 and latest plugin main

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-10-13 12:16:17 +08:00
JiahaoandCodex f0cf8e7198 feat(usage): highlight latency by fixed thresholds (#412)
Color first-output and total latency independently using fixed thresholds.
Keep the existing layout and details popover without a side indicator.

Closes #407

Co-authored-by: Codex <noreply@openai.com>
2026-10-13 11:45:38 +08:00
a9a86071bf fix: preserve terminal failure and cleanup causes (#417)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-13 11:31:10 +08:00
27cef052c9 fix(accounts): preserve quota forecast totals on same-window reversals (#414)
* fix(accounts): preserve quota forecast totals on same-window reversals

Share exhausted-window recovery evidence between account recovery and
forecast sampling. Keep recorded cycle usage on ordinary reversals and
restart totals only after the recovery evidence is satisfied.

Co-authored-by: Codex <noreply@openai.com>

* fix(accounts): separate forecast cycle boundaries from quota recovery

Keep same-window usage totals after quota reversals, including observations near exhaustion. Leave access recovery in the OpenAI provider and reset forecast totals only at an identified window or plan boundary.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-13 11:20:04 +08:00
ef40088303 fix(deps): update quinn-proto to address security advisories (#415)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-13 11:11:41 +08:00
ZyyandCodex 05ba116eef chore(release): v3.25.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-12 17:16:50 +08:00
ZyyandCodex 3ef4743e1a fix(store): persist retries after transport commit
Let Core decide replay eligibility after WebSocket preludes or HTTP
keepalives. Persist subsequent attempts while still enforcing the running
lifecycle and consecutive attempt numbers, preserving delivery facts and
the upstream send watermark.

Cover retry metadata, duplicate and skipped attempts, and terminal request
immutability with a PostgreSQL regression test.

Co-authored-by: Codex <noreply@openai.com>
2026-10-12 17:06:03 +08:00
ZyyandCodex 9e02e52fec fix(responses): keep SSE alive before first output
Start downstream keepalives while admitted streaming requests await their first output, including middleware processing. Separate transport commitment from the replay barrier so heartbeat frames preserve safe retries and execution accounting.

Co-authored-by: Codex <noreply@openai.com>
2026-10-12 16:57:28 +08:00
ZyyandCodex 42ff5b74d0 fix(observability): count incomplete requests in availability
Include incomplete final outcomes in service health metrics and lower the
stable threshold to 95%. Keep the health popover at four outcomes by
folding incomplete requests into the service failure display.

Use consistent service success rates across dashboard trends, health
timelines and usage insights. Cover the reported counts, threshold edges,
caller exclusions and final success after failed attempts in regressions.

Refs #406

Co-authored-by: Codex <noreply@openai.com>
2026-10-12 16:42:33 +08:00
ZyyandCodex eb2eefd51e feat(openai): add ultrafast pricing
Add published GPT-6 Astra and GPT-6.1 Sol rates for short and long context. Expose Ultrafast override bands in the pricing API and editor, with billing and validation coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-10-12 15:46:36 +08:00
think-backandClaude Opus 5.5 abd60ce60a fix(store): acknowledge analytics versions after projection commits (#404)
The analytics worker acknowledged projected queue versions inside its
repeatable-read projection transaction. Request updates keep rewriting
pending queue rows through the source triggers, and once such a change
commits after the worker snapshot, the acknowledgement row lock fails with
a serialization error instead of skipping the stale version. Under steady
traffic every cycle rolled back the whole projection batch, the queue kept
growing and the worker stayed in backoff.

Commit the projection and publication first, then acknowledge the exact
versions in a separate read-committed transaction. Row locks there
re-check the latest committed version, so rewritten versions no longer
match and stay queued for replay, while locked rows are still skipped.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:50:02 +08:00
ZyyandCodex 2f96c02ea0 fix(routing): bind known models to their providers
Preserve known model ownership when provider catalog refreshes fail.
Reject ambiguous unknown-provider routes and exclude ChatGPT OAuth
accounts from Grok requests while preserving API key passthrough.

Refs #391

Co-authored-by: Codex <noreply@openai.com>
2026-10-12 13:04:32 +08:00
think-backandClaude Opus 5.5 0f575e279a fix(openai): keep account selection through quota observation updates (#400)
Successful responses keep refreshing the quota observation time of the
account they used. The post-selection credential reload compared the
whole account snapshot, so a busy account could hit three observation-only
conflicts in a row and fail with account_snapshot_conflict before any
upstream request was sent.

Accept the reloaded snapshot when only the observation time of a
non-exhausted quota moved. Credential, state, scheduling, scope and quota
conclusion changes still invalidate the selection and retry as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:50:19 +08:00
dependabot[bot] 634b8fc674 chore(deps): bump node from 662933c to 86f07bc in /deploy (#402)
Bumps node from `662933c` to `86f07bc`.

---
updated-dependencies:
- dependency-name: node
  dependency-version: 26-bookworm-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-12 12:45:36 +08:00
dependabot[bot] 00bfb1699d chore(deps): bump debian from 3783cc0 to 7c7b2c9 in /deploy (#401)
Bumps debian from `3783cc0` to `7c7b2c9`.

---
updated-dependencies:
- dependency-name: debian
  dependency-version: bookworm-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-12 12:45:27 +08:00
dependabot[bot] 956ca70a14 chore(deps): bump the minor-and-patch group in /backend with 5 updates (#399)
Bumps the minor-and-patch group in /backend with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [uuid](https://github.com/uuid-rs/uuid) | `1.26.1` | `1.27.0` |
| [jsonschema](https://github.com/Stranger6667/jsonschema) | `0.58.4` | `0.58.6` |
| [h2](https://github.com/hyperium/h2) | `0.4.19` | `0.4.20` |
| [regex](https://github.com/rust-lang/regex) | `1.13.0` | `1.13.1` |
| [zeroize](https://github.com/RustCrypto/utils) | `1.9.0` | `1.9.1` |


Updates `uuid` from 1.26.1 to 1.27.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.26.1...v1.27.0)

Updates `jsonschema` from 0.58.4 to 0.58.6
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/cli-v0.58.4...ruby-v0.58.6)

Updates `h2` from 0.4.19 to 0.4.20
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.4.19...v0.4.20)

Updates `regex` from 1.13.0 to 1.13.1
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.13.0...1.13.1)

Updates `zeroize` from 1.9.0 to 1.9.1
- [Commits](https://github.com/RustCrypto/utils/compare/zeroize-v1.9.0...zeroize-v1.9.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: jsonschema
  dependency-version: 0.58.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: h2
  dependency-version: 0.4.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: regex
  dependency-version: 1.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: zeroize
  dependency-version: 1.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-12 12:05:19 +08:00
ZyyandCodex 24dd7b94f4 fix(openai): limit gateway retries to explicit recovery scenarios (#398)
Preserve ordinary upstream failures for client retry decisions and allow
unsent native TLS handshake EOFs through bounded connection recovery.

Co-authored-by: Codex <noreply@openai.com>
2026-10-12 11:57:49 +08:00
ZyyandCodex 94c769fc9b fix(plugins): exclude managed next waits from RPC budgets (#397)
Pause Runtime and SDK processing budgets while middleware delegates to host next, then resume the remaining budget. Preserve request deadlines and cancellation, share callback deadline state, and add timeout diagnostics and regression coverage.

Fixes #392

Co-authored-by: Codex <noreply@openai.com>
2026-10-12 11:40:01 +08:00
ZyyandCodex eea16f5275 chore(release): v3.24.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-12 00:16:21 +08:00
ZyyandCodex 8a406ff594 fix(observability): preserve query budgets and usage history
Hold PostgreSQL observation slots through connection cleanup and optimize exact usage pagination. Keep usage panels visible when analytics queries fail and backfill missing historical identity snapshots in one migration.

Co-authored-by: Codex <noreply@openai.com>
2026-10-12 00:15:40 +08:00
ZyyandCodex 676044c43f chore(release): v3.24.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-11 23:19:45 +08:00
ZyyandCodex e7161da277 refactor(migrations): consolidate unreleased request analytics schema
Fold migrations 0028 through 0035 into one migration while preserving every released migration through v3.23.1. Define the final observation validators, history schema, recovery leases, token columns, indexes, and analytics triggers once.

Refresh the migration manifest, remove superseded intermediate upgrade tests, and retain final-schema business validation. Verify clean installation, released-baseline upgrades, schema equivalence, and historical data using temporary PostgreSQL checks.

Co-authored-by: Codex <noreply@openai.com>
2026-10-11 23:14:54 +08:00
ZyyandCodex 97faca0c45 fix(routing): prioritize known models over discovery fallbacks
Prefer providers whose catalogs contain the mapped model before providers accepting unlisted models. Preserve account scope, route constraints, provider order within each tier, and unknown-model passthrough.

Add compiled mixed-catalog regressions and document candidate ordering. Refs #391.

Co-authored-by: Codex <noreply@openai.com>
2026-10-11 23:04:51 +08:00
45b473c7e2 fix(openai): preserve Lite interrupts across prelude retries (#390)
* fix(openai): preserve Lite interrupts across prelude retries

Deliver known Responses Lite WebSocket preludes before semantic output while retaining existing recovery guards. Keep interrupt intent across retries and retarget it only after the replacement response is delivered.

Co-authored-by: Codex <noreply@openai.com>

* fix(openai): forward Lite events without waiting for output

Deliver unclassified Lite WebSocket events in order and reuse the existing commit boundary to stop replay after their delivery. Preserve error classification, retry budgets, account recovery, and interrupt handling.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-11 22:53:59 +08:00
ZyyandCodex cc69368f77 perf(store): scale request analytics and isolate request history (#389)
Build durable request facts and bounded aggregate queries, freeze historical
identities, and move recovery lease updates out of request history rows.
Align token fields across storage, APIs and pages.

Invalidate analytics only after actual request deletions and track embedded
migration inputs during builds.

Co-authored-by: Codex <noreply@openai.com>
2026-10-11 20:57:36 +08:00
ZyyandCodex 67691d18d3 refactor(openai): clarify adapted model catalog generation
Co-authored-by: Codex <noreply@openai.com>
2026-10-11 11:48:00 +08:00
hi_yuebanandCodex 85c64d35d5 fix(store): avoid revalidating unchanged request observations (#387)
Validate observation shape on insert and changed JSON only, preserving atomic account deletion and historical snapshots. Add upgrade, validation-cost, bulk deletion, and rollback coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-10-11 10:30:04 +08:00
ZyyandCodex 9bf86f7a7c fix(frontend): smooth health timeline popover tracking
Co-authored-by: Codex <noreply@openai.com>
2026-10-10 18:13:25 +08:00
ZyyandCodex 6c56a23508 chore(release): v3.23.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-10 16:51:52 +08:00
b5f3cb402c fix(openai): reject sends on closed WebSocket connections (#381)
Record observed closure before terminal delivery can block, reject queued
unsent commands, and skip commands cancelled before socket writes.
Preserve connection diagnostics and test account egress across quota writes.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-10 16:36:40 +08:00
57e284d7d1 fix(openai): preserve stream recovery before semantic output (#380)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-10 16:30:26 +08:00
21597f9e98 fix(store): restore observation query optimization (#378)
Scope timing projection barriers to filtered metric and diagnostic queries, preserving ordinary aggregate index and parallel plans.

Refs #377

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-10 16:30:05 +08:00
ZyyandCodex fa0b87dd0a refactor(frontend): adopt UI v0.4.0 component APIs
Migrate the management UI to Z components and managed notifications, register the library loading directive, and align fixed dependencies and submodules with the published UI version.

Co-authored-by: Codex <noreply@openai.com>
2026-10-10 14:54:56 +08:00
ZyyandCodex 389dc63633 chore(release): v3.23.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-10 00:45:56 +08:00
ZyyandCodex 78b2250faa feat(settings): add configurable Codex privacy policies (#375)
Add configurable privacy rules and shared previews for Codex metadata, Desktop context, request bodies and headers. Freeze policy per request and preserve atomic failure handling across HTTP and WebSocket transports.

Include settings presets, persistence, API contracts, migration and transport regressions.

Co-authored-by: Codex <noreply@openai.com>
2026-10-10 00:41:25 +08:00
ZyyandCodex bbb8278281 fix(openai): enforce image account eligibility (#374)
Co-authored-by: Codex <noreply@openai.com>
2026-10-09 22:57:24 +08:00
ZyyandCodex 7a5521953f fix(openai): align Desktop client time context overrides
Apply configured request locations to Desktop developer time contexts alongside environment contexts while preserving role and content-kind boundaries.

Cover date alignment, disabled overrides, proxy precedence, and HTTP/WebSocket requests. Update the location contract and troubleshooting documentation.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 20:53:59 +08:00
f78b1ca487 feat(openai): allow HTTP upstream API key accounts (#372)
Allow HTTP API key base URLs for private networks and container services while retaining OAuth startup URL restrictions. Keep URL credential, query and fragment validation, and cover import, export and address rotation.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-09 20:41:53 +08:00
ZyyandCodex 661649cc90 refactor(compat): remove expired upgrade and plugin adapters
Use the current host manifest format and require restart confirmation on
all management UI paths. Document v3.18.1 as the minimum online upgrade
source while retaining old release manifest reads for supported rollbacks.

Retire middleware v3 and upstream_adapter v1 Fast projections after their
compatibility window. Align SDK and host declarations with the current
contracts, keep risk warnings and per-instance failure isolation, and
exercise native fast_mode values through real plugin subprocesses.

Keep deployment, API and SDK documentation focused on current behavior.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 20:41:53 +08:00
ZyyandCodex 705f0fad06 chore(release): v3.22.2
Co-authored-by: Codex <noreply@openai.com>
2026-10-09 18:11:24 +08:00
ZyyandCodex f0c60f501b fix(client): enable remote compaction in Codex config
Declare the V2 remote compaction capability in generated Codex configuration and deployment examples. Document that CC Switch imports do not preserve the full capability configuration.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 18:05:07 +08:00
ZyyandCodex d1febdd249 fix(openai): remove precommit byte limit
Keep large structural events buffered until semantic output, terminal completion, EOF, or the replay grace deadline. Retain byte counts for diagnostics and preserve historical byte_limit exports.

Refs #369

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 18:05:01 +08:00
ZyyandCodex 53d4086cc0 fix(openai): preserve opaque request fields and websocket messages
Forward numeric and extended reasoning effort values and preserve standalone endpoint business headers.

Retain business metadata and original WebSocket text through provider, middleware, and client delivery. Validate rewritten plugin output without rejecting unchanged heartbeats or opaque messages.

Add HTTP, WebSocket, and plugin process regressions for the passthrough boundaries.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 17:23:40 +08:00
ZyyandCodex ce03e8119d fix(diagnostics): retain health and xAI worker failure causes
Record health component failures through operational diagnostics with transition deduplication. Preserve quota and catalog failure context without interrupting the account batch.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 17:10:29 +08:00
ZyyandCodex 3f05b6450c fix(usage): preserve precommit facts in diagnostics export
Export allowlisted release reasons, prefetch bytes and wait times, and document diagnostics schema v4.

Refs #369

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 16:55:12 +08:00
ZyyandCodex a4b73099b5 fix(openai): pass through websocket control frames
Forward non-create text frames unchanged over the original upstream WebSocket while preserving response.create admission and serialization. Keep the control transport available after completion and relay idle upstream events without creating another inference attempt.

Cover unknown frames, late interrupts, terminal settlement, binding revocation, and connection reuse.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 16:26:31 +08:00
ZyyandCodex d875a62652 chore(release): v3.22.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-09 14:10:51 +08:00
ZyyandCodex 98704dfde8 fix(usage): parse response critical path timings
Read response-scoped engine and API overhead metrics while excluding logical-turn totals and incomplete observations. Validate nested response IDs and cover the observed payload over SSE and WebSocket.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 13:51:40 +08:00
ZyyandCodex b07bca312a fix(upgrade): handle long migrations and unify forced shutdown
Extend the startup healthcheck grace period to five minutes and report elapsed migration time every fifteen seconds without changing published migrations.

Use one shutdown path for OS signals, internal cancellation, and managed restarts. Stop listening without waiting for active sessions, preserve bounded worker cleanup, and remove the obsolete connection drain timeout.

Cover shutdown entry points and worker cleanup with real HTTP lifecycle tests and document the deployment behavior.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 13:51:14 +08:00
ZyyandCodex 60966d935e chore(release): v3.22.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-09 13:03:32 +08:00
ZyyandCodex 52981f40f9 fix(providers): align Codex 0.162 protocol handling
Apply latest incremental tool declarations during Grok normalization and align nested Retry-After header parsing with official Codex semantics.

Cover developer instructions in input, Responses Lite tool declarations, partial_answer phases, and end_turn preservation.

Pin the upstream SQLx fix for cancelled BEGIN rollback and add a deterministic pooled-connection regression test.

Report safe Redis error categories and timeout status when the cooldown persistence regression fails.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 12:10:02 +08:00
ZyyandCodex a51c4cd640 refactor(plugins): simplify lifecycle and reuse unchanged instances
Share unchanged prepared instances across extension sets and close each
process when its last owner releases it. Keep inflight request plans intact.

Unify migration recovery and publication, centralize authentication binding
validation, avoid duplicate package inspection, and separate middleware plans.

Validated with 292 admin, 182 runtime, 37 plugin storage and 15 architecture
tests, rustfmt, and strict workspace Clippy.

Co-authored-by: Codex <noreply@openai.com>
2026-10-09 00:12:29 +08:00
ZyyandCodex bda6d973a8 docs: streamline documentation around current behavior
Remove version migration narratives, consolidate repeated explanations, and organize settings and architecture references.

Co-authored-by: Codex <noreply@openai.com>
2026-10-08 22:26:12 +08:00
ZyyandCodex 66a7e4a695 fix(client): align Codex config export with CC Switch 4.0.4
Export config.toml with provider bearer authentication and send supported connection and usage fields to CC Switch. Remove the separate auth.json output and size the configuration preview to its content.

Co-authored-by: Codex <noreply@openai.com>
2026-10-08 22:09:02 +08:00
think-backandCodex 9483f2c91b perf(groups): avoid aggregate queries when loading group selectors
Load account and API key group selectors through a lightweight admin options
endpoint while preserving pagination, filters, disabled groups and revision
metadata. Keep aggregate statistics on the group management endpoint.

Co-authored-by: Codex <noreply@openai.com>
2026-10-08 18:20:24 +08:00
ZyyandCodex faf50c28ad feat(usage): unify request observations and upstream performance
Consolidate request diagnostics into typed request_observation_json with
migration 0025, preserving historical facts and query constraints.

Use upstream response timestamps for output throughput, align TTFT event
boundaries, and surface official timing metrics in the latency popover.

Co-authored-by: Codex <noreply@openai.com>
2026-10-08 18:17:09 +08:00
ZyyandCodex cb397ae53e chore(release): v3.21.3
Co-authored-by: Codex <noreply@openai.com>
2026-10-08 12:12:34 +08:00
ZyyandCodex 63802f9b1a fix(usage): exclude cached tokens from displayed input
Reuse the fresh input calculation across usage records, token details and key usage metrics.

Co-authored-by: Codex <noreply@openai.com>
2026-10-08 11:43:26 +08:00
ZyyandCodex de599ed768 fix(openai): apply timezone overrides without content kinds
Recognize complete Codex environment contexts when custom providers omit
content classifications, while keeping explicit classifications authoritative.

Cover HTTP and WebSocket overrides and proxy location precedence, and
document timezone and client privacy boundaries.

Refs #366

Co-authored-by: Codex <noreply@openai.com>
2026-10-08 11:16:31 +08:00
ZyyandCodex 3315ddbff6 fix(openai): isolate guardian concurrency from ordinary requests
Give Guardian requests a separate per-account lease pool and wait queue while preserving ordinary concurrency and shared request intervals. Use the ordinary pool when the Guardian limit is zero, and align scheduling, diagnostics, settings copy, and documentation.

Co-authored-by: Codex <noreply@openai.com>
2026-10-08 10:38:52 +08:00
ZyyandCodex 6c583e341a fix(openai): reject invalid continuation replay before account selection
Check the recorded native continuation scope before selecting an account
or committing a session binding. Reuse the same scope validation after
middleware while preserving native connection checks and migration policy.

Cover ReplayOwner and ReplayAny with persisted provider metadata or a
connection-local pin, with and without an existing session binding.
Rejected replays must not acquire leases, write bindings or aliases, send
upstream requests, or redirect subsequent child requests.

Refs #364

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 22:45:42 +08:00
ZyyandCodex fb15f3e36c chore(release): v3.21.2
Co-authored-by: Codex <noreply@openai.com>
2026-10-07 20:21:38 +08:00
ZyyandCodex a940eb599f fix(openai): allow atomic first binding from any session request
Allow requests with a reliable session identity to claim a missing account
binding, including child threads and independent background tasks. Remove
the missing-binding wait queue while preserving existing migration rights
and bound-account waiting behavior.

Cover concurrent root/child claims, memory prewarm and turn reuse,
middleware identity changes, cancellation, and bound-account timeouts.
Update the documented strict-affinity contract.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 20:18:03 +08:00
ZyyandCodex 22a848b360 refactor(backend): clarify ownership and align xai contracts
Separate execution, provider ports, administration, and plugin lifecycle responsibilities.

Fix shutdown ordering, account lock reclamation, multipart cancellation, budget windows, and WebSocket close handling.

Align xAI catalog, reasoning, tools, replay, quota, and cost handling with current Grok contracts and remove legacy fallbacks.

Update API and architecture documentation with regression coverage. Validated backend suites, strict workspace Clippy, isolated PostgreSQL/Redis integration, and real Codex/Grok client flows.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 19:34:10 +08:00
ZyyandCodex 754da83b02 refactor(backend): simplify query ownership and resource handling
Move observability query policies into Admin and reuse account state reads.
Bound Redis reads to measured batches and share protocol and transport helpers.
Close backup process and staging cleanup gaps, and remove unused entry points,
redundant models, dependencies, and obsolete tests.

Validated 3,526 workspace tests, then all 352 Store tests after batch tuning,
plus strict workspace Clippy and formatting checks. Local release benchmarks
cover concurrent account reads and controlled transport delays.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 15:43:29 +08:00
ZyyandCodex f174320e2a chore(release): v3.21.1
Co-authored-by: Codex <noreply@openai.com>
2026-10-07 03:57:15 +08:00
ZyyandCodex 9db8528758 fix(metrics): align TTFT and throughput with sub2api
Count non-preamble Responses events for first-token timing while preserving
semantic output boundaries and request-level timing origins. Calculate
output throughput over the complete request duration in usage rows and
aggregate queries.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 03:52:59 +08:00
ZyyandCodex 119032c067 feat(settings): add preferred account affinity mode
Route relaxed requests through the configured scheduler and keep the session primary during preferred-mode spillover. Default new settings to strict while preserving saved modes.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 03:50:59 +08:00
ZyyandCodex 3d1dc79f5d fix(auth): refresh sessions only after authentication failures
Use read-only status checks for initial authentication and remove activity-driven renewal. Keep failed and newly queued requests waiting for the current refresh before replaying with the updated cookie.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 03:10:50 +08:00
ZyyandCodex 128ced4093 chore(release): v3.21.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-07 02:49:11 +08:00
ZyyandCodex 693025afa4 refactor(store): consolidate unreleased database migrations
Merge error details and account affinity settings into migration 0023. Preserve all migrations shipped in v3.20.0 and validate upgrades from its 0022 baseline.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 02:49:01 +08:00
ZyyandCodex 0bfeff28fc refactor(backend): centralize shared models and settings validation
Reuse domain models for observability, account statistics, execution facts,
and group validation. Compose shared settings values while preserving
HTTP and SDK contracts and explicit storage and runtime boundaries.

Verify serialization, concurrency, persistence, and snapshot behavior with
3528 backend tests, strict Clippy, formatting, and architecture checks.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 02:32:53 +08:00
ZyyandCodex cb7daffb66 feat(settings): configure account affinity and rotation limits
Add relaxed and strict affinity modes, configurable binding retention, and per-request account rotation limits. Freeze policies for each request and preserve continuation ownership and plugin selection behavior.

Expose the controls in a dedicated affinity settings card and cover scheduling, persistence, renewal, and retry boundaries.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 01:30:10 +08:00
ZyyandCodex d922c3af60 fix(diagnostics): preserve error chains and avoid logging backpressure
Preserve native failure causes through store, provider, plugin, admin, and
worker boundaries, including secondary cleanup failures. Route operational
failures through the shared diagnostics sink with protected error details.

Use bounded nonblocking file logging with batched writes and reserved
warning/error capacity. Reserve observation capacity for failures and
request lifecycle records, and rate-limit overload warnings.

Validate error propagation and blocked-output HTTP, admin, SSE, and WebSocket
behavior; keep the local migration plan out of version control.

Co-authored-by: Codex <noreply@openai.com>
2026-10-07 00:25:18 +08:00
ZyyandCodex 366bc5f1ca feat(client-profile): expose desktop OS version setting
Co-authored-by: Codex <noreply@openai.com>
2026-10-06 18:29:12 +08:00
ZyyandCodex a5e4041295 fix(diagnostics): preserve failure causes across request tracing
Retain preparation and account-selection diagnostics, preserve plugin fault
details, and record failures before attempt startup. Keep failure
classification when trace summaries are truncated.

Carry native error sources through Core, infrastructure availability errors,
OpenAI credential selection, provider inference, and plugin upstream adapters.
Preserve original upstream codes independently of failure classification and
project bounded cause chains and upstream bodies into protected error details.

Rename persisted and admin API error detail fields, keep raw details out of
ordinary logs and diagnostic exports, and account for retained error data in
the observation queue memory budget. Add regression coverage for source
retention, safe output, and migration upgrades; remove account-dependent live
tests. Document the implemented diagnostic boundaries.

Refs #357

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 18:19:04 +08:00
ZyyandCodex b27877c99b fix(memory): release request copies and preallocate XZ buffers
Release preliminary Responses decodes before middleware execution and move
WebSocket frame ownership without cloning the request body. Reserve XZ
block buffers from validated lengths while retaining overflow detection.

Add allocation regression coverage for 24 MiB and 32 MiB XZ blocks.

Refs #338

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 15:47:25 +08:00
ZyyandCodex 7505459ece fix(accounts): constrain long plan badges to available width
Truncate overflowing plan names and show the full display name on hover while preserving the existing subscription column width.

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 13:52:29 +08:00
ZyyandCodex 102d064ab0 fix(metrics): align first-token timing with request duration
Record semantic output from the first stream chunk and reuse Core timing for xAI. Add regression and opt-in live transport coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 13:52:24 +08:00
ZyyandCodex 7616fdf4ab feat(auth): add seamless admin session renewal
Renew active administrator sessions with atomic Redis expiry updates,
synchronized cookies, and idle and absolute lifetime limits. Preserve
revocation after logout and password changes.

Coordinate session recovery, bounded retries, error handling, and raw
responses through request plugins. Gate initial navigation in the router
and organize frontend API modules by their views.

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 12:49:50 +08:00
ZyyandCodex 5cfe0c68cc fix(deps): update source-map-js to 1.2.2
Resolve GHSA-68fv-2mgg-jv7q in transitive frontend dependencies.

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 11:22:48 +08:00
ZyyandCodex 7940735866 chore(release): v3.20.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-06 11:04:29 +08:00
ZyyandCodex 7741503b03 refactor(store): consolidate unreleased runtime migrations
Merge refresh margin and group Fast mode changes into migration 0022 before v3.20.0. Preserve the v3.19.0 migration history and verify upgrades from migration 0021.

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 11:03:28 +08:00
ZyyandCodex 74f749f49d fix(openai): unify conversation account bindings
Share the current account across root and descendant requests, including
Search, Images turn aliases, and Live creation. Keep root recovery and
explicit plugin scheduling intact while descendant requests wait for the
current owner and follow root migrations.

Use versioned admission before sending so concurrent claims and late
responses cannot overwrite newer bindings. Cover queue lifecycle,
continuation recovery, endpoint routing, and Redis concurrency behavior.

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 07:04:41 +08:00
c4d8732a73 feat(usage): add performance column and align Fast indicators (#354)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-06 02:36:41 +08:00
0a47b5efc0 feat(groups): add Fast modes with plugin compatibility warnings (#343)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-06 01:21:25 +08:00
ZyyandCodex 35a7fa5a73 fix(openai): isolate live profiles and turn routing state
Freeze the selected client profile for Live sideband, reconnect and hangup,
while reloading current account credentials and outbound proxy settings.
Share identity-header protection, clear turn state across wire entry points,
and include Guardian and residency headers in WebSocket connection matching.
Normalize the existing Cargo.lock http reference without changing versions.

Validated 1389 tests, workspace strict Clippy, Rustfmt and architecture checks.

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 00:51:22 +08:00
KumikoLoveandCodex 0ce81eea1d feat: add account scheduling controls and client profile options (#339)
Stagger credential refresh and quota observations, bound each request to three account rotations, and expose terminal and version lag configuration.

Retain main's bounded refresh 401 recovery policy. Version lag accepts integers from 1 to 10 or an empty value.

Co-authored-by: Codex <noreply@openai.com>
2026-10-06 00:09:02 +08:00
2han9wen71anandCodex 22acbd1d10 feat(openai): add Codex live voice call proxying (#308)
Add scoped call bootstrap, pinned-account sideband relay and hangup. Preserve model metadata, use standard WebSocket handshakes, and release session claims safely on cancellation.

Validated with regression tests, CI and a real OAuth/WebRTC call.

Co-authored-by: Codex <noreply@openai.com>
2026-10-05 23:48:55 +08:00
78ba7e412e fix(codex): align upstream contracts and transport behavior (#352)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-05 23:41:46 +08:00
ZyyandCodex f8439eb4b9 chore(deps): upgrade syn and prettyplease together (#349)
Co-authored-by: Codex <noreply@openai.com>
2026-10-05 13:47:14 +08:00
ZyyandCodex 55062228e4 fix(frontend): prevent proxy exit IP overflow
Fixes #351

Co-authored-by: Codex <noreply@openai.com>
2026-10-05 13:36:35 +08:00
dependabot[bot] f7585ede0c chore(deps): bump jsonschema from 0.57.0 to 0.58.4 in /backend (#348)
Bumps [jsonschema](https://github.com/Stranger6667/jsonschema) from 0.57.0 to 0.58.4.
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/cli-v0.57.0...cli-v0.58.4)

---
updated-dependencies:
- dependency-name: jsonschema
  dependency-version: 0.58.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 13:31:18 +08:00
dependabot[bot] 94ae5f693f chore(deps): bump the minor-and-patch group across 1 directory with 5 updates (#347)
Bumps the minor-and-patch group with 5 updates in the /backend directory:

| Package | From | To |
| --- | --- | --- |
| [config](https://github.com/rust-cli/config-rs) | `0.15.26` | `0.15.27` |
| [quote](https://github.com/dtolnay/quote) | `1.0.46` | `1.0.47` |
| [http-body](https://github.com/hyperium/http-body) | `1.0.1` | `1.1.0` |
| [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust) | `1.149.0` | `1.152.0` |
| [aws-smithy-runtime-api](https://github.com/smithy-lang/smithy-rs) | `1.18.0` | `1.19.0` |



Updates `config` from 0.15.26 to 0.15.27
- [Changelog](https://github.com/rust-cli/config-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/config-rs/compare/v0.15.26...v0.15.27)

Updates `quote` from 1.0.46 to 1.0.47
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.46...1.0.47)

Updates `http-body` from 1.0.1 to 1.1.0
- [Release notes](https://github.com/hyperium/http-body/releases)
- [Commits](https://github.com/hyperium/http-body/compare/v1.0.1...v1.1.0)

Updates `aws-sdk-s3` from 1.149.0 to 1.152.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `aws-smithy-runtime-api` from 1.18.0 to 1.19.0
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-version: 1.152.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-runtime-api
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: config
  dependency-version: 0.15.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: http-body
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: quote
  dependency-version: 1.0.47
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 13:31:13 +08:00
ZyyandCodex 4961788acf docs: align documentation and skills with current implementation
Co-authored-by: Codex <noreply@openai.com>
2026-10-05 13:22:07 +08:00
ZyyandCodex 24f39f7f0e docs(backend): standardize Rust file comments
Add Chinese responsibility summaries to every backend Rust source, test,
and build script. Remove sentence-ending periods from comments while
preserving meaningful dots in URLs, versions, identifiers, and examples.

Keep generated SDK documentation in sync with its contract generator,
group middleware module declarations and exports, and record the comment
conventions in the development skill.

Validated with Rustfmt, strict Clippy, backend tests, documentation tests,
and skill validation. Database-backed integration cases were skipped
because dedicated PostgreSQL and Redis test services were not configured.

Co-authored-by: Codex <noreply@openai.com>
2026-10-05 11:52:56 +08:00
ZyyandCodex 9c5fbdba70 refactor(frontend): clarify module boundaries and simplify state
Move shared components and helpers to their actual owners, standardize
component directory names, and use concise names for common utilities.
Remove redundant wrappers, copied API models, and duplicate state.

Separate API key editing from key usage, isolate settings drafts, reuse
paged queries, and tie debounced queries and requests to their scopes.

Validated with frontend lint, type checking and production build, local
import checks, and 15 temporary behavioral regression groups.

Co-authored-by: Codex <noreply@openai.com>
2026-10-05 10:34:10 +08:00
ZyyandCodex 28a57cbde5 feat(deploy): add opt-in low-memory allocator configuration
Keep the glibc tuning value in config.yaml's app-runtime bridge and let
Compose inject it before the application starts. Leave tuning disabled
by default and document the 128 KiB preset, recreation, and upgrade steps.

Refs #338, #345

Co-authored-by: Codex <noreply@openai.com>
2026-10-05 09:44:32 +08:00
ZyyandCodex 1c8a74e572 fix(core): release dropped cancellation waiters
Use event-listener registrations that unregister when cancellation futures
are dropped, including waits attached to ancestor tokens. Recheck the
cancelled flag after registration to preserve wakeups during races.

Add allocation regressions for select churn and child cancellation, plus
broadcast and concurrent cancellation coverage.

Refs #338, #345

Co-authored-by: Codex <noreply@openai.com>
2026-10-05 09:27:42 +08:00
ZyyandCodex 7c266b469b fix(core): remove the default model request timeout
Allow long model responses to finish without a fixed 600-second deadline,
while preserving explicit plugin deadlines and upstream idle timeouts.

Renew request recovery, client admission and provider scheduling leases
for the session lifetime, and release their renewal tasks with the owners.
Keep observation renewal best-effort and cancel on lost concurrency leases.

Fixes #342

Co-authored-by: Codex <noreply@openai.com>
2026-10-03 03:30:39 +08:00
WeiSiTaPanandCodex d37dc32731 feat(usage): add a yellow Fast tier badge (#341)
Co-authored-by: Codex <noreply@openai.com>
2026-10-02 23:18:11 +08:00
ZyyandCodex 9a749d1a03 fix(api): omit timezone offsets from chart labels
Format display labels without UTC offsets while retaining timestamps as chart identities.

Co-authored-by: Codex <noreply@openai.com>
2026-10-02 00:03:54 +08:00
ZyyandCodex fef52b3717 chore(release): v3.19.0
Co-authored-by: Codex <noreply@openai.com>
2026-10-01 23:24:57 +08:00
ZyyandCodex 45e72682e6 refactor(store): persist warmup progress in runtime settings
Replace warmup slot history with a monotonic UTC cursor and consolidate unreleased scheduling migrations.

Co-authored-by: Codex <noreply@openai.com>
2026-10-01 23:21:48 +08:00
90e241f74b feat(openai): configure Guardian concurrency priority at runtime (#306)
Configure Guardian reservations through persisted runtime settings and frozen request policies. Combine concurrency controls and queues in the admin UI, with the scheduling strategy shown first.

Prioritize Guardian account waiters, recheck displaced queue heads before lease acquisition, and report request-specific available capacity. Preserve account, key and wait-budget limits.

Co-authored-by: Yongzhao Chen <yongzhao.derek@gmail.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-01 22:49:11 +08:00
ZyyandCodex a0e8f768dd fix(ci): align container timezone checks with UTC
Assert the UTC PostgreSQL timezone and runtime timezone configured by the deployment files. Keep the application business timezone independent from the infrastructure checks.

Co-authored-by: Codex <noreply@openai.com>
2026-10-01 21:40:27 +08:00
ZyyandCodex 318f8a748e fix(modal): preserve content through close transitions
Keep modal content and request state stable until the exit transition completes, and defer credential cleanup until after leave. Update the UI dependency and submodules to v0.3.2, including the plugin workbench consumer.

Co-authored-by: Codex <noreply@openai.com>
2026-10-01 21:40:22 +08:00
ZyyandCodex 0dcba14572 refactor(bootstrap): separate startup modes and plugin wiring
Co-authored-by: Codex <noreply@openai.com>
2026-10-01 16:00:07 +08:00
ZyyandCodex ab3a2e30c5 feat(time): unify deployment timezone across display and scheduling
Use one validated IANA timezone for backend display projections, calendar queries, budget windows, warmup slots, backups, and logs. Remove frontend date formatting and the independent backup timezone setting.

Preserve UTC instants and active budget windows, persist warmup deduplication, and advance backup schedules transactionally. Cover DST boundaries, timezone changes, invalid calendar anchors, and account cooldown displays.

Co-authored-by: Codex <noreply@openai.com>
2026-10-01 15:53:22 +08:00
ZyyandCodex 0490e28cfc feat(accounts): show Codex credit balances in quota reset dialog
Expose upstream credit facts through the account quota API while preserving
decimal balances and distinguishing zero, unlimited, and missing values.
Clear stale balances on passive updates without changing quota availability.

Display credits below reset cards, label OpenAI quota as Codex quota, and
standardize the account quota action icons.

Co-authored-by: Codex <noreply@openai.com>
2026-10-01 13:22:20 +08:00
ZyyandCodex 9c10676764 chore(release): v3.18.3
Co-authored-by: Codex <noreply@openai.com>
2026-09-30 22:20:06 +08:00
ZyyandCodex caa6c8ffc8 refactor(plugin): keep external quota cycles out of host ledger (#332)
Restore the pre-window host implementation and document plugin-owned policy and presentation. Keep native reset RPC coverage and run plugin integration tests with the standard isolated test services.

Co-authored-by: Codex <noreply@openai.com>
2026-09-30 21:47:50 +08:00
9d651856be fix(outbound): avoid duplicate Host headers on HTTP/2 (#337)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-30 19:26:58 +08:00
ZyyandCodex d10b8890e0 fix(admin): improve quota forecast and subscription display
Move the quota forecast entry after reset credits and propagate account updates from the quota panel. Include each error account current subscription in troubleshooting and unify subscription labels.

Co-authored-by: Codex <noreply@openai.com>
2026-09-30 18:45:08 +08:00
ZyyandCodex d97c6e064b fix(keys): restart budget windows on use after reset (#335)
Co-authored-by: Codex <noreply@openai.com>
2026-09-30 17:54:47 +08:00
ZyyandCodex f85801edb3 fix(usage): balance diagnostic column widths
Size diagnostic names by dimension and share available width across metric columns. Keep API key names compact.

Co-authored-by: Codex <noreply@openai.com>
2026-09-30 04:33:21 +08:00
ZyyandCodex e30aad4755 chore(release): v3.18.2
Co-authored-by: Codex <noreply@openai.com>
2026-09-30 03:41:55 +08:00
ZyyandCodex 945f7912ac chore(deps): sync UI v0.3.1 and plugin references
Update the admin UI dependency and lockfile, and align the UI and plugin submodules with the released modal fix.

Co-authored-by: Codex <noreply@openai.com>
2026-09-30 03:38:10 +08:00
ZyyandCodex 19038f264a fix(admin): correct usage diagnostics and refine management views
Use all matching requests for diagnostic shares and count retried requests once. Show current account plans in diagnostics and request records with compact, aligned columns.

Simplify quota forecasts and remove unused remaining-capacity response fields. Open system updates immediately with the existing loading skeleton and clip masked key prefixes without an ellipsis.

Co-authored-by: Codex <noreply@openai.com>
2026-09-30 03:28:58 +08:00
ZyyandCodex 71c3b03878 fix(openai): add built-in GPT-6.1 Sol pricing
Co-authored-by: Codex <noreply@openai.com>
2026-09-30 03:27:01 +08:00
ZyyandCodex 70d1557b55 chore(release): v3.18.1
Co-authored-by: Codex <noreply@openai.com>
2026-09-30 01:36:39 +08:00
ZyyandCodex b4d11694ae fix(update): confirm incompatible plugin shutdown before restart
Co-authored-by: Codex <noreply@openai.com>
2026-09-30 01:36:32 +08:00
ZyyandCodex 718b959fc1 chore(release): v3.18.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-30 00:14:35 +08:00
ZyyandCodex 11c076ef5a fix(release): seal current plugin compatibility declaration
Validate the v2 host declaration while retaining the v1 release envelope, and sync the plugin submodule to stable v0.2.0.

Co-authored-by: Codex <noreply@openai.com>
2026-09-30 00:14:22 +08:00
ZyyandCodex b37d790144 fix(api): default HTTP responses to JSON when stream is omitted
Preserve explicit stream preferences and the WebSocket streaming default without rewriting request bodies or changing upstream execution.

Add decoder and HTTP delivery regression coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-09-29 22:15:19 +08:00
ZyyandCodex 3a04e166c2 refactor: clarify ownership and simplify plugin management
Centralize audit classifications and retention policy, move cleanup orchestration to Host, and require explicit plugin lifecycle implementations.

Remove unused interfaces and redundant state, type-check the standalone plugin bridge, and split management flows by responsibility.

Update architecture boundaries and add focused regression coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-09-29 20:56:10 +08:00
ZyyandCodex ec21d83415 refactor(backend): flatten leaf modules
Co-authored-by: Codex <noreply@openai.com>
2026-09-29 18:42:28 +08:00
ZyyandCodex fe78c230e5 feat(plugin): expose request lifecycles through onion middleware
Compose HTTP, WebSocket, request, attempt and public service calls through
shared onion middleware with scoped settings and invocation context.

Adopt trusted-process manifests, unified observation and forward-compatible
SDK contracts. Align management UI, documentation and the workbench reference.
Preserve native response bytes and WebSocket failure lifecycles.

Co-authored-by: Codex <noreply@openai.com>
2026-09-29 18:20:22 +08:00
Zyy d83d3eb803 feat(plugin): add managed upstream adapters (#326)
feat(plugin): add managed upstream adapters for built-in providers
2026-09-28 23:31:25 +08:00
ZyyandCodex faee3b2642 feat(plugin): add managed upstream adapters for built-in providers
Co-authored-by: Codex <noreply@openai.com>
2026-09-28 23:23:41 +08:00
ZyyandCodex a5a844af16 fix(openai): align connection settings with Codex defaults
Use the official reqwest defaults for idle pooling, TCP keepalive, and HTTP/2 keepalive.

Co-authored-by: Codex <noreply@openai.com>
2026-09-28 20:02:04 +08:00
dependabot[bot] d6bdb9d191 chore(deps): bump redis in /backend in the minor-and-patch group (#321)
Bumps the minor-and-patch group in /backend with 1 update: [redis](https://github.com/redis-rs/redis-rs).


Updates `redis` from 1.7.0 to 1.7.1
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-1.7.0...redis-1.7.1)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 1.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 18:48:08 +08:00
dependabot[bot] 33aa161863 chore(deps): bump node from 2d49d87 to 662933c in /deploy (#320)
Bumps node from `2d49d87` to `662933c`.

---
updated-dependencies:
- dependency-name: node
  dependency-version: 26-bookworm-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 18:47:30 +08:00
ZyyandCodex dc45aee5da ci: streamline cache handling and release jobs
Validate Cargo mount snapshots before saving, group cache tooling in a local action, and decouple image builds from release packaging. Use prebuilt security tooling and narrow unrelated CI triggers.

Co-authored-by: Codex <noreply@openai.com>
2026-09-28 18:33:20 +08:00
ZyyandCodex b3fa5322f1 perf(test): reduce backend fixture and timeout overhead
Reuse immutable plugin archives across nextest processes and share admin fixtures within test matrices. Reduce log scanning, hashing, compression, database setup, and SSE scanning costs while preserving isolation and boundary coverage.

Use controllable clocks for recovery budgets and quota scheduling, retaining real socket and HTTP verification.

Validation: 3200 backend tests, 132 plugin persistence tests, 54 Redis ACL tests, 30 clock stress iterations, clippy, and formatting checks passed. Local full-suite runtime improved from 182.8s to 74.0s at unchanged concurrency.

Co-authored-by: Codex <noreply@openai.com>
2026-09-28 17:49:23 +08:00
ZyyandCodex 8fb42300c1 ci: streamline release checks and protect contribution rules
Reuse test binaries and frontend builds, reduce redundant cache writes, and overlap release preparation with quality checks.

Protect skills, contribution rules, and GitHub configuration, and simplify workflow names.

Co-authored-by: Codex <noreply@openai.com>
2026-09-28 16:54:13 +08:00
ZyyandCodex a1fc368474 docs: streamline documentation and skill workflows
Keep documentation focused on current behavior and clear ownership.
Load skill references by task and clarify official source priority and reply tone.

Co-authored-by: Codex <noreply@openai.com>
2026-09-28 15:54:49 +08:00
dependabot[bot] 9a1e90dd3d chore(deps): bump jsonschema from 0.56.0 to 0.57.0 in /backend (#315)
Bumps [jsonschema](https://github.com/Stranger6667/jsonschema) from 0.56.0 to 0.57.0.
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/ruby-v0.56.0...ruby-v0.57.0)

---
updated-dependencies:
- dependency-name: jsonschema
  dependency-version: 0.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 14:40:21 +08:00
dependabot[bot] e7b0551f1a chore(deps): bump the minor-and-patch group in /backend with 6 updates (#314)
Bumps the minor-and-patch group in /backend with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [thiserror](https://github.com/dtolnay/thiserror) | `2.0.20` | `2.0.21` |
| [config](https://github.com/rust-cli/config-rs) | `0.15.25` | `0.15.26` |
| [headers](https://github.com/hyperium/headers) | `0.4.1` | `0.4.2` |
| [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust) | `1.148.0` | `1.149.0` |
| [aws-smithy-runtime-api](https://github.com/smithy-lang/smithy-rs) | `1.16.2` | `1.18.0` |
| [aws-smithy-types](https://github.com/smithy-lang/smithy-rs) | `1.6.4` | `1.8.1` |


Updates `thiserror` from 2.0.20 to 2.0.21
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.20...2.0.21)

Updates `config` from 0.15.25 to 0.15.26
- [Changelog](https://github.com/rust-cli/config-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/config-rs/compare/v0.15.25...v0.15.26)

Updates `headers` from 0.4.1 to 0.4.2
- [Release notes](https://github.com/hyperium/headers/releases)
- [Commits](https://github.com/hyperium/headers/compare/headers-v0.4.1...headers-v0.4.2)

Updates `aws-sdk-s3` from 1.148.0 to 1.149.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `aws-smithy-runtime-api` from 1.16.2 to 1.18.0
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `aws-smithy-types` from 1.6.4 to 1.8.1
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-version: 2.0.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: config
  dependency-version: 0.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: headers
  dependency-version: 0.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-sdk-s3
  dependency-version: 1.149.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-runtime-api
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-types
  dependency-version: 1.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 14:39:54 +08:00
dde4a91974 feat(plugin): include account name and email in data facts (#313)
* feat(plugin): include account name and email in data facts

Co-authored-by: Codex <noreply@openai.com>

* fix(plugin): allow additive fields in data responses

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-09-28 14:25:01 +08:00
ZyyandCodex bb859455f9 fix(update): retain binary installation path for self-restart
Resolve and retain the executable path at startup so updates, rollbacks and
restarts keep using the installation path after the running binary is renamed.
Document binary self-restart configuration and cover renamed executable restarts.

Refs #316

Co-authored-by: Codex <noreply@openai.com>
2026-09-28 13:06:04 +08:00
ZyyandCodex e06ca56f93 chore(plugins): update examples and require official source references
Advance plugin examples to v0.1.17 and clarify source priorities for plugin development.

Co-authored-by: Codex <noreply@openai.com>
2026-09-28 03:25:57 +08:00
ZyyandCodex 2a65b53b44 fix(accounts): refine capacity alignment and daily usage display
Co-authored-by: Codex <noreply@openai.com>
2026-09-28 01:10:25 +08:00
ZyyandCodex 3915aa259b test(accounts): align capacity tests with module layout
Co-authored-by: Codex <noreply@openai.com>
2026-09-27 22:53:03 +08:00
ZyyandCodex d310286e33 chore(release): v3.17.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-27 22:41:22 +08:00
Zyy ea4e3059db feat(accounts): show current concurrency capacity 2026-09-27 22:36:51 +08:00
3bf7030fba fix(openai): widen and trace stream replay grace (#304)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-27 19:51:41 +08:00
5d5965c6ca feat(plugin): expose non-secret client key group facts (#302)
Co-authored-by: root <root@DESKTOP-PRVCERP.localdomain>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-27 19:47:35 +08:00
ZyyandCodex 0710ed1f7c feat(plugins): expose native budgets and quota observations (#300)
Expose independent key budget management and quota observation capabilities
through the public plugin SDK, runtime authorization, and native admin services.
Reuse existing budget transactions and Provider quota refresh paths without
adding workflow-specific state or automatic account-to-key orchestration.

Cover authorization, partial budget updates, resets, quota refresh projection,
transaction rollback, and SDK request behavior. Remove ignored external probes.

Co-authored-by: Codex <noreply@openai.com>
2026-09-27 18:44:32 +08:00
4634d8dc6d fix(openai): clarify quota failures without changing credential state (#301)
Preserve quota HTTP status and bounded error codes, and expose actionable
static messages through the existing admin error response. Record background
quota failures in structured logs without mutating credential snapshots.

Cover OAuth terminal results during concurrent quota rejection, successful
recovery, account fact preservation, and safe HTTP error propagation.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-27 15:31:37 +08:00
ZyyandCodex 8a0759905e fix(frontend): stabilize loading states and unify key usage actions
Keep client identity fields visible while presets load and preserve dashboard
content during silent refreshes. Align key usage toolbar backgrounds, spin the
refresh icon, and disable logout without replacing its icon.

Co-authored-by: Codex <noreply@openai.com>
2026-09-27 13:27:13 +08:00
ZyyandCodex e830b2ef48 fix(openai): isolate account context and scope websocket interrupts (#298)
Filter account-owned routing and identity context at API and Provider boundaries. Bind WebSocket interrupts to the active response on its original account and socket, preserving valid continuations.

Co-authored-by: Codex <noreply@openai.com>
2026-09-27 05:27:07 +08:00
ae8da00459 fix(openai): preserve affinity while waiting for account capacity (#297)
Classify saturated account snapshots as capacity unavailable and prefer
the bound account's existing queue when account waiting is enabled.
Preserve explicit policy picks, child affinity delegation and queue limits.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-27 03:30:09 +08:00
ZyyandCodex 7bbea42fba docs(redis): clarify ACL setup and add regression coverage
Document ACL usernames in connection URLs and the Compose URL override.
Run Store tests with a dedicated ACL user and the default user disabled
after the existing password-authenticated suite.

Co-authored-by: Codex <noreply@openai.com>
2026-09-27 02:49:41 +08:00
ZyyandCodex 2e0669d462 fix(frontend): distinguish historical system update failures
Keep previous operation failures available as history without marking the current installation as failed. Preserve current task errors, response-loss recovery, and restart status.

Fixes #236

Co-authored-by: Codex <noreply@openai.com>
2026-09-27 02:08:48 +08:00
ZyyandCodex 2df9494e99 fix(frontend): stabilize system update progress layout
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 22:26:10 +08:00
ZyyandCodex 0534dd8f26 chore(release): v3.16.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 22:02:00 +08:00
ZyyandCodex 525f3d51c1 refactor(store): consolidate unreleased scheduling migration
Fold smart scheduling settings into migration 0019 while preserving all released migrations through 0018. Update the checksum manifest and upgrade test.

Development databases that applied the previous 0019 or 0020 cannot upgrade in place because the migration history differs.

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 21:59:05 +08:00
CharmskyandCodex 33cc427b86 feat(scheduling): add configurable smart scheduling preferences (#295)
Make smart scheduling coefficients configurable and expose optional
higher-weight switchback while preserving the original defaults.

Reuse current quota reset signals, process-local queues, and frozen request
settings while preserving continuation ownership and queue fairness.

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 21:51:28 +08:00
ZyyandCodex d1bf90d5fd fix(frontend): support HTTP plugin pages and consolidate shared helpers
Reuse secure UUID generation for plugin frame channels and sessions on HTTP.
Consolidate utility modules and shorten the plugin views store name.
Declare route roles and default entries in metadata instead of guard mappings.
Keep source integration build output under node_modules/.vite.

Validated frontend lint, production and source builds, browser plugin flows,
and 20 route guard scenarios with simulated sessions and plugin resources.

Fixes #288

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 19:33:17 +08:00
eb23a0fb3b fix(plugins): align management request timeouts (#294)
Use a shared 180-second timeout for management route HTTP calls and the page bridge while keeping the backend RPC limit at 120 seconds. Fix the existing Vue attribute-order lint warning.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 19:11:01 +08:00
niceSpriteandCodex ca90f2b586 fix(openai): recover oversized Codex requests before WebSocket send (#287)
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 19:03:40 +08:00
ZyyandCodex cfcf7c486a fix(plugins): detect latest releases and streamline upgrades (#293)
Refresh explicit GitHub release checks while preserving concurrent fetch sharing and fixed-release download caching.

Compare verified package versions and offer a direct upgrade with pinned digests, revision checks, permission disclosure, and configuration fallback.

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 19:00:23 +08:00
b981e42342 feat(plugins): provision owned resources and reconcile account changes (#292)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 18:46:12 +08:00
ZyyandCodex 02177af921 fix(accounts): refresh OAuth tokens while scheduling is disabled (#291)
Separate scheduling availability from OAuth candidate selection and credential
state writes across OpenAI, xAI, and the shared account store. Cover disabled
refresh, terminal failures, and in-flight scheduling changes with regression tests.

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 18:32:48 +08:00
ZyyandCodex 2d20f03bcb refactor(admin): simplify diagnostics and refresh shared UI integration
Adopt UI v0.3.0 and its shared Vite adapter, preserve request-volume ordering in diagnostics, stabilize sidebar animation, and track workbench v0.1.15.

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 17:14:54 +08:00
ZyyandCodex c12c600b42 fix(models): scope catalog visibility to discovering accounts (#286)
Preserve OpenAI catalog account sources in runtime snapshots and apply
frozen account scope and model policies when publishing model metadata.
Keep discovery catalogs separate from inference authorization and cover
mixed policies, aliases, group changes, and catalog generation updates.

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 15:51:13 +08:00
ZyyandCodex 1c6b5a8f10 fix(accounts): restore OAuth reauthorization flow
Generate authorization links when reopening account authorization and regenerate them in a single action. Remove the added authorization page shortcut and unused reset event wiring.

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 05:32:04 +08:00
ZyyandCodex 589e1bc999 chore(release): v3.15.2
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 04:51:51 +08:00
ZyyandCodex 2956f07723 feat(system): add update channels and reconcile installation state
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 04:41:53 +08:00
ZyyandCodex bb76399e59 chore(release): v3.15.1
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 03:19:48 +08:00
ZyyandCodex 66a3474baf fix(openai): update desktop Core extraction and identity layout
Read bundled Core from the current CodexCLI.app layout and reject legacy
and launcher paths. Keep client identity aligned to the right edge on the
same row and to the left edge after wrapping on narrow cards.

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 03:16:06 +08:00
ZyyandCodex 5e7196cfbf chore(release): v3.15.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 02:10:20 +08:00
ZyyandCodex a340bec472 docs: standardize cpr skills and contribution requirements
Namespace repository skills with the cpr prefix and clarify automatic task routing. Require UI screenshots and AI model disclosure in pull requests, preserve existing page designs, and strengthen backend self-review.

Co-authored-by: Codex <noreply@openai.com>
2026-09-26 02:06:40 +08:00
5782c71f73 feat(openai): support complete identities with official release updates (#272)
Reuse official daily release checks for complete TUI and Exec identities
without third-party catalogs. Keep settings and key overrides on a compact
preset and version-policy editor, with matching previews and loading states.
Prefill custom versions with the current user agent. Compare settings by
value, align toolbar icon backgrounds, and compact the key creation form.

Release transaction locks before rejecting stale proxy test results,
with a regression covering an immediate edit during connection cleanup.

Co-authored-by: Wei Hu <whuae@connect.ust.hk>
Co-authored-by: Codex <noreply@openai.com>
2026-09-26 01:35:43 +08:00
ZyyandCodex 999fcc1770 feat(dev): integrate source modules and plugin multi-select settings
Co-authored-by: Codex <noreply@openai.com>
2026-09-25 23:59:13 +08:00
ZyyandCodex b920dcec83 feat(plugin): extend model, data and retry capabilities
Add model aliases, minimal read-only account and quota facts, bounded retry
policies and middleware capability declarations. Preserve unchanged payloads
and expose the new contracts in the SDK and plugin configuration UI.

Remove the ignored external observer example test.

Co-authored-by: Codex <noreply@openai.com>
2026-09-25 22:52:59 +08:00
ZyyandCodex 21c0711a66 chore(release): v3.14.1
Co-authored-by: Codex <noreply@openai.com>
2026-09-25 08:26:04 +08:00
ZyyandCodex f770ba127d fix(plugin): separate body transport from buffer budgets
Define the initial plugin manifest and wire protocol without legacy branches.
Preserve large request payloads and isolate local RPC encoding failures.

Co-authored-by: Codex <noreply@openai.com>
2026-09-25 08:11:16 +08:00
ZyyandCodex 66719f22e2 docs(skills): clarify plugin development and release guidance
Align the visible skill name with plugin-dev and document published resources, host platform selection, configuration defaults, and release installation behavior.

Co-authored-by: Codex <noreply@openai.com>
2026-09-25 07:11:27 +08:00
ZyyandCodex 00a1956df7 fix(plugins): clarify release asset selection
Show release assets with filename tooltips, size tags, and a compact selection state. Remove duplicate release version labels.

Co-authored-by: Codex <noreply@openai.com>
2026-09-25 06:53:10 +08:00
ZyyandCodex f2ccf6b42c chore(release): v3.14.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-25 06:03:40 +08:00
ZyyandCodex 044bdc8663 fix: finalize plugin runtime and development database integration
Consolidate unreleased migrations, fix plugin startup and header projection, verify Grok boundaries, and update current documentation and plugin development guidance.

Co-authored-by: Codex <noreply@openai.com>
2026-09-25 06:02:10 +08:00
24a9f12a78 fix: contain connection failures and retry unsent requests (#276)
Integrate PR #276 on the current main baseline. Keep local connection
capacity failures out of upstream retry and WebSocket breaker accounting.
Keep existing settings assertions valid when the #192 migration adds columns.

Source-commit: f9828a588f

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-25 04:53:46 +08:00
ZyyandCodex 1e14250b82 fix(pricing): add GPT-6 Sol and Luna built-in prices (#263)
Co-authored-by: Codex <noreply@openai.com>
2026-09-25 03:51:50 +08:00
ea31bf0cf7 feat(openai): add scheduled account warmup (#192)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>

Co-authored-by: Codex <noreply@openai.com>
2026-09-25 03:36:37 +08:00
jiahao6635andCodex 7a0502cabd feat(openai): support OAuth HTTP transport (#265)
Co-authored-by: Codex <noreply@openai.com>
2026-09-25 03:04:27 +08:00
jiahao6635andZyy d6eb319ac6 feat(accounts): export selected Codex model catalog (#258)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-09-25 01:49:46 +08:00
816677fc3d fix(openai): isolate Grok downstream compatibility (#196)
Keep Grok request markers, header filtering, and exact instruction-prefix handling in the downstream adapter. Apply the body adaptation only after selecting a Codex OAuth account and only when an observed Grok marker is present. Preserve unmarked and API-key client fields.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-25 01:00:11 +08:00
JiahaoandCodex e0e51c7da6 fix(accounts): avoid provider catalog waits when saving (#260)
Publish committed account and permission facts with the current model catalog while the extension set is unchanged. Preempt in-flight background catalog reconciliation for a new configuration commit; periodic reconciliation refreshes the catalog afterward. Confirm account saves before asynchronously reloading lists and groups.

Co-authored-by: Codex <noreply@openai.com>
2026-09-24 23:26:58 +08:00
zhangwentian dca573b748 fix(openai): classify upstream close 1009 as message_too_big request fault
Treat upstream WebSocket close 1009 as a request-scoped message-too-big error.
Return a structured HTTP 413 or WebSocket error without penalizing the account,
opening the provider circuit, or consuming the session WebSocket failure budget.
Keep the post-send state ambiguous so the gateway never replays the payload.

Cover repeated 1009 closes and the next successful WebSocket request.

(cherry picked from commit fb6ef1d080)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-24 23:13:25 +08:00
jiahao6635 d2f43f4edc fix(openai): preserve WebSocket pump exit diagnostics
Distinguish pump termination and stream EOF from actual upstream Close
frames without changing send-state or replay-safety classification.
Cover Pong timeout diagnostics on fresh and reused connections.

Refs #249

Co-authored-by: Codex <noreply@openai.com>
(cherry picked from commit 5a89bbc26b)
2026-09-24 23:13:25 +08:00
Ruichen Zhou 00240fbb66 fix(groups): attribute usage costs by serving account membership
Use the account that served each completed request to attribute group costs.
Multi-group client keys no longer charge groups that did not serve the request.
Historical costs follow the account's current group membership.

(cherry picked from commit 6f16c8efd1)
Co-authored-by: Codex <noreply@openai.com>
2026-09-24 23:13:20 +08:00
0f40d000d4 feat(proxies): derive and fill request location from proxy exit IP (#209)
Detect and persist proxy locations on enable, connection changes, and explicit
tests. Preserve manual locations and guard stale detection results. Allow a
successful outlet probe to fill the manual location fields on request.

Co-authored-by: Yanshan Chen <16632048+chenyanshan@users.noreply.github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-24 23:05:34 +08:00
4759fe9e0b chore(deps): update backend lockfile (#261)
Integrate the three Dependabot updates to Cargo.lock.

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-24 22:48:18 +08:00
Zyy d7f6635346 chore(deps): update UI package to v0.1.1 (#264)
Update @codex-proxy/ui and its pnpm lockfile.
2026-09-24 22:48:18 +08:00
2f545fb768 chore(deps): bump Debian Docker base image (#253)
Update the pinned Debian Bookworm Slim image digest.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-24 22:48:18 +08:00
997b696953 chore(deps): bump Rust Docker base image (#252)
Update the pinned Rust 1.98 Bookworm image digest.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-24 22:48:18 +08:00
ZyyandCodex 687ebd0331 feat(plugin): add the complete plugin development and management workflow (#256)
Introduce SDK 0.4, manifest v3, protocol v4, the isolated plugin runtime,
and cpr-plugin packaging CLI for independently maintained plugins.

Support installation, permissions, one active configuration per plugin,
version-specific settings, rollback, management pages, and CLI extensions.
Isolate plugin failures so native gateway startup and requests remain available.

Keep provider registration and UI choices fixed to built-in OpenAI and xAI.
Support nine extension capabilities with permission-scoped model and account
callbacks to the host.

Unify plugin settings and version management, support plugin icons,
and improve narrow layouts. Use the fixed GitHub UI dependency and pnpm 12
for standalone frontend and container builds. Reduce runtime test packaging
cost and align preparation modules with the repository layout.

Co-authored-by: Codex <noreply@openai.com>
2026-09-24 22:48:07 +08:00
ZyyandCodex 60e0d4ee92 chore(release): v3.13.1
Co-authored-by: Codex <noreply@openai.com>
2026-09-22 23:55:01 +08:00
ZyyandCodex 312059ed74 fix(openai): scope passthrough turn metadata installation identity
Same-account requests preserved opaque x-codex-turn-metadata headers after
account scoping, allowing the downstream installation ID to overwrite the
rebuilt header. Apply the existing turn metadata scoping to each opaque
value before serialization, retaining repeated values and extension fields.

Verify HTTP and WebSocket header/body identity consistency for same-account
and cross-account requests, including mixed-case repeated metadata headers.
Match the Responses metadata and compatibility projections in
openai/codex@c6c09fe29f,
codex-rs/core/src/responses_metadata.rs, lines 324-409.

Refs #226

Co-authored-by: Codex <noreply@openai.com>
2026-09-22 23:08:01 +08:00
ZyyandCodex 0fba502b4d fix(openai): isolate account installation identity from downstream headers
Treat x-codex-installation-id as a managed identity header so HTTP and
WebSocket requests cannot carry a downstream installation ID alongside
account-scoped client_metadata. Keep installation identity in the body,
matching Codex Core's Responses metadata and compatibility headers.

Reference: openai/codex@7498521d28,
codex-rs/core/src/responses_metadata.rs, lines 315-382.

Cover same-account and cross-account scoping through mock HTTP and
WebSocket upstreams, plus mixed-case and repeated opaque headers.

Fixes #226

Co-authored-by: Codex <noreply@openai.com>
2026-09-22 22:34:11 +08:00
ZyyandCodex ef050837de fix(accounts): recover snapshot races and support unlimited concurrency (#225)
Retry account snapshot conflicts before reserving scheduling capacity.

Allow zero as an unlimited default account concurrency limit, preserving per-account overrides, lease accounting, and request intervals.

Keep connection-test diagnostics expanded within the modal scroll area.

Co-authored-by: Codex <noreply@openai.com>
2026-09-22 19:33:59 +08:00
ZyyandCodex be0715e96b fix: preserve model metadata and terminal state, improve quota reset (#224)
Preserve versioned API key model metadata and terminal request state across clock rollback. Close reset credit dialogs after confirmed consumption and refresh account quota in place.

Fixes #217
Fixes #216

Co-authored-by: Codex <noreply@openai.com>
2026-09-22 17:45:59 +08:00
ZyyandZyy 16fa303a0c chore(deps): upgrade VueUse to 15 (#220)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-09-22 15:57:28 +08:00
dependabot[bot] 65088c0608 chore(deps): bump the minor-and-patch group in /frontend with 5 updates (#205)
Bumps the minor-and-patch group in /frontend with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.45.0` | `1.47.0` |
| [vue](https://github.com/vuejs/core) | `3.5.42` | `3.5.43` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.5.1` | `26.6.2` |
| [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) | `6.0.8` | `6.0.9` |
| [eslint](https://github.com/eslint/eslint) | `10.10.0` | `10.11.0` |


Updates `@lucide/vue` from 1.45.0 to 1.47.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.47.0/packages/vue)

Updates `vue` from 3.5.42 to 3.5.43
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.42...v3.5.43)

Updates `@types/node` from 26.5.1 to 26.6.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitejs/plugin-vue` from 6.0.8 to 6.0.9
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@6.0.9/packages/plugin-vue)

Updates `eslint` from 10.10.0 to 10.11.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.10.0...v10.11.0)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: vue
  dependency-version: 3.5.43
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@types/node"
  dependency-version: 26.6.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@vitejs/plugin-vue"
  dependency-version: 6.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: eslint
  dependency-version: 10.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-22 15:16:22 +08:00
dependabot[bot] eab933584c chore(deps): bump the minor-and-patch group in /backend with 6 updates (#204)
Bumps the minor-and-patch group in /backend with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [syn](https://github.com/dtolnay/syn) | `3.0.5` | `3.0.6` |
| [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust) | `1.146.0` | `1.148.0` |
| [aws-smithy-runtime-api](https://github.com/smithy-lang/smithy-rs) | `1.16.0` | `1.16.2` |
| [aws-smithy-types](https://github.com/smithy-lang/smithy-rs) | `1.6.3` | `1.6.4` |
| [crc32fast](https://github.com/srijs/rust-crc32fast) | `1.5.0` | `1.5.2` |
| [jsonwebtoken](https://github.com/Keats/jsonwebtoken) | `11.0.0` | `11.1.0` |


Updates `syn` from 3.0.5 to 3.0.6
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/3.0.5...3.0.6)

Updates `aws-sdk-s3` from 1.146.0 to 1.148.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `aws-smithy-runtime-api` from 1.16.0 to 1.16.2
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `aws-smithy-types` from 1.6.3 to 1.6.4
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `crc32fast` from 1.5.0 to 1.5.2
- [Commits](https://github.com/srijs/rust-crc32fast/compare/v1.5.0...v1.5.2)

Updates `jsonwebtoken` from 11.0.0 to 11.1.0
- [Changelog](https://github.com/Keats/jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Keats/jsonwebtoken/compare/v11.0.0...v11.1.0)

---
updated-dependencies:
- dependency-name: syn
  dependency-version: 3.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-sdk-s3
  dependency-version: 1.148.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-runtime-api
  dependency-version: 1.16.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-types
  dependency-version: 1.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: crc32fast
  dependency-version: 1.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: jsonwebtoken
  dependency-version: 11.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-22 15:15:49 +08:00
dependabot[bot] f041140757 chore(deps): bump the minor-and-patch group with 2 updates (#203)
Bumps the minor-and-patch group with 2 updates: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [docker/build-push-action](https://github.com/docker/build-push-action).


Updates `docker/setup-buildx-action` from 4.3.0 to 4.4.1
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/37fe631027851001ddb9b187196cc803df7f5f0e...f87e5991a6d7451dcb8d9637bfbc97413f497069)

Updates `docker/build-push-action` from 7.3.0 to 7.4.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/53b7df96c91f9c12dcc8a07bcb9ccacbed38856a...c3c9e263c25d99ce0380d002d59b67737d91b0dc)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: docker/build-push-action
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-22 15:15:18 +08:00
ZyyandCodex debba0e13c chore(release): v3.13.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-21 01:06:20 +08:00
ZyyandCodex a0836ec3f2 feat(key-usage): add API key budgets to CC Switch imports (#202)
Closes #198

Co-authored-by: Codex <noreply@openai.com>
2026-09-21 00:26:36 +08:00
ZyyandCodex dc0cdf2930 fix(openai): normalize rejected reasoning replay fields
Refs #194, #200.

Co-authored-by: Codex <noreply@openai.com>
2026-09-20 23:09:23 +08:00
ZyyandCodex d76bb57b8c fix(usage): show current account notes below email
Read account notes by internal account ID for usage lists and Dashboard recent
requests. Show nonempty notes as secondary text using the existing label style.

Cover same-email accounts, note changes and removal, deleted-account history,
and the admin API and client key visibility boundaries.

Refs #190

Co-authored-by: Codex <noreply@openai.com>
2026-09-20 19:13:52 +08:00
ZyyandCodex 6434edd554 fix(accounts): preserve quota when recovering disabled accounts
Enable scheduling without clearing observed quota, credential errors, or cooldowns for disabled accounts. Keep explicit recovery for enabled accounts and document both paths.

Co-authored-by: Codex <noreply@openai.com>
2026-09-20 18:41:22 +08:00
ZyyandCodex 3a90d8d0b6 fix(accounts): forecast quota totals within the current cycle
Preserve recorded cycle usage and estimate only remaining capacity from
recent samples. Restart cycle totals and sampling after quota resets,
including resets following an exhausted observation.

Clarify cycle totals and duration conversions in the quota modal and API
documentation, and prevent narrow layouts from overflowing.

Validated with the full backend suite using isolated PostgreSQL and Redis,
strict Clippy, Rustfmt, frontend lint and build, and browser checks.

Co-authored-by: Codex <noreply@openai.com>
2026-09-20 18:25:17 +08:00
ZyyandCodex f3236d0410 feat(key-usage): add about dialog with build information
Reuse the shared About dialog from a toolbar icon and expose version and commit identifiers through an authenticated key-only endpoint. Align the About and refresh button backgrounds with the configuration button in dark mode.

Validate the response field allowlist and session boundaries, and document the new endpoint.

Co-authored-by: Codex <noreply@openai.com>
2026-09-20 16:27:55 +08:00
ZyyandCodex ccb7357914 fix(usage): indicate long-context billing with a yellow info icon
Replace the inline long-context badge with a themed yellow detail trigger and retain the billing context in its title and accessible label.

Co-authored-by: Codex <noreply@openai.com>
2026-09-20 15:44:46 +08:00
ZyyandCodex c0ad2ae656 fix(openai): restrict capacity cooldown to explicit overload
Count only confirmed capacity rejections toward account cooldown, excluding
generic server failures and unrecognized upstream errors. Classify streamed
invalid_prompt failures as invalid requests, matching official Codex.

Cover HTTP and WebSocket failures around the cooldown threshold while
preserving retry boundaries, upstream error payloads, and Smart scoring.
Update the capacity cooldown contract in the API and architecture docs.

Validation: Rustfmt, strict workspace Clippy, and all backend suites with
isolated PostgreSQL 18 / Redis 8 (--test-threads=4).

Reference: openai/codex@7498521d28,
codex-rs/codex-api/src/sse/responses.rs.

Refs #177

Co-authored-by: Codex <noreply@openai.com>
2026-09-20 14:01:29 +08:00
ZyyandCodex 798d4f518e fix(update): run updates in background and retain release notes (#188)
Detach online updates from HTTP request lifetimes and recover their persisted status in the admin UI. Retain current release notes when no upgrade is available while preserving upgrade restrictions.

Fixes #185

Co-authored-by: Codex <noreply@openai.com>
2026-09-20 13:29:48 +08:00
ZyyandCodex 6819682176 docs(ui): define concise copy and layout guidance
Co-authored-by: Codex <noreply@openai.com>
2026-09-20 11:28:17 +08:00
ZyyandCodex b2d4a7e833 docs(readme): simplify punctuation and community guidance
Co-authored-by: Codex <noreply@openai.com>
2026-09-20 11:13:39 +08:00
ZyyandCodex 01006380ee chore(release): v3.12.1
Co-authored-by: Codex <noreply@openai.com>
2026-09-20 01:18:37 +08:00
ZyyandCodex c79e032575 fix(ui): refine usage page layout and controls
Co-authored-by: Codex <noreply@openai.com>
2026-09-20 01:14:13 +08:00
Akide LiuandClaude Fable 5 96cf46cca8 fix(openai): expand string input into a user message item (#179)
The public Responses API accepts `input` as a plain string, but the Codex
backend only accepts an item list and rejects such requests with
400 "Input must be a list". Expand a string `input` into a single user
message item in the downstream body compatibility layer so that HTTP/SSE
and WebSocket share the same rule; arrays and other shapes pass through
untouched.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-20 00:58:58 +08:00
ZyyandCodex ec605b3d1c chore(release): v3.12.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 23:43:27 +08:00
ZyyandCodex 890fb3299b fix(openai): stop client retries for exhausted account pools
Return 429 usage_limit_reached when all eligible accounts have exhausted their quota. Preserve provider fallback, continuation recovery, and other unavailability causes.

Cover HTTP and WebSocket error contracts and account selection boundaries. Verified with 2516 backend integration tests, strict Clippy, and the official Codex CLI using gpt-5.6-luna and a quota-exhausted account.

Co-authored-by: Codex <noreply@openai.com>
2026-09-19 23:28:37 +08:00
helenandCodex a18f0a2e11 fix(openai): normalize system messages for Codex (#172)
Normalize explicit system messages to developer at the shared Codex request boundary while preserving message content and other fields. Cover HTTP and WebSocket request bodies and document the compatibility behavior.

Co-authored-by: Codex <noreply@openai.com>
2026-09-19 22:43:49 +08:00
ZyyandCodex a1ba6c0b61 fix(update): enforce release channel upgrade rules
Refs #169

Co-authored-by: Codex <noreply@openai.com>
2026-09-19 22:38:23 +08:00
ZyyandCodex 5c11b62f93 fix(ui): refine theme contrast and table interactions
Separate light control and table fills, unify input hover and focus feedback, and add smooth table row transitions. Keep column drag highlights attached to their keys and reduce account note text size.

Co-authored-by: Codex <noreply@openai.com>
2026-09-19 22:38:23 +08:00
ZyyandCodex 748c77da6a feat(ui): add key configuration and reorderable table columns
Add session-scoped Codex configuration and CCSwitch import for key holders. Persist table-column ordering with drag animations and refine key, quota, and usage layouts.

Validated with 395 gateway-api integration tests, Rustfmt, and strict Clippy for gateway-api and gateway-admin. Frontend validation is left to the maintainer as requested.

Co-authored-by: Codex <noreply@openai.com>
2026-09-19 19:44:14 +08:00
d0180b74b3 fix(openai): refresh expired quota windows after reset grace (#170)
Refresh non-exhausted accounts after the quota reset grace period while preserving observed usage and exhausted-window attribution. Keep periodic checks for exhausted accounts to detect early upstream resets, and cover refresh timing, throttling, and display regressions.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 18:13:33 +08:00
4669a29b46 feat(usage): show key names and long-context billing badges (#171)
Show current client key names in usage and error records, and persist the selected long-context pricing band in billing snapshots.

Display a long-context badge without assuming a fixed multiplier. Keep missing historical flags false while preserving reconstructed billing details, with regression coverage for OpenAI and xAI.

Validated with 36 billing tests, strict Clippy, Rustfmt, frontend lint/typecheck/build, and browser checks for both usage views.

Co-authored-by: 陈燕山 <chenyanshan@yanshan-mac.local>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 18:13:14 +08:00
e63c5fa28d chore(deps): align TLS with Codex Desktop 26.915 (#175)
Update rustls and AWS-LC to the versions bundled with Codex Core 0.155,
refresh the captured ClientHello baseline, and remove the rustls audit waiver.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 17:39:48 +08:00
1acd4a5f59 feat(settings): unify OpenAI and xAI client identities (#174)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 17:39:29 +08:00
ZyyandCodex 6659d0c1f9 fix(ui): shorten weekly budget label
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 14:22:52 +08:00
ZyyandCodex ab828650e6 chore(release): v3.11.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 06:41:19 +08:00
ZyyandCodex 4c7e4e08f6 feat: refine admin controls and overload protection
Refine settings navigation, selective pricing sync, model deletion, and admin interactions. Consolidate unreleased migrations and keep Fast policy scoped to groups. Increase confirmed capacity-rejection feedback without changing ordinary failure recovery.

Co-authored-by: Codex <noreply@openai.com>
2026-09-19 06:41:03 +08:00
16ae8c7afa feat(auth): add administrator password changes (#166)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 06:07:17 +08:00
956f627f68 feat(keys): add manual budget reset and compact row actions (#165)
Add an admin-only reset for daily, weekly, or all key budget usage with atomic auditing and serialized settlement boundaries.

Keep budget limits, original expiration times, and billing history while moving secondary row actions into the existing popover pattern.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 04:46:22 +08:00
f3076e2338 feat(openai): fallback auxiliary account endpoints to official upstream on 404 (#160)
* feat(openai): fallback auxiliary account endpoints to official upstream on 404

When a custom upstream base URL is configured (such as a Codex Relay for chat and inference), non-inference auxiliary account endpoints (usage, profile statistics, reset credits, subscriptions) may return 404 Not Found if the relay only proxies /backend-api/codex/*. Automatically fallback to the official ChatGPT backend endpoint to retrieve account metadata and quota statistics, while keeping chat and model traffic routed through the configured relay.

* fix(openai): preserve account fallback failures and query budgets

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 02:44:52 +08:00
ZyyandCodex 7f320797a0 docs: add public quick preview access
Co-authored-by: Codex <noreply@openai.com>
2026-09-19 01:30:31 +08:00
ZyyandCodex 7bcc207055 fix(openai): stop cancelled artifact reads during shutdown
Return a terminal I/O error for cancelled or expired Desktop artifact reads so standard read helpers cannot retry forever and block runtime shutdown.

Cover cancellation before reading and during an in-flight range request.

Co-authored-by: Codex <noreply@openai.com>
2026-09-19 00:14:25 +08:00
fba8941068 feat(pricing): add configurable model prices and manual catalog sync (#159)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-18 20:22:15 +08:00
ZyyandCodex ede456877b docs(release): define experimental publishing and title numbering
Co-authored-by: Codex <noreply@openai.com>
2026-09-18 16:55:07 +08:00
Zyy e6587b6ca2 feat(openai): add configurable upstream client identities (#156)
Add six Desktop and CLI presets, global defaults and per-key overrides, custom identity parameters, and official release tracking. Keep the resolved identity stable throughout each request and its retries.
2026-09-18 16:39:29 +08:00
ZyyandCodex 4d1ed7917d fix(api): make final overload failures retryable for clients (#152)
Project capacity errors at the HTTP, SSE, and WebSocket delivery boundary while preserving upstream facts and existing gateway recovery policies.

Co-authored-by: Codex <noreply@openai.com>
2026-09-18 16:38:08 +08:00
ZyyandCodex 5fe892de82 docs(community): configure Chinese discussions and feedback routing
Add Chinese discussion forms, community guidance, and README and Issue chooser links.

Co-authored-by: Codex <noreply@openai.com>
2026-09-18 15:55:36 +08:00
ZyyandCodex 3328a4356a chore(release): v3.10.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 20:10:53 +08:00
77b0c29354 feat(proxies): add dual-stack diagnostics without blocking binding (#132)
Probe IPv4 and IPv6 exit endpoints concurrently and accept either successful address family. Persist both exit addresses with migration 0015 while preserving existing migration checksums and the legacy exitIp field.

Remove stale frontend IP preference payloads and display exit addresses without redundant family prefixes. Treat probe results as diagnostics and allow users to select and bind saved proxies regardless of probe status, while preserving missing-proxy checks, configuration consistency, import guards, and no-direct-fallback behavior.

Co-authored-by: zhangwentian <2han9wen71an@gmail.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 19:58:22 +08:00
ZyyandCodex c872bc7881 refactor(settings): refine Fast display and save feedback
Move the revised service-tier description into the card header and simplify the switch label. Keep the undo button filled and place the unsaved indicator before it.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 19:05:54 +08:00
487b2e87e2 feat: add global and group controls to disable Fast (#146)
Freeze global and bound-group Fast restrictions in each request snapshot, then apply the effective policy before OpenAI transport headers, observations, and billing. Preserve omitted update values and existing data with an additive migration.

Keep the current split settings layout, add a service-tier card, and require WebSocket transport from the first turn in the continuation regression test.

Validation: frontend lint, typecheck and build; Rust formatting; migration checksums; 15 focused tests. Live priority/default/priority requests preserved context on one WebSocket, but the upstream returned default for all three, so actual priority-tier execution remains unverified. Cloud CI was still running when the maintainer requested immediate merge.

Co-authored-by: Yanshan Chen <16632048+chenyanshan@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 18:55:31 +08:00
ZyyandCodex eb8a01541a refactor(ui): refine settings and account interactions
Split settings into focused cards while retaining the original tabs and page layout. Simplify save feedback and help controls, preserve settings drafts, and align account dialog and model restriction widths.

Keep select option scrolling tied to keyboard navigation and focus so hovering does not shift the dropdown.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 18:29:47 +08:00
ZyyandCodex 0acf114fa7 ci(release): remove major-version upgrade warning
Remove the same warning from release installation notes and deployment documentation.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 18:25:15 +08:00
2han9wen71anandZyy 5d941008fc fix(providers): let disabled accounts source their own model catalogs (#143)
* fix(providers): let disabled accounts source their own model catalogs

The admin per-account model query (connection test model dropdown)
resolves the account's plan catalog through refresh_account_catalog,
whose candidates come from catalog_candidates_by_scope over the
scheduling account listing. Disabled accounts never appear there, so a
disabled account either failed with NoEligibleCredential (no enabled
account in its plan scope) or silently sourced the catalog from other
accounts instead of itself, and the panel showed no test models.

Keep the pinned account reachable: when the target account is missing
from the eligibility-filtered candidates, append it (for xAI, load its
full credential) and let the preferred-first ordering try it before any
same-scope peers. A fully filtered listing is now treated as empty
candidates instead of an early error so the pinned account can still
answer alone. Client-facing catalog discovery, background sync and
scheduling listings continue to exclude disabled accounts.

* fix(xai): keep catalog credential revision aligned

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-09-17 16:07:58 +08:00
b44a790777 fix(openai): coalesce SOCKS proxy negotiation writes (#142)
* fix(openai): coalesce SOCKS proxy negotiation writes

Buffer proxy handshake writes until the existing protocol flush points to support SOCKS services that reject partial method greetings. Preserve the dependency handshake and return the unbuffered stream before TLS/WebSocket traffic.

Refs #139

Co-authored-by: Codex <noreply@openai.com>

* test(openai): make SOCKS negotiation coverage deterministic

Replace TCP peek boundary assumptions with an in-memory write transcript and fragmented responses. Cover authenticated and unauthenticated negotiation, retain the existing account proxy integration coverage, and verify the handshake buffer is drained.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Zyy <39424122+zyycn@users.noreply.github.com>
2026-09-17 15:54:09 +08:00
06e94ef69b fix(settings): make Responses decompression limit configurable (#141)
Persist the output limit in runtime settings and use the authenticated snapshot for each compressed Responses request. Expose the setting in MiB and preserve bounded decoding and the independent zstd window limit.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 15:29:14 +08:00
0f2e0b18a9 feat(openai): recover quota-exhausted continuations through client replay (#140)
Project replay-safe quota rejections to the existing client history recovery
contract while preserving upstream failure facts and account isolation.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 15:07:31 +08:00
71e26f44b7 fix(usage): bill sent models and show response model mismatches (#136)
* feat(usage): show upstream response model mismatches

Persist observed response models separately from routing and billing facts.
Surface mismatches with the existing model arrow and native title tooltips.
Keep response observations isolated across retries and WebSocket reuse.

Co-authored-by: Codex <noreply@openai.com>

* fix(usage): calculate costs from the outbound model

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 14:55:54 +08:00
yertandCodex 8232e19bc0 fix(ui): make reauthorization account copyable (#135)
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 14:55:22 +08:00
dcc61915e9 fix(openai): default missing downstream store to false (#134)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 14:54:52 +08:00
47d08560a7 fix(api): allow ChatGPT remote Desktop sessions without app versions (#133)
* fix(api): allow ChatGPT remote Desktop sessions without app versions

Recognize the official Android and iOS remote User-Agent suffixes when
Desktop application version metadata is absent. Preserve explicit version
validation and Client Key authentication, cover the streaming request path,
and document the version enforcement boundary.

Refs #129

Co-authored-by: Codex <noreply@openai.com>

* fix(api): match ChatGPT remote client name family

Accept nonempty platforms in codex_chatgpt_*_remote while preserving suffix
token validation and explicit Desktop version enforcement. Cover future
platforms and malformed names in parser and streaming route tests.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 14:54:17 +08:00
ZyyandCodex 590c4a163b fix(store): release rejected proxy import locks before returning
Explicitly unlock rejected import reservations before returning their error so immediate proxy operations do not race connection teardown. Cover repeated rejection followed by writes with PostgreSQL integration tests.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 05:36:00 +08:00
ZyyandCodex 9cfcd371d6 chore(release): v3.9.1
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 05:19:07 +08:00
ZyyandCodex 223021630e fix(openai): unify API key and OAuth passthrough
Share the simulated client profile and existing request/response paths across authentication modes. Forward native capabilities and unknown models upstream while preserving credential isolation and routing permissions.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 05:15:31 +08:00
ZyyandCodex 42c2b249c2 chore(release): v3.9.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 04:08:44 +08:00
ZyyandCodex 0b3ebc23f5 chore(workflow): refine documentation and release checks
Require documentation diff review by purpose, distinguish release migration baselines, and use the version tag directly in release commit titles.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 04:08:30 +08:00
ZyyandCodex 65bb9ab9cf docs: clarify setup and current contracts
Document one-click installation, consolidate manual deployment instructions, and describe current API and architecture contracts. Keep applicable configuration and upgrade guidance without recording implementation history.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 04:08:30 +08:00
ZyyandCodex 50d6f7444d fix(deploy): render installer configuration with Bash
Replace the embedded Python configuration writer with Bash quoting and placeholder validation. Reject control characters in administrator passwords and preserve existing deployment files on reruns.

Validation: Bash syntax, isolated installation checks, and loading generated YAML through the application configuration loader passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 04:08:17 +08:00
ZyyandCodex bcfea792e8 fix(config): tolerate unused startup settings
Ignore unused YAML fields across startup configuration and report their paths without values. Report missing required fields while retaining existing type and value validation.

Validation: configuration and bootstrap tests, strict Clippy, and architecture checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 04:08:17 +08:00
ZyyandCodex 1e7ea8d46f refactor(store): consolidate unreleased account migrations
Default account auto-freeze to off in its initial migration and renumber request location to 0011. Update checksum entries and migration tests.

These migrations were not included in v3.8.0. Existing PR freeze rules remain unchanged.

Validation: 18 gateway-store tests with isolated PostgreSQL, upgrade from the v3.8.0 schema, migration checksums and numbering, and Rustfmt.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 03:47:53 +08:00
bd61e0391a feat(settings): configure global and per-proxy request location (#123)
* feat(proxies): configure per-proxy request location

* feat(settings): manage global request timezone and location

Move request location from YAML into runtime settings with an enable switch. Preserve client fields when disabled and keep per-proxy overrides first. Share location inputs and simplify proxy and settings controls.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 03:30:54 +08:00
499b43043e feat(accounts): add opt-in capacity cooldown and recovery (#126)
Add opt-in account capacity cooldown, recovery probes, and atomic adaptive concurrency updates. Include overload and slow_down in Smart scheduling feedback, expose rate-limit reasons, and keep probe results fenced by cooldown generation.

Exclude diagnostic probes and local connection failures from capacity evidence. Default auto-freeze to disabled and preserve explicitly saved settings with a forward migration.

Closes #120

Co-authored-by: zhangwentian <2han9wen71an@gmail.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 03:23:38 +08:00
ZyyandCodex ae2da31380 fix(accounts): improve provider selection and forecast hints
Expand provider choices to full-width labeled options and move the forecast caveat into an icon-only explanation popover.

Validation: frontend lint, typecheck and build passed; verified narrow-screen selection, keyboard navigation and explanation popover. Available forecast data layout was not exercised because local observations are insufficient.

Co-authored-by: Codex <noreply@openai.com>
2026-09-17 00:45:55 +08:00
1e6d69a349 fix(openai): align downstream requests with Codex protocol (#117)
Centralize downstream SDK, browser and proxy header compatibility in the
OpenAI provider. Normalize the session header alias and isolate known
non-Codex body parameters while preserving ordinary extensions.

Align account installation metadata with Codex Core and remove unverified
identity header classifications. Cover HTTP and WebSocket behavior and
document the request boundaries.

Validation: 1,105 scoped and architecture tests, Rustfmt and strict scoped
Clippy passed. Four real-account HTTP/WebSocket cases passed on gpt-5.5,
including connection reuse. Earlier gpt-reserve overload failures were
recorded separately without changing production retry policy.

Co-authored-by: Clarence <1297529513@qq.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 00:30:52 +08:00
2han9wen71anandCodex dac57a81c6 fix(providers): keep disabled accounts reachable for admin connection tests (#122)
Admin connection tests pin an account and bypass local availability, but
the scheduling store excludes disabled accounts before selection. Load
the pinned diagnostic account explicitly for OpenAI and xAI while keeping
normal scheduling and disabled-account state-write guards unchanged.

Apply the shared authentication and transport filters after restoring
OpenAI candidates. Align the memory stores with PostgreSQL filtering and
cover disabled-account diagnostics plus API key transport restrictions.

Validation: 652 OpenAI, 403 xAI, 350 Core, and 42 architecture/bootstrap
tests passed; Rustfmt and strict workspace Clippy passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-16 18:53:52 +08:00
d162f9b7f6 fix(accounts): run bulk imports as background tasks (#119)
* fix(openai): sync account plans with quota updates

Co-authored-by: Codex <noreply@openai.com>

* fix(accounts): run bulk imports as background tasks

Co-authored-by: Codex <noreply@openai.com>

* test(store): preserve settings in account plan refresh fixture

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 18:24:34 +08:00
866f71e3fb fix(openai): sync account plans with quota updates (#112)
* fix(openai): sync account plans with quota updates

Co-authored-by: Codex <noreply@openai.com>

* test(store): preserve settings in account plan refresh fixture

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 18:22:01 +08:00
yertandCodex 47ab48b4cf feat(deploy): add one-click installer (#116)
Automate version-matched Compose deployment, credential generation, bind-mount permissions, and startup health validation while preserving existing deployments.

Co-authored-by: Codex <noreply@openai.com>
2026-09-16 18:12:38 +08:00
e3cf4ab8be feat(ui): add persistent table column settings (#121)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 18:07:58 +08:00
4fb10f33f1 fix(update): reuse the served asset directory (#111)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 18:05:14 +08:00
c9318c54cd fix(proxy): support IPv6 egress probes (#110)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 18:05:08 +08:00
8d9aaf0bbd feat(accounts): support upstream API key accounts (#107)
Add Responses-compatible upstream accounts with isolated credentials, transport,
model catalogs, atomic settings updates, and account management controls.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 18:04:41 +08:00
ZyyandCodex 52d24d29c7 chore(github): improve issue reporting templates
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 17:29:01 +08:00
ZyyandCodex 3fbd447d94 chore(release): prepare v3.8.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 03:03:43 +08:00
ZyyandCodex 5a5cdbdcde fix(ui): refine account settings and key displays
Show all batch account settings by default while updating only edited fields. Add ellipsis and full-text titles to API key names and labels, and adjust the token usage summary icon size and alignment.

Co-authored-by: Codex <noreply@openai.com>
2026-09-16 02:54:23 +08:00
ZyyandCodex cca89f2f96 feat(accounts): add per-account model restrictions (#105)
Add per-account model allowlists and denylists to frozen routing, account settings, and CPR import/export. Preserve existing scheduling and account state boundaries.

Closes #97

Co-authored-by: Codex <noreply@openai.com>
2026-09-16 02:29:56 +08:00
e25aad2231 feat(gateway): add bounded concurrency queues for keys and accounts (#104)
* feat(auth): add API key workspace

Add persistent API key sessions and enforce admin/key authorization boundaries. Expose key-scoped quotas, trends, health, and paginated usage records through server-projected data.

Share the application shell, system overview, and usage views between admin and key sessions, reorganize frontend domains around view ownership, and omit account, provider, and authentication details from the key workspace.

BREAKING CHANGE: Browser authentication now uses /api/auth/* and cpr_session; existing browser sessions must sign in again.

Validated frontend lint, type checking, production build, backend workspace tests, and live authenticated key views.

Co-authored-by: Codex <noreply@openai.com>

* feat(console): refine key workspace experience

Unify role-aware console routes and use mode/role in authentication contracts.
Reuse request state, stable pagination and performance projections across workspaces.
Refine quota cards, usage panels, empty states and key-aware theme previews.
Skip session restoration on login and retain form inputs until unmount.
Keep the full health timeline grid visible without samples.

Co-authored-by: Codex <noreply@openai.com>

* feat(key-usage): replace shared workspace with focused usage page

Preserve unified admin/key authentication, restore admin pages, and remove the previous client workspace implementation. Add a scoped usage page with metrics, trends, budgets, request logs, and section loading placeholders.

Co-authored-by: Codex <noreply@openai.com>

* feat(gateway): queue requests at key and account concurrency limits

Share one wait deadline across client admission and account selection.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 02:00:26 +08:00
84382761fc feat(auth): add focused API key usage page (#102)
* feat(auth): add API key workspace

Add persistent API key sessions and enforce admin/key authorization boundaries. Expose key-scoped quotas, trends, health, and paginated usage records through server-projected data.

Share the application shell, system overview, and usage views between admin and key sessions, reorganize frontend domains around view ownership, and omit account, provider, and authentication details from the key workspace.

BREAKING CHANGE: Browser authentication now uses /api/auth/* and cpr_session; existing browser sessions must sign in again.

Validated frontend lint, type checking, production build, backend workspace tests, and live authenticated key views.

Co-authored-by: Codex <noreply@openai.com>

* feat(console): refine key workspace experience

Unify role-aware console routes and use mode/role in authentication contracts.
Reuse request state, stable pagination and performance projections across workspaces.
Refine quota cards, usage panels, empty states and key-aware theme previews.
Skip session restoration on login and retain form inputs until unmount.
Keep the full health timeline grid visible without samples.

Co-authored-by: Codex <noreply@openai.com>

* feat(key-usage): replace shared workspace with focused usage page

Preserve unified admin/key authentication, restore admin pages, and remove the previous client workspace implementation. Add a scoped usage page with metrics, trends, budgets, request logs, and section loading placeholders.

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 01:49:37 +08:00
0a09c1572f feat(accounts): add notes and refine account interactions (#100)
Persist optional account notes across creation, editing, imports, and credential refreshes. Show notes in compact account rows with overflow-only popovers.

Support vertically resizable textareas, bounded select menus with custom scrollbars, and quota updates without table loading.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 01:36:34 +08:00
ZyyandCodex 90deabdfe3 docs(skills): ground upstream compatibility reviews in evidence
Co-authored-by: Codex <noreply@openai.com>
2026-09-15 16:36:59 +08:00
2han9wen71an b1c24e276b fix(gateway-api): project bare WebSocket errors to response.failed (#99)
codex 的 WS 客户端(codex-api/src/endpoint/responses_websocket.rs)只消费
两类 error 帧:带非 2xx HTTP status 的包装错误,以及
websocket_connection_limit_reached / previous_response_not_found 内置可重
试错误码。OpenAI 上游的中流错误帧(如 server_is_overloaded)没有 status
字段,map_wrapped_websocket_error_event 返回 None,落回
process_responses_event 也没有 error 分支——整帧被静默忽略。

客户端收不到可识别的终态,只能空等到自身 300s stream_idle_timeout 超时
再重连,5 次重连累计 20-30 分钟"正在思考",最后才把失败呈现出来。CPA
等实现之所以秒级报错,正是因为它把错误负载重写成 codex 可消费的终态。

#90 已在 SSE 边界做了同样的投影,但客户端 WS 边界(push_websocket)仍原
样转发;使用 WS transport 的 codex 桌面端不受 #90 保护。本提交在该边界
对不可消费的错误帧投影成 response.failed(消息 JSON 自带 type 字段,经
process_responses_event 的 response.failed 分支映射为可重试错误并立即重
试);带非 2xx status 或特殊错误码的可消费形状原样透传,保持
deliverable_business_failures_are_not_rewritten... 既有契约。
2026-09-15 16:31:07 +08:00
ZyyandCodex c8fdc2b5cd chore(release): prepare v3.7.2
Co-authored-by: Codex <noreply@openai.com>
2026-09-15 15:55:09 +08:00
2han9wen71an dfabf0f9dd fix(openai): recover quota after same-window usage drops (#95)
OpenAI 周窗口的 used_percent 可以在同一窗口内回落(滚动计量),而
reset_at 固定到窗口边界才变化。恢复规则要求「reset 前进 + 用量低于
10%」,两个条件在滑动周窗口上都无法成立:耗尽账号被钉在旧 reset
日期(最长一周)无法参与调度,尽管 /usage 已显式报告 allowed=true /
limit_reached=false 且 5h 窗口全新。

新增第二条解除路径:同一窗口(reset 未滚动)内连续两次观测都未触顶
即解除。候选记录持久化在 _quota_recovery,跨 worker 周期与重启累积;
连续两次的要求保持对「耗尽后立刻拉到的旧快照」竞态的防护(单次旧
快照撑不过两次观测)。原有「reset 前进 + 用量<10%」快路径不变。
2026-09-15 15:38:46 +08:00
ZyyandCodex 817b0f2d6d chore(release): commit version and notes together
Allow staged or unstaged release notes and commit their final contents with the version update before tagging. Continue rejecting unrelated tracked changes and require notes to remain uncommitted.

Update the release skill, skip duplicate push CI for release metadata alone, and check the publish script syntax in CI. Validated publication behavior in temporary Git repositories, Bash syntax, and Actionlint.

Co-authored-by: Codex <noreply@openai.com>
2026-09-15 15:14:18 +08:00
ZyyandCodex a975868046 fix(release): publish version-matched deployment assets
Publish the configuration template and Compose file with release checksums, pin the packaged Compose image to the release version, and include the template in platform archives.

Download deployment assets from one release tag, link versioned deployment documentation, and translate the release installation notes into Chinese.

Validated Actionlint, stable and prerelease packaging for all platforms, SHA-256 checksums, and resolved Docker Compose image versions.

Co-authored-by: Codex <noreply@openai.com>
2026-09-15 15:02:49 +08:00
ZyyandCodex cc2dfab19a fix(openai): preserve client location when unset
Accept omitted and null request locations without rewriting web search location or environment dates and timezones. Keep complete configured overrides validated and document the passthrough behavior.

Validated 626 OpenAI tests, 38 gateway tests including architecture checks, Rustfmt, and strict workspace Clippy.

Co-authored-by: Codex <noreply@openai.com>
2026-09-15 15:02:36 +08:00
ZyyandCodex 040a1e82a6 chore: bump version to 3.7.1
Co-authored-by: Codex <noreply@openai.com>
2026-09-15 11:32:29 +08:00
ZyyandCodex 4f39bfc5a8 docs(release): prepare v3.7.1 notes
Document configurable request location, WebSocket SSE error delivery, relaxed Compose resource limits, and the required image/config version alignment.

Co-authored-by: Codex <noreply@openai.com>
2026-09-15 11:32:20 +08:00
ZyyandCodex 3e3616863d fix(deploy): remove fixed container resource limits
Allow the default Compose deployment to run on hosts with fewer than two CPUs and leave CPU and memory policy to the deployment environment.

Validated the rendered Docker Compose configuration.

Co-authored-by: Codex <noreply@openai.com>
2026-09-15 11:01:21 +08:00
1bbae67040 fix(gateway-api): translate upstream WebSocket error events into SSE response.failed (#90)
* fix(gateway-api): translate upstream WebSocket error events into SSE response.failed

WS 上游协议用 `error` 事件报告流中途失败(如 OpenAI 的 server_is_overloaded),
SSE 协议的对应形态是 `response.failed`。SSE 交付边界此前把 `error` 事件按
未知事件原样透传,而 codex 等 SSE 客户端会静默忽略未知事件类型:客户端只看到
无原因的流 EOF("stream closed before response.completed"),丢失了上游的
完整错误负载(message/code/type),也无法据此分类重试。

现在 SSE 编码器在交付边界把 `error` wire 事件翻译成 `response.failed`:
错误负载(type/code/message/param)原样保留,response id 沿用已交付的
response.created。WS 客户端交付不变(WS 协议原生支持 error 帧)。
原有的会话级失败兜底(finish_with_gateway_error 合成 response.failed)不变。

* fix(gateway-api): preserve upstream response failures

Co-authored-by: Codex <noreply@openai.com>

---------

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-15 10:49:29 +08:00
ZyyandCodex fc07ac0803 fix(ci): scope rustls security waiver
Temporarily waive RUSTSEC-2026-0285 until 2026-09-21 UTC to preserve the pinned Codex TLS fingerprint. Keep all other audit failures blocking.

Validated cargo audit and waiver expiry behavior.

Co-authored-by: Codex <noreply@openai.com>
2026-09-15 10:49:29 +08:00
ZyyandCodex c7823a7821 feat(openai): make request location configurable
Group country, region, city, and timezone under openai.wire_profile.location while retaining the existing defaults. Share the configured location across web search and environment context, preserve epoch timestamps and release updates, and add validation, regression tests, and deployment documentation.

Co-authored-by: Codex <noreply@openai.com>
2026-09-15 10:49:29 +08:00
ZyyandCodex 61d1bcb405 refactor(api): standardize request parameters and trim unused options
Use data for POST bodies and params: data for GET queries. Remove request options that have no callers while retaining cancellation, timeout, and silent polling support. Update affected backup and client-download call sites and document the convention.

Co-authored-by: Codex <noreply@openai.com>
2026-09-15 10:49:25 +08:00
ZyyandCodex 21a436b76a chore: bump version to 3.7.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-14 17:33:59 +08:00
ZyyandCodex 849aa73a28 docs(release): prepare v3.7.0 notes
Summarize account personal information, reset credit fixes, OpenAI retry and transport changes, and service tier accounting. Document the admin API replacement and upgrade scope.

Co-authored-by: Codex <noreply@openai.com>
2026-09-14 17:33:52 +08:00
ZyyandCodex fb89bda177 docs: clarify documentation scope and required development skills
Centralize documentation update rules in CONTRIBUTING and reference them from the development and PR skills. Require frontend-design for frontend development and rust-best-practices for Rust development.

Co-authored-by: Codex <noreply@openai.com>
2026-09-14 17:28:07 +08:00
ZyyandCodex 9e6c8c9734 fix(openai): retry capacity rejections before rotating accounts
Retry explicit model capacity rejections on the same account up to three
times with bounded backoff, then reuse ordinary account rotation. Honor
the existing replay, delivery, cancellation and total request budgets.

Keep capacity rejections out of account health scores and provider
circuits, preserve original client errors, and extend the WebSocket
rejection boundary introduced by #89. Cover HTTP and WebSocket behavior
and document the policy.

Co-authored-by: Codex <noreply@openai.com>
2026-09-14 17:19:38 +08:00
2han9wen71an 3f69bc861c fix(openai): rotate accounts instead of same-socket retry on WebSocket account-level rejections (#89)
WebSocket 升级或交换被上游账号级拒绝(额度耗尽、限流、封禁等)时,恢复策略
仍会把错误交给同账号传输重试,且退避直接采用上游 retry-after(额度窗口通常
是小时级)。请求在 deadline 内无法恢复,错误原样返回客户端,Core 也失去了
换号机会——而同错误在 HTTP 路径上因回放安全会立即换号重试。

现在回放安全且带账号级失败记录的 WebSocket 拒绝不进入传输恢复:保留账号失败
记录(健康度与额度标记立即生效),把换号决策交还 Core,与 HTTP 路径语义一致。
会话 WebSocket 预算与禁用标记也不被账号级拒绝消耗。
2026-09-14 17:15:47 +08:00
ZyyandCodex cea4e1d8e9 fix(openai): use outbound service tier for usage and billing
Keep response-reported tiers as diagnostic metadata without overriding outbound-tier usage or cost estimates. Cover HTTP and WebSocket behavior and document the updated accounting policy.

Co-authored-by: Codex <noreply@openai.com>
2026-09-14 16:41:02 +08:00
539764f7eb feat(accounts): aggregate personal information on demand (#83)
Load profile statistics and OpenAI subscription information concurrently through one on-demand admin endpoint. Keep subscription queries out of quota refreshes, preserve partial results and account identity checks, and show subscriptions in the approved personal-information dialog layout.

Update the current API and architecture contracts while leaving README unchanged.

Co-authored-by: Yanshan Chen <16632048+chenyanshan@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-14 15:48:12 +08:00
c9be17778d fix(accounts): consume reset credits without card details (#81)
Allow count-only reset inventories to request upstream card selection without a credit ID. Preserve confirmation and idempotent retry guards, and place the fallback action in the shared modal footer with balanced inventory spacing.

Co-authored-by: Yanshan Chen <16632048+chenyanshan@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-14 15:47:32 +08:00
d8bc91efce fix(openai): align structured location metadata (#88)
Rewrite only Codex-generated environment context and web search location fields to Piketon, Ohio. Preserve user-authored text and absolute timestamps.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-14 14:14:08 +08:00
ZyyandZyy e8e2f18927 fix(openai): align WebSocket wire fingerprint (#87)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-09-14 12:52:06 +08:00
0977de3713 chore(deps): align Codex transport dependencies (#86)
Align HTTP and WebSocket TLS dependencies with the locally installed official Codex 0.154.0 client, preserve the hyper-util SOCKS IPv6 fix, and lock captured ClientHello behavior in tests.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-14 12:02:44 +08:00
dependabot[bot] eaa37d9faa chore(deps): bump the minor-and-patch group in /backend with 4 updates (#85)
Bumps the minor-and-patch group in /backend with 4 updates: [syn](https://github.com/dtolnay/syn), [uuid](https://github.com/uuid-rs/uuid), [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust) and [redis](https://github.com/redis-rs/redis-rs).


Updates `syn` from 3.0.4 to 3.0.5
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/3.0.4...3.0.5)

Updates `uuid` from 1.26.0 to 1.26.1
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.26.0...v1.26.1)

Updates `aws-sdk-s3` from 1.144.0 to 1.146.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `redis` from 1.6.0 to 1.7.0
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-1.6.0...redis-1.7.0)

---
updated-dependencies:
- dependency-name: syn
  dependency-version: 3.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: uuid
  dependency-version: 1.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-sdk-s3
  dependency-version: 1.146.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: redis
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 11:48:32 +08:00
4e588ffa1d chore(deps): ignore Codex-aligned dependencies (#84)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-14 11:01:34 +08:00
dependabot[bot] b9e919f510 chore(deps): bump rust from 82150a5 to 9a73a50 in /deploy (#80)
Bumps rust from `82150a5` to `9a73a50`.

---
updated-dependencies:
- dependency-name: rust
  dependency-version: 1.98-bookworm
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 10:58:58 +08:00
dependabot[bot] 32f9f3675d chore(deps): bump the minor-and-patch group in /frontend with 5 updates (#79)
Bumps the minor-and-patch group in /frontend with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.41.0` | `1.45.0` |
| [dompurify](https://github.com/cure53/DOMPurify) | `3.4.14` | `3.4.15` |
| [marked](https://github.com/markedjs/marked) | `18.0.11` | `18.0.13` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.1` | `26.5.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.2.2` | `8.3.0` |


Updates `@lucide/vue` from 1.41.0 to 1.45.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.45.0/packages/vue)

Updates `dompurify` from 3.4.14 to 3.4.15
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.14...3.4.15)

Updates `marked` from 18.0.11 to 18.0.13
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v18.0.11...v18.0.13)

Updates `@types/node` from 26.4.1 to 26.5.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `vite` from 8.2.2 to 8.3.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.3.0/packages/vite)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: dompurify
  dependency-version: 3.4.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: marked
  dependency-version: 18.0.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@types/node"
  dependency-version: 26.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: vite
  dependency-version: 8.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 10:58:33 +08:00
dependabot[bot] 221d557d5e chore(deps): bump pnpm/action-setup in the minor-and-patch group (#76)
Bumps the minor-and-patch group with 1 update: [pnpm/action-setup](https://github.com/pnpm/action-setup).


Updates `pnpm/action-setup` from 6.0.10 to 6.1.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/0977fd99725f1db4007ccb2928dbb4e90d06cc86...ea17c68df8912ef543352723c149a84f56e3d413)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 10:57:06 +08:00
ZyyandCodex 4105c1ece6 chore: bump version to 3.6.1
Co-authored-by: Codex <noreply@openai.com>
2026-09-13 04:30:39 +08:00
ZyyandCodex 4d59687cbc docs(release): prepare v3.6.1 notes
Co-authored-by: Codex <noreply@openai.com>
2026-09-13 04:30:35 +08:00
ZyyandCodex 8621e24e7c fix: tolerate partial quota usage and remove retired model pricing
Keep quota capacity and remaining estimates based on recorded usage when some token or cost facts are missing. Preserve unknown values and explain potential underestimation without blocking usable forecasts.

Remove OpenAI pricing rules for shutdown models and the GPT-4-0314 alias while preserving historical costs and active regular prices. Document actual tier fallback and model-specific pricing behavior.

Validated with backend test targets, strict workspace Clippy, Rustfmt, frontend lint/typecheck/build, and mocked browser checks for partial, missing, complete, weekly/monthly, and mobile dark forecast states.

Co-authored-by: Codex <noreply@openai.com>
2026-09-13 04:27:49 +08:00
ZyyandCodex 3bebccac5d chore: bump version to 3.6.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-13 03:50:04 +08:00
ZyyandCodex 98287195ca docs(release): prepare v3.6.0 notes
Co-authored-by: Codex <noreply@openai.com>
2026-09-13 03:49:59 +08:00
ZyyandCodex fd1b159f71 docs: clarify document responsibilities and simplify setup
Keep documentation focused on current contracts, responsibilities, and required operations. Define when PRs need documentation updates and consolidate setup commands with explicit ownership and permissions.

Co-authored-by: Codex <noreply@openai.com>
2026-09-13 03:43:15 +08:00
Yanshan ChenandCodex 7b03fdc8ab feat(keys): support custom keys and search by name
Support optional custom client keys across creation, storage, and authentication. Search keys and request records by name, remove credential and prefix searches, and reject duplicate names during create and update using the existing control-plane transaction lock.

Simplify the request diagnostics panel and use the shared empty state.

Validated with 1,029 backend tests using dedicated PostgreSQL and Redis, strict workspace Clippy, frontend lint/typecheck/build, and browser checks for key creation, name validation, search, diagnostics refresh/export, and mobile layouts.

Co-authored-by: Codex <noreply@openai.com>
2026-09-13 03:21:42 +08:00
Yanshan ChenandCodex 851a50962d fix(admin): support HTTP login and clipboard without configuration
Support same-origin HTTP admin sessions without an additional configuration setting. Derive Cookie security from a validated browser Origin, retain Secure for HTTPS and unknown origins, and enable VueUse's existing clipboard fallback.

Validated with 312 API tests, 36 gateway tests, strict workspace Clippy, frontend lint/typecheck/build, and browser login/reload/logout over HTTP and HTTPS-to-HTTP proxying.

Co-authored-by: Codex <noreply@openai.com>
2026-09-13 02:41:05 +08:00
ZyyandCodex b6e17db0c6 fix(openai): preserve native Codex model catalogs
Forward complete upstream model objects and client versions without rebuilding metadata. Keep account-scoped catalogs in a bounded, coalesced cache with explicit invalidation and preserve model aliases without mixing fields.

Cover service tiers, native metadata, account isolation, cache behavior, and failure handling with regression tests; document the catalog contract.

Co-authored-by: Codex <noreply@openai.com>
2026-09-13 02:02:17 +08:00
ZyyandCodex b98c2f01a5 fix(accounts): fix quota forecasts and simplify capacity view
Exclude confirmed OpenAI prewarm requests from completed usage while
preserving audit records and unknown inference pricing.

Keep only the quota capacity card, trim unused Admin/API diagnostics,
and use the actual quota observation time without changing forecast gates.
Reduce warning noise and align capacity headings with the cards.

Co-authored-by: Codex <noreply@openai.com>
2026-09-13 00:45:26 +08:00
ZyyandCodex 14e7967e1c chore: bump version to 3.5.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-12 23:23:43 +08:00
ZyyandCodex a4efb77223 docs(release): prepare v3.5.0 notes
Co-authored-by: Codex <noreply@openai.com>
2026-09-12 23:23:32 +08:00
ZyyandCodex 5041d6ba26 feat(accounts): add sampled weekly and monthly quota forecasts
Expose a dedicated read-only forecast endpoint with bounded quota history sampling, recent progress blocks, reset continuity checks, and independent token and USD completeness gates. Keep prediction state separate from authoritative quota and billing.

Add the account forecast dialog with weekly/monthly switching, sampling details, loading skeletons, themed cards, and refresh-icon feedback. Document estimation formulas and limits, and cover model, provider, HTTP, and PostgreSQL boundaries.

Validated 28 forecast-related tests including isolated PostgreSQL, 443 Admin/API regression tests, 12 architecture checks, strict affected-crate Clippy, Rustfmt, and frontend lint/typecheck/build. Browser acceptance remains user-owned.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 23:09:52 +08:00
ZyyandCodex 322b602154 fix(frontend): centralize admin API error notifications
Show server envelope messages from the response interceptor, with shared transport fallbacks, cancellation handling, and explicit silent requests. Remove duplicate page notifications while preserving contextual batch and irreversible-operation feedback.

Propagate request options through API modules and document notification ownership. Verify the notification-only tree with ESLint, vue-tsc, and a Vite build.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 23:09:04 +08:00
ZyyandCodex de15722ee1 fix(observability): search usage by account email and name
Search historical account email and name snapshots while preserving literal prefix matching. Add regression coverage and document the search contract.

Fixes #70

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 19:57:45 +08:00
ZyyandCodex 380f04bace fix(admin): preserve actionable provider error messages
Retain safe provider messages across the admin HTTP boundary while keeping internal diagnostics private. Distinguish refresh contention, known upstream rejection, and unknown outcomes without changing worker retry or terminal-state policies.

Cover OpenAI refresh-token failure reasons, including token_expired, and improve adjacent OpenAI/xAI admin guidance. Add provider and HTTP regressions and synchronize API and architecture contracts.

Validated 1400 related backend tests, 12 architecture checks, strict Clippy, Rustfmt, gateway build, and an isolated real-upstream invalid-token browser flow.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 19:08:00 +08:00
ZyyandCodex 194cc0e0c9 fix(core): record failures before provider stream creation
Persist accepted pre-stream failures through existing request finalization
without fabricating attempts, accounts, transports, or send facts.
Retain terminal write futures across cancellation, preserve registered cold
streams, and release rejected stream leases before observation writes.
Add coordinator, lifecycle, PostgreSQL, queue, and API regressions, and
document the recording and troubleshooting boundaries.

Validated relevant backend suites, strict Clippy, Rustfmt, and isolated
PostgreSQL/Redis with real OpenAI success and pre-stream failure paths.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 18:44:19 +08:00
ZyyandCodex d3acb42d37 fix: improve error diagnostics and request correlation
Preserve confirmed upstream error details and correlate responses with existing request IDs without schema changes.
Harden diagnostic sanitization and export allowlisted feedback bundles.
Refresh usage and error queries with consistent filters and pagination snapshots.
Add protocol and privacy regressions and document troubleshooting boundaries.

Validated 2064 backend tests with dedicated PostgreSQL and Redis, strict Clippy, Rustfmt, frontend lint and build, and real OpenAI HTTP/WebSocket success and failure paths with 81 UI assertions.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 17:53:03 +08:00
ZyyandCodex bfca754714 fix: resolve maintainability audit findings
Serialize snapshot publication and preserve cancellation-safe execution cleanup.
Move the read-only catalog contract to routing and reuse xAI egress and JWKS state.
Unify admin post-commit observations, retain account UI state across remounts, and remove unused contracts and brittle tests.
Document regression evidence, integration verification, and remaining coverage gaps.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 15:17:22 +08:00
ZyyandCodex 881e78975e docs: align contribution guidance with official source evidence
Unify problem validation, official implementation references, and overlap checks across repository guidance and skills. Clarify issue release evidence and deployment-aware UI verification, and simplify the README overview and navigation.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 07:25:10 +08:00
2han9wen71anandCodex eb3baef3aa fix(openai): preserve upstream model context window limits (#68)
Preserve upstream default and maximum context windows independently,
including unknown values, and keep xAI limits provider-owned. Add catalog
and API regression coverage and document the client contract.

Co-authored-by: zhangwentian <2han9wen71an@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-12 07:00:11 +08:00
ZyyandCodex 5205c07d60 chore: bump version to 3.4.1
Co-authored-by: Codex <noreply@openai.com>
2026-09-12 00:24:21 +08:00
ZyyandCodex 60eb08cb96 docs(release): prepare v3.4.1 notes
Summarize quota reset, downstream transport isolation, compressed request decoding, and weekly/monthly usage window fixes since v3.4.0.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 00:24:04 +08:00
ZyyandCodex fd4a49e64f test(openai): merge websocket proxy header regression (#67)
Preserve the original contributor commit and its two-frame Cloudflare header regression. Resolve overlap with the existing transport-header fix while keeping the identity-encoding fixture and ingress decompression documentation from main.

Validation: 385 API, protocol, and application tests, Rustfmt, and strict workspace Clippy passed. The new regression also passed independently. No new live Cloudflare/OpenAI end-to-end run was performed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 00:17:39 +08:00
ZyyandCodex be2ee7f3b8 fix(openai): bound request decompression and validate encodings
Stream all supported encodings through a bounded decoder, cap buffer growth and the zstd window, and consume every gzip member. Borrow uncompressed bodies, reject stacked encodings, and remove the unused error variant.

Add decoding and HTTP boundary regressions, preserve downstream transport-header isolation, and document the ingress compression contract.

Validation: 384 API, protocol, and application tests, Rustfmt, and strict workspace Clippy passed. Twenty real HTTP requests passed with synthetic upstream execution. An 8,210-byte zstd probe now stops at the 64 MiB output limit, with a largest Rust allocation of 64 MiB instead of 256 MiB. Official Codex client fallback and real upstream execution were not exercised.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 00:10:23 +08:00
xbz123andCodex 757abdb4d1 fix(openai): strip downstream proxy headers from upstream requests
Co-authored-by: Codex <noreply@openai.com>
2026-09-11 17:55:11 +02:00
ZyyandCodex cb5e0ba4c0 fix(openai): merge request content encoding support (#66)
Preserve the original contribution before hardening its decoding limits and regression coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 23:54:42 +08:00
ZyyandCodex d2b418f8cc fix(admin): use weekly and monthly quota windows for usage
Prefer account-wide weekly usage windows, falling back to monthly windows when weekly usage is unavailable. Require complete window boundaries and do not fall back to short-term or historical totals.

Expose the selected window label in account responses, update usage panels and Token tooltips, and align summary text baselines. Preserve quota bar and Dashboard selection behavior.

Validation: 421 related and application tests, Rustfmt, strict workspace Clippy, and frontend lint/type checks/build passed. Browser checks with synthetic weekly, monthly, and empty fixtures passed in dark, light, and narrow layouts.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 23:27:34 +08:00
zhangwentian 2390783900 fix(openai): decode gzip/deflate/zstd request bodies on POST /v1/responses
官方 Codex 客户端对较大的 /v1/responses 请求体默认 zstd 压缩(level 3)并
携带 Content-Encoding: zstd,也会使用 gzip。网关此前只在出站方向按官方指纹
做 zstd 压缩(client_sse.rs),入站方向却不接受压缩正文,压缩请求一律命中
400 invalid_json("Request body must be valid JSON.")。官方后端接受压缩
请求,因此直连官方可用的会话经网关转发时会在 HTTP 回退路径稳定失败。

现在 POST /v1/responses 入站解码按 Content-Encoding 解压后再进入 JSON
解析:

- 支持 gzip、deflate、zstd;未压缩与 identity 原样透传
- 解压结果超过 64 MiB 上限时拒绝,防御解压炸弹
- 未知 Content-Encoding 返回 400 unsupported_content_encoding
- 损坏的压缩载荷统一按 invalid_json 返回,不泄露内部细节

WebSocket 帧路径不受影响(permessage-deflate 由传输层协商处理)。
2026-09-11 23:21:45 +08:00
ZyyandCodex c75f0e7fd9 fix(openai): isolate downstream transport headers
Share transport-header classification between API ingress and OpenAI encoding. Strip proxy metadata and downstream compression negotiation while preserving opaque business extensions and upstream response diagnostics.

Add protocol, decoder, HTTP, and WebSocket regressions and document the transport boundary.

Validation: 946 related and application tests, Rustfmt, and strict workspace Clippy passed. Application tests passed with incremental compilation disabled after a compiler cache panic. Real-account HTTP/WebSocket checks and a two-turn WebSocket session through Cloudflare Tunnel passed.

Refs #63, #64

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 22:58:38 +08:00
ZyyandCodex d5e9360ed7 fix(admin): support quota resets over plain HTTP
Generate canonical UUIDv4 redeem IDs with crypto.getRandomValues so reset
confirmation works on non-secure HTTP origins. Preserve the pending
operation ID when retrying uncertain consumption.

Validation: frontend lint/type checks/build, browser composable checks
with mocked APIs on HTTP/localhost/HTTPS, and 10 related backend tests
passed. No real reset credits were consumed.

Refs #62

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 20:41:32 +08:00
ZyyandCodex 3fcb8e5f2d docs(release): translate v3.4.0 notes into Chinese
Translate the release summary and upgrade instructions while preserving
the version, technical identifiers, and documented behavior.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 17:25:57 +08:00
ZyyandCodex 8cdaf8b2e6 chore: add contribution workflows and repository skills
Define shared contribution and review standards, add a YAML Issue Form,
and guard AGENTS.md changes using PR metadata.

Move project skills into .agents/skills and add PR, Issue, and release
workflows with consistent names and scoped triggers. Keep generic
engineering guidance in the personal skill directory.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 17:25:57 +08:00
ZyyandCodex 68f05a4481 chore: bump version to 3.4.0
Co-authored-by: Codex <noreply@openai.com>
2026-09-11 16:08:59 +08:00
ZyyandCodex bb4d178b5d docs(release): prepare v3.4.0 release notes
Document managed proxies, connection tests before saving, credential and
certificate fixes, account and interface improvements, and migration
behavior.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 16:08:26 +08:00
ZyyandCodex 73487fd98b fix(api-keys): trim trailing zeros from budget amounts
Display budget amounts with up to two decimal places without padding
trailing zeros. Keep exact amounts in the details popover and preserve
accounting precision.

Validation: frontend lint/type checks/build and browser checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 16:08:20 +08:00
ZyyandCodex 66fd09eba6 feat(proxies): test connections before saving and refine controls
Separate connection testing from saving in proxy forms. Probe unsaved URLs
with the shared certificate configuration and test concurrency limit,
without changing saved proxy settings or linked accounts.

Keep saved authentication when testing an unchanged connection, use
clearer placeholders, fix action column widths, and remove the redundant
list refresh button.

Validation: 5 proxy API tests, scoped strict Clippy, rustfmt, frontend
lint/type checks/build, and browser checks passed. Codex CLI requests
through real HTTP and SOCKS5 proxies completed with matching upstream
text while Clash TUN remained enabled.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 16:08:14 +08:00
ZyyandCodex bd152dec00 feat: integrate managed proxies and WebSocket fixes (#60)
Integrate managed account proxies and the non-streaming WebSocket fix.
Include shared CA trust, credential import protection, paginated linked
account management, and the reviewed interface refinements.

Preserve the original contribution and the separate proxy and UI commits.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 14:54:24 +08:00
ZyyandCodex 7090dca502 fix(ui): correct modal dragging and sidebar scrolling
Use positional offsets for modal dragging to prevent seams between table
cells. Scroll sidebar navigation independently while keeping the brand
and footer fixed, with an auto-hiding scrollbar at the outer edge.

Validation: frontend lint/type checks/build and browser checks for
modal dragging, sidebar overflow, alignment, and auto-hide passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 14:43:45 +08:00
ZyyandCodex 029f3124bf feat(proxies): paginate linked accounts and support removal
Load linked accounts on demand with shared plan metadata and guarded
removal to direct connections. Preserve credentials, scheduling settings,
and groups when clearing a proxy binding.

Align proxy forms and tables with shared components, integrate endpoint
details into outbound selectors, and consolidate probe results into the
latency column.

Validation: 11 proxy regressions, strict workspace Clippy, rustfmt,
frontend lint/type checks/build, and local browser checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 14:43:31 +08:00
ZyyandCodex 840707e5a1 fix(proxies): share CA trust and preserve credentials during import
Co-authored-by: Codex <noreply@openai.com>
2026-09-11 13:07:31 +08:00
Yanshan Chen 704b652f86 feat: add managed account proxies and fix non-streaming WebSocket requests 2026-09-11 11:55:09 +08:00
ZyyandCodex 2b98839c29 fix(admin): unify plan labels and refine budget popover
Normalize provider plan display names to PascalCase while preserving raw
plan values. Replace native budget tooltips with the shared borderless
popover, retaining exact amounts and daily/weekly reset information.
Reuse the dayjs date helper for explicit Beijing time formatting.

Validation: 371 Admin/API tests, strict Clippy, rustfmt, frontend lint and
type checks passed. Local frontend and backend health checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 05:47:34 +08:00
ZyyandCodex 90dd5b5a57 chore: bump version to 3.3.32
Co-authored-by: Codex <noreply@openai.com>
2026-09-11 05:15:12 +08:00
ZyyandCodex 5ad9378b33 docs(release): prepare 3.3.32 release notes
Co-authored-by: Codex <noreply@openai.com>
2026-09-11 05:15:08 +08:00
ZyyandCodex 4a413f5bb7 fix(budgets): remove manual charge reconciliation
Settle only observed USD costs and treat missing costs as zero without
blocking client keys. Preserve known retry costs and idempotent settlement.
Remove reconciliation controls, endpoints, pending records, and obsolete
schema fields; migrate legacy unknown charges to zero without changing
recorded usage.

Display daily and weekly amounts with two decimal places while retaining
full precision for accounting and tooltips. Distinguish missing WebSocket
closing handshakes from TCP resets in connection diagnostics.

Validation: workspace tests with PostgreSQL/Redis, targeted regressions,
strict Clippy, rustfmt, frontend lint/build, and migration checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 05:13:06 +08:00
ZyyandCodex 0bab566d04 fix(xai): correct subscription plans and Codex tool handling
Co-authored-by: Codex <noreply@openai.com>
2026-09-11 04:49:22 +08:00
ZyyandCodex 9b19cf083a feat(release): publish release notes from Markdown
Require committed, non-empty notes matching the target release version before
creating a tag. Read notes from the tagged source and place them before the
installation guide and generated release changelog.

Add the 3.3.31 feature summary and keep maintainer instructions in the release
script help and repository skill instead of the deployment README.

Validation: shell syntax and workflow lint passed. Nine isolated checks cover
local publication, stale or missing notes, and release-body rendering.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 03:40:00 +08:00
ZyyandCodex 8b22788746 chore: bump version to 3.3.31
Co-authored-by: Codex <noreply@openai.com>
2026-09-11 03:34:04 +08:00
ZyyandCodex e973fc516e feat(accounts): separate setup from account import
Share account settings across import, single-account editing, and batch editing.
Commit credentials and import settings in the same transaction. Improve modal
actions and fix the flash at the end of light/dark theme transitions.

Reauthenticate each execution against the current key policy so existing
WebSocket connections apply updated RPM, concurrency limits, and enabled state.
Add regression coverage for policy updates and unavailable snapshots.

Validation: all 1,950 backend tests, workspace Clippy, frontend lint/typecheck/build,
migration checks, Compose validation, and workflow lint passed. Completed 24 real
requests with a free account using gpt-5.6-luna to verify daily/weekly budgets,
RPM, concurrency, reconciliation, WebSocket policy updates, and budget persistence
across restarts and usage-log cleanup.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 03:26:48 +08:00
ZyyandCodex 12ecbf574a fix(accounts): fill missing plans from provider quota snapshots
Use explicit plan types from OpenAI quota snapshots to fill missing or
unknown account plans before building account and dashboard display names.
Preserve known plan subtypes and reuse cached quota without extra requests
or changes to persisted credential facts.

Cover quota projection and account list/detail fallback behavior, and
document the backend display contract.

Validation: gateway-admin, OpenAI, and xAI tests (1049 passed), strict
Clippy for affected backend crates, gateway build, and diff whitespace
checks passed. A real account JSON import and upstream quota refresh
confirmed free/Free in list, detail, and cached quota responses.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 02:27:45 +08:00
ZyyandCodex 443bd6ad17 feat: integrate account proxies and daily key budgets (#58)
Enforce daily and seven-day client key budgets with a durable charge ledger,
and route provider requests through each account's configured proxy.

Preserve backend plan display names and Codex personal access token imports.
Keep PAT validation on the selected proxy, preserve credential refreshes
when account egress changes, and include the upstream SOCKS IPv6 fix.

Use the PR migration layout, keep the account proxy and key forms aligned
with existing controls, and document the ownership and settlement rules
in the architecture guide.

Validation: 1942 workspace tests passed with isolated PostgreSQL/Redis;
strict Clippy, rustfmt, frontend lint/build, and migration checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 02:07:28 +08:00
ZyyandCodex c795098a87 feat(openai): support Codex personal access token imports
Accept at- personal access tokens through existing AT and JSON imports,
including the official personal_access_token field. Verify identity with
auth whoami instead of trusting document metadata, preserve optional email,
and exclude PAT credentials from OAuth refresh scheduling.

Expose explicit static PAT validation errors without leaking credentials
or raw upstream responses. Cover rejection, invalid identity, timeouts,
redirect protection, export round trips, and unchanged JWT import behavior.

Validation: OpenAI provider tests (562 passed), gateway-admin tests
(100 passed), strict Clippy for both crates with all targets and features,
workspace rustfmt, and diff whitespace checks passed.
Upstream behavior was tested with mocks; no live PAT was used.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 02:02:51 +08:00
ZyyandCodex 1f0009341d fix(accounts): resolve plan display names in the backend
Map OpenAI plan identifiers to official desktop labels in the provider.
Expose planTypeDisplay in account and dashboard responses while preserving
raw planType values, and render backend labels directly in the frontend.
Show missing plans as unknown and retain raw identifiers in badge tooltips.

Validation: Admin/API tests (368 passed), OpenAI admin tests (29 passed),
strict Clippy for affected backend crates with all targets and features,
frontend production build, targeted ESLint, and diff whitespace checks.

Co-authored-by: Codex <noreply@openai.com>
2026-09-11 02:02:51 +08:00
Codex 1359eaa894 feat: add account egress and client key USD budgets
Import sub2api proxy bindings, enforce durable daily and seven-day budgets alongside existing key concurrency, and integrate matching admin controls and audited reconciliation.
2026-09-10 23:41:35 +08:00
ZyyandCodex 74318af570 fix(routing): report missing models with explicit errors
Return model_not_found with HTTP 404 when all scoped provider catalogs
confirm the requested or mapped model is absent. Explain missing mapping
targets separately and preserve unavailable-provider errors for circuit
blocks, empty account scopes, and unsupported operations.

Cover missing models, aliases, empty and unknown catalogs, provider
fallback, and streaming and buffered HTTP error responses.

Validation: gateway-core and gateway-api tests (541 passed), strict Clippy
for both crates with all targets and features, workspace rustfmt, and diff
whitespace checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-10 22:23:42 +08:00
Zyy ddc10d7c1a docs: add Apache 2.0 license and community link 2026-09-10 16:14:44 +08:00
ZyyandCodex 6bc2189e6f refactor(frontend): streamline quota reset dialog and spacing
Show remaining usage limits and an always-visible reset credit list with per-credit reset actions. Use backend-provided credit titles and quota window labels while preserving confirmation and ambiguous-result recovery.

Display credit expiration in UTC+8 without a timezone suffix. Balance modal title and reset section spacing, and remove inline baseline whitespace from account quota summary cells without changing row heights or the borderless design.

Validation: browser layout and quota popover interaction checks, expiration timezone checks, frontend typecheck, targeted ESLint, and diff whitespace checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-10 15:33:43 +08:00
ZyyandCodex b206ee8271 chore: bump version to 3.3.30
Co-authored-by: Codex <noreply@openai.com>
2026-09-09 19:28:01 +08:00
ZyyandCodex 70ff923033 fix(frontend): prevent pagination overflow on narrow screens
Adapt visible page buttons to the pagination container width while preserving the existing button styles. Hide the total count on narrow containers and keep the page-size selector on the left with navigation on the right. Retain the total count and full pager on wider layouts.

Validation: browser layout checks across 320–1280px widths and up to 1380 pages, pagination interactions and disabled states, frontend typecheck, targeted ESLint, and diff whitespace checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-09 19:26:55 +08:00
ZyyandCodex a2fbc073a4 refactor(store): consolidate database migrations into initial baseline
Fold the final columns, constraints, and indexes into the initial schema and remove superseded migrations and historical backfills. Refresh the frozen checksum manifest, simplify migration test setup, and retain canonical audit identity coverage. Keep migration documentation focused on general rules.

Validation: PostgreSQL schema dumps match before and after consolidation, all 125 PostgreSQL tests passed, and frozen checksum, workspace rustfmt, and diff whitespace checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-09 19:11:49 +08:00
ZyyandCodex 9944041dc8 fix(openai): allow more time for WebSocket heartbeat responses
Increase the default heartbeat timeout from 5 to 30 seconds so brief upstream stalls have more time to recover during response generation. Document that heartbeat checks also apply to active pooled connections and accept any inbound frame as liveness.

Validation: WebSocket pool tests (26 passed), strict OpenAI Clippy with all targets and features, workspace rustfmt, and diff whitespace checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-09 17:46:45 +08:00
ZyyandCodex 18a04f1213 fix(openai): restore periodic quota checks and timely reset recovery
Recheck exhausted accounts every 30 minutes even before their recorded reset, so early provider resets can be discovered. Allow each pending reset boundary to trigger an earlier check after a two-minute grace period, then return to periodic retries if exhaustion persists.

Separate quota checks from model catalog refreshes. Scan quota eligibility every 30 seconds while retaining the configured catalog interval and independent per-window recovery rules. Keep quota timing regressions in the mirrored integration-test directory and exercise the public service.

Validation: quota integration tests (46 passed), gateway application and architecture tests (36 passed), strict OpenAI Clippy with all targets and features, workspace rustfmt, and diff whitespace checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-09 15:39:41 +08:00
ZyyandCodex 35988d0eb7 fix(billing): correct provider pricing and service tier accounting
Use the actual response tier for OpenAI costs and persisted observations.
Replace broad model-prefix pricing with explicit variants and documented
aliases and snapshots. Retain regular Sol prices, excluding temporary
promotions, and add GPT Image 2.5 Sunburst and Flare token pricing.

Include File Search call fees and leave incomplete tool totals unpriced.
Add Grok 4.6 and confirmed Priority rates, persist the served xAI tier,
and preserve authoritative zero-cost responses. Reject inconsistent cache
counts, invalid totals and token-count overflow during local estimation.

Validation: OpenAI, xAI and gateway-protocol tests, additional billing and
compaction regressions, workspace rustfmt, strict workspace Clippy with
all targets and features, and diff whitespace checks passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-09 13:37:12 +08:00
ZyyandCodex 6673b6bd1e chore: bump version to 3.3.29
Co-authored-by: Codex <noreply@openai.com>
2026-09-08 17:55:36 +08:00
ZyyandCodex 2a7fd08e9e perf(ci): reuse release images and persist Cargo build caches
Share container verification between CI and release image jobs. Validate both native release architectures and publish manifests from the verified digests, eliminating the duplicate release source build.

Persist BuildKit Cargo cache mounts across CI runs, refresh release caches per commit, and generate SARIF and vulnerability gate results from one Trivy scan.

Validation: actionlint, composite action ShellCheck, embedded shell syntax, Trivy severity and malformed-report checks, and manifest digest validation passed locally. Full GitHub workflow timing remains to be measured.

Co-authored-by: Codex <noreply@openai.com>
2026-09-08 17:50:17 +08:00
ZyyandCodex 7f69e81176 docs(readme): streamline project overview and client setup
Condense the feature overview and client setup guidance, and link to detailed operational documentation for troubleshooting and maintenance.

Validation: checked local Markdown links, anchors, shell syntax and diff whitespace.

Co-authored-by: Codex <noreply@openai.com>
2026-09-08 17:36:49 +08:00
ZyyandCodex 93dc417b79 fix(observability): preserve failure causes and complete log retention
Preserve safe failure stages and causes through OpenAI and xAI transport
mapping, Core traces and persisted errors without changing replay safety.
Distinguish missing WebSocket close handshakes from TCP resets and surface
connection facts in the borderless diagnostics panel.

Retain complete UTC date groups instead of limiting segment counts. Default
application and OAuth recovery logs to seven days and full payload logs to
one day. Compress closed segments, drain bounded file queues on shutdown
and report write or maintenance failures through the logging health probe.
Normalize file names and expose an independent OAuth recovery switch that
defaults to off. Remove max_files; leave old file names for manual cleanup.

Validation: workspace backend tests with isolated PostgreSQL, final Host
and application tests (104 passed), OpenAI tests (542 passed), xAI tests
(371 passed), rustfmt and strict Clippy. Frontend lint/build, light/dark
and mobile browser checks, and Compose validation passed. Redis integration
tests were not configured and followed the existing skip convention.

Co-authored-by: Codex <noreply@openai.com>
2026-09-08 17:15:42 +08:00
ZyyandCodex f905e4b3fb chore: bump version to 3.3.28
Co-authored-by: Codex <noreply@openai.com>
2026-09-08 02:42:09 +08:00
ZyyandCodex ed3e23887b fix(openai): calculate standalone image generation costs
Estimate gpt-image-2 generation and edit costs from the official standard
API rates for text input, image input, cached input and image output.
Emit calculated costs before image delivery while preserving raw responses.
Leave unsupported models, invalid usage and ambiguous mixed cache totals
unpriced. Mark calculated total-only amounts with an approximation sign.

Validation: 1,107 related backend tests, workspace rustfmt and strict
OpenAI/gateway-api Clippy. Replaying the previously verified live usage
of 18 text input and 229 image output tokens yields exactly USD 0.00696.

Co-authored-by: Codex <noreply@openai.com>
2026-09-08 02:41:14 +08:00
ZyyandCodex 91f35d88cf fix(openai): record image usage and endpoint request models
Persist models declared by standalone image and search requests without
routing them through the text model catalog. Extract image token usage
before delivering the unchanged JSON response, preserving unknown counts
and upstream totals without double-counting image token details.

Validation: 1,474 related backend tests, workspace rustfmt and strict
Clippy passed. One authorized live gpt-image-2 generation reported 18 input,
229 output and 247 total tokens, matching the canonical usage exactly.
Search responses inspected in official source and production logs did not
report usage, so their token counts remain unknown.

Co-authored-by: Codex <noreply@openai.com>
2026-09-08 02:23:00 +08:00
ZyyandCodex f465a337e8 refactor(openai): simplify connection and endpoint module files
Restore connection.rs for production and integration tests by folding the
heartbeat tests into the existing connection test module. Keep all heartbeat
coverage while satisfying the workspace layout rules without a subdirectory.

Rename provider_endpoint.rs to endpoint.rs in production and tests. Update
module declarations, imports and frozen file manifests to match.

Validation: gateway application tests (36 passed), all gateway-api tests
(263 passed), workspace rustfmt, and strict gateway-api Clippy with all
targets and features.

Co-authored-by: Codex <noreply@openai.com>
2026-09-08 01:51:00 +08:00
ZyyandCodex 37465e2872 fix(tests): align WebSocket connection module layout
Move production and test connection modules to connection/mod.rs so the
heartbeat submodule follows the workspace directory module convention.
Keep the production and integration test layouts mirrored, and update the
frozen file manifests to include the heartbeat regression tests.

Validation: gateway application tests (36 passed), all gateway-api tests
(263 passed), workspace rustfmt, and strict gateway-api Clippy with all
targets and features.

Co-authored-by: Codex <noreply@openai.com>
2026-09-08 01:46:08 +08:00
ZyyandCodex 70c18dadd6 chore: bump version to 3.3.27
Co-authored-by: Codex <noreply@openai.com>
2026-09-08 01:37:11 +08:00
ZyyandCodex 881a0eaef6 fix(openai): keep downstream WebSocket connections alive
Send a downstream Ping every 25 seconds while responses are pending or the
connection is idle. Match the official Codex client's Pong handling without
adding a Pong deadline or extending business request timeouts. Skip missed
ticks and keep heartbeat writes bounded and cancellable.

Record Ping/Pong counts and read/write idle durations in disconnect logs.
Serialize correlation IDs as borrowed strings so gateway-api checks do not
require the serde/rc feature.

Cover silent active responses, delayed ticks, missing Pongs, blocked writes
and lifecycle cancellation. Adapt connection expiry coverage for heartbeats.

Validation: gateway-api WebSocket tests (38 passed), workspace rustfmt,
and strict gateway-api Clippy with all targets and features, without serde/rc.

Co-authored-by: Codex <noreply@openai.com>
2026-09-08 01:36:22 +08:00
ZyyandCodex 95f7a5a636 fix(openai): isolate response metadata across WebSocket reuse
Restore opening response metadata before returning a completed WebSocket to
its pool. Prevent per-response headers, model names and ETags from leaking
into subsequent exchanges and growing downstream response headers.

Match the official Codex response-scoped metadata handling while preserving
opening headers, legitimate multi-value headers and turn-state updates.
Cover 40 consecutive exchanges on one socket with a regression test that
fails on the second exchange before the fix.

Validation: provider-openai transport tests (292 passed), workspace rustfmt,
and strict provider-openai Clippy with all targets and features.

Co-authored-by: Codex <noreply@openai.com>
2026-09-08 01:10:44 +08:00
dependabot[bot] 871406ab24 chore(deps): bump the minor-and-patch group in /backend with 9 updates (#54)
Bumps the minor-and-patch group in /backend with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [tokio](https://github.com/tokio-rs/tokio) | `1.52.3` | `1.53.1` |
| [tower-http](https://github.com/tower-rs/tower-http) | `0.7.0` | `0.7.1` |
| [flate2](https://github.com/rust-lang/flate2-rs) | `1.1.9` | `1.1.10` |
| [aws-smithy-runtime-api](https://github.com/smithy-lang/smithy-rs) | `1.15.0` | `1.16.0` |
| [aws-smithy-types](https://github.com/smithy-lang/smithy-rs) | `1.6.2` | `1.6.3` |
| [aws-lc-rs](https://github.com/aws/aws-lc-rs) | `1.16.2` | `1.18.1` |
| [hyper](https://github.com/hyperium/hyper) | `1.8.1` | `1.11.1` |
| [rustls-native-certs](https://github.com/rustls/rustls-native-certs) | `0.8.3` | `0.8.4` |
| [rustls-pki-types](https://github.com/rustls/pki-types) | `1.14.0` | `1.15.1` |


Updates `tokio` from 1.52.3 to 1.53.1
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.3...tokio-1.53.1)

Updates `tower-http` from 0.7.0 to 0.7.1
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.7.0...tower-http-0.7.1)

Updates `flate2` from 1.1.9 to 1.1.10
- [Release notes](https://github.com/rust-lang/flate2-rs/releases)
- [Commits](https://github.com/rust-lang/flate2-rs/compare/1.1.9...1.1.10)

Updates `aws-smithy-runtime-api` from 1.15.0 to 1.16.0
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `aws-smithy-types` from 1.6.2 to 1.6.3
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `aws-lc-rs` from 1.16.2 to 1.18.1
- [Release notes](https://github.com/aws/aws-lc-rs/releases)
- [Commits](https://github.com/aws/aws-lc-rs/compare/v1.16.2...v1.18.1)

Updates `hyper` from 1.8.1 to 1.11.1
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.8.1...v1.11.1)

Updates `rustls-native-certs` from 0.8.3 to 0.8.4
- [Release notes](https://github.com/rustls/rustls-native-certs/releases)
- [Commits](https://github.com/rustls/rustls-native-certs/compare/v/0.8.3...v/0.8.4)

Updates `rustls-pki-types` from 1.14.0 to 1.15.1
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.14.0...v/1.15.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.53.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: tower-http
  dependency-version: 0.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: flate2
  dependency-version: 1.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-runtime-api
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-types
  dependency-version: 1.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-lc-rs
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: hyper
  dependency-version: 1.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: rustls-native-certs
  dependency-version: 0.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: rustls-pki-types
  dependency-version: 1.15.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 00:49:34 +08:00
dependabot[bot] 38a1cc6933 chore(deps): bump the minor-and-patch group in /frontend with 5 updates (#55)
Bumps the minor-and-patch group in /frontend with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.37.0` | `1.41.0` |
| [vue-router](https://github.com/vuejs/router) | `5.3.0` | `5.3.1` |
| [@antfu/eslint-config](https://github.com/antfu/eslint-config) | `9.3.0` | `9.5.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.0` | `26.4.1` |
| [eslint](https://github.com/eslint/eslint) | `10.9.1` | `10.10.0` |


Updates `@lucide/vue` from 1.37.0 to 1.41.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.41.0/packages/vue)

Updates `vue-router` from 5.3.0 to 5.3.1
- [Release notes](https://github.com/vuejs/router/releases)
- [Commits](https://github.com/vuejs/router/compare/v5.3.0...v5.3.1)

Updates `@antfu/eslint-config` from 9.3.0 to 9.5.1
- [Release notes](https://github.com/antfu/eslint-config/releases)
- [Commits](https://github.com/antfu/eslint-config/compare/v9.3.0...v9.5.1)

Updates `@types/node` from 26.4.0 to 26.4.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.9.1 to 10.10.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.9.1...v10.10.0)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: vue-router
  dependency-version: 5.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@antfu/eslint-config"
  dependency-version: 9.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@types/node"
  dependency-version: 26.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 00:42:23 +08:00
dependabot[bot] d38e0d4296 chore(deps): bump softprops/action-gh-release (#53)
Bumps the minor-and-patch group with 1 update: [softprops/action-gh-release](https://github.com/softprops/action-gh-release).


Updates `softprops/action-gh-release` from 3.0.2 to 3.0.3
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/3d0d9888cb7fd7b750713d6e236d1fcb99157228...efb35369e0ad2afab669f228072c1b0d510eae64)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 00:42:11 +08:00
ZyyandCodex d12d75a037 fix(openai): preserve WebSocket continuation and surface streaming errors
Keep non-persistent downstream WebSocket requests on pooled upstream sockets,
even when opening exceeds the fast-path budget or HTTP fallback was selected.
This preserves the response state needed by subsequent previous_response_id
requests.

Select troubleshooting records by non-cancellation error_kind instead of the
request outcome. Share the predicate between the list and count queries, and
remove missing-error fallbacks so incomplete streaming errors remain visible.

Validation: provider-openai tests (532 passed), PostgreSQL observability tests
(19 passed), workspace rustfmt, and strict workspace Clippy with all targets
and features.

Co-authored-by: Codex <noreply@openai.com>
2026-09-07 12:24:28 +08:00
ZyyandCodex a2922b410e chore: bump version to 3.3.26
Co-authored-by: Codex <noreply@openai.com>
2026-09-07 09:49:49 +08:00
ZyyandCodex d303efc6f7 fix(openai): preserve ASCII encoding in scoped turn metadata
Keep turn metadata ASCII after account scoping by escaping Unicode as UTF-16 code units. Preserve metadata values and Unicode request input across HTTP headers, body aliases and WebSocket client metadata.

Cover Unicode workspace paths, emoji, escaped literals and account changes with HTTP and WebSocket regression tests.

Validation: provider-openai tests (528 passed) and workspace rustfmt.

Co-authored-by: Codex <noreply@openai.com>
2026-09-07 09:37:08 +08:00
ZyyandCodex dd17332c31 chore: bump version to 3.3.25
Co-authored-by: Codex <noreply@openai.com>
2026-09-07 04:48:14 +08:00
ZyyandCodex 47d08786ad fix(scheduling): retain valid quota signals across idle periods
Keep OpenAI quota windows eligible for scheduling until their reset time,
independently of the projection cache TTL. Expire undated observations and
reset windows separately, and clear old signals when a fresh observation
contains no active window.

Record bounded candidate diagnostics with quota, concurrency, smart scores
and affinity results. Cover idle 74%/96% accounts with one concurrent title
request, independent window expiry, cache replacement and trace retention.

Validation: workspace tests (1852 passed), rustfmt, and strict workspace
Clippy with all targets and features. PostgreSQL/Redis integration services
were not configured.

Co-authored-by: Codex <noreply@openai.com>
2026-09-07 04:47:30 +08:00
ZyyandCodex 3434ad5e72 fix(ui): polish diagnostics and account usage displays
Use the shared scrollbar for request diagnostics, improve timeline spacing
and disclosure styling, and show export loading feedback with duplicate
click protection and a minimum visible duration.

Hide reset times in metric account usage windows while retaining them in
the detail variant.

Validation: frontend typecheck and build, targeted ESLint for both files,
and browser checks for diagnostic scrolling, expansion, mobile layout,
loading feedback, and exported content.

Co-authored-by: Codex <noreply@openai.com>
2026-09-07 04:25:16 +08:00
ZyyandCodex e91ec88a1f chore: bump version to 3.3.24
Co-authored-by: Codex <noreply@openai.com>
2026-09-07 03:40:53 +08:00
ZyyandCodex 6f7a50b1b6 fix(scheduling): rotate smart selections among similarly scored accounts
Score load, remaining quota and first-output latency on fixed scales so
small differences do not monopolize unbound requests. Rotate accounts
within a narrow score tolerance in stable ID order using the existing
cursor, preserving session affinity and availability checks.

Cover small signal differences, changing score order, material advantages
and quota invalidation after credential rotation.

Validation: workspace tests (1844 passed, none ignored), rustfmt, and strict
workspace Clippy with all targets and features.

Co-authored-by: Codex <noreply@openai.com>
2026-09-07 03:40:34 +08:00
ZyyandCodex db0c294e83 fix(openai): prioritize session affinity and isolate child failover
Prefer client-scoped session bindings over ordinary account weights while
keeping existing availability checks and failover behavior. Let child
threads inherit the root account on first selection and update only their
own binding when they migrate.

Share session and thread identity extraction across Responses, Search and
image endpoints. Renew existing Redis bindings for 24 hours and preserve
account-bound state isolation without adding a table or scheduler.

Validation: workspace tests (1838 passed, none ignored), rustfmt, and strict
workspace Clippy with all targets and features.

Co-authored-by: Codex <noreply@openai.com>
2026-09-07 03:16:45 +08:00
ZyyandCodex d8181e8e91 docs(readme): install published images with Docker Compose
Download Compose and configuration files directly for installation, and
wait for healthy services before the first health check. Align deployment
paths and image upgrade instructions with the installation directory,
while documenting source checkout requirements for development and builds.

Validation: checked 16 shell examples and 17 local documentation links;
downloaded the deployment files into a temporary directory and verified
Compose configuration and a startup dry run without a source checkout.

Co-authored-by: Codex <noreply@openai.com>
2026-09-07 03:10:55 +08:00
ZyyandCodex af134cf488 fix(docker): apply system updates to the runtime base image
Upgrade installed Debian packages before adding runtime dependencies so
digest-pinned base images receive available fixes, including the PCRE2
security update.

Validation: rebuilt the amd64 runtime-base image without cache; Trivy
reported zero fixable OS vulnerabilities across all severities; curl,
pg_dump, and pg_restore started successfully as the cpr user.

Co-authored-by: Codex <noreply@openai.com>
2026-09-07 03:05:59 +08:00
Zyy d918149c2f fix(openai): recover exhausted quota windows independently
Persist per-window recovery baselines separately from current usage. Identify windows by upstream limit ID and actual duration, using protocol slots to distinguish equal or unspecified durations. Apply the same reset advancement and usage-below-10% rule to all periods without requiring allowed=true, retaining partial recovery across refreshes and restarts.

Use the exhausted window for reset tracking, preserve access during failure follow-ups, and return the updated account status after an admin quota refresh. Remove the ignored live Desktop artifact download test.

Validation: workspace tests (1828 passed), final OpenAI suite (516 passed, none ignored), fmt, clippy, and build. Real-account checks cover quota refresh and monthly recovery; seven controlled replay scenarios cover independent and partial recovery through the backend and PostgreSQL.
2026-09-07 00:59:48 +08:00
Zyy aa6bf6497f refactor(openai): unify WebSocket recovery and session fallback
Consolidate retry decisions in the provider and share the failure budget with session HTTP fallback. Preserve WebSocket priority and the 800ms cold connection budget.

Remove transport-local retries, fresh recovery probes, and metadata-based replay exceptions; use a 300s stream idle timeout.

Validation: backend tests (1816 passed, 1 ignored), fmt, clippy, and real Codex CLI 10-turn WebSocket continuation run.
2026-09-07 00:45:08 +08:00
ZyyandCodex 074c07b5ca refactor(store): tighten persisted facts and trim redundant observability
Remove unused event occurrence counts across storage, API and UI; count event
rows directly and keep provider metadata focused on facts absent from typed
observations. Preserve actual upstream-reported model and handshake status.

Narrow four live-reference indexes, drop eight low-value indexes, enforce
complete grouped facts and normalize administrator audit identities. Make
backup storage changes invalidate verification and pause schedules atomically,
while unchanged saves preserve state and revision.

Validation: 1,816 backend tests passed with isolated PostgreSQL and Redis,
including the official artifact download test. Strict Clippy, rustfmt, migration
freeze checks, frontend lint/typecheck/build and actionlint passed. Synthetic
10,000-row query plans verified live-reference and historical pagination indexes.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 17:27:29 +08:00
ZyyandCodex f70f46e87e fix(observability): exclude tokenless endpoints from usage
Define completed usage from delivery plus model, image, token, or cost evidence so standalone search remains operationally visible without appearing in usage records.

Exclude model-less provider endpoints from model diagnostics and keep attempt cost coverage on the shared usage fact.

Validation: 169 gateway-store tests passed against PostgreSQL; Clippy and rustfmt passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 17:02:41 +08:00
ZyyandCodex c21633655e fix(api): keep SSE responses alive during upstream silence
Send SSE keepalive comments after 15 seconds without output while preserving
the pending execution future. Disable response buffering and transformation
so long compaction requests can maintain the downstream connection.

Cover four minutes of upstream silence, cancellation, first-event commitment,
and conflicting upstream headers. Document transport errors and the separate
Codex event idle timeout.

Validation: 259 gateway-api tests and Clippy passed with serde/rc enabled;
cargo fmt and git diff --check passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 16:49:35 +08:00
Zyy 1bafbb3864 fix(openai): support Auth0 profile avatars 2026-09-06 16:38:21 +08:00
Zyy 353dccedc4 feat: add request diagnostics and align Codex transport contracts 2026-09-06 16:24:14 +08:00
ZyyandCodex 697f1870a0 docs: refresh user-facing setup and existing guides
Refocus the README on installation, client setup and common questions. Consolidate native image generation, auth.json compatibility and obsolete Codex settings in the existing deployment guide.

Correct account import formats, API references, backup recovery and password rotation guidance. Synchronize architecture, theme, migration and repository skill documentation without adding new documents or changing temporary investigation notes.

Validated 53 local links, 22 shell examples, generated configuration parity and TOML syntax, Compose configuration, skill metadata and whitespace checks.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 15:28:32 +08:00
ZyyandCodex a404c2e09e feat(codex): enable native image generation with managed auth
Generate server-managed authentication settings for native image generation while preserving API-key auth.json and CCSwitch compatibility. Remove obsolete client settings and retain provider-level WebSocket control.

Keep gateway bearer authentication mandatory, strip client actor authorization headers before forwarding upstream, and cover the boundary with backend regression tests. Document setup and simplify the configuration dialog guidance.

Validated with backend tests, live Codex CLI image generation, WebSocket and legacy-auth checks, frontend build, and strict CLI configuration loading.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 15:09:05 +08:00
ZyyandCodex a6cfd124b0 fix(theme): balance popover header surfaces
Derive a dedicated popover header component token so light themes keep clear separation from striped table rows while dark themes retain the softer tertiary fill.

Apply the token consistently to structured popover headers and the health timeline arrow, and expose it through the theme editor.

Validated across all preset light and dark palettes, token overrides, frontend lint, typecheck and production build.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 14:18:04 +08:00
ZyyandCodex 4441b577c3 fix(theme): strengthen popover header separation
Promote structured popover headers from the tertiary fill to the secondary fill so they remain visually distinct from striped and hovered table rows in both light and dark themes.

Keep the health timeline arrow matched to its header surface when the popover placement flips.

Validated across all preset light and dark palettes, frontend lint, typecheck and production build.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 14:14:53 +08:00
ZyyandCodex a704fbc3f3 fix(websocket): serialize queued response requests
Queue incoming response.create frames until the active response finishes instead of closing the connection, aligning execution with the Codex client serial request flow.

Separate pump exit notifications from the bounded business queue so disconnects and shutdown remain prompt. Preserve expiry handling with a full queue, retain protocol validation, and document the serial execution contract.

Validated with 253 API tests, 12 architecture tests, strict workspace Clippy and Rust formatting.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 13:55:14 +08:00
ZyyandCodex dab221cdb9 refactor(theme): standardize container color roles
Rename primary, semantic and preset background roles to container and their paired foreground roles to on-container across derivation, CSS variables, utilities, consumers and documentation.

Preserve existing palette values and contrast relationships while making text on neutral surfaces distinct from content inside semantic containers.

Validated with token equivalence across presets and custom seeds, frontend typecheck, lint and production build.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 12:48:19 +08:00
ZyyandCodex 4f434ec525 fix(theme): restore semantic color roles
Separate semantic text on neutral surfaces from high-contrast text on tinted backgrounds, derive each role against its actual surfaces, and migrate badges and alerts to explicit on-background tokens.

Restore vivid usage-table accents and align input hover, active, and error outlines across light and dark themes.

Validated with semantic contrast checks across presets and custom seeds, frontend typecheck, lint and production build.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 12:08:34 +08:00
ZyyandCodex 2585d4cd7d refactor(architecture): consolidate usage and provider contracts
Move Dashboard quota projection to the backend, share pagination request state, and fix independent usage loads while keeping trend switches free of loading overlays.

Stream billing facts within the query budget, share pending authorization and usage validation, and separate Core value ownership and response observations. Remove the unused theme helper and strengthen dependency checks.

Validated with workspace integration tests against isolated PostgreSQL and Redis, strict Clippy, Rust formatting, and frontend typecheck, lint and build.

Co-authored-by: Codex <noreply@openai.com>
2026-09-05 05:29:12 +08:00
ZyyandCodex 93a34b0f56 fix(openai): add gpt-6-astra billing
Add standard, Flex, Fast, long-context and cache-write rates, and classify Astra as a reasoning model for preview web-search pricing. Cover tier aliases, the context threshold and cost components with backend regression tests.

Co-authored-by: Codex <noreply@openai.com>
2026-09-05 05:27:32 +08:00
ZyyandCodex aa3a07209a fix(theme): improve token contrast and color derivation
Guard text and primary button contrast across surfaces and interaction states, derive dark semantic text from its seed, and reuse focus tokens. Document the palette and component rules.

Co-authored-by: Codex <noreply@openai.com>
2026-09-05 05:11:17 +08:00
ZyyandCodex c2332bc8d0 chore: bump version to 3.3.23
Co-authored-by: Codex <noreply@openai.com>
2026-09-04 19:16:46 +08:00
ZyyandCodex 8f242e2875 feat(logging): add opt-in request dumps
Capture complete OpenAI Responses headers and payloads in a dedicated rotated log, keep the target out of normal sinks, and default the sensitive switch to off.

Co-authored-by: Codex <noreply@openai.com>
2026-09-04 19:15:28 +08:00
ZyyandCodex 51e9981bee fix(openai): resume metadata-close websocket requests
Treat metadata turn state as a same-request checkpoint, retry on the original account over WebSocket, and keep ambiguous requests without a checkpoint fail-closed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-04 18:48:36 +08:00
ZyyandCodex 65fbbcb3ff chore: bump version to 3.3.22
Co-authored-by: Codex <noreply@openai.com>
2026-09-04 16:52:27 +08:00
ZyyandCodex bd086a7e3c fix(observability): show reasoning token usage
Expose aggregate reasoning tokens on account responses and use them in the token structure panel while retaining cache-write fields for API compatibility.

Co-authored-by: Codex <noreply@openai.com>
2026-09-04 16:51:18 +08:00
ZyyandCodex 3554ba13db fix(openai): preserve websocket turn state on failure
Bridge both metadata event names, project sticky state per response.create, and return captured state on pre-terminal failures without replaying ambiguous requests.

Co-authored-by: Codex <noreply@openai.com>
2026-09-04 16:51:01 +08:00
ZyyandCodex bcccebc9fa chore: bump version to 3.3.21
Co-authored-by: Codex <noreply@openai.com>
2026-09-04 11:52:25 +08:00
ZyyandCodex d67b46a9e2 fix(frontend): default Codex websocket config off
Keep inline switch text legible in the off state and omit WebSocket settings from generated Codex and CCSwitch configuration unless explicitly enabled.

Co-authored-by: Codex <noreply@openai.com>
2026-09-04 11:50:47 +08:00
ZyyandCodex 20c6d33cf4 fix(observability): retain recovered transport failures
Link successful same-session HTTP fallbacks to pending WebSocket failures, keep recovered records visible in Ops diagnostics, and label their recovery state.

Co-authored-by: Codex <noreply@openai.com>
2026-09-04 11:50:41 +08:00
ZyyandCodex cf0e84c548 fix(openai): isolate websocket continuation lanes
Include the downstream connection identity in pooled upstream WebSocket keys so concurrent clients cannot contend for connection-local response state.

Co-authored-by: Codex <noreply@openai.com>
2026-09-04 11:50:34 +08:00
ZyyandCodex 2332af9a08 chore: bump version to 3.3.20
Co-authored-by: Codex <noreply@openai.com>
2026-09-04 00:47:35 +08:00
ZyyandCodex 024fc9fcf0 fix(openai): fall back after websocket fast-path misses
Treat the 800 ms fast-path budget as a routing decision instead of a provider error, while preserving background preconnect for the next request.

Co-authored-by: Codex <noreply@openai.com>
2026-09-04 00:36:50 +08:00
ZyyandCodex 4220f4e095 fix(openai): recover websocket sessions after ambiguous failures
Co-authored-by: Codex <noreply@openai.com>
2026-09-03 23:33:26 +08:00
ZyyandCodex a709acb149 chore: bump version to 3.3.19
Co-authored-by: Codex <noreply@openai.com>
2026-09-03 17:56:10 +08:00
ZyyandCodex 466842acd8 fix(observability): search usage by API key
Resolve sk_ searches through client key references while only sending the visible key prefix from the frontend, and clarify the request detail search hint.

Co-authored-by: Codex <noreply@openai.com>
2026-09-03 17:38:29 +08:00
ZyyandCodex b91f28f95b fix(frontend): align configuration and usage copy
Generate the displayed Codex files and CCSwitch import payload from one source, carry the full settings through deep links, and clarify the quota percentage label.

Co-authored-by: Codex <noreply@openai.com>
2026-09-03 16:37:53 +08:00
ZyyandCodex 87164db3da chore: bump version to 3.3.18
Co-authored-by: Codex <noreply@openai.com>
2026-09-03 15:41:50 +08:00
ZyyandCodex ad16dec3db fix(openai): align official client routes
Remove unsupported review and model extension routes, preserve review semantics on standard Responses requests, and refine usage observability for review and empty performance data.

Co-authored-by: Codex <noreply@openai.com>
2026-09-03 15:37:14 +08:00
ZyyandCodex a0da0f4713 feat(openai): proxy standalone search
Add /v1/alpha/search as a transparent raw JSON endpoint, route requests through the OpenAI credential pipeline, and preserve account-scoped turn metadata and upstream response headers.

Co-authored-by: Codex <noreply@openai.com>
2026-09-03 14:27:15 +08:00
ZyyandCodex 65e5cde60d chore: bump version to 3.3.17
Co-authored-by: Codex <noreply@openai.com>
2026-09-03 12:44:42 +08:00
ZyyandCodex 0f0b464f4e fix(frontend): refine account usage presentation
Select quota summaries by the most recently used model, streamline dashboard account identities, and align diagnostic client metadata with successful records.

Co-authored-by: Codex <noreply@openai.com>
2026-09-03 12:21:15 +08:00
ZyyandCodex bd223f6540 chore: bump version to 3.3.16
Co-authored-by: Codex <noreply@openai.com>
2026-09-02 22:45:09 +08:00
ZyyandCodex 168be0ca68 fix(openai): add continuation recovery observability
Persist stable continuation and WebSocket lifecycle facts, correlate successful client replays, and hide recovered failures from default business metrics.

Co-authored-by: Codex <noreply@openai.com>
2026-09-02 21:38:48 +08:00
ZyyandCodex f2e91bbdaa fix(openai): preserve websocket continuation routing
Limit session-sticky HTTP fallback to new chains, keep continuation requests on their safe transport, and classify rebuild-required failures separately from account health.

Co-authored-by: Codex <noreply@openai.com>
2026-09-02 18:21:30 +08:00
ZyyandCodex d0c9248478 fix(openai): refine smart scheduler failure scoring
Limit account feedback to an explicit upstream reason allowlist, aggregate same-account retries, and decay failure rates over time.

Co-authored-by: Codex <noreply@openai.com>
2026-09-02 15:08:31 +08:00
ZyyandCodex 22c697bbff fix(usage): refine record details and error filters
Co-authored-by: Codex <noreply@openai.com>
2026-09-02 14:43:44 +08:00
ZyyandCodex 08d518324a fix(frontend): refine account quota layout
Co-authored-by: Codex <noreply@openai.com>
2026-09-02 12:14:03 +08:00
ZyyandCodex dfacb20e43 fix(frontend): tighten usage attempt table layout
Co-authored-by: Codex <noreply@openai.com>
2026-09-01 19:46:49 +08:00
ZyyandCodex 55c9291c35 chore: bump version to 3.3.15
Co-authored-by: Codex <noreply@openai.com>
2026-09-01 18:14:49 +08:00
ZyyandCodex 6e8e787b82 fix(openai): harden websocket failure recovery
Prevent hidden retries after ambiguous WebSocket sends, enforce connection-local continuation ownership, and persist raw upstream failure details for operations diagnostics.

Co-authored-by: Codex <noreply@openai.com>
2026-09-01 18:12:48 +08:00
ZyyandCodex 2a0a50adc5 refactor(backend): complete module boundary cleanup
Extract runtime snapshot lifecycle into its semantic owner, split oversized provider and admin modules, and enforce mirrored src/tests module layouts.

Co-authored-by: Codex <noreply@openai.com>
2026-09-01 17:17:57 +08:00
ZyyandCodex 5e13b6af6a refactor(backend): align module ownership boundaries
Normalize Rust module layout, assign shared core contracts to semantic owners, rename metering concepts, and add workspace architecture guards and documentation.

Co-authored-by: Codex <noreply@openai.com>
2026-09-01 16:25:59 +08:00
ZyyandCodex bd2454a5b6 feat(settings): enforce minimum client versions
Add configurable minimum versions for Codex Desktop and CLI, return 426 for unsupported recognized clients, and provide install and Windows offline download guidance.

Co-authored-by: Codex <noreply@openai.com>
2026-09-01 13:37:04 +08:00
ZyyandCodex 2640a03219 chore: bump version to 3.3.14
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 19:23:58 +08:00
ZyyandCodex 7d0e9a0a20 fix(frontend): refine dashboard account metadata
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 19:23:10 +08:00
ZyyandCodex 09982d7221 fix(openai): align websocket retry fallback
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 19:14:18 +08:00
ZyyandCodex c4db1bb244 fix(observability): prefer per-frame Codex metadata
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 19:08:16 +08:00
ZyyandCodex e7c2f025cc chore: bump version to 3.3.13
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 13:29:18 +08:00
ZyyandCodex 4ee294b208 fix(frontend): improve mobile layout sizing
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 13:28:50 +08:00
Zyy 93f1a82d3f fix(frontend): improve observability chart continuity 2026-08-31 13:09:24 +08:00
ZyyandCodex c46f98cd2b fix: align wire profiles and diagnostics
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 12:29:29 +08:00
ZyyandCodex 5c69b1b082 chore: bump version to 3.3.12
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 11:56:13 +08:00
dependabot[bot]andZyy 6a78fb9232 chore(deps): bump argon2 from 0.5.3 to 0.6.0 in /backend (#49)
* chore(deps): bump argon2 from 0.5.3 to 0.6.0 in /backend

Bumps [argon2](https://github.com/RustCrypto/password-hashes) from 0.5.3 to 0.6.0.
- [Commits](https://github.com/RustCrypto/password-hashes/compare/argon2-v0.5.3...argon2-v0.6.0)

---
updated-dependencies:
- dependency-name: argon2
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(admin): update argon2 password hashing API

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-08-31 11:47:53 +08:00
ZyyandCodex 02a03df79a fix(observability): preserve upstream failure evidence
Log original OpenAI error payloads and WebSocket close reasons with request, account, attempt, transport, and connection correlation. Project PostgreSQL inet values as bare client addresses in observability queries.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 11:36:28 +08:00
dependabot[bot] 2aca8fb79b chore(deps): bump the minor-and-patch group across 1 directory with 3 updates (#48)
Bumps the minor-and-patch group with 3 updates in the /backend directory: [syn](https://github.com/dtolnay/syn), [uuid](https://github.com/uuid-rs/uuid) and [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust).


Updates `syn` from 3.0.3 to 3.0.4
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/3.0.3...3.0.4)

Updates `uuid` from 1.24.1 to 1.26.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.24.1...v1.26.0)

Updates `aws-sdk-s3` from 1.142.0 to 1.144.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-version: 1.144.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: syn
  dependency-version: 3.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: uuid
  dependency-version: 1.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 11:13:35 +08:00
dependabot[bot] 891b227b69 chore(deps): bump debian from abd67ff to 8820086 in /deploy (#52)
Bumps debian from `abd67ff` to `8820086`.

---
updated-dependencies:
- dependency-name: debian
  dependency-version: bookworm-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 11:13:27 +08:00
dependabot[bot] 04096dddb7 chore(deps): bump rust from e70e2ee to 82150a5 in /deploy (#51)
Bumps rust from `e70e2ee` to `82150a5`.

---
updated-dependencies:
- dependency-name: rust
  dependency-version: 1.98-bookworm
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 11:13:20 +08:00
dependabot[bot] 3a9b1c479f chore(deps): bump the minor-and-patch group in /frontend with 8 updates (#50)
Bumps the minor-and-patch group in /frontend with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.33.0` | `1.37.0` |
| [axios](https://github.com/axios/axios) | `1.19.0` | `1.20.0` |
| [es-toolkit](https://github.com/toss/es-toolkit) | `1.51.0` | `1.52.0` |
| [marked](https://github.com/markedjs/marked) | `18.0.10` | `18.0.11` |
| [vue](https://github.com/vuejs/core) | `3.5.41` | `3.5.42` |
| [vue-router](https://github.com/vuejs/router) | `5.2.0` | `5.3.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.2.0` | `26.4.0` |
| [eslint](https://github.com/eslint/eslint) | `10.9.0` | `10.9.1` |


Updates `@lucide/vue` from 1.33.0 to 1.37.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.37.0/packages/vue)

Updates `axios` from 1.19.0 to 1.20.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.19.0...v1.20.0)

Updates `es-toolkit` from 1.51.0 to 1.52.0
- [Release notes](https://github.com/toss/es-toolkit/releases)
- [Changelog](https://github.com/toss/es-toolkit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/toss/es-toolkit/compare/v1.51.0...v1.52.0)

Updates `marked` from 18.0.10 to 18.0.11
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v18.0.10...v18.0.11)

Updates `vue` from 3.5.41 to 3.5.42
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.41...v3.5.42)

Updates `vue-router` from 5.2.0 to 5.3.0
- [Release notes](https://github.com/vuejs/router/releases)
- [Commits](https://github.com/vuejs/router/compare/v5.2.0...v5.3.0)

Updates `@types/node` from 26.2.0 to 26.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.9.0 to 10.9.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.9.0...v10.9.1)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: axios
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: es-toolkit
  dependency-version: 1.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: marked
  dependency-version: 18.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: vue
  dependency-version: 3.5.42
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: vue-router
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@types/node"
  dependency-version: 26.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: eslint
  dependency-version: 10.9.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 11:13:12 +08:00
dependabot[bot] 944d3f299e chore(deps): bump github/codeql-action/upload-sarif (#47)
Bumps the minor-and-patch group with 1 update: [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/upload-sarif` from 4.37.8 to 4.37.9
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28...cdf488f595d80d6e07e03d4674febd5ab45fa938)

---
updated-dependencies:
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 11:12:30 +08:00
ZyyandCodex 56451601fb feat(observability): enrich error request traces
Expose request, client, model, transport, and account facts in operations errors and surface them in the error table and detail view.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 10:57:53 +08:00
ZyyandCodex 4d53ef6068 fix(openai): recover premature websocket closes
Pin the selected account and retry over HTTP/SSE when a new-chain WebSocket fails before downstream delivery.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 10:57:35 +08:00
ZyyandCodex a62bb196f7 fix(accounts): proxy profile avatars through backend
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 09:56:23 +08:00
ZyyandCodex 7ad0ca48c5 fix(frontend): improve radio contrast in light mode
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 09:56:01 +08:00
ZyyandCodex e25639c11b fix(release): gate publishing on container validation
Skip standalone CI for version-only release commits so they do not cancel the preceding source run. Reuse the container workflow in Release and require it before publishing.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 04:40:24 +08:00
ZyyandCodex 8abac080a2 chore: bump version to 3.3.11
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 04:22:47 +08:00
ZyyandCodex 37cd059531 fix(openai): normalize HTTP previous response errors
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 04:22:11 +08:00
ZyyandCodex c46910df22 fix(observability): restore direct page navigation
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 04:09:31 +08:00
ZyyandCodex 8cfd7f53be chore: bump version to 3.3.10
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 03:30:13 +08:00
ZyyandCodex 75e22d7a5b fix(openai): harden downstream websocket delivery
Move downstream socket ownership into a bounded pump, preserve official Codex control-frame behavior, and bound downstream writes.

Enforce the official connection lifetime after active terminal delivery, add structured transport diagnostics, and mirror connection/protocol coverage under the test tree without adding stream_id.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 03:27:11 +08:00
ZyyandCodex 6b2d335bc5 fix(observability): align completed usage projections
Co-authored-by: Codex <noreply@openai.com>
2026-08-31 02:58:16 +08:00
ZyyandCodex 270b1540cb perf(observability): bound retention cleanup cycles
Rotate bounded deletion batches across retained tables with row, batch, duration, and pacing budgets. Report per-cycle progress, honor worker cancellation, and leave rollups or parallel writer lanes behind measured production thresholds.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 02:40:40 +08:00
ZyyandCodex 660214d9a5 refactor(store): simplify PostgreSQL lifecycle reads
Reuse the direct runtime-settings read for the single-row control-plane path, collapse repeated provider snapshot lookups in request lifecycle writes, and remove the unused credential revision vector surface.

No schema migration is added because no candidate crossed the EXPLAIN and write-amplification gate.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 02:40:40 +08:00
ZyyandCodex 2231fee768 perf(observability): narrow usage and ops read models
Separate Usage list and detail reads, use keyset paging with first-page totals, push Ops predicates into source branches, and merge repeated account and diagnostics aggregates. Apply the shared per-query PostgreSQL budget, coalesce identical Dashboard windows, and preserve the existing IP, User-Agent, and latency detail surface.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 02:40:40 +08:00
ZyyandCodex a2e60c1c6e refactor(admin): page account read models in PostgreSQL
Move account, account-group, and client-key filtering, stable ordering, counts, and current-page membership reads into PostgreSQL. Keep Redis cooldown and lease enrichment in the Admin application boundary, share stable page state in the UI, and supervise client-key last-used flushing through the host worker lifecycle.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 02:40:40 +08:00
ZyyandCodex fc388bb799 refactor(providers): bound credential refresh queries
Move refresh candidate filtering, ordering, exclusions, and limits into one PostgreSQL query for OpenAI and xAI. Keep refresh policy in each provider and preserve the final credential revision fence without adding a scheduling cache or catalog table.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 02:34:49 +08:00
ZyyandCodex 62a35c3960 feat(store): add PostgreSQL connection guardrails
Raise the recommended pool default, reserve data-plane capacity dynamically, configure PostgreSQL session timeouts, isolate migrations, and add a fair per-query budget primitive with regression coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 02:34:32 +08:00
ZyyandCodex 82653544ac perf(observability): eliminate attempt metrics temp spill
Materialize only the narrow request facts shared by the request and ops aggregates, then compute each aggregate once. This removes repeated scans of the wide model_requests CTE while preserving usage-fact cost coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 02:34:21 +08:00
ZyyandCodex f10f825404 fix(observability): correct provider failure semantics
Count provider request, attempt, and failure facts across the full execution audit while restricting token totals to completed usage facts. Add a PostgreSQL regression assertion for the mixed success/failure boundary.

Co-authored-by: Codex <noreply@openai.com>
2026-08-31 02:34:21 +08:00
ZyyandCodex a268f5ca66 fix(observability): restore WebSocket usage records
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 22:40:16 +08:00
ZyyandCodex 832e2e8bdd chore: bump version to 3.3.9
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 09:49:48 +08:00
ZyyandCodex be703a0c5f fix(frontend): refine chart clarity and mobile controls
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 09:33:31 +08:00
ZyyandCodex c2c86928c5 fix(frontend): refine responsive interface states
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 05:40:31 +08:00
ZyyandCodex bda21ccc61 chore: bump version to 3.3.8
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 03:38:03 +08:00
ZyyandCodex 72c56680b9 fix(frontend): polish interface presentation
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 03:37:35 +08:00
ZyyandCodex a6808499f0 chore: bump version to 3.3.7
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 02:24:41 +08:00
ZyyandCodex fdb954b3d1 fix(frontend): refine semantic theme states
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 02:23:29 +08:00
ZyyandCodex a069001f01 chore: bump version to 3.3.6
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 01:55:01 +08:00
ZyyandCodex d3a87d11ad fix(frontend): restore theme visual consistency
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 01:53:00 +08:00
ZyyandCodex d7275dfd08 chore: bump version to 3.3.5
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 00:53:46 +08:00
ZyyandCodex 3396f0880f fix(frontend): refine login controls and spacing
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 00:46:01 +08:00
ZyyandCodex d5fa6c4921 refactor(frontend): remove redundant aria hiding
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 00:46:01 +08:00
ZyyandCodex d4855cca9c feat(observability): expose runtime pressure insights
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 00:46:01 +08:00
ZyyandCodex 1d862922db refactor(theme): rebuild runtime token architecture
Derive semantic and component tokens from structured appearance seeds, align visual consumers and Tailwind v4 base utilities, and keep popover arrows matched to their adjacent surfaces across placement changes.

Co-authored-by: Codex <noreply@openai.com>
2026-08-29 22:14:57 +08:00
ZyyandCodex 4830ef093b refactor(architecture): remove obsolete compatibility scaffolding
Remove orphaned fixtures, unused capability and resource branches, and stale credential APIs.

Retire legacy Usage metadata fallbacks and record the completed cleanup.

Co-authored-by: Codex <noreply@openai.com>
2026-08-29 22:08:34 +08:00
ZyyandCodex ad20064d15 fix(admin): unify control-plane error responses
Co-authored-by: Codex <noreply@openai.com>
2026-08-29 20:22:14 +08:00
ZyyandCodex 515487baee docs(api): document official profile statistics
Co-authored-by: Codex <noreply@openai.com>
2026-08-29 04:07:54 +08:00
ZyyandCodex a38c328975 style(frontend): format account usage summary
Co-authored-by: Codex <noreply@openai.com>
2026-08-29 03:57:06 +08:00
ZyyandCodex 97d95d25f1 chore: bump version to 3.3.4
Co-authored-by: Codex <noreply@openai.com>
2026-08-29 03:51:49 +08:00
ZyyandCodex abfd24267b fix(frontend): refine usage visual hierarchy
Co-authored-by: Codex <noreply@openai.com>
2026-08-29 03:51:05 +08:00
ZyyandCodex a363682929 feat(frontend): refine surfaces and draggable modals
Co-authored-by: Codex <noreply@openai.com>
2026-08-29 03:25:40 +08:00
ZyyandCodex 57422275bb feat(accounts): add official profile statistics
Co-authored-by: Codex <noreply@openai.com>
2026-08-29 02:36:45 +08:00
Zyy cb7f69850b feat(openai): add session affinity observability 2026-08-28 02:20:55 +08:00
ZyyandCodex 5ea3740de3 refactor(openai): remove agent identity support
Co-authored-by: Codex <noreply@openai.com>
2026-08-27 13:27:10 +08:00
ZyyandCodex 3920624862 chore: bump version to 3.3.3
Co-authored-by: Codex <noreply@openai.com>
2026-08-27 01:53:05 +08:00
Zyy d804bca495 fix(openai): strip unsupported Codex request fields 2026-08-27 01:51:30 +08:00
ZyyandCodex ca527d5833 fix(accounts): use personal usage reports only
Remove the workspace token-breakdown probe and its dedicated statistics mode so usage aggregation always uses the personal model-credit and token-total reports.

Drop the invariant mode field from provider projections, the admin response, and the account usage UI.

Co-authored-by: Codex <noreply@openai.com>
2026-08-26 11:31:55 +08:00
ZyyandCodex ae4eb49afc chore: bump version to 3.3.2
Co-authored-by: Codex <noreply@openai.com>
2026-08-26 00:51:31 +08:00
ZyyandCodex 5d97086511 feat(accounts): add official usage statistics
Fetch Codex cycle usage in the provider, calculate model and daily Token costs with the shared OpenAI pricing rules, and expose the result through the admin API.

Add the account usage statistics modal, structured loading state, and feature-scoped quota components.

Co-authored-by: Codex <noreply@openai.com>
2026-08-26 00:46:53 +08:00
ZyyandCodex 0e6da1e731 refactor: consolidate migration baseline and remove design source
Fold the nullable requested-model change into the initial migration and keep the frozen manifest and project guidance aligned.

Remove the staged Pen design source.

Co-authored-by: Codex <noreply@openai.com>
2026-08-25 17:53:29 +08:00
ZyyandCodex 5d0922b9d1 fix(theme): align functional colors with Ant Design
Keep functional base tokens on Ant Design P6 so light themes preserve the configured seed while dark themes retain palette adaptation.

Co-authored-by: Codex <noreply@openai.com>
2026-08-25 17:06:05 +08:00
ZyyandCodex ed3d622554 chore: bump version to 3.3.1
Co-authored-by: Codex <noreply@openai.com>
2026-08-25 15:53:32 +08:00
dependabot[bot] 0f71cf6df2 chore(deps): bump rust in /deploy in the minor-and-patch group (#46)
Bumps the minor-and-patch group in /deploy with 1 update: rust.


Updates `rust` from 1.97-bookworm to 1.98-bookworm

---
updated-dependencies:
- dependency-name: rust
  dependency-version: 1.98-bookworm
  dependency-type: direct:production
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 15:37:44 +08:00
dependabot[bot] a1d3eba874 chore(deps): bump the minor-and-patch group in /frontend with 8 updates (#45)
Bumps the minor-and-patch group in /frontend with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.31.0` | `1.33.0` |
| [dayjs](https://github.com/iamkun/dayjs) | `1.11.21` | `1.11.23` |
| [dompurify](https://github.com/cure53/DOMPurify) | `3.4.13` | `3.4.14` |
| [es-toolkit](https://github.com/toss/es-toolkit) | `1.50.0` | `1.51.0` |
| [marked](https://github.com/markedjs/marked) | `18.0.9` | `18.0.10` |
| [eslint](https://github.com/eslint/eslint) | `10.8.1` | `10.9.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.2.1` | `8.2.2` |
| [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) | `3.3.10` | `3.3.11` |


Updates `@lucide/vue` from 1.31.0 to 1.33.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.33.0/packages/vue)

Updates `dayjs` from 1.11.21 to 1.11.23
- [Release notes](https://github.com/iamkun/dayjs/releases)
- [Changelog](https://github.com/iamkun/dayjs/blob/v1.11.23/CHANGELOG.md)
- [Commits](https://github.com/iamkun/dayjs/compare/v1.11.21...v1.11.23)

Updates `dompurify` from 3.4.13 to 3.4.14
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.13...3.4.14)

Updates `es-toolkit` from 1.50.0 to 1.51.0
- [Release notes](https://github.com/toss/es-toolkit/releases)
- [Changelog](https://github.com/toss/es-toolkit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/toss/es-toolkit/compare/v1.50.0...v1.51.0)

Updates `marked` from 18.0.9 to 18.0.10
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v18.0.9...v18.0.10)

Updates `eslint` from 10.8.1 to 10.9.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.8.1...v10.9.0)

Updates `vite` from 8.2.1 to 8.2.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.2.2/packages/vite)

Updates `vue-tsc` from 3.3.10 to 3.3.11
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.3.11/packages/tsc)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: dayjs
  dependency-version: 1.11.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: dompurify
  dependency-version: 3.4.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: es-toolkit
  dependency-version: 1.51.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: marked
  dependency-version: 18.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: eslint
  dependency-version: 10.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: vite
  dependency-version: 8.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: vue-tsc
  dependency-version: 3.3.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 15:37:10 +08:00
dependabot[bot] d22c1d4ab9 chore(deps): bump the minor-and-patch group in /backend with 4 updates (#44)
Bumps the minor-and-patch group in /backend with 4 updates: [uuid](https://github.com/uuid-rs/uuid), [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust), [aws-smithy-runtime-api](https://github.com/smithy-lang/smithy-rs) and [redis](https://github.com/redis-rs/redis-rs).


Updates `uuid` from 1.24.0 to 1.24.1
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.24.0...v1.24.1)

Updates `aws-sdk-s3` from 1.141.0 to 1.142.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `aws-smithy-runtime-api` from 1.14.0 to 1.15.0
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `redis` from 1.5.0 to 1.6.0
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-1.5.0...redis-1.6.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.24.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-sdk-s3
  dependency-version: 1.142.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-runtime-api
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: redis
  dependency-version: 1.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 15:36:39 +08:00
dependabot[bot] cc8dfe3dd8 chore(deps): bump the minor-and-patch group with 2 updates (#43)
Bumps the minor-and-patch group with 2 updates: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `docker/setup-buildx-action` from 4.2.0 to 4.3.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e)

Updates `github/codeql-action/upload-sarif` from 4.37.7 to 4.37.8
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd...db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 15:36:01 +08:00
ZyyandCodex f25edadd5f fix(ci): keep OpenAI regression tests out of sources
Move the thread-spawn transport identity regression behind the public initialized-provider boundary so production source files remain free of embedded test modules.

Co-authored-by: Codex <noreply@openai.com>
2026-08-25 15:27:28 +08:00
ZyyandCodex 4f50c67332 fix(openai): preserve root session account affinity
Share the preferred account across derived child executions while keeping their continuation and WebSocket transport identities isolated.

Claim and migrate affinity atomically, clear account-bound state on fallback, reject cross-account previous-response continuations, and preserve complete replay transcripts.

Co-authored-by: Codex <noreply@openai.com>
2026-08-25 14:22:36 +08:00
ZyyandCodex 75f9d2a290 fix(openai): remove websocket pool slot caps
Co-authored-by: Codex <noreply@openai.com>
2026-08-25 11:22:07 +08:00
ZyyandCodex b93b8859f9 fix(frontend): polish theme controls and page headers
Remove the redundant preview mode switch, round the color picker input, and restore inline right-aligned page header actions.

Co-authored-by: Codex <noreply@openai.com>
2026-08-24 20:21:16 +08:00
ZyyandCodex 3510a45742 chore: bump version to 3.3.0
Co-authored-by: Codex <noreply@openai.com>
2026-08-24 04:07:37 +08:00
ZyyandCodex 85eb283b40 docs(readme): streamline project introduction
Co-authored-by: Codex <noreply@openai.com>
2026-08-24 04:06:49 +08:00
ZyyandCodex b8e1c5077d docs(theme): document runtime theme architecture
Describe the token model, preset themes, runtime stylesheet, isolated editor preview, naming conventions, and extension workflow.

Co-authored-by: Codex <noreply@openai.com>
2026-08-24 04:04:59 +08:00
ZyyandCodex 8422240967 feat(theme): add runtime token theme editor
Introduce Seed-to-Component token derivation, isolated live previews, semantic base component theming, and responsive theme controls.

Co-authored-by: Codex <noreply@openai.com>
2026-08-24 04:04:22 +08:00
ZyyandCodex fbd922117e fix(accounts): return persisted OAuth account ID
Co-authored-by: Codex <noreply@openai.com>
2026-08-22 19:37:03 +08:00
ZyyandCodex 941c4d57fa chore: bump version to 3.2.16
Co-authored-by: Codex <noreply@openai.com>
2026-08-22 09:28:34 +08:00
ZyyandCodex cf046ad9a4 fix(admin): close quota reset modal on success
Co-authored-by: Codex <noreply@openai.com>
2026-08-22 09:25:05 +08:00
ZyyandCodex 7699a954ac fix(admin): format account counts with separators
Co-authored-by: Codex <noreply@openai.com>
2026-08-22 09:21:11 +08:00
ZyyandCodex 2c31708d1b feat(admin): allow selecting quota reset credits
Co-authored-by: Codex <noreply@openai.com>
2026-08-22 09:19:20 +08:00
ZyyandCodex 80f338ce01 fix(admin): show raw account counts
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 23:12:54 +08:00
ZyyandCodex 299088a6e7 chore: bump version to 3.2.15
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 20:00:59 +08:00
ZyyandCodex a23bfca848 fix(admin): normalize OAuth credential formats
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 14:57:16 +08:00
ZyyandCodex 8951a64a05 fix(admin): raise billing abbreviation threshold
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 14:15:22 +08:00
ZyyandCodex eb20561893 fix(admin): align xAI quota usage displays
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 13:58:02 +08:00
ZyyandCodex a33637a99e fix(openai): use Codex image endpoint paths
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 13:02:40 +08:00
ZyyandCodex 97cd7a7097 chore: bump version to 3.2.14
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 01:11:57 +08:00
ZyyandCodex b0fe132736 docs: refresh architecture and project guides
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 01:01:07 +08:00
ZyyandCodex e2c75d1054 feat(admin): add direct token import and polish displays
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 00:33:26 +08:00
ZyyandCodex 94f298927d feat(openai): add active quota reset credits
Co-authored-by: Codex <noreply@openai.com>
2026-08-21 00:33:21 +08:00
ZyyandCodex 3be827204f fix(openai): keep image routes model-agnostic
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 19:05:14 +08:00
ZyyandCodex 227a1174b9 feat(openai): proxy image generation and edits
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 17:41:15 +08:00
ZyyandCodex c0defcb6df fix(admin): prefer account-wide dashboard quota
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 16:06:05 +08:00
ZyyandCodex 53739ebe5c chore: bump version to 3.2.13
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 15:20:29 +08:00
ZyyandCodex 7f420257a6 fix(api): keep provider degradation out of healthz
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 15:14:28 +08:00
ZyyandCodex 4f85209dc9 fix(config): centralize runtime data paths
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 15:14:28 +08:00
ZyyandCodex cd89e2e8fc fix(admin): align quota metadata and usage layout
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 15:14:28 +08:00
ZyyandCodex 0cb5a29699 chore: bump version to 3.2.12
Co-authored-by: Codex <noreply@openai.com>
2026-08-19 23:52:11 +08:00
ZyyandCodex 61788804b4 fix(openai): recover cross-account websocket continuations
Co-authored-by: Codex <noreply@openai.com>
2026-08-19 23:40:40 +08:00
ZyyandCodex 2751ea6aa4 fix(openai): preserve cross-account session recovery
Co-authored-by: Codex <noreply@openai.com>
2026-08-19 23:05:48 +08:00
ZyyandCodex 58fe66789c chore(deps): bump h2 to 0.4.16
Resolve RUSTSEC-2026-0258 reported by cargo audit.

Co-authored-by: Codex <noreply@openai.com>
2026-08-19 11:34:07 +08:00
ZyyandCodex 573b6b26db fix(openai): isolate quota buckets by limit id
Co-authored-by: Codex <noreply@openai.com>
2026-08-18 16:48:37 +08:00
ZyyandCodex d425428ebe chore: bump version to 3.2.11
Co-authored-by: Codex <noreply@openai.com>
2026-08-17 20:02:02 +08:00
ZyyandCodex b26d3b386a fix(admin): align quota diagnostics and usage layout
Co-authored-by: Codex <noreply@openai.com>
2026-08-17 20:00:29 +08:00
ZyyandCodex cf0b14c5fd chore: bump version to 3.2.10
Co-authored-by: Codex <noreply@openai.com>
2026-08-17 15:40:35 +08:00
ZyyandCodex 056561de41 refactor(frontend): clarify component boundaries
Co-authored-by: Codex <noreply@openai.com>
2026-08-17 15:33:36 +08:00
dependabot[bot] f2215df72e chore(deps): bump the minor-and-patch group in /frontend with 4 updates
Bumps the minor-and-patch group in /frontend with 4 updates: [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue), [pinia](https://github.com/vuejs/pinia), [eslint-plugin-vuejs-accessibility](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility) and [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc).


Updates `@lucide/vue` from 1.30.0 to 1.31.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.31.0/packages/vue)

Updates `pinia` from 4.0.2 to 4.0.3
- [Release notes](https://github.com/vuejs/pinia/releases)
- [Commits](https://github.com/vuejs/pinia/compare/v4.0.2...v4.0.3)

Updates `eslint-plugin-vuejs-accessibility` from 2.5.0 to 2.6.0
- [Release notes](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/releases)
- [Changelog](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/compare/v2.5.0...v2.6.0)

Updates `vue-tsc` from 3.3.9 to 3.3.10
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.3.10/packages/tsc)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: pinia
  dependency-version: 4.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: eslint-plugin-vuejs-accessibility
  dependency-version: 2.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: vue-tsc
  dependency-version: 3.3.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:05:19 +08:00
dependabot[bot] 72f8957163 chore(deps): bump the minor-and-patch group in /backend with 5 updates
Bumps the minor-and-patch group in /backend with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [thiserror](https://github.com/dtolnay/thiserror) | `2.0.19` | `2.0.20` |
| [async-trait](https://github.com/dtolnay/async-trait) | `0.1.91` | `0.1.92` |
| [futures](https://github.com/rust-lang/futures-rs) | `0.3.33` | `0.3.34` |
| [aws-smithy-types](https://github.com/smithy-lang/smithy-rs) | `1.6.1` | `1.6.2` |
| [cookie](https://github.com/SergioBenitez/cookie-rs) | `0.18.1` | `0.18.2` |


Updates `thiserror` from 2.0.19 to 2.0.20
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.19...2.0.20)

Updates `async-trait` from 0.1.91 to 0.1.92
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.91...0.1.92)

Updates `futures` from 0.3.33 to 0.3.34
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.33...0.3.34)

Updates `aws-smithy-types` from 1.6.1 to 1.6.2
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `cookie` from 0.18.1 to 0.18.2
- [Changelog](https://github.com/rwf2/cookie-rs/blob/0.18.2/CHANGELOG.md)
- [Commits](https://github.com/SergioBenitez/cookie-rs/compare/0.18.1...0.18.2)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-version: 2.0.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: async-trait
  dependency-version: 0.1.92
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: futures
  dependency-version: 0.3.34
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-smithy-types
  dependency-version: 1.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: cookie
  dependency-version: 0.18.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:05:19 +08:00
dependabot[bot] 73f0c15790 chore(deps): bump github/codeql-action/upload-sarif
Bumps the minor-and-patch group with 1 update: [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/upload-sarif` from 4.37.6 to 4.37.7
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd)

---
updated-dependencies:
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 14:05:19 +08:00
ZyyandCodex 0558693949 fix(openai): harden refresh recovery handling
Co-authored-by: Codex <noreply@openai.com>
2026-08-17 13:37:54 +08:00
ZyyandCodex 2a9cb02cc9 fix(openai): align OAuth refresh error handling
Co-authored-by: Codex <noreply@openai.com>
2026-08-17 12:30:05 +08:00
ZyyandCodex 5ce36931a8 chore: bump version to 3.2.9
Co-authored-by: Codex <noreply@openai.com>
2026-08-17 11:38:49 +08:00
ZyyandCodex 0d826d9eba fix(openai): align OAuth token refresh
Co-authored-by: Codex <noreply@openai.com>
2026-08-17 11:14:03 +08:00
ZyyandCodex 304242957a fix(accounts): preserve upstream error messages
Co-authored-by: Codex <noreply@openai.com>
2026-08-16 13:28:28 +08:00
ZyyandCodex ef37ae8d9e chore: bump version to 3.2.8
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 23:31:04 +08:00
ZyyandCodex fc13d4f2b6 fix(frontend): restore responsive component behavior
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 20:38:48 +08:00
ZyyandCodex 78f9c0c800 chore: bump version to 3.2.7
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 18:54:52 +08:00
ZyyandCodex a3bc7bc445 fix(frontend): increase account table row height
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 18:54:02 +08:00
ZyyandCodex 0132114285 fix(openai): let upstream validate catalog models
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 18:45:06 +08:00
ZyyandCodex 5970efbb7c fix(container): refresh runtime packages before scans
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 18:34:41 +08:00
ZyyandCodex 7bf28b571d chore: bump version to 3.2.6
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 15:47:32 +08:00
ZyyandCodex bc3bd6bc43 refactor(frontend): unify component design system
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 15:39:31 +08:00
ZyyandCodex 9acfab0a86 chore(frontend): enforce template interpolation layout
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 12:31:10 +08:00
ZyyandCodex 3e5a84d2a0 fix(frontend): refine dashboard capacity display
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 12:31:06 +08:00
ZyyandCodex 92948f51a9 fix(frontend): remove table bottom gap
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 11:59:58 +08:00
ZyyandCodex 776637fccb chore: bump version to 3.2.5
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 01:52:54 +08:00
ZyyandCodex 82acb08d26 fix(frontend): restore diagnostic table scrolling
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 01:51:56 +08:00
ZyyandCodex 7744e1b0b4 fix(accounts): preserve rolling window usage
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 01:28:44 +08:00
ZyyandCodex 0b8a1add18 feat(frontend): refresh account setup defaults and copy
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 01:09:31 +08:00
ZyyandCodex 2dd60c08b6 chore: bump version to 3.2.4
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 00:29:44 +08:00
ZyyandCodex cce9880937 fix(xai): align Grok behavior with sub2api
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 23:36:12 +08:00
Zyy b8ea1cd076 fix(core): normalize account scheduling load 2026-08-14 23:27:06 +08:00
ZyyandCodex 4117840f75 fix(xai): align OAuth and Codex compatibility
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 22:24:08 +08:00
ZyyandCodex f642764f34 fix(openai): align billing with official pricing
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 21:14:36 +08:00
ZyyandCodex 482e192e2d fix(openai): align desktop oauth identity
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 17:43:07 +08:00
ZyyandCodex ee51a07de4 fix(openai): align desktop wire profile
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 16:13:11 +08:00
ZyyandCodex bf23658aaf chore: bump version to 3.2.3
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 01:25:44 +08:00
ZyyandCodex bc4728332e fix(frontend): polish dashboard and table interactions
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 01:24:49 +08:00
ZyyandCodex 2cb5cda077 fix(quota): report exhausted usage as full
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 01:07:20 +08:00
ZyyandCodex 9d90dedb81 fix(frontend): refresh account data without table loading
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 01:07:12 +08:00
ZyyandCodex 6074cff68c chore: bump version to 3.2.2
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 00:43:13 +08:00
ZyyandCodex 6ab8e7d0a6 fix(frontend): grade model success rate colors
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 00:42:20 +08:00
ZyyandCodex 17f9ef8915 fix(observability): omit inactive accounts from daily usage
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 00:39:59 +08:00
ZyyandCodex 15e0a28bd0 fix(frontend): refine refresh loading and group alignment
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 00:39:50 +08:00
ZyyandCodex 58bc438992 chore: bump version to 3.2.1
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 00:13:58 +08:00
ZyyandCodex b04616f59f refactor(migrations): squash schema into initial baseline
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 00:13:09 +08:00
ZyyandCodex 82ba4fcbba fix(observability): align dashboard and quota window usage
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 00:11:56 +08:00
ZyyandCodex 819f428ac3 fix(frontend): align loading and daily usage display
Co-authored-by: Codex <noreply@openai.com>
2026-08-14 00:10:13 +08:00
ZyyandCodex 35f28e8aab chore: bump version to 3.2.0
Co-authored-by: Codex <noreply@openai.com>
2026-08-13 22:43:48 +08:00
ZyyandCodex 65645c7403 fix(accounts): gate quota recovery and add manual reset
Co-authored-by: Codex <noreply@openai.com>
2026-08-13 22:43:03 +08:00
ZyyandCodex 71159d5633 refactor(account-groups): remove unused members endpoint
Co-authored-by: Codex <noreply@openai.com>
2026-08-13 17:28:43 +08:00
ZyyandCodex f2ebb09e34 docs: describe group routing and account scheduling
Co-authored-by: Codex <noreply@openai.com>
2026-08-13 16:58:36 +08:00
ZyyandCodex 07acc67ce6 feat(frontend): add account group management
Co-authored-by: Codex <noreply@openai.com>
2026-08-13 16:58:36 +08:00
ZyyandCodex 01ad8bba8a feat(routing): add account groups and weighted scheduling
Co-authored-by: Codex <noreply@openai.com>
2026-08-13 16:58:36 +08:00
ZyyandCodex 986a128d15 fix(observability): separate live status from history range
Co-authored-by: Codex <noreply@openai.com>
2026-08-13 00:26:10 +08:00
ZyyandCodex 6cad2bb6a1 fix(openai): align account import with official token claims
Co-authored-by: Codex <noreply@openai.com>
2026-08-12 23:51:26 +08:00
ZyyandCodex 2b8b3de0e4 refactor(accounts): unify five-state account status
Co-authored-by: Codex <noreply@openai.com>
2026-08-12 23:50:31 +08:00
ZyyandCodex 4127886584 chore: bump version to 3.1.21
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 17:42:54 +08:00
ZyyandCodex ee3e5db6c2 fix(accounts): isolate model-specific quota usage
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 17:42:02 +08:00
ZyyandCodex 153cb67b9c refactor(login): refine panel details
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 17:21:31 +08:00
ZyyandCodex f370d355b6 fix(accounts): align quota ordering and summaries
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 17:21:23 +08:00
ZyyandCodex a31dad992c fix(frontend): stabilize button loading indicator
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 17:21:16 +08:00
ZyyandCodex 55a88f5f6b chore: bump version to 3.1.20
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 15:13:40 +08:00
ZyyandCodex 5d90a92582 fix(logging): rename OAuth recovery log file
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 15:06:40 +08:00
ZyyandCodex 93abe8efcd fix(logging): generalize OAuth recovery logs
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 14:55:59 +08:00
ZyyandCodex f4b75c42ed fix(openai): isolate quota and recovery signals
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 13:37:16 +08:00
ZyyandCodex 36ff038c78 chore: bump version to 3.1.19
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 12:17:28 +08:00
ZyyandCodex 545009590c fix(openai): persist authoritative quota exhaustion
Co-authored-by: Codex <noreply@openai.com>
2026-08-11 12:16:34 +08:00
ZyyandCodex 5b7b9b21ee fix(openai): require low usage for quota recovery
Co-authored-by: Codex <noreply@openai.com>
2026-08-10 15:55:58 +08:00
ZyyandCodex 44a2a87b69 fix(frontend): center button loading spinner
Co-authored-by: Codex <noreply@openai.com>
2026-08-10 15:55:58 +08:00
ZyyandCodex 184e88083b chore: bump version to 3.1.18
Co-authored-by: Codex <noreply@openai.com>
2026-08-10 12:25:42 +08:00
ZyyandCodex 53b40aea46 chore(deps): ignore fingerprint-sensitive updates
Co-authored-by: Codex <noreply@openai.com>
2026-08-10 12:23:45 +08:00
ZyyandCodex aa328b6dbf fix(openai): restore official TLS fingerprint pins
Co-authored-by: Codex <noreply@openai.com>
2026-08-10 12:23:36 +08:00
dependabot[bot]andZyy 64231ff403 chore(deps): bump the minor-and-patch group in /backend with 4 updates (#37)
Bumps the minor-and-patch group in /backend with 4 updates: [base64](https://github.com/marshallpierce/rust-base64), [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust), [hyper-rustls](https://github.com/rustls/hyper-rustls) and [rustls](https://github.com/rustls/rustls).


Updates `base64` from 0.23.0 to 0.23.1
- [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.23.0...v0.23.1)

Updates `aws-sdk-s3` from 1.140.0 to 1.141.0
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `hyper-rustls` from 0.27.7 to 0.27.9
- [Release notes](https://github.com/rustls/hyper-rustls/releases)
- [Commits](https://github.com/rustls/hyper-rustls/compare/v/0.27.7...v/0.27.9)

Updates `rustls` from 0.23.36 to 0.23.43
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.36...v/0.23.43)

---
updated-dependencies:
- dependency-name: base64
  dependency-version: 0.23.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: aws-sdk-s3
  dependency-version: 1.141.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: hyper-rustls
  dependency-version: 0.27.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: rustls
  dependency-version: 0.23.43
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zyy <39424122+zyycn@users.noreply.github.com>
2026-08-10 12:05:00 +08:00
dependabot[bot]andZyy d7b80a2066 chore(deps): bump the minor-and-patch group with 5 updates (#36)
Bumps the minor-and-patch group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.37.4` | `4.37.6` |
| [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) | `2.9.1` | `2.9.2` |
| [pnpm/action-setup](https://github.com/pnpm/action-setup) | `6.0.9` | `6.0.10` |
| [dorny/paths-filter](https://github.com/dorny/paths-filter) | `4.0.2` | `4.0.3` |
| [actions/attest](https://github.com/actions/attest) | `4.2.1` | `4.2.2` |


Updates `github/codeql-action/upload-sarif` from 4.37.4 to 4.37.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/f205ea1c3313d32999d8d6a48b4f6530d4437b38...5595ccaf912efad79be6eef63a5619ff05969be3)

Updates `Swatinem/rust-cache` from 2.9.1 to 2.9.2
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/swatinem/rust-cache/compare/c19371144df3bb44fab255c43d04cbc2ab54d1c4...6323deb102c322ba6fcbdcafc7e3dddab59af2b6)

Updates `pnpm/action-setup` from 6.0.9 to 6.0.10
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/0ebf47130e4866e96fce0953f49152a61190b271...0977fd99725f1db4007ccb2928dbb4e90d06cc86)

Updates `dorny/paths-filter` from 4.0.2 to 4.0.3
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dorny/paths-filter/compare/7b450fff21473bca461d4b92ce414b9d0420d706...ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d)

Updates `actions/attest` from 4.2.1 to 4.2.2
- [Release notes](https://github.com/actions/attest/releases)
- [Changelog](https://github.com/actions/attest/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest/compare/508db95dd578ae2727ebd6217d5ba78e4fbda05d...1e69f48acb82d1966a394da916b4c1698aa569d6)

---
updated-dependencies:
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: Swatinem/rust-cache
  dependency-version: 2.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: actions/attest
  dependency-version: 4.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zyy <39424122+zyycn@users.noreply.github.com>
2026-08-10 12:01:38 +08:00
dependabot[bot] 08d63ea3f3 chore(deps): bump debian from 7b140f3 to abd67ff in /deploy (#39)
Bumps debian from `7b140f3` to `abd67ff`.

---
updated-dependencies:
- dependency-name: debian
  dependency-version: bookworm-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 11:58:47 +08:00
dependabot[bot] ac9c4c85ad chore(deps): bump the minor-and-patch group in /frontend with 8 updates (#38)
Bumps the minor-and-patch group in /frontend with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.28.0` | `1.30.0` |
| [dompurify](https://github.com/cure53/DOMPurify) | `3.4.12` | `3.4.13` |
| [marked](https://github.com/markedjs/marked) | `18.0.7` | `18.0.9` |
| [vue](https://github.com/vuejs/core) | `3.5.40` | `3.5.41` |
| [@antfu/eslint-config](https://github.com/antfu/eslint-config) | `9.2.0` | `9.3.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.2` | `26.2.0` |
| [eslint](https://github.com/eslint/eslint) | `10.8.0` | `10.8.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.2.0` | `8.2.1` |


Updates `@lucide/vue` from 1.28.0 to 1.30.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.30.0/packages/vue)

Updates `dompurify` from 3.4.12 to 3.4.13
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.12...3.4.13)

Updates `marked` from 18.0.7 to 18.0.9
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v18.0.7...v18.0.9)

Updates `vue` from 3.5.40 to 3.5.41
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.40...v3.5.41)

Updates `@antfu/eslint-config` from 9.2.0 to 9.3.0
- [Release notes](https://github.com/antfu/eslint-config/releases)
- [Commits](https://github.com/antfu/eslint-config/compare/v9.2.0...v9.3.0)

Updates `@types/node` from 26.1.2 to 26.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.8.0 to 10.8.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.8.0...v10.8.1)

Updates `vite` from 8.2.0 to 8.2.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.2.1/packages/vite)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: dompurify
  dependency-version: 3.4.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: marked
  dependency-version: 18.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: vue
  dependency-version: 3.5.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@antfu/eslint-config"
  dependency-version: 9.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@types/node"
  dependency-version: 26.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: eslint
  dependency-version: 10.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: vite
  dependency-version: 8.2.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 11:54:36 +08:00
ZyyandCodex 492c401599 fix(openai): preserve state on quota auth failures
Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: Zyy <zyycn@users.noreply.github.com>
2026-08-10 11:46:19 +08:00
ZyyandCodex a063258a40 chore: bump version to 3.1.17
Co-authored-by: Codex <noreply@openai.com>
2026-08-09 15:14:06 +08:00
ZyyandCodex dc590f2b5f fix: make OAuth refresh margin runtime-only
Co-authored-by: Codex <noreply@openai.com>
2026-08-09 15:12:43 +08:00
ZyyandCodex ca52e2bfd9 chore: bump version to 3.1.16
Co-authored-by: Codex <noreply@openai.com>
2026-08-09 11:59:08 +08:00
ZyyandCodex 43604e36dc fix: correct first token timing
Co-authored-by: Codex <noreply@openai.com>
2026-08-07 18:56:13 +08:00
ZyyandCodex a4a997e351 fix: use lifetime dashboard card totals
Co-authored-by: Codex <noreply@openai.com>
2026-08-07 18:56:13 +08:00
ZyyandCodex 73ca463924 feat: distinguish pro plan badges
Co-authored-by: Codex <noreply@openai.com>
2026-08-07 18:56:13 +08:00
ZyyandCodex 9eeec2c78c chore: bump version to 3.1.15
Co-authored-by: Codex <noreply@openai.com>
2026-08-07 15:15:00 +08:00
ZyyandCodex 90890be2b4 fix(openai): persist OAuth tokens before quota sync
Align OAuth token persistence and ID token metadata handling with Codex, and retry initial quota observation asynchronously.

Co-authored-by: Codex <noreply@openai.com>
2026-08-07 14:59:42 +08:00
ZyyandCodex 2e4bab17a8 chore: bump version to 3.1.14
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 16:01:07 +08:00
ZyyandCodex b052e79a16 fix(openai): preserve imported token expiry
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 15:49:42 +08:00
ZyyandCodex 9775451e83 fix(web): preserve button labels while loading
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 15:27:00 +08:00
ZyyandCodex c22a2ad144 test(logging): move recovery filter coverage
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 15:14:26 +08:00
ZyyandCodex 7bd362d544 chore: bump version to 3.1.13
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 14:56:08 +08:00
ZyyandCodex e7036a616f fix(web): center button loading indicators
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 14:53:53 +08:00
ZyyandCodex 98652d4c68 fix(openai): write oauth recovery to app logs
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 14:22:13 +08:00
ZyyandCodex d0dc7ef92b fix(openai): complete import profile from access token
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 12:31:13 +08:00
ZyyandCodex 194fb20d5f chore: bump version to 3.1.12
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 11:14:32 +08:00
ZyyandCodex 30f528f084 fix(openai): simplify import and oauth validation
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 11:14:18 +08:00
ZyyandCodex 28d024ceda chore: bump version to 3.1.11
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 09:54:05 +08:00
ZyyandCodex 6b9c98c59f fix(openai): persist successful refresh tokens directly
Co-authored-by: Codex <noreply@openai.com>
2026-08-06 09:50:03 +08:00
ZyyandCodex 1294e50b76 chore: bump version to 3.1.10
Co-authored-by: Codex <noreply@openai.com>
2026-08-05 12:22:46 +08:00
ZyyandCodex a0e540642b fix(oauth): bound refresh recovery and persist failure reasons
Co-authored-by: Codex <noreply@openai.com>
2026-08-05 12:21:16 +08:00
ZyyandCodex 14d6a7de31 fix(dashboard): align summary with daily trend
Co-authored-by: Codex <noreply@openai.com>
2026-08-05 10:46:14 +08:00
ZyyandCodex cd7b60a3ba chore: bump version to 3.1.9
Co-authored-by: Codex <noreply@openai.com>
2026-08-05 00:03:39 +08:00
ZyyandCodex ebbed22f36 fix(openai): keep Codex wire OS exact
Co-authored-by: Codex <noreply@openai.com>
2026-08-05 00:03:21 +08:00
ZyyandCodex f6e5d8f673 fix(openai): drop null secondary quota windows
Co-authored-by: Codex <noreply@openai.com>
2026-08-05 00:03:11 +08:00
ZyyandCodex bf1c6211b8 chore: bump version to 3.1.8
Co-authored-by: Codex <noreply@openai.com>
2026-08-04 22:30:40 +08:00
ZyyandCodex d388fa344a fix(openai): drop placeholder secondary quota windows
Co-authored-by: Codex <noreply@openai.com>
2026-08-04 22:30:04 +08:00
ZyyandCodex eabb61b08c fix(openai): restore Codex Core app-server identity
Co-authored-by: Codex <noreply@openai.com>
2026-08-04 22:30:01 +08:00
ZyyandCodex a5c3438927 fix(openai): isolate thread-spawn session affinity
Co-authored-by: Codex <noreply@openai.com>
2026-08-04 22:29:53 +08:00
ZyyandCodex b6948542bb fix: align Codex Desktop HTTP identity
- send the official Desktop HTTP user agent across OpenAI upstream paths
- keep the audited model catalog version separate from the wire identity
- remove CLI release drift and clarify the dashboard client identity

Co-authored-by: Codex <noreply@openai.com>
2026-08-04 18:36:03 +08:00
ZyyandCodex 0242bd3193 chore: bump version to 3.1.7
Co-authored-by: Codex <noreply@openai.com>
2026-08-04 16:19:58 +08:00
ZyyandCodex 229ef48470 fix: harden backup verification and detail UI
- read every multipart backup payload to its requested length
- make remote verification failures diagnosable
- refine status details, loading icons, and attempt-chain presentation

Co-authored-by: Codex <noreply@openai.com>
2026-08-04 16:18:53 +08:00
ZyyandCodex 19c35ba901 chore: bump version to 3.1.6
Co-authored-by: Codex <noreply@openai.com>
2026-08-04 15:12:29 +08:00
ZyyandCodex 3c06ae97e8 fix: reconcile quota, backup, and session behavior
- preserve confirmed quota exhaustion until its recorded reset advances
- normalize account quota presentation and subagent session affinity
- repair S3-compatible backup handling and related admin UI details

Co-authored-by: Codex <noreply@openai.com>
2026-08-04 15:12:15 +08:00
ZyyandCodex f34966e4c7 chore: bump version to 3.1.5
Co-authored-by: Codex <noreply@openai.com>
2026-08-04 08:54:00 +08:00
ZyyandCodex e9ebf45640 fix(accounts): reconcile quota status and dashboard metrics
Co-authored-by: Codex <noreply@openai.com>
2026-08-04 08:53:29 +08:00
ZyyandCodex c00e5bd243 chore: bump version to 3.1.4
Co-authored-by: Codex <noreply@openai.com>
2026-08-04 01:10:51 +08:00
ZyyandCodex ab5f18a437 fix(accounts): update admin list test for cooldown-driven rate_limited
quota_limit_reached without Redis cooldown maps to quota_exhausted
now that rate_limited comes only from 429 cooldown. Adjust summary
assertions: quota_exhausted 1→2, rate_limited 1→0.

Co-authored-by: Codex <noreply@openai.com>
2026-08-04 01:10:40 +08:00
ZyyandClaude Fable 5 330b222359 feat(accounts): separate rate-limited status into Redis cooldown
429 临时限流从 quota 快照分离为 Redis 冷却(ProviderCooldownPort):
- apply_rate_limit_429 只写 Redis(put_if_later,不缩短已有冷却),不再合成 quota 窗口,额度重置时间不被 429 污染
- 调度选择时排除冷却中的账号,到期自动恢复;诊断请求绕过
- 状态派生分离:rate_limited_until 存在 → 限流中;quota_limit_reached → 配额耗尽
- store 层(账号列表/筛选 + dashboard poolSummary)注入 cooldown 端口,精确区分限流中与配额耗尽,"正常"筛选不再混入限流中
- wire 透传 rate_limited_until,前端状态弹窗改为状态卡片(icon 块 + 状态名 + 原因/到期明细),BasePopover 加 hover delay
- 更新 429 相关测试断言,新增配额耗尽映射测试

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 00:39:38 +08:00
ZyyandCodex d81cb2d4f0 chore: bump version to 3.1.3
Co-authored-by: Codex <noreply@openai.com>
2026-08-03 21:29:16 +08:00
ZyyandClaude Opus 4.8 8e2edb680d feat(accounts): converge status classification and persist upstream errors
Unify account status derivation in gateway-admin via AccountStatusSignals,
removing the duplicated rules in store and use_case. AccountStatus now has
five operational classes (normal/quota_exhausted/rate_limited/disabled/error),
with AccountErrorReason carrying the concrete cause.

Add provider_accounts.last_error_message, written by record_failure with the
raw upstream error and cleared on recovery to ready. The admin error badge
shows the cause and upstream text on hover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 21:23:28 +08:00
ZyyandCodex 00879be47d fix(accounts): separate rate-limited account status
Co-authored-by: Codex <noreply@openai.com>
2026-08-03 17:01:23 +08:00
ZyyandCodex 34656a3f07 refactor(ui): standardize card spacing and layouts
Co-authored-by: Codex <noreply@openai.com>
2026-08-03 17:01:23 +08:00
ZyyandCodex 27cf037309 fix(ui): center button loading spinner
Co-authored-by: Codex <noreply@openai.com>
2026-08-03 17:01:23 +08:00
ZyyandCodex 2198fa69ab refactor(migrations): consolidate schema into initial baseline
Co-authored-by: Codex <noreply@openai.com>
2026-08-03 17:01:23 +08:00
ZyyandCodex 95f6434443 chore: bump version to 3.1.2
Co-authored-by: Codex <noreply@openai.com>
2026-08-03 13:00:45 +08:00
ZyyandZyy 8734b0d3d5 fix(openai): keep fingerprint-pinned TLS versions (#35)
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-08-03 11:59:59 +08:00
dependabot[bot] 790d0d9628 chore(deps): bump the minor-and-patch group in /backend with 3 updates (#32)
Bumps the minor-and-patch group in /backend with 3 updates: [redis](https://github.com/redis-rs/redis-rs), [hyper-rustls](https://github.com/rustls/hyper-rustls) and [rustls](https://github.com/rustls/rustls).


Updates `redis` from 1.4.1 to 1.5.0
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-1.4.1...redis-1.5.0)

Updates `hyper-rustls` from 0.27.7 to 0.27.9
- [Release notes](https://github.com/rustls/hyper-rustls/releases)
- [Commits](https://github.com/rustls/hyper-rustls/compare/v/0.27.7...v/0.27.9)

Updates `rustls` from 0.23.36 to 0.23.43
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.36...v/0.23.43)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: hyper-rustls
  dependency-version: 0.27.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: rustls
  dependency-version: 0.23.43
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-03 11:39:06 +08:00
dependabot[bot]andZyy 3c562563c5 chore(deps): bump p256 from 0.13.2 to 0.14.0 in /backend (#33)
* chore(deps): bump p256 from 0.13.2 to 0.14.0 in /backend

Bumps [p256](https://github.com/RustCrypto/elliptic-curves) from 0.13.2 to 0.14.0.
- [Commits](https://github.com/RustCrypto/elliptic-curves/compare/p256/v0.13.2...p256/v0.14.0)

---
updated-dependencies:
- dependency-name: p256
  dependency-version: 0.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(xai): update p256 point encoding API

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
2026-08-03 11:38:22 +08:00
dependabot[bot] f19785d0e1 chore(deps): bump the minor-and-patch group in /frontend with 6 updates (#34)
Bumps the minor-and-patch group in /frontend with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.27.0` | `1.28.0` |
| [@vueuse/core](https://github.com/vueuse/vueuse/tree/HEAD/packages/core) | `14.3.0` | `14.4.0` |
| [axios](https://github.com/axios/axios) | `1.18.1` | `1.19.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.1` | `26.1.2` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.5` | `8.2.0` |
| [vue-tsc](https://github.com/vuejs/language-tools/tree/HEAD/packages/tsc) | `3.3.8` | `3.3.9` |


Updates `@lucide/vue` from 1.27.0 to 1.28.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.28.0/packages/vue)

Updates `@vueuse/core` from 14.3.0 to 14.4.0
- [Release notes](https://github.com/vueuse/vueuse/releases)
- [Commits](https://github.com/vueuse/vueuse/commits/v14.4.0/packages/core)

Updates `axios` from 1.18.1 to 1.19.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.18.1...v1.19.0)

Updates `@types/node` from 26.1.1 to 26.1.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `vite` from 8.1.5 to 8.2.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.2.0/packages/vite)

Updates `vue-tsc` from 3.3.8 to 3.3.9
- [Release notes](https://github.com/vuejs/language-tools/releases)
- [Changelog](https://github.com/vuejs/language-tools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/language-tools/commits/v3.3.9/packages/tsc)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@vueuse/core"
  dependency-version: 14.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: axios
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@types/node"
  dependency-version: 26.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: vite
  dependency-version: 8.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: vue-tsc
  dependency-version: 3.3.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-03 11:37:31 +08:00
dependabot[bot] a3d098a87c chore(deps): bump the minor-and-patch group with 3 updates (#31)
Bumps the minor-and-patch group with 3 updates: [github/codeql-action/upload-sarif](https://github.com/github/codeql-action), [actions/attest](https://github.com/actions/attest) and [docker/login-action](https://github.com/docker/login-action).


Updates `github/codeql-action/upload-sarif` from 4.37.3 to 4.37.4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...f205ea1c3313d32999d8d6a48b4f6530d4437b38)

Updates `actions/attest` from 4.2.0 to 4.2.1
- [Release notes](https://github.com/actions/attest/releases)
- [Changelog](https://github.com/actions/attest/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest/compare/f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6...508db95dd578ae2727ebd6217d5ba78e4fbda05d)

Updates `docker/login-action` from 4.5.1 to 4.6.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/abd2ef45e78c5afb21d64d4ca52ee8550d9572c7...dbcb813823bdd20940b903addbd779551569679f)

---
updated-dependencies:
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: actions/attest
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-03 11:37:12 +08:00
ZyyandClaude Fable 5 449d7c4218 refactor(openai): inline single-call recovery confirmation helper
quota_recovery_confirmed had one call site; inlining the evidence match
into quota_success_state makes the recovery rules (successful response
always recovers; refreshed snapshot requires old reset elapsed) visible
at the decision point. Rust best practices §1.8: don't extract helpers
for single-call simple logic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 03:36:17 +08:00
ZyyandClaude Fable 5 d199141a91 feat(store): materialize quota_limit_reached for single-source limit status
Dashboard reads the materialized column instead of parsing raw quota
JSON (design §3.3/§5.3): Provider computes snapshot-level limit_reached
on quota writes, store persists it, Admin account_status and Dashboard
both consume the same value.

- QuotaObservation gains limit_reached; openai writes fact.exhausted()
  (429 always true), xai writes used_percent>=100
- compare_and_swap_provider_quota updates the column; get_quotas and
  ACCOUNT_SELECT read it back
- ProviderAccountSummary carries quota_limit_reached; admin_account_status
  derives RateLimited from it (Unknown -> Invalid)
- Dashboard provider_account_metrics reads quota_limit_reached instead of
  provider_quota_json->'rate_limit'
- Migration 0003 adds the column with backfill (coalesce for NULL when
  rate_limit key is absent); 0004 fixes NULL backfill for already-applied
  environments

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 03:32:17 +08:00
ZyyandClaude Fable 5 df83ffdafb refactor(quota): split quota module and align scheduling/display semantics
Split credential/quota.rs into quota/{mod,snapshot,recovery}.rs with
corresponding test modules. Snapshot aggregation is snapshot-level:
top-level limit_reached/allowed and any window used>=100 count as
exhaustion, scheduling signals survive limit-only windows, and window
rolling is O(1) arithmetic.

402 recovery uses the pre-write confirmed reset (old reset elapsed plus
fresh snapshot not exhausted, or explicit allowed=true) instead of the
new snapshot's future reset, fixing the worker recovery deadlock.
Scheduling projection TTL is capped at the nearest active reset so
cache expiry never outlives the limit.

Admin derives seven mutually exclusive statuses (Active/RateLimited/
QuotaExhausted/Expired/Invalid/Disabled/Banned) from availability plus
snapshot-level quota.limitReached; Unknown maps to invalid. Dashboard
metrics are mutually exclusive (rate_limited disjoint from active),
ignore reset-expired windows, and cover additional rate limits.

xAI: bare 402 writes a short account cooldown instead of persisting
QuotaExhausted; ModelQuotaExhausted keeps model scope; runtime cooldown
survives credential rotation; account deletion clears all account and
model-scoped cooldown keys via new ProviderCooldownPort::clear_all.

Migration 0002 is frozen in the sha256 manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 03:04:43 +08:00
ZyyandClaude Fable 5 cf03c56cfa refactor(quota): separate rate limiting from account availability
Remove the persistent Cooldown state and cooldown_until/availability_reason
columns; rate limiting (429/503) now lives entirely in provider quota data
with projected window rolling, while availability carries only terminal or
intervention-required states (QuotaExhausted for 402, worker auto-recovers
on quota reset). xAI migrated to the same model (runtime cooldown via Redis,
no persistent state). Frontend derives 限流中 from quota window limitReached.

Align quota window parsing with the official codex schema: drop the invented
monthly_limit/spend_control windows, add the top-level code_review_rate_limit
window, prefer upstream limit_name for display labels, and keep duration
guessing only for grouping.

Also fix: account directory degrades one failing quota to an empty projection
instead of failing the whole page; dashboard rate_limited bucket SQL; fake
store Mutex re-entrancy deadlock; remove dead tokenRefreshing field.

Migration 0002 drops cooldown columns and rewrites the availability CHECK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 00:13:56 +08:00
ZyyandCodex 3d1e33558a style(openai): fix formatting in websocket pump context changes
Co-authored-by: Codex <noreply@openai.com>
2026-08-02 18:27:42 +08:00
ZyyandCodex f314ebb3a2 fix(openai): align wire profile os_type and harden quota 5xx backoff
- Default wire profile os_type to macOS to match the audited official
  desktop user-agent
- Retry quota usage 5xx rejections with 1s/2s exponential backoff,
  leaving 4xx to the existing quota state transitions
- Classify 503 with Retry-After into a resettable cooldown instead of
  an unclassified transient warning
- Add account_id and conversation hash to WebSocket pump exit logs so
  idle-connection resets can be attributed to an account

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 14:27:04 +08:00
ZyyandCodex 31d6ef9585 chore: bump version to 3.1.1
Co-authored-by: Codex <noreply@openai.com>
2026-08-02 04:29:44 +08:00
ZyyandCodex fdfb77c4a2 ci(release): reuse caches across versions
Dispatch releases from the long-lived publishing branch so Cargo caches are shared across version tags. Store prebuilt image caches in per-architecture GHCR refs and include toolchain and workspace manifests in Cargo cache invalidation.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 04:27:49 +08:00
ZyyandCodex a93e3853f7 chore: consolidate migrations and tune settings colors
Fold the completed provider snapshot, S3 backup, and redundant-index changes into the initial PostgreSQL baseline. Keep the settings section tabs aligned with the input surface color.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 04:07:37 +08:00
zyycnandCodex 34da9d7cc8 chore: bump version to 3.1.0
Co-authored-by: Codex <noreply@openai.com>
2026-08-02 03:05:16 +08:00
zyycnandCodex b3dd6bbe46 feat(openai): transparently retry websocket connection limit errors
When upstream rejects a NewChain request with websocket_connection_limit_reached
before the first deliverable event, retry once on a fresh connection and
downgrade to HTTP/SSE if the new connection is still limited or the retry
preparation fails, matching the official retry semantics. Non-NewChain
requests keep the previous behavior of delivering the error frame as-is,
and the code is persisted in upstream diagnostics. All pre-delivery
fallbacks share one helper that marks the transport decision and wait
metrics.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 03:04:06 +08:00
zyycnandCodex 9ab3e45288 feat(openai): zstd-compress responses request bodies like the official client
The official Codex app-server enables enable_request_compression by
default (Stable, default_enabled=true) and sends /v1/responses bodies as
zstd level 3 with Content-Encoding: zstd. Serialize the upstream request
to JSON, compress with zstd level 3, and attach the encoding header on
the HTTP/SSE path; WebSocket continues to use permessage-deflate.

Adds RequestBodyEncode/RequestCompression error variants, a zstd capture
helper for wiremock tests, and a transport test asserting the compressed
body decodes back to the original JSON.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 02:27:46 +08:00
zyycnandCodex d255a86644 test(gateway): sync config rejection fixtures to current example
The example config moved to codex_version 0.146.0 and desktop_build 6119,
but the rejection tests still replaced the old values, silently no-op'ing
and accepting invalid configs.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 02:20:29 +08:00
zyycnandCodex f3f50a818f fix(openai): install ring crypto provider before rustls use
rustls 0.23 compiles both ring and aws-lc-rs in this workspace (aws-sdk-s3
pulls aws-lc-rs), so the process-level CryptoProvider cannot be auto-selected
and any TLS use panics inside spawned tasks - which made the websocket idle
timeout test hang forever. Install the pinned ring provider once before the
first TLS use, with a startup call in the gateway binary as a fallback.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 02:20:28 +08:00
zyycnandCodex 1ff520659f fix(openai,xai): match official accept-encoding and block attestation passthrough
Official Codex Desktop app-server and Grok CLI build reqwest without
compression features, so upstream requests carry no Accept-Encoding.
Drop the gzip/brotli/zstd/deflate features and the explicit
'accept-encoding: identity' Grok header so the wire matches official
clients. Verified: reqwest only inserts Accept-Encoding through the
tower-http Decompression layer, which is compiled solely when a
compression feature is enabled; the official binary's 'gzip, deflate'
string is the h2 HPACK static table entry, not a request header.

Also stop downstream clients from injecting device attestation/integrity
headers (x-oai-attestation, x-oai-is, x-oai-is-update) at both the API
passthrough boundary and the provider header builder; only the official
client or the gateway itself may supply those.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 02:13:08 +08:00
zyycnandCodex 33381fc472 fix(openai): pin TLS stack to official Codex Desktop fingerprint
Pin rustls =0.23.36 and hyper-rustls =0.27.7 so the upstream ClientHello
matches the official app-server binary (reqwest 0.12.28 + ring unchanged).
Verified against ChatGPT.app Resources/codex from the current 26.727.51351
build: rustls 0.23.36, hyper-rustls 0.27.7, ring 0.17.14, reqwest 0.12.28.

rustls 0.23.37+ reorders ClientHello extensions, which changes JA3 even
though cipher suites, groups, signature algorithms and ALPN stay identical.
Keep the ring provider: reqwest rustls-tls-native-roots is __rustls-ring
and matches the official binary; do not switch the TLS path to aws-lc-rs
without re-capturing the handshake.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:59:58 +08:00
zyycnandCodex e3d767bf54 fix(frontend): fill brand mark box with favicon tile
The sidebar and login logo boxes kept a #1c2a3d background around a
smaller favicon image, leaving an inconsistent ring in dark mode. The
image now fills the box and the divergent box background is removed, so
the favicon's own black tile is the visual mark.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:53:05 +08:00
zyycnandCodex 8f82d0e100 chore(config): refresh wire profile baselines to current official releases
OpenAI: Codex Core 0.146.0, Desktop 26.727.51351 build 6119, and os_type
macOS (official Rust app-server os_info output) so the startup User-Agent
matches the official client before the periodic release check catches up.
xAI: grok-shell client_version 0.2.118, the current @xai-official/grok
npm release.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:46:31 +08:00
zyycnandCodex 3085e5aabe style(core): apply rustfmt to decimal canonical test
The accounting delegate commit left this test unformatted, which broke
`cargo fmt --check` in CI.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:35:23 +08:00
zyycnandCodex 36dce639a9 test: freeze settings contract fixture and exact workspace DAG
Adds a full-field RuntimeSettings wire fixture with request/response field
lockstep, and a cargo-metadata based guard that compares every workspace
internal runtime edge against a frozen allowed-edges table.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:32:49 +08:00
zyycnandCodex 6e15757150 refactor(api): stop duplicating core fields into usage metadata
UsageRecord metadata now carries only provider observations; protocol,
latency details, http version, status codes, websocket pool and image
generation facts moved to the top-level record contract. Contract tests
were updated to pin the new wire shape.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:25:57 +08:00
zyycnandCodex 365d389a14 refactor(frontend): phase system-update state and clear remaining any
system-update now uses a discriminated phase union so mutually exclusive
operation states cannot be expressed together; connection-test event shapes
and cost chart points are typed instead of any.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:25:53 +08:00
zyycnandCodex e79d57eaf9 refactor(frontend): normalize usage records into a single view model
Components now consume `UsageViewModel` produced by `normalizeUsageRecord`;
metadata fallback exists only in the normalizer. Also complete the typed
request params for ops errors so the usage module stays build-clean.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:25:50 +08:00
zyycnandCodex 8d5dfea6de fix(openai,api): unify upstream fingerprint to official Codex Desktop profile
Block client originator/User-Agent at both the API passthrough boundary
and the provider header builder, so every upstream request always carries
the runtime wire profile instead of leaking downstream client fingerprints.

Generate User-Agent in the official Codex Desktop Rust-core format
(originator/version (os; arch) terminal) and drop the fabricated
'(Codex Desktop; <desktop version>)' suffix. Verified against the current
official client 26.727.51351, which spawns the app-server with
CODEX_INTERNAL_ORIGINATOR_OVERRIDE=Codex Desktop and never appends a
desktop version to the model-request User-Agent.

WebSocket pool fingerprint tests now rotate on the Codex Core release
instead of the desktop release, matching the fields that actually change
the User-Agent.

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:24:06 +08:00
zyycnandCodex e02e5b186e refactor(store,api): split hotspot modules per boundary audit
Split the four oversized files into mod.rs-style module directories without
changing public ports or behavior:

- gateway-store: observability/{model,mapping,admin_adapter,queries/*},
  provider_accounts/{rows,repository,mapping,admin_adapter,core_adapter},
  root {config,bundle,workers,admin_adapter,value}
- gateway-api: admin/observability/{query,wire,routes,presenter} with
  mirrored contract tests and updated frozen architecture manifest

Co-authored-by: Codex <noreply@openai.com>
2026-08-02 01:00:42 +08:00
zyycnandClaude Fable 5 2dc670875a feat(ui): type system update log events
Replace any[] updateLogs with SystemUpdateEvent (id, level, message, at,
terminal) so the log pipeline is type-checked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:31:44 +08:00
zyycnandClaude Fable 5 9768921ff7 refactor(accounting): delegate admin decimal ops to core Decimal
Core Decimal gains canonical() and checked_div_u64(); gateway-admin
DecimalAmount now delegates arithmetic to core instead of reimplementing
scaled parsing, removing the duplicate scaled_amount logic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:31:43 +08:00
zyycnandClaude Fable 5 db90f0631a refactor(store): make admin api key comments functional
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:31:43 +08:00
zyycnandClaude Fable 5 8a0a8f9af4 refactor(store): field-level admin api key update
Replace read-full-control-plane-and-copy-all-fields with a targeted
mutation that updates only admin_api_key plus revision and audit. No
longer implicitly coupled to unrelated runtime settings fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:31:43 +08:00
zyycnandClaude Fable 5 8ca2c68e90 refactor(provider): self-contained schemaVersion comments
Drop cross-references to audit docs in code comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:31:43 +08:00
zyycnandClaude Fable 5 d26fc44fe6 feat(provider): version provider observation metadata with schemaVersion
OpenAI and xAI now stamp provider_observation_json with schemaVersion: 1 so
historical records can be decoded per version (missing version = v0). Per
project-redundancy-boundary-audit DB-03.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:31:43 +08:00
zyycnandClaude Fable 5 7ed2a6668a feat(usage): mark attempt list as best-effort observations
Decide attempt facts are best-effort (adr-002), not authoritative history:
- Usage detail response exposes attemptsComplete: false
- frontend labels the attempt chain as best-effort
- architecture.md documents the semantics

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:31:39 +08:00
zyycnandClaude Fable 5 3c4ac99ae4 feat(api): type request params across admin API modules
Replace data: object with precise parameter types derived from call sites,
using type inference (Omit/union) over bulk interface declarations where
possible. Covers accounts, api-keys, usage, system, auth, dashboard,
settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:08:52 +08:00
zyycnandClaude Fable 5 63d276767e refactor(api): dedupe admin presenter helpers and page metadata
- extract format_number/format_compact_number into admin/presenter.rs
- use gateway_admin model china_day_start instead of local copy
- unify PageMeta onto admin::wire, drop observability duplicate
- accounts reuse map_admin_service_error instead of local match

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:02:23 +08:00
zyycnandClaude Fable 5 60dcc5b0db feat(db): drop redundant provider_accounts_id_kind_uq index
The (id, provider_kind) unique index adds no constraint since id is already
the primary key and nothing references the composite key. New forward
migration 0004 per project-redundancy-boundary-audit DB-01; frozen 0001
untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 00:02:19 +08:00
zyycnandCodex 0d2c648422 fix(deps): resolve RustSec dependency advisories
- bump event-listener to 5.4.2 for RUSTSEC-2026-0221
- use the AWS SDK modern HTTPS client to remove rustls-webpki 0.101 affected by RUSTSEC-2026-0098, RUSTSEC-2026-0099, and RUSTSEC-2026-0104

Co-authored-by: Codex <noreply@openai.com>
2026-08-01 23:53:55 +08:00
zyycnandClaude Fable 5 56f7ddd1dc test: mirror test directory structure to production modules
Reorganize integration tests so tests/ mirrors src/ per architecture rule.

- gateway-admin: split flat backup tests into tests/backup/{policy,task,support}
  and tests/model/backup.rs, sharing fakes via backup::support
- gateway-store: move coordination_buffer and execution_buffer tests into
  tests/redis/ and tests/postgres/ to match src layout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 23:28:27 +08:00
zyycnandClaude Fable 5 930e311f51 docs(backup): document S3 backup API and architecture
- api.md: backup endpoints under /api/admin/settings/backups/*
- architecture.md: backup service wiring notes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 22:13:59 +08:00
zyycnandClaude Fable 5 5df8c71b89 chore: ignore local .claude directory
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 22:12:27 +08:00
zyycnandClaude Fable 5 dfedb5069d feat(backup): add S3-compatible PostgreSQL backups with expiry TTL
Implement end-to-end backup to S3-compatible object storage (Cloudflare R2):
admin settings UI, scheduled and manual jobs, records, retention and expiry,
download via presigned URL, and delete convergence.

- gateway-admin: backup model, schedule/retention policy, daemon worker,
  storage identity lock and audit
- gateway-store: pg_dump staging, S3 client with probe, backup repository
- gateway-api: backup settings/records/create/download/delete routes
- migrations: fold expiry TTL into 0003_s3_backup.sql (single migration)
- frontend: S3 storage, schedule and records cards under settings/backup

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 22:12:23 +08:00
zyycnandCodex 7016c8f401 chore(docs): remove outdated provider account snapshot doc
Co-authored-by: Codex <noreply@openai.com>
2026-08-01 01:16:58 +08:00
zyycnandCodex 5a14b74ff2 feat(frontend): restyle favicon with vintage film grain and mist
- keep black base and white mark unchanged
- add sparse white film-grain noise and radial white mist
- add short, straight, battle-worn scratch marks with rough edges

Co-authored-by: Codex <noreply@openai.com>
2026-08-01 01:16:58 +08:00
zyycnandCodex 00bab8d390 chore: bump version to 3.0.33
Co-authored-by: Codex <noreply@openai.com>
2026-08-01 00:49:05 +08:00
zyycnandCodex f6e364ca33 feat(observability): surface affinity, quota and catalog failure details
- carry underlying redis/serialization error into native continuation logs
- attach provider/account to affinity write failures and timeouts
- log quota cycle summary counts and per-account transient causes
- include upstream status in catalog/quota error diagnostics
- record request_id/method/uri in HTTP trace spans for failed responses

Co-authored-by: Codex <noreply@openai.com>
2026-08-01 00:42:33 +08:00
zyycnandCodex 4cd98889a7 refactor(core): centralize rotation strategy and circuit policy
Own RotationStrategy wire mapping in gateway-core and re-export it from gateway-admin, move ProviderCircuitPolicy into the execution port contract, and rename the Core coordination timeout without a Redis implementation leak.

Co-authored-by: Codex <noreply@openai.com>
2026-07-31 23:23:23 +08:00
zyycnandCodex c9b613659c fix(openai): refresh GPT pricing from upstream
Align GPT-5.6 Terra/Luna standard, Fast, cache and long-context rates with the current upstream pricing page, and add GPT-4o/4o-mini cached-input rates.

Co-authored-by: Codex <noreply@openai.com>
2026-07-31 22:43:18 +08:00
zyycnandCodex 01b4029998 feat(observability): polish usage detail and error diagnostics
Show attempt chains and P95 latency, surface client status/component/endpoint, and remove unused dashboard summary fields.

Co-authored-by: Codex <noreply@openai.com>
2026-07-31 22:28:24 +08:00
zyycnandCodex f583e50e88 feat(observability): snapshot provider account identity for history
Persist provider account name, email, and authentication kind at write time so request, attempt, and error views survive account deletion; add stable diagnostic keys with display-name resolution (account email/name, API key name); count only errored requests in failure diagnostics.

Co-authored-by: Codex <noreply@openai.com>
2026-07-31 21:25:38 +08:00
Zyy a7ea957843 test(openai): move provider mapping tests to integration suite 2026-07-31 17:37:20 +08:00
ZyyandCodex c92c95a733 chore: bump version to 3.0.32
Co-authored-by: Codex <noreply@openai.com>
2026-07-31 16:27:35 +08:00
ZyyandCodex ae7608440e fix(openai): replay client-owned conversation history
Co-authored-by: Codex <noreply@openai.com>
2026-07-31 16:07:32 +08:00
ZyyandCodex 5c9da8a39d fix(openai): preserve upstream failures during fallback
Co-authored-by: Codex <noreply@openai.com>
2026-07-31 15:33:22 +08:00
zyycnandCodex e012f24ef2 chore: bump version to 3.0.31
Co-authored-by: Codex <noreply@openai.com>
2026-07-31 02:20:58 +08:00
zyycnandCodex 756c9c7158 fix(openai): preserve exhausted quotas until reset
Co-authored-by: Codex <noreply@openai.com>
2026-07-31 02:20:37 +08:00
zyycnandCodex ab4bf75d0e chore: bump version to 3.0.30
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 21:43:11 +08:00
zyycnandCodex 7a9715ed41 chore(db): fold schema updates into initial migration
Collapse the completed response ID, service tier, and quota cooldown changes into the fresh-database baseline. Refresh the migration contract and state consistently that cross-major online upgrades are unsupported.

Co-authored-by: Codex <noreply@openai.com>
2026-07-30 21:42:57 +08:00
zyycnandCodex c353a6159e fix(openai): preserve failover before stream commit
Hold structural response events for a bounded replay grace so a split-frame 429 can rotate accounts without disconnecting the client. Release immediately on semantic output and keep post-commit failures non-replayable.

Co-authored-by: Codex <noreply@openai.com>
2026-07-30 21:42:30 +08:00
zyycnandCodex f5dca4848a fix(openai): harden exhausted quota recovery
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 21:07:53 +08:00
zyycnandCodex 36321a9af4 chore: bump version to 3.0.29
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 04:03:58 +08:00
zyycnandCodex b485b8bd4a fix(openai): reconcile exhausted account quotas
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 04:03:04 +08:00
zyycnandCodex cae2c8656a fix(xai): preserve quota exhaustion state
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 03:10:35 +08:00
zyycnandCodex acf574129f chore: bump version to 3.0.28
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 02:26:35 +08:00
zyycnandCodex 6426e0d3de fix(accounts): align availability summaries
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 02:22:23 +08:00
zyycnandCodex eedc39b33a chore: bump version to 3.0.27
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 01:55:51 +08:00
zyycnandCodex 9672a2e000 fix(xai): prioritize structured quota signals
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 01:53:43 +08:00
zyycnandCodex 0fffd2c550 fix(accounts): converge account state and imports
Co-authored-by: Codex <noreply@openai.com>
2026-07-30 01:23:42 +08:00
ZyyandCodex 5671a57dc8 chore: bump version to 3.0.26
Co-authored-by: Codex <noreply@openai.com>
2026-07-29 19:44:44 +08:00
Zyy 166e8754a2 fix(xai): restore tool replay and billing details 2026-07-29 18:28:22 +08:00
2183 changed files with 497255 additions and 129408 deletions
+86
View File
@@ -0,0 +1,86 @@
---
name: cpr-dev-guide
description: Codex Proxy RS 开发、修复、排障、代码审查和文档修改入口;页面、样式、文案、接口、调度、数据库及宿主 SDK、Runtime、插件接入均适用;PR、Issue、独立网关插件与发版叠加对应技能
---
# 开发指南
## 首要写作要求:言简意赅
所有面向人的文字都按此要求编写和审查,包括界面、通知、后端错误、任务状态、命令行、日志、注释、文档和交付说明
- 先写读者需要知道的事实或动作。删掉一句不影响理解、判断或操作,就删;标题、正文和通知不重复同一信息
- 保留会改变决定的条件、权限、后果和恢复步骤。错误通知只说已知问题,有明确办法才补一句;后端返回的提示在后端改,前端不重复解释。上游原文、用户内容和诊断事实保持原样
## 先确定领域与必读规范
按用户意图、受影响行为与对象初判,定位后按实际文件复核,不要求用户说出“前端”“后端”或技能名。**所有命中行共同构成本次必读规范,先读完再确定方案、修改或给出审查结论**;跨领域叠加,不能只选最像当前步骤的一行
职责边界决定问题由谁修复;即使代码位于账号、调度或 API 模块,只要影响插件接入或为插件增加处理,也命中插件边界。确定方案时说明相关业务事实的归属及已有实现的复用依据,不能以功能恢复或测试通过替代职责判断
| 领域或操作,行为影响与文件路径满足其一即可 | 确定方案前读取与执行 |
| --- | --- |
| 调整页面、弹窗、表单、按钮、文案、样式或交互;目标在 `frontend/`、`modules/ui/`、插件前端,或是 `.vue` 文件 | [前端流程](references/frontend.md),局部修改同样按影响范围检查组件复用、文案、Token、样式、视觉状态和运行证据 |
| 开发、排查或审查插件接入、宿主 SDK、Runtime、扩展接口或独立网关插件;目标在 `backend/crates/gateway-plugin/`、`modules/plugins/` | 完整读取[插件职责边界](references/plugin-boundaries.md)及所用 [SDK](../../../backend/crates/gateway-plugin/sdk/README.md) 类型与能力合同;宿主维护自身合同、资源与稳定性,插件业务错误在插件侧修复,不在宿主、SDK 或 Runtime 中补偿;独立插件开发、排查、审查另用 [cpr-plugin-dev](../cpr-plugin-dev/SKILL.md) |
| 开发、排查或审查服务端接口、路由、调度、账号与用量规则、持久化、后台任务或协议行为;目标是 `backend/` 中的实现、配置、迁移,或 Rust 源码 | [Workspace 边界](../../../docs/architecture.md#3-workspace-边界)的模块与依赖规则、所属模块及[后端自审](../../../docs/architecture.md#后端自审);Rust 使用 `$rust-best-practices` 和下方后端注释要求,宿主 Rust 同时读取 [Rust 模块组织约定](../../../docs/architecture.md#32-rust-模块组织约定) |
| 修改页面接口调用,或开发、排查、审查面向管理端或模型客户端的 HTTP/WS API 合同 | [API](../../../docs/api.md)对应章节;优先复用已有接口,核对调用方 → 服务端解析 → 实际查询或执行,不能只凭前端类型认定参数生效;按实际改动叠加前后端规范 |
| 新增、修改或审查客户端可见错误、失败提示、后台任务状态文案 | 下方[错误文案](#错误文案),并按实际出口核对[错误与诊断三层边界](../../../docs/architecture.md#错误与诊断三层边界) |
| 模块或调用链变更 | [Workspace 边界](../../../docs/architecture.md#3-workspace-边界)及所属模块章节,不通读整份架构 |
| 新增、调整、清理或审查测试 | 下方[测试维护](#测试维护),按当前行为、职责归属与已有覆盖判断保留、合并或删除 |
| 修改部署行为、`deploy/` 配置或数据库迁移 | 按范围读取[部署](../../../deploy/README.md)、[迁移](../../../backend/migrations/README.md)对应章节 |
| 修改或审查文档;实现或审查用法、公开合同、职责、约束或运维操作的变化 | [文档检查](references/documentation.md)及相关章节,尚无文档差异时也检查是否漏更;纯文档任务不默认读取代码开发、页面验收或发布流程 |
| 代码审查,包括 PR 审查 | [审查标准](../../../CONTRIBUTING.md#审查标准),叠加被审查行为所属领域的规范;审查默认只读 |
| PR / Issue / 发版 | 按当前意图选 [PR](../cpr-github-pr/SKILL.md)、[Issue](../cpr-github-issue/SKILL.md)或[发版](../cpr-release/SKILL.md),普通提交不创建 PR |
纯文档任务按文档分支执行;核实文中的合同与职责时读对应领域条款,不因提及代码路径或语言加载实现、构建与页面验收流程
首次命中的流程引用完整读取,并落实其中指定的必读章节;长文档先用 `rg -n '^#{1,4} ' 文件` 定位,分段读取指定章节,避免输出截断。`#章节` 不会自动截取,“按需”仅用于未命中的领域和后续操作资料,不能省略必读规范;读取记录与验证证据的复用按 [任务延续与收尾](../../../AGENTS.md#任务延续与收尾) 执行
## 参考仓库的优先级
开发、排障和代码审查先按[问题与方案依据](../../../CONTRIBUTING.md#问题与方案依据)核实问题与方案。涉及 Codex 行为时执行其中的官方行为优先约束:对应版本的官方源码优先,以透明转发和最小必要适配为默认方案,先判断偏离是否必要再评价实现
## 执行与收尾
1. 遵守已提供的 [AGENTS.md](../../../AGENTS.md),按其 CodeGraph 约定定位代码;确认工作区、任务范围与已有改动,找到事实归属、同类实现和调用链,补齐上表命中规范及[项目约定](../../../CONTRIBUTING.md#项目约定)对应条目后再修改
2. 以最终差异重新匹配上表,补齐新增领域的规范并检查实际实现,再处理本次引入的职责越界、重复规则、冗余状态与无依据分支;删除或替换功能时一并清理失效的配置、类型、调用、测试与文档,已部署数据和公开合同仍遵守所属迁移及兼容规则;仅审查时报告,不自行修复
3. 按最终行为执行[文档检查](references/documentation.md),核对所属正文及相关概述、示例和引用,在本任务内完成必要同步;不以尚无文档差异跳过检查。内部重构等未改变文档所述事实时无需改文档;常规文档只写当前状态,普通开发不自动修改 `release/notes.md`
4. 按变更范围执行[验证](../../../CONTRIBUTING.md#验证)及其停止条件,后端命令见[验证命令](../../../docs/architecture.md#验证命令);复用仍适用的验证证据与用户已确定的验收分工,区分通过、失败、跳过与未执行
5. 交付前按[首要写作要求](#首要写作要求言简意赅)通读本次新增或修改的文字。交代实际结果与验证缺口;前端按[前端流程](references/frontend.md)核对运行证据,后端按上表检查生命周期,不把构建通过当作行为验收
6. 按授权完成提交与交付;提交前按[提交约定](../../../CONTRIBUTING.md#项目约定)准备提交信息与实际共同作者署名,提交后核对实际提交消息,PR 的 AI 披露不能替代提交署名
## 测试维护
- 测试随当前功能维护,名称、夹具和断言描述当前输入、行为与结果;功能变更时优先更新所属模块的现有用例,不为每次改动叠加一组记录变更经过的测试
- 清理只证明旧字段、旧路由或旧实现已经消失的用例与附带断言;未知字段校验使用合法请求基线加无业务含义的未知字段,避免因缺少必填项等其他错误而误通过
- 同一层对同一行为的重复覆盖合并为清晰的场景矩阵,复用相邻测试夹具;跨层测试确有不同职责或失败路径时保留,不为统一而扩大测试辅助抽象
- 断言可观察的行为与业务不变量,避免复述结构体赋值、罗列表列或字段清单、匹配 SQL 文本与源码布局;项目明确要求的架构边界检查继续维护
- 当前协议兼容、数据读取兼容、安全边界、并发和资源生命周期仍需覆盖,不按 `old`、`legacy` 等名称机械删除,也不能用改名代替无效用例清理
- 不在仓库中新增或保留迁移 SQL 专项测试,包括迁移文本、schema 演变与指定版本升级用例;需要验证迁移执行、数据保留或回滚时,在临时目录编写一次性验证并使用隔离数据库,测试通过即可,临时脚本不纳入维护测试集
- 持久化功能测试使用当前完整 schema,验证现行读写、事务与数据约束;插件运行时状态迁移等业务能力按当前功能测试,不归为迁移 SQL 专项测试
## 后端注释
- 每个 Rust 文件顶层使用简短的中文 `//!` 注释说明文件职责,覆盖生产源码、测试与构建脚本;测试文件说明测试范围或辅助用途,生成文件同步维护生成器
- 注释不使用中文或英文句号,多句说明按语义分行;保留 URL、版本号、标识符和代码示例中有语义的点号
- 文件说明聚焦当前功能与职责边界,局部注释解释原因和约束,避免复述代码或记录修改经过
## 错误文案
适用于项目自定义的客户端提示,包括 HTTP、WS、SSE 和任务失败状态;原始上游错误与内部诊断遵守[三层边界](../../../docs/architecture.md#错误与诊断三层边界),不为统一外观翻译、截短或改写
- 管理端自定义提示统一中文,模型 API 自定义提示统一英文;英文句首大写、句末使用句号,API、Codex 等缩写及专有名词保持正确大小写,中文短提示不加句末句号
- 默认一句说明具体问题,确有必要才补第二句处理办法,不设机械字数限制;明确字段、对象或限制,避免只说「请求不合法」或堆叠内部术语
- 处理办法必须明确且适用;不把「请稍后重试」「联系管理员」作为统一后缀,不罗列尚未确认的网络、权限、磁盘等可能原因,不对执行结果未知的操作笼统建议重试
- 机器码、状态与结构化字段负责程序判断,文案供人阅读;只改措辞时保留分类、重试和账号处置语义,先核对现有调用方是否依赖消息文本
- 在拥有错误事实的模块提供安全提示,保留独立的内部原因链;不在响应出口批量首字母大写、翻译或裁剪任意消息,以免改写上游原文
| 场景 | 文案示例 |
| --- | --- |
| 日费用额度耗尽 | `Daily spending limit reached.` |
| 请求过大 | `Request too large. Reduce the input size.` |
| 客户端过旧 | `Codex CLI is outdated. Upgrade to 0.152.0 or later.` |
| 名称为空 | `请输入 API Key 名称` |
| 更新包校验失败 | `更新包校验失败,请重新下载` |
审查时沿实际出口检查语言、大小写、标点、长度和处理建议;复用现有行为测试核对错误码及敏感信息边界,不为逐条文案建立重复的快照或字符串清单测试
@@ -0,0 +1,6 @@
interface:
display_name: "cpr-dev-guide"
short_description: "开发、排障与审查入口,按行为影响匹配领域边界并验证实现"
default_prompt: "使用 $cpr-dev-guide 按请求、行为影响和实际文件匹配领域必读规范,读完职责边界再确定方案,完成已授权的开发或审查、自审与验证。"
policy:
allow_implicit_invocation: true
@@ -0,0 +1,39 @@
# 文档检查
修改或审查文档,以及实现或审查涉及文档更新条件的行为变化时,完整读取本流程和 [文档职责与更新条件](../../../../CONTRIBUTING.md#文档职责与更新条件),覆盖内容归属与层次行文
## 核实与同步范围
- 以当前目标分支及任务内工作区的实现、配置和有效运行证据核实行为;提交记录用于发现待核实的主题,不能直接当作现行事实或按提交顺序组织正文。按 [CodeGraph 约定](../../../../AGENTS.md#codegraph)同步索引后定位,证据不足时报告缺口,不把计划或推测写成已支持能力
- 开发任务按最终行为检查哪些用法、合同、职责、约束或运维操作需要同步,即使尚无 Markdown 差异也要检查;独立文档更新按用户范围核对现行说明,不等到发版时再补普通开发所需的文档
- 沿同一事实查找所属正文及相关概述、示例和引用,重点核对“支持/不支持”等能力声明、默认值、配置模板和命令。概述保留必要摘要并链接详细合同;不能只更新详细章节,留下 README 或示例中的矛盾说明,也不扩展为无关全仓改写
## 写之前
先确定读者、必须说明的当前事实与所属章节,再决定是否需要修改及详略;文档职责与更新条件按上节核对。涉及合同或职责说明时,补读所属架构、API 或 SDK 的对应条款,核实事实;不因文档提及实现而执行无关的开发与构建流程
在所属章节就地替换失效说明,先组织读者现在需要的操作、结果和限制,不从变更摘要改写正文。仅介绍被替代实现、修复动机或修改经过的段落直接删除;删掉“新增”“此前”“不再”等词后仍在回答变更经过的内容,也不保留
## 逐段验收
检查工作区和暂存区中本任务的最终文档差异,并连同受影响章节及相关概述、示例通读,按内容用途处理,不只搜索关键词:
| 段落回答的问题 | 处理 |
| --- | --- |
| 现在如何使用、配置或理解结果 | 保留当前事实与必要边界,放回所属章节 |
| 谁负责状态、事务、资源和失败处理 | 放入所属架构章节,避免在 API 或部署文档复述实现 |
| 这次做了什么、以前怎样、为什么这样修、跑过哪些测试 | 移入 PR、Issue 或任务报告,不以换成现在时的方式留在常规文档 |
| 特定配置或数据状态下如何升级、恢复 | 写明适用条件、操作及限制,删除演变过程 |
| 别处已完整说明的事实 | 合并或改为必要摘要与链接 |
脱离提交日期、Issue 和本次任务背景后,读者仍应能独立理解正文
业务中的请求历史、审计记录、旧数据兼容与回滚操作是当前功能,不能因含“历史”“旧”等词而删除
## 交付检查
- 完成上述逐段验收及[行文检查](../../../../CONTRIBUTING.md#层次与行文),发现问题先处理,未完成内容审查不能称文档已检查
- 检查相对链接、章节锚点、表格与命令示例,`git diff --check` 或构建通过不能代替内容审查
- 交付时简述已同步的文档范围;涉及更新条件却未改文档时,说明现有内容为何仍准确或还有什么缺口。仅报告实际核对范围,不把链接检查通过称为事实核实,也不把检查记录写进常规文档
只有明确的发布说明任务才进入 `cpr-release`;发布说明与常规文档的边界按[内容归属](../../../../CONTRIBUTING.md#内容归属)执行
@@ -0,0 +1,40 @@
# 前端开发与审查
适用于页面、组件、弹窗、表单及局部文案和样式修改,不以是否创建 PR 为触发条件;仅审查时核对和报告,不自行修改
## 领域必读
确定页面方案或审查结论前,读取[前端职责](../../../../docs/architecture.md#34-前端模块职责)、[文案与信息层级](../../../../docs/theme.md#界面文案与信息层级)和[界面验证](../../../../CONTRIBUTING.md#界面验证)的完整章节,局部修改同样读取。明确本次用户操作、原页面、最接近的同类实现及可复用基础组件,沿用既有设计与请求层,按本轮验收分工留存所需截图
## 按改动补齐规范
用户要求看效果或自行验收时,按[源码联调](../../../../docs/development.md#源码联调)提供当前工作区的可访问预览与页面入口,确认热更新和依赖来源;需要模拟数据时用于实际页面并标明,不把静态稿、旧构建或仅有本机文件路径当作可用预览
按改动补读,不把“未修改主题算法”当作跳过页面样式规范的理由:
| 改动 | 规范入口 |
| --- | --- |
| Vue 组件、逻辑、请求或状态 | 使用 `$vue-best-practices`,读取[项目约定](../../../../CONTRIBUTING.md#项目约定)的前端条目;请求与接口合同同时执行开发入口的 API 分支 |
| 颜色、间距、字体、边框、阴影、局部 CSS 或交互样式 | [通用颜色消费](../../../../docs/theme.md#通用颜色消费)、[样式与命名约定](../../../../docs/theme.md#样式与命名约定),核对无边常态及 Hover、Focus 等视觉状态 |
| 主题算法、编辑器或共用组件视觉行为 | 所改主题章节及[扩大验证范围](../../../../docs/theme.md#验证) |
| 明确指定设计技能,或建立、重塑视觉方向 | 使用指定技能或 `$frontend-design`,设计取舍仍以项目主题、用户任务和已有页面为依据 |
普通维护和新增业务页面沿用现有设计;设计技能中的配色、字体、布局探索不覆盖项目规范,主题算法只在涉及时读取
## 验收范围与收尾
验证前按[界面验证](../../../../CONTRIBUTING.md#界面验证)明确本次受影响的用户路径、状态和通过条件。局部修改围绕受影响区域及相关操作验收;主题、窗口尺寸和空/加载/错误状态仅在受影响时补充,共用主题或基础组件变更按主题规范扩大范围
通过后按[验证停止条件](../../../../CONTRIBUTING.md#验证)收尾;只有新改动、失败或未解疑点时补验相关部分。补交截图或 PR 材料时复用仍有效的运行证据
## 交付前
对照最终差异和实际页面核对适用项,不以读取过规范代替检查:
- **复用与范围**:是否沿用参考页面和基础组件,已有组件能承载时不手写替代控件;共用缺陷按职责修复,修复后清理本次已失效的页面补丁,不顺带重排无关区域或新增重复入口
- **文案与层级**:标点是否合规,标题、状态和帮助是否重复,详细说明与当前决策必需提示的位置是否合适
- **样式与状态**:是否使用对应 Token 与 utility,局部样式是否符合适用边界,受影响的主题、窄屏及交互状态是否正确
- **查询与动作**:区分首次加载、刷新、分页、单行与批量操作,核对 loading、禁用状态的影响范围;沿用请求层错误处理,不在页面叠加同一错误的提示
- **运行证据**:按界面验证核对截图与实际交互,证据对应当前实现;页面 PR 的可访问图片和 Draft 条件按该节执行
交付简述所复用的页面或组件、实际验证范围及缺口;环境不足时如实说明,不能仅凭构建、源码或截图推断未验证的行为
@@ -0,0 +1,15 @@
# 插件职责边界
开发、排查或审查插件接入、SDK、Runtime、宿主扩展接口及独立网关插件时完整读取,不以文件位于插件目录或正在选择能力为条件
确定方案前读取[职责与源码入口](../../../../docs/architecture.md#职责与源码入口)、[发布与执行边界](../../../../docs/architecture.md#发布与执行边界)及[信任与资源边界](../../../../docs/architecture.md#信任与资源边界),明确问题归属、必要宿主保护与插件责任
**宿主提供可组合能力,守住自身合同、资源与稳定性;插件业务正确性和兼容修复由插件负责**
- 宿主提供可独立组合的受控原生能力,负责授权、资源隔离、事务与审计,复用已有服务与状态归属
- 插件自身的触发条件、跨资源关联、策略和流程编排由插件实现,不能把某个使用场景做成宿主专属流程、状态或补偿逻辑
- 插件行为不可预测,只保留宿主安全解码、权限、资源限制及一致性所需的检查,不为插件业务正确性增加语义校验、未知字段拒绝或重复检查;失败按已有调用、会话、实例边界及能力策略拒绝、隔离和回收,不把单次业务错误扩大为全局停用
- 不靠猜测插件意图、补造业务字段、修正业务结果、重放操作或增加插件专属兼容分支让错误插件继续工作;问题归插件时在插件侧修复,不把补偿搬进宿主 SDK 或 Runtime
- 通用协议与会话辅助、公开合同规定的默认值和失败恢复仍按既有合同执行,不能因拒绝业务兜底而删掉必要的宿主保护;故障隔离按[信任与资源边界](../../../../docs/architecture.md#信任与资源边界)评估,不假定完整信任的插件具有安全沙箱
- 开放已有能力时复用宿主服务与数据模型,不为插件的导入、修正、重算等编排新增表、字段、账本或任务系统;确有宿主能力缺口时,界定可独立使用的基础接口及输入输出、权限、事务、失败和重复调用语义,不夹带插件私有模型与工作流
- 新增防御、兼容、重试、缓存或状态前,说明要保护的宿主不变量、触发依据和最小处理范围;仅为迁就某个插件或猜测未来行为时不加入宿主
- 自审调用链、合同、测试与文档中的业务判断、自动串联和专属状态,仅换成通用名称不算解除耦合;验证失败与取消后的资源释放、状态一致性和其他调用受影响范围,不以插件恢复成功代替宿主边界验证
+63
View File
@@ -0,0 +1,63 @@
---
name: cpr-github-issue
description: 草拟、创建、补充或回复 GitHub Issue,包括缺陷、功能和协作规则反馈;单独排障或修复不自动发布 Issue
---
# Issue 协作
## 目标与范围
从用户 URL、remote 和上下文确认接收仓库,fork 场景区分自己的仓库与上游
草拟只准备正文,明确创建或提交才发布;已有授权直接执行,自动命中技能本身不授权远程写入
补充已有 Issue 先读原帖和讨论,按授权更新或追加,不重复新建;无法推断的目标或关键事实才向用户补齐
## 按动作读取与核实
目标、授权、事实与脱敏要求适用于草拟、提交和回复;按下列动作读取必需规则与证据,再组织正文。症状反馈可保留未知根因,进入技术排查或方案判断时补齐对应规范
- 新建 Issue 时,读取本仓库的[提交 Issue](../../../CONTRIBUTING.md#提交-issue)及本次匹配的一张表单:[接口问题](../../../.github/ISSUE_TEMPLATE/api-bug-report.yml)、[常规问题](../../../.github/ISSUE_TEMPLATE/general-bug-report.yml)或[功能建议](../../../.github/ISSUE_TEMPLATE/feature-request.yml);协作规则提案可用普通 Issue
- 回复时读取原帖、相关讨论与支撑答复的证据,按下方回复口吻组织内容,不套用新建 Issue 的表单
- 其他仓库读取它自己的相关规则与实际模板,长文档先定位标题再截取,不预读全部模板或 CONTRIBUTING
- 搜索同场景 open/closed Issues、相关 PR 和发布记录,区分主分支修复与已发布修复,不凭标题判断重复;没有新证据时返回已有链接,用户仍要求新建则按其要求处理
- 需要技术排查、判断实现方案或修改时执行 [cpr-dev-guide](../cpr-dev-guide/SKILL.md)的领域必读,不能仅凭症状建议跨职责补偿;提交症状反馈不以找到根因为前提
- 引用上游行为或提出方案时执行[问题与方案依据](../../../CONTRIBUTING.md#问题与方案依据),注明适用版本、来源与缺口
## 正文与证据
标题说明症状或期望行为,区分发生问题的部署版本、实测结果、预期结果与根因假设
功能和规则提案说明场景、缺少的能力及建议结果,无需硬套缺陷复现步骤
YAML 表单按 `attributes.label` 和字段顺序组织正文,手工核对必填项、选项与确认项,CLI 不会代做表单校验
未知信息如实标明,影响理解或复现的关键事实先补齐,不提交原始 YAML、占位答案或未确认的勾选项
日志和附件仅含定位所需的脱敏片段,不公开凭据、完整配置或请求转储,不擅自修改代理、TUN、证书环境取证
附件须获授权且可访问,本机路径不算附件;labels 和 assignees 仅按模板或用户要求设置,并确认实际存在
Issue 可记录复现、排查和变更经过;涉及常规文档修改时执行[文档检查](../cpr-dev-guide/references/documentation.md)
## 回复口吻
草拟、发布或修改 Issue 回复时,按 [GitHub 回复口吻](../../../CONTRIBUTING.md#github-回复口吻)将核实结果写成面向提问者的交流答复,具体口吻规则集中维护在该节
按平常回帖的口吻写,语气自然、委婉,说清判断和原因,少用铺垫、转折和自我说明;暂不采纳或提出不同意见时说明具体原因
发送或更新前从提问者视角通读:判断和原因是否清楚,建议是否便于尝试,补充材料的用途是否明确。删去不影响理解的客套、重复解释和过程说明;发现生硬时调整整句,不能仅加缓和词,保留已核实的结论
## 发布与收尾
使用可用连接器或 `gh`,不为提交反馈安装工具、修改仓库配置或启用服务
多行正文通过结构化字段或 UTF-8 文件传递,以下变量须先从已确认的目标与正文赋值:
```bash
gh issue create --repo "$issue_repo" --title "$issue_title" --body-file "$issue_body_file"
```
发布前核对目标、必填内容、脱敏与重复记录,更新时保留无关原文;不擅自评论、重开或关闭重复 Issue
超时或结果不明确时先查询远端再重试,发布后回读确认并返回链接;仅草拟或发布失败时提供正文并注明未提交
@@ -0,0 +1,6 @@
interface:
display_name: "cpr-github-issue"
short_description: "整理、提交或补充 Issue,核对复现、重复反馈与发布范围"
default_prompt: "使用 $cpr-github-issue 按当前请求整理、提交或补充 Issue,核对目标、复现证据、重复记录和脱敏,不将草拟自动变成发布。"
policy:
allow_implicit_invocation: true
+50
View File
@@ -0,0 +1,50 @@
---
name: cpr-github-pr
description: 准备、创建、更新或审查 GitHub PR,含回复讨论与处理审查意见;普通 Git 提交和无 PR 上下文的排障不创建 PR
---
# PR 协作
## 先确定范围
沿用当前会话的目标与授权,创建 PR 包含任务范围内的提交和推送,审查默认只读;二者都不包含合并
仅准备标题或正文不写远端,“能合并吗”按审查处理,发布评论、批准或请求修改须有对应授权
从 URL、remote 和上下文确认仓库,区分 fork 的推送端与 PR 接收端;存在无法消除的歧义时只补齐必要信息
本仓库目标分支为 `main`,其他仓库使用其可信基线规则与模板,PR 内修改的规则不能降低审查要求
为本仓库拟写或更新 PR 标题时,完整标题使用英文,沿用提交信息的 Conventional Commits 格式,例如 `fix(store): preserve client key ledgers after deletion`
涉及上游行为的判断按[问题与方案依据](../../../CONTRIBUTING.md#问题与方案依据)执行
## 按模式读取
先完成上述范围判断,再按当前动作与差异涉及的领域叠加下表。命中的流程引用完整读取,长文档读取指定章节;只复用已实际读取且仍适用的内容,单纯整理正文或回复不扩展为完整实现审查
| 当前动作 | 读取与执行 |
| --- | --- |
| 只整理标题或描述 | 当前 PR、最终 diff、实际模板、[AI 披露](../../../CONTRIBUTING.md#ai-使用披露),在授权范围内改文案,不自行修改实现;不读取推送或审查流程 |
| 创建或更新 PR | [创建流程](references/create.md)、[PR 流程](../../../CONTRIBUTING.md#pr-流程)与[规则文件保护](../../../CONTRIBUTING.md#规则文件保护) |
| 审查 PR | [审查流程](references/review.md)、[审查标准](../../../CONTRIBUTING.md#审查标准) |
| 回复讨论或审查意见 | 目标评论、相关上下文与实际处理结果,按下方回复口吻答复;仅回复不加载创建或完整审查流程 |
| 实现自审、审查或修改代码与文档 | 执行 [cpr-dev-guide](../cpr-dev-guide/SKILL.md)的领域必读与对应检查,包括后端、插件、测试和 API 等命中项;只读审查同样执行,不能仅核对 PR 正文与 CI |
| 含前端改动 | [前端流程](../cpr-dev-guide/references/frontend.md),核对适用规范与当前实现的运行证据 |
| 含文档改动 | [文档检查](../cpr-dev-guide/references/documentation.md) |
## 回复口吻
草拟和发布 PR 讨论、审查意见的回复时,执行 [GitHub 回复口吻](../../../CONTRIBUTING.md#github-回复口吻)
## 创建与审查的公共核对
- 按[问题与方案依据](../../../CONTRIBUTING.md#问题与方案依据)核实触发条件、发生版本与目标分支现状,分别判断问题是否成立、是否属于目标项目职责及方案是否必要
- 核对最新目标分支及同场景 open/closed PR、Issue,按实际行为和测试判断重复,不凭标题下结论,也不擅自关闭他人 PR
- 自审或审查覆盖完整差异与调用链,前端与文档按模式表检查
- 核对[AI 使用披露](../../../CONTRIBUTING.md#ai-使用披露),填写实际工具与模型,不猜型号;必需截图或型号待补时保留 Draft
- 验证结果对应实际 head,区分通过、失败、跳过与未执行;无文档改动时只核对现有说明是否失真
远程操作优先使用可用连接器,`gh` 显式指定 `--repo`,多行正文使用结构化字段或 `--body-file`
返回链接或会话内结论,说明实际范围与缺口,不将检查通过称为维护者批准
@@ -0,0 +1,6 @@
interface:
display_name: "cpr-github-pr"
short_description: "提交、更新与审查 PR,核对问题、自审、页面截图和 AI 模型披露"
default_prompt: "使用 $cpr-github-pr 按当前请求准备、提交、更新或审查 PR,实现自审与审查叠加开发指南的领域规范,核实问题、方案、验证、页面截图和 AI 模型披露。"
policy:
allow_implicit_invocation: true
@@ -0,0 +1,29 @@
# 创建或更新 PR
仅在创建、推送或更新 PR 时读取,先完成[入口](../SKILL.md)的范围判断与公共核对
1. 检查工作区、当前分支、远端和近期提交格式。读取实际存在的 PR 模板,按最终变更重写标题与描述。
不用提交日志拼接正文,也不假定仓库一定有模板。
2. 更新并确认 base/head,查看相对 merge-base 的完整差异和待提交内容;核对仓库的受保护文件与准入规则。
只纳入任务范围的文件和提交;需要整理时用独立功能分支,保留用户其他改动,不擅自改写已发布历史。
3. 已完成的改动也须按入口公共核对复核问题与方案。原问题不成立时不包装成已修复的缺陷,证据不足时保留结论。
实现与自审按 [cpr-dev-guide](../../cpr-dev-guide/SKILL.md)执行,包括适用的后端生命周期和前端检查;需要修正时只修改已授权范围。
4. 查找同一来源仓库、head 和 base 的现有 PR;已存在则复用其链接,不重复创建。
更新现有标题或正文须在本次整理/更新请求范围内。
5. 按[验证](../../../../CONTRIBUTING.md#验证)执行适用检查,记录对应提交或工作区状态;文档、页面证据与模型披露按入口核对。
6. 将任务范围内的提交推送到正确远端,核对远端 head。标题遵循项目约定,正文先说明问题成立的依据和最终行为,
再给必要的设计取舍、自审结论、验证与遗留事项,按模板附页面截图和 AI 使用情况。涉及上游行为时,按[问题与方案依据](../../../../CONTRIBUTING.md#问题与方案依据)写明官方源码位置、版本、处理方式及本次方案关系。按规模压缩,不强填无关章节。
7. 明确要求草稿或仍有阻塞项时创建或保持 Draft PR,并解释原因;必需截图或 AI 模型披露待补时也不标为可审阅。具备审查条件且未要求草稿时创建普通 PR。
GitHub Draft 状态与“只写本地草稿正文”不同,后者不创建远程对象。
8. 创建或更新后回读正文与附件,核对链接、base、head、草稿状态、模型披露和最新检查结果。网络超时或结果不确定时先查询远端,再决定是否重试。
CLI 示例中的变量必须先根据上述检查赋值;`pr_body_file` 是精确写入 UTF-8 正文的临时文件:
```bash
gh pr create --repo "$pr_repo" --base "$pr_base" --head "$pr_head" \
--title "$pr_title" --body-file "$pr_body_file"
```
按需添加 `--draft`。不要用 `gh pr create --dry-run` 做只读预览,它仍可能推送 Git 变更。
多行正文通过文件或连接器的结构化字段传递,不把未转义正文拼进 shell 命令。
创建完成返回链接、目标分支和验证摘要,不称为已合并。
@@ -0,0 +1,24 @@
# 审查 PR
仅在审查 PR 时读取,执行[入口](../SKILL.md)的公共核对,默认只在当前会话报告
1. 获取 PR 的 base/head SHA、描述、文件差异、已有讨论及 CI 状态。记录实际审查的 head,使用 merge-base 差异。
需要源码上下文时先遵守目标仓库的定位规则;需检出时使用隔离工作区,保留用户当前分支和运行环境。
按实际行为与文件执行 [cpr-dev-guide](../../cpr-dev-guide/SKILL.md)的领域必读,再判断方案与实现;只读审查也核对职责边界与生命周期。
2. 按入口公共核对在目标分支基线核实问题,区分原有问题、环境或配置差异与 PR 新引入的问题,说明主分支已覆盖的部分。
3. 按[问题与方案依据](../../../../CONTRIBUTING.md#问题与方案依据)评估解决方向,涉及上游时说明官方处理方式及本次方案关系;缺少必要性依据的偏离明确指出,不因测试通过就认定方案合理。
4. 按[审查标准](../../../../CONTRIBUTING.md#审查标准)追踪完整调用链,找到共享业务事实、同类实现及已有验证。
对既有特殊分支查清服务的场景,再判断本次改动是否引入或加重问题。
主动检查本次变更新引入或加重的冗余与代码异味,说明具体位置、维护成本或行为影响及最小修复方向;
不能仅因功能可用、CI 通过而略过,也不把个人写法偏好判为缺陷。
5. 前端改动执行[前端流程](../../cpr-dev-guide/references/frontend.md),浏览器可用时检查相关交互;缺少必需截图时要求补齐并列为未具备审阅条件。
6. 核对 CI 与本地证据是否覆盖当前 head;根据风险补充必要验证,不触发无关部署或真实账号业务请求。
文档差异按[文档检查](../../cpr-dev-guide/references/documentation.md)核对最终 diff。
核对 AI 使用与模型披露,缺项明确指出。工具不可用、集成测试跳过或环境不足时具体说明影响范围。
7. 按审查标准用中文输出问题是否成立、方案是否合理及对应依据,再列实现问题与验证缺口;复用建议指出具体已有实现。
没有问题的类别可省略,未发现问题时直接说明并交代覆盖范围。
8. 结束前核对 head 是否变化。发生变化时检查新增差异并更新结论,或明确结论只适用于原 SHA。
审查本身不修改源码、不发表评论、不提交 GitHub 的批准或请求修改状态。
用户明确要求发布时,先复核当前 head 和重复评论,再通过结构化字段或 `--body-file` 发布对应内容;
需要绑定具体提交时使用支持 commit ID 的审查接口。不要在旧提交结论上批准新的 head。
+44
View File
@@ -0,0 +1,44 @@
---
name: cpr-plugin-dev
description: 开发、修复、排查、审查或打包 Codex Proxy RS 独立网关插件,含处理器、清单与插件页面;仅改宿主能力时使用 cpr-dev-guide 的插件边界,不适用于 Codex 插件或 Vue 插件
---
# 网关插件开发
## 先读职责边界
插件在独立项目中通过公开 SDK 实现业务,插件业务错误在插件侧修复;宿主维护自身合同、资源与稳定性,不猜测插件意图、不补造业务数据,不在宿主、SDK 或 Runtime 中增加插件专属补偿
开发、排查和审查先完整读取[插件职责边界](../cpr-dev-guide/references/plugin-boundaries.md),再选择实现步骤;已有处理器修复和页面改动也执行,不以是否新增能力为条件。需要新增宿主能力时,先界定可独立使用的基础接口及其职责;必要的协议保护与资源回收仍由宿主负责
同时执行 [cpr-dev-guide](../cpr-dev-guide/SKILL.md)的领域必读与收尾要求;已实际读取且仍适用的内容复用,不递归重读入口。仅构建、打包或分发已有插件时按对应操作读取,若需诊断或修改行为,再补齐开发分支
## 范围与合同
- 确认用途、工程位置、目标宿主版本与平台,沿用已有工程;位置无法推断时只补齐该信息,不把插件源码塞进宿主 workspace
- 核对运行宿主、[SDK 版本](../../../backend/crates/gateway-plugin/sdk/Cargo.toml)、[宿主支持清单](../../../backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json)及所用 CLI,清单、进程和能力版本分别判断
- `contributes` 声明处理器,`bindings` 选择挂载与匹配范围;安装成功不等于功能生效。当前清单拒绝 `permissions`,安装者完整信任插件,`trustedProcess` 不提供操作系统沙箱或访问域隔离,见[完整信任](../../../backend/crates/gateway-plugin/sdk/docs/manifest.md#完整信任)
- Codex 兼容性适配按[问题与方案依据](../../../CONTRIBUTING.md#问题与方案依据)执行,不因位于插件中扩大改写或补偿范围;业务改写限于用户授权,宿主能力以目标 SDK 为准
## 按当前步骤读取
完成上述适用边界后,按当前步骤读取下表;同时涉及多项时叠加。命中的实现或页面参考完整读取,长文档读取指定章节,示例、前端和打包资料在对应步骤加载
| 当前工作 | 入口 |
| --- | --- |
| 选择、使用或审查能力 | [能力与方法](../../../backend/crates/gateway-plugin/sdk/docs/capabilities.md#能力与方法)及所用能力章节,不以已有调用能编译代替合同核对 |
| 开发、排查或审查清单与 Rust 处理器 | [实现参考](references/implementation.md)、所用 SDK 类型;涉及清单字段时读[清单](../../../backend/crates/gateway-plugin/sdk/docs/manifest.md)对应章节 |
| 创建、修改或审查页面 | [页面参考](references/frontend.md),不需要页面的插件不创建前端 |
| 准备工程或联调依赖 | [工程与依赖](references/development.md#工程与依赖),需要示例时再读[复用示例](references/development.md#复用示例) |
| 构建或打包 | [构建与打包](references/development.md#构建与打包) |
| 实际验收 | [验证与交付](references/development.md#验证与交付),按实现范围验证 |
| 分发插件 | [发布与安装来源](references/development.md#发布与安装来源),不自动进入宿主发版流程 |
| 修改插件文档 | [文档检查](../cpr-dev-guide/references/documentation.md) |
## 交付
说明源码与产物位置、兼容范围、能力与绑定、实际验证及缺口,只报告真正生成的安装包或完成的业务验证
修改源码不默认授权在目标实例安装启用、调用真实上游或发布,沿用用户当前授权判断后续动作
PR 任务才使用 [cpr-github-pr](../cpr-github-pr/SKILL.md),文档检查按开发入口收尾执行
@@ -0,0 +1,6 @@
interface:
display_name: "cpr-plugin-dev"
short_description: "独立网关插件开发、排障与审查,先核对宿主边界再实现和验证"
default_prompt: "使用 $cpr-plugin-dev 按当前请求开发、排查、审查或打包独立网关插件,先读取适用职责边界,插件业务错误在插件侧修复,再按步骤读取实现与验证资料。"
policy:
allow_implicit_invocation: true
@@ -0,0 +1,116 @@
# 构建、联调与验证
仅在准备工程、构建或验证插件时读取对应章节,分发规则只在准备分发时读取
这里给出执行入口,不复制 SDK 协议或安装 API 的字段手册
## 工程与依赖
| 位置 | 用途 |
| --- | --- |
| 主仓 `backend/crates/gateway-plugin/sdk/` | Rust 公开合同、会话辅助与能力文档 |
| 主仓 `backend/apps/plugin-cli/` | `cpr-plugin package`,只校验与打包,不编译插件源码 |
| [codex-proxy-plugins](https://github.com/zyycn/codex-proxy-plugins) 的 `examples/workbench/` | 基础能力体验与文本工作流示例,按需选择功能,不整体复制 |
| [codex-proxy-ui](https://github.com/zyycn/codex-proxy-ui) | 可选的 Vue UI 库、组件文档与 playground |
先检查用户给定工程、`modules/plugins` 子模块或同级仓库是否存在,读取实际仓库约定及当前步骤所需的 README 章节、依赖与锁文件,不预读无关前端配置
只需中间件时不复制示例的管理 API 和页面;CLI 等能力从 SDK 对应合同起步,不给示例添加无关能力
本仓库子模块联调使用[开发文档](../../../../docs/development.md#源码联调)中的 source 模式和本地 SDK 验证入口,不改写正式依赖或锁文件
其他工程按其自身联调配置临时使用包级链接或 override,路径须真实可解析,不从业务源码跨仓 `import .../src/...`
对外交付前核实 UI/SDK 的可用版本和安装来源:仅引用确实存在的发布版本,或包含所需合同的固定 Git commit;
不要把浮动 `main` 当版本,也不要自动发布依赖以补齐条件。本地 override 尚未移除时,明确说明独立安装缺口。
所用工具链以目标项目的配置为准,不因本机安装了更新版本而顺手升级。
## 复用示例
找到 `codex-proxy-plugins` 后,只读根目录约定与 `examples/workbench/README.md` 中当前能力或构建所需的章节
该仓库使用 `bash scripts/package` 构建页面、Rust 二进制并打包;前端依赖和工具配置位于示例的 `frontend/`,根目录不维护 Node 工程。执行前核对脚本适用的插件路径和目标平台。
| 任务 | 示例入口 |
| --- | --- |
| 身份、声明与资源 | `examples/workbench/plugin.json` |
| 会话启动与处理器组合 | `examples/workbench/backend/src/main.rs`、`app.rs` |
| 管理接口 | `examples/workbench/backend/src/management/` |
| 页面与主题 | `examples/workbench/frontend/src/` |
| 管理接口调用 | `examples/workbench/frontend/src/api/modules/` |
| 包内静态资源构建 | `examples/workbench/frontend/vite.config.ts` |
若派生新插件,同时修改包名、二进制名、插件身份、扩展项 ID、注册描述和构建目标,不只改展示名称。
保留当前项目确实需要的依赖、资源和能力声明;不要把示例中的全请求范围直接用到共享环境
## 构建与打包
先运行目标工程已有的测试和构建入口。Rust 检查示意如下,路径、工具链和 target 按实际工程替换:
```bash
cargo fmt --manifest-path /path/to/plugin/Cargo.toml -- --check
cargo clippy --manifest-path /path/to/plugin/Cargo.toml --all-targets --all-features --locked -- -D warnings
cargo test --manifest-path /path/to/plugin/Cargo.toml --locked
cargo build --manifest-path /path/to/plugin/Cargo.toml --release --locked --target x86_64-unknown-linux-gnu
```
新工程尚无锁文件时先生成并检查锁文件;不要把缺少锁文件的 `--locked` 失败当作源码问题。
有页面时另跑该前端项目的检查与生产构建,确认产物路径、资源清单和 MIME 类型相符;无页面的插件无需 Node 项目。
先检查实际调用的 `cpr-plugin` 路径与构建来源。已有工具只有在支持目标作者清单时才复用;
遇到派生字段被要求手写或支持能力不符时,对照目标 SDK 和 CLI 合同,不通过改写清单迁就过期工具。
需要从目标宿主源码构建工具时,在主仓根目录运行以下命令,把 `--help` 换成实际参数即可,不必全局安装:
```bash
cargo +1.99.1 run --manifest-path backend/Cargo.toml -p codex-proxy-plugin-cli --locked -- package --help
```
直接打包的示意命令:
```bash
cpr-plugin package \
--manifest /path/to/plugin/plugin.json \
--binary /path/to/plugin/target/x86_64-unknown-linux-gnu/release/plugin \
--target x86_64-unknown-linux-gnu \
--resource-map web=frontend/dist \
--output-dir /path/to/plugin/dist
```
没有 `web` 资源时去掉 `--resource-map`。映射源相对于作者清单目录,而非命令执行目录;资源必须留在插件工程内。
目标平台指运行宿主的服务器/容器,不是访问管理端的浏览器所在电脑;平台与二进制必须匹配,不能只改 target 参数伪装交叉编译。支持平台以 [CLI 文档](../../../../backend/apps/plugin-cli/README.md)及实际 `--help` 为准。
输出为 `.tar.gz` 与 `.sha256`,不是源码 zip、npm 包或页面目录。归档根目录应有生成后的 `plugin.json`、可执行文件和声明资源。
安装时以本次输出的包摘要为准,核对包内版本、平台、能力和 `engines` 与实际运行宿主匹配;
源码已更新不代表 `dist` 中旧包已重建。版本要求未满足时使用兼容宿主或确有依据的兼容声明,不跳过宿主检查。
本地同版本反复试验与对外发布区分处理;发布新内容使用明确的新版本,不覆盖已发布版本。
## 发布与安装来源
仅在用户要求发布或准备分发时执行;沿用目标插件仓库的发布流程,核对源码提交、包内版本、目标平台与产物摘要。
- GitHub 安装读取 Release 附件,发布时上传实际生成的 `.tar.gz`/`.tgz` 和校验文件;仅推送 tag 或保留 GitHub 自动生成的源码归档,不能提供可安装插件
- 稳定版应发布为非 Draft、非 Pre-release;需要支持留空标签安装时,再核对最新稳定版查询能返回预期 Release。版本号没有预发行后缀,不代表 GitHub 的发布状态已经是正式版
- 当前宿主在标签留空时只查询最新稳定版;预发行版安装需要同时指定标签并允许预发行,不把该开关解释成自动发现预发行版
- 多平台或多插件附件由用户按实际发布内容选择,文件名帮助辨识,包内清单才是平台、版本与兼容范围的依据
- 发布完成后,从公开下载来源核对附件和摘要,再按已授权的目标环境验证安装;本地包验证不等于远程发行附件已经验证
## 验证与交付
按[插件使用](../../../../docs/plugins.md)验证“校验安装包 → 确认来源并接受完整信任 → 自动准备默认配置 → 实际使用”;只在缺少业务必填值时补配置,不添加权限或资源选择向导
只有获得目标环境操作授权才安装和启用;不把修改源码的授权扩展到生产实例或真实凭据操作。
需要 API 自动化时查[插件管理 API](../../../../docs/api.md#12-插件管理),不要依据记忆拼路由。
按实际实现选择验证,不为不存在的能力增加测试任务:
| 实现范围 | 应验证的行为 |
| --- | --- |
| 中间件 | 匹配/不匹配范围、单次 `next`、未改写字节保留、下游错误与取消;支持流式时检查多帧、终态和断开;若涉及续链,使用真实目标客户端检查 HTTP/SSE、WS 及后续轮次 |
| 上游适配器 | 绑定匹配、账号认证与代理、HTTP/SSE 或 WS 事件解析、发送状态、用量和终态;支持续接时核对 Key、账号、凭据版本与代次隔离,失败不能透明重放已发送操作 |
| 模型路由/调度 | 真实入口能命中规则,未匹配请求保持原有行为;使用官方客户端正常携带的业务头和会话别名,确认投影不会拒绝合法请求,也不暴露身份字段 |
| 生命周期/用量/WS 观察 | 完成一次实际模型请求并核对对应事件;WS 观察需确认实际上游采用 WS,HTTP 请求成功不能证明 WS 观察生效 |
| 管理页面 | 从宿主打开,标题/副标题、明暗主题、加载与错误状态;管理桥请求成功;停用或版本切换后旧入口失效 |
| 入口认证/CLI | 对应身份和账号范围、缺少授权、取消与错误;不把帮助输出、注册成功或一次健康探测当完整验收 |
| 持久状态或升级 | 读写版本冲突、停用再启用后的持久状态、升级兼容性及迁移失败;版本配置恢复与私有状态迁移分别验证,不覆盖并发配置或把回滚版本误当恢复历史数据 |
真实请求按用户选择的客户端、模型和测试预算执行;没有这些能力或授权时列为缺口,不模拟成功。
外部示例测试若依赖未提供的仓库或制品,应记录为未执行;当前示例可用于实际能力联调,但不能把被忽略的测试计为通过。
临时账号、Key 或配置只在明确的测试范围内创建,记录并清理自己创建的对象,不输出密钥、原始请求或插件 secret。
交付简述:源码与产物位置、兼容范围、所需能力与绑定、执行过的命令及结果、尚未验证的场景
只有确实生成了产物才报告“已打包”,只有目标环境业务验证成功才报告“已生效”;提交、推送与发布按用户授权另行执行。
@@ -0,0 +1,10 @@
# 插件页面
创建、修改或审查插件页面时完整读取,先完成[插件入口](../SKILL.md#先读职责边界)的边界判断并执行[前端流程](../../cpr-dev-guide/references/frontend.md),再核对以下插件特有合同
- 优先参考独立示例仓库的 `examples/workbench/frontend/src/api/`:业务路由放在 `modules/`,`request.ts` 封装公开宿主桥;按需使用 `@codex-proxy/ui` 包出口,不跨仓导入宿主或 UI 的内部源码
- Vue 页面保留 SFC,逻辑、脚本和配置使用 TypeScript;只实现业务内容,标题和副标题由 `ManagementPage` 交给宿主显示
- 隔离页面通过 `window.codexProxyPlugin.request` 调用已注册管理路由;`models.responses` 通过普通请求链交付 JSON/SSE,支持取消且不暴露 Key 明文。不直接 `fetch` 宿主、不读取管理 Cookie 或借用宿主 Vue 实例
- 管理路由的响应正文由插件业务定义,不默认套用宿主管理 API 的 `{ code, message, data }`;页面与处理器共享实际合同,GET 无正文时不附带 JSON 正文声明
- `host.model.*` 子请求跳过发起插件,不能用它证明本插件中间件/路由已参与;需要完整请求链的示例使用页面模型桥或真实客户端
- JS、CSS、图标等资源随包构建和声明;沿用宿主桥的主题同步。Vite 本地预览不等于已安装插件的热更新,宿主验证仍需重新构建、打包和切换版本
@@ -0,0 +1,25 @@
# 清单与处理器实现
开发、排查或审查插件清单、Rust 处理器时完整读取,先完成[插件入口](../SKILL.md#先读职责边界)的边界判断,涉及 Rust 时执行[开发入口](../../cpr-dev-guide/SKILL.md)的 Rust 分支
使用能力前读取 SDK 的[能力与方法](../../../../backend/crates/gateway-plugin/sdk/docs/capabilities.md#能力与方法)及所用能力章节,已有实现的修复与审查也核对合同
## 清单与处理器
- `publisher` 与机器短名 `name` 派生插件 ID,`displayName` 用于展示;复制示例后同时调整扩展项 ID 与注册结果
- `contributes` 每种能力最多一项,派生 ID 和固定阶段可省略;能力版本与中间件阶段按[扩展项简写](../../../../backend/crates/gateway-plugin/sdk/docs/manifest.md#扩展项简写)声明。自定义完整扩展项 ID 属于自己的插件命名空间,运行注册与规范化清单一致
- 作者清单用 `Manifest::from_author_slice` 规范化,不手写 `package`、固定阶段或重复注册;CLI 生成平台、协议与资源摘要,版本范围不能使用全版本通配
- 安装后宿主会按 `configurationSchema` 准备默认配置,配置完整即可启用;默认绑定的空范围不限制请求。需要业务参数时声明真实必填项,通过 `secretFields` 声明敏感字段,不依赖用户再走一遍自建安装向导
- Rust 插件只依赖公开 `gateway-plugin-sdk`;需要异步会话辅助时开启 `io`,使用 `PluginSession` 管理握手、回调关联、流控、取消和关闭
- 优先用 `PluginBuilder::from_json` 组合 `.middleware`、`.management`、`.command_line` 与 `.on(methods::..., handler)`,由构建器生成注册并检查处理器;单一中间件也可用 `MiddlewarePlugin`
- stdout 是二进制协议通道,不能用 `println!` 输出诊断。使用基础设施 `host.log`,不输出 secret 或完整请求
## 中间件与宿主服务
- `request` 包裹整个逻辑请求,`attempt` 在每次选定账号的尝试中执行;根据作用范围选择,避免重试时重复副作用
- `next` 只能消费一次。使用 SDK 的正文保留与流式映射能力,不自行拼一套 SSE/取消/流控机制;HTTP、WebSocket、服务和模型请求按各自的类型化视图处理
- 持久化状态使用已声明命名空间的 `host.state.*`,账号读写使用 `host.auth.*`,不直接访问宿主数据库。见[账号与凭据](../../../../backend/crates/gateway-plugin/sdk/docs/capabilities.md#账号与凭据)和[状态、日志与迁移](../../../../backend/crates/gateway-plugin/sdk/docs/capabilities.md#状态日志与迁移)
- 按[完整信任](../../../../backend/crates/gateway-plugin/sdk/docs/manifest.md#完整信任)使用宿主回调,不添加权限或访问域声明;资源选择属于插件业务,父调用、期限、取消、实例 revision 与事务校验仍须满足
- 模型、网络和亲和查询使用当前父调用的受管回调。管理/CLI 等独立入口调用模型时传明确的 Key ID;请求处理阶段继承父身份。见[Key、模型与模型调用](../../../../backend/crates/gateway-plugin/sdk/docs/capabilities.md#key模型与模型调用)
- SDK 操作名不是管理员 HTTP 路由,不能据此拼接口地址;主动调用 HTTP 使用 `dispatch_http`,类型化服务使用 `HostClient::service`,实际执行与账单事实仍由宿主持有
- 路由与调度只读取宿主投影的请求事实,身份头可能被隐藏;使用已有会话和亲和合同,不依赖原始认证头或根据缓存键补造客户端身份
- 需要自定义上游编码、路径与响应解析时使用[受管上游适配器](../../../../backend/crates/gateway-plugin/sdk/docs/upstream-adapters.md),它是已选号后的终端,不调用 `next` 或自行组织换号重试;普通请求和响应加工仍使用中间件
+40
View File
@@ -0,0 +1,40 @@
---
name: cpr-release
description: Codex Proxy RS 的版本规划、发布说明、正式或预发行发布与失败恢复;部署重启和普通 Git tag 清理不属于发版
---
# 发版
## 范围与授权
沿用用户确定的版本、范围与授权,准备版本或询问能否发布不执行远程写入;明确发布时完成已授权步骤,无需重复确认
仅打 tag 不自动扩展为完整发版,发布也不包含运行实例升级或合并实验功能
稳定版与 alpha、beta、rc 从 `main` 发布,exp 从对应 `experimental/<用途>` 分支发布
其他仓库使用其发布入口与规则,不套用本仓库路径;规则已在上下文中时不重复读取
## 必须保持的边界
- Codex 兼容性变化按[问题与方案依据](../../../CONTRIBUTING.md#问题与方案依据)核对已有依据与验证范围,缺少依据列为发行风险,不以 CI 通过代替
- `release/publish --check <version>` 只读检查发行准备条件;不带 `--check` 会提交、打 tag、推送并触发工作流,实际发布仍需相应授权
- 同名版本、超时或中断先查本地与远端状态,不删已发布 tag、不移动 tag、不强推掩盖冲突
- 备版只更新源码的构建版本;通道快照是版本 Release 的 `version.yaml` 附件,验收时核对固定预发行页 `update-index` 指向的公开快照
- 预发行不得覆盖 GitHub Latest 或镜像 `latest`,完成后回读核对
- `release/notes.md` 只承载当前发行的说明;常规文档修改另按开发入口执行[文档检查](../cpr-dev-guide/references/documentation.md)
- 交付版本、提交、Release/工作流链接与实际结果,未完成构建或缺失产物时不称发布成功
## 按任务读取
先落实上述范围与边界,再读取当前阶段的完整指定章节;实际发布包含准备与验收,不能只读执行命令。阶段变化时补齐读取,准备文案不加载执行流程,实际发布前须核对发布入口的完整副作用
| 任务 | 读取范围 |
| --- | --- |
| 推荐版本号 | [基线与版本](references/preparation.md#基线与版本) |
| 编写或翻译发布说明 | [基线与版本](references/preparation.md#基线与版本)、[发布说明](references/preparation.md#发布说明) |
| 检查发行条件 | [发行准备检查](references/preparation.md#发行准备检查) |
| 实际发布 | 先完成[基线与版本](references/preparation.md#基线与版本)、[发布说明](references/preparation.md#发布说明)、[发行准备检查](references/preparation.md#发行准备检查),再执行[通道核对](references/operations.md#发行通道核对)、[执行发版](references/operations.md#执行发版)与[恢复与验收](references/operations.md#恢复与验收) |
| 失败恢复或验收 | [恢复与验收](references/operations.md#恢复与验收),先确定已完成步骤,不直接重跑入口 |
| 实验版 | 在对应阶段补读[实验流程](references/experimental.md);实际发布必须覆盖分支与版本、发布能力、文案和执行与验收 |
| 修复代码、工作流或常规文档 | 使用 [cpr-dev-guide](../cpr-dev-guide/SKILL.md),有 PR 任务再使用 [cpr-github-pr](../cpr-github-pr/SKILL.md) |
@@ -0,0 +1,6 @@
interface:
display_name: "cpr-release"
short_description: "版本规划、中文发布说明、正式与预发行发布及失败恢复"
default_prompt: "使用 $cpr-release 按当前请求准备版本与中文说明、执行已授权的发版或恢复失败步骤,核对渠道与实际结果。"
policy:
allow_implicit_invocation: true
@@ -0,0 +1,65 @@
# 实验版发布
按当前任务只读版本、发布能力、文案或验收章节,不因准备实验说明而读取执行流程
适用于独立于主版本维护、供用户主动选择体验的实验功能,入口使用仓库的 `release/publish`,构建复用目标实验分支的工作流与平台清单
## 分支与版本
- 从用户指定或会话已确定的 `experimental/<用途>` 分支发布,核对它的 upstream 和实验功能范围。
使用独立 worktree 时保留其他工作区内容;不把实验代码合入 `main`,也不顺带升级运行实例。
- 版本采用 `vX.Y.Z-exp.N`:`X.Y.Z` 表示本轮采用的正式版基线,`N` 从 1 开始递增。
同一基线的 `exp.N` 只代表同一轮实验,不得用于不同实验分支;开始其他实验时使用不同的实际基线,不能仅接续编号。
后续只有实际同步了新的正式版基线,才调整 `X.Y.Z`;调整后属于另一实验线,发布说明需交代手动迁移方式。
- 核对已有 tag、Release 与目标分支源码,先区分重试和新发行。已发布的旧命名版本保留,
不为统一名称移动 tag 或替换原有产物。
- GitHub Release 展示标题采用 `vX.Y.Z-<用途>.M`,`<用途>` 取实验分支名去掉 `experimental/` 前缀后的部分。
发布前通过 `gh api` 核对相同正式版基线与用途的已有 Release 标题和旧命名 tag,新发行取已有最大编号加 1,首次从 1 开始。
展示编号 `M` 独立于实际 tag 的 `exp.N`,所有同基线、同用途的已有发行均计入,不随 tag 命名格式重置;同一发行重试沿用已确定编号。
例如已有 `v3.10.0-codex-anti-degradation.1`,下一次 tag 即使是 `v3.10.0-exp.1`,标题也应为 `v3.10.0-codex-anti-degradation.2`。
- tag 和产物继续使用 `exp.N`:镜像版本去掉 tag 开头的 `v`,安装包沿用项目的版本、系统和架构命名。
发布说明首行匹配实际 tag,关联 Issue 和试验目的写在正文中,已有 Release 不因新发行而改名
## 先核对发布能力
在目标实验分支读取实际工作流,确认以下行为;新实验分支若缺少支持,先在既有流程中补齐并验证:
- `exp.N` 产生 `experimental` 构建类型,所有平台的二进制与镜像使用同一版本和源码提交。
应用内更新仅允许同一轮实验,规则见[版本命名与升级规则](../../../../deploy/README.md#版本命名与升级规则)。
发版前核对上一实验版是否已包含该更新器;仍禁用在线更新的旧构建需要先手动升级。
- GitHub Release 标为 Pre-release,且不设为 Latest;Docker 发布版本标签,不覆盖稳定版 `latest`。
发布任务应将该基线的最高公开实验版写入版本 Release 通道快照的 `published[大版本].exp[基线]`,不推进普通通道。
- 复用正式版平台清单、质量检查、完整安装包与多架构镜像,以及部署文件、校验和和现有产物证明。
对照本次清单确认平台,不以“实验版”为由仅发布单架构镜像。
- 部署说明交代与稳定实例并存的目录、配置、端口和数据库要求;需要 Compose 隔离配置时,
核对发布附件确实包含它,不能只在文案中承诺。
- 实验版正文使用下方完整文案,不再由工作流重复追加安装章节或自动生成混入正式版基线的提交列表。
- Release 的 `name` 使用上述展示标题。工作流若默认以 tag 为标题,发布成功后用 `gh release edit --title` 设置标题,
再通过 `gh api` 回读核对;仅修改 `name`,保留 tag、产物、正文和预发行状态。
## 中文发布说明
在目标分支的 `release/notes.md` 编写,首行为 `# vX.Y.Z-exp.N`。后续内容按实际需要组织:
1. **实验说明**:开头简述正式版基线、验证的问题与适用人群。效果尚未验证时明确说明,
不把假设写成已解决的问题,不承诺实验功能一定进入主版本。
2. **本次变化**:首次发行概括相对正式版基线新增的实验能力,关联 PR;后续发行只列相对上个实验版的变化。
同步主分支修复时说明它对实验用户的影响,不复制整份正式版更新记录。
3. **安装与使用**:给出本版本真实镜像命令、支持平台、部署文件及实验部署文档链接。
交代功能是否默认启用和必要配置;保留工作流实际提供的校验与证明说明。
4. **使用须知**:只写本次适用的已知限制、数据隔离、迁移与回退边界,以及在线更新条件和手动迁移方式。
5. **反馈与致谢**:链接关联 Issue 或已有反馈入口,感谢实际贡献者。没有相应内容的章节可以省略。
用户要求先审核文案时,先展示完整正文;已批准的同一版本文案无需重复确认。
正常新发行把说明纳入版本提交并从 tag 构建,不先创建公开空 Release 再回填附件。
## 执行与验收
按[版本与说明准备](preparation.md)的对应章节准备后,再按[执行发版](operations.md#执行发版)在实验分支调用入口
入口创建版本提交和 tag,并从该分支 dispatch 发布工作流;不要从 `main` 发布实验代码。
按[发行通道核对](operations.md#发行通道核对)确认稳定入口未改变,并检查实验线隔离与上一版本的更新能力。
检查展示标题的分支用途与历史续号、它所对应的实际 tag,以及目标 tag 的源码提交、完整产物、校验和、多架构平台和 Pre-release 标记。
同基线的 `X.Y.Z-exp.N` 按 SemVer 排序低于正式 `X.Y.Z`;渠道隔离仍以更新器过滤和发布标签规则为准。
失败时按[恢复与验收](operations.md#恢复与验收)从未完成步骤恢复,不通过换版本号或移动 tag 掩盖失败。
仅维护已有页面文案时不重新构建;报告实际完成状态、发布链接及验证缺口。
@@ -0,0 +1,52 @@
# 执行、恢复与验收
只在实际发布、恢复失败步骤或验收发行时读取对应章节,先核对[入口](../SKILL.md)中的范围与授权
## 发行通道核对
- alpha 是下一正式版本的早期开发阶段,beta 是功能测试阶段,rc 是发布候选,exp 是临时功能实验。
预发行采用 `vX.Y.Z-<阶段>.N`,`N` 从 1 开始;正式版不带预发行后缀,不新增未约定的通道名称。
- 更新候选由本次检查或安装选择的通道决定:Stable 仅正式版,RC 包含正式版,Beta 包含 RC 与正式版,Alpha 包含所有常规阶段。
仅接收同一大版本内严格更高的版本,可跟随后续小版本与补丁版本,默认按运行版本推导通道;临时选择不保存、不允许降级。
exp 仅接收同基线、同一轮实验的后续编号,不能跨实验线或转入其他阶段;不同实验分支不得共用一个 `X.Y.Z-exp.N` 系列。
- 核对目标工作流的构建类型:正式版及 alpha、beta、rc 为 `release`,exp 为 `experimental`。
所有预发行均标记为 Pre-release,不设置 GitHub Latest、不覆盖镜像 `latest`。
发布预发行前记录这两个稳定入口,发布后回读确认未改变。
- 验收更新器时,核对检查更新与执行更新使用相同规则。只有当前构建支持更新且存在允许的更高目标时
才能返回 `hasUpdate=true`;被禁止的目标即使版本领先,也不能出现在更新标记、可用版本或发布说明中。
检查失败须单独报告。核对上一发行实际包含的更新能力,不能因新包支持在线更新就宣称旧包也能在线安装它。
## 执行发版
调用入口前重新核对当前脚本的副作用、目标版本和待推送提交,保留无关工作区内容,不自动清理或暂存用户文件。
先编辑 `release/notes.md`,保留为未提交改动(已暂存或未暂存均可);其他已跟踪文件的改动应先单独提交。
脚本只改 `release/version.yaml` 的顶层 `version`,按[接口弃用规则](../../../../backend/crates/gateway-plugin/sdk/docs/manifest.md#接口弃用)校验并维护 `plugin-api-deprecations.json`,将这些差异与最终工作区中的发布说明一起提交为 `chore(release): vX.Y.Z`。
不要提前单独提交说明或把版本文件改成目标版本;脚本要求说明相对 HEAD 有改动,并自行更新版本号。
本任务需要的提交按[项目约定](../../../../CONTRIBUTING.md#项目约定)保留英文 Conventional Commits 与相应署名。
在准备完成且用户已授权发布后,将 `release_version` 设为已确定的版本,执行:
```bash
release/publish "$release_version"
```
该命令会创建上述版本提交与 annotated tag,原子推送分支和 tag,再从长期分支手动触发 `release.yml`。
只读预检使用 `release/publish --check "$release_version"`。发布工作流读取 tag 中的说明,修改 tag 之后的本地文件不会改变该次构建。
只调整已发布页面的标题或正文时,编辑对应 GitHub Release 的 `name` 或 `body` 并回读核对;
保持已有 tag、源码提交、附件和预发行状态,不重新执行发布入口。
## 恢复与验收
- 脚本中断或网络超时时,先核对本地提交与 tag、远端分支与 tag、对应工作流 run 及 Release,确定已完成到哪一步。
若本地已创建版本提交或 tag,不直接重跑入口,也不通过删 tag、回退版本或强推来消除报错。
- 提交和 tag 已推送但工作流未触发时,核对目标后使用脚本提示的 dispatch 命令补触发;
工作流已存在则跟踪或恢复对应 run,避免重复发布。发现 tag 指向不同提交时报告冲突,不移动已发布 tag。
- 跟踪与本次 tag 对应的 run ID,核对工作流解析的目标提交。触发工作流成功只表示已开始,不能当作发布完成。
- 构建与打包中间产物保留 7 天,在有效期内可重跑失败任务;产物过期后重跑完整工作流,不移动原 tag。
- 发布完成后核对 GitHub Release 的版本、正式版/预发行状态和中文变更正文;对照平台清单与工作流确认归档、校验和及镜像产物。
工作流附加的安装说明和自动提交列表按其当前配置处理,不用它们替代手写的中文变更说明。
- 回读固定预发行页 `update-index`(不是 GitHub Latest),按正文 JSON 的 `current` 下载对应 `v<current>` 的 `version.yaml`。核对快照版本及 `published`:正式版参与同一大版本四个普通通道的比较;RC 参与 RC、Beta、Alpha,Beta 参与 Beta、Alpha,Alpha 只参与自身。实验版只推进 `exp["X.Y.Z"]`。晚完成的发行不得回退较高候选或丢失其他发行线
- 只有指针写入失败时,在既有授权内执行 `gh workflow run release.yml --ref main -f tag="v$release_version" -f index_only=true`。恢复任务与发布共用锁,只核对已公开版本的附件和待发布快照摘要,不重新打包、改版本 tag 或写 main。完成后回读指针与附件;待发布记录不能当作成功。索引附件最多 128 KiB
- 返回版本、tag 对应提交、Release/工作流链接及实际结果。失败、仍在构建或缺失产物时明确说明,不能称为发布成功;
Release 和镜像已生成也不表示用户的运行实例已经升级。
@@ -0,0 +1,39 @@
# 版本与说明准备
只在规划版本、编写发布说明或准备发行时读取对应部分,不因准备文案加载执行流程
## 基线与版本
1. 确认类型、分支、upstream、目标仓库、工作区与完整待发布范围,保留无关改动;未跟踪文件也需核对
2. 读取 [version.yaml](../../../../release/version.yaml)、相关 tag 和实际 Release,区分源码构建版本和 `update-index` 指向的发行附件中的 `published` 通道表,结合目标通道确定上一发布基线与本次目标提交,再核对二者间的最终 diff,不只看最近几条提交
3. 使用用户确定的目标版本,建议版本时说明兼容性、能力或修复依据;同名 tag 先区分重试与新发行,不递增版本掩盖冲突
4. 命名与晋级按[版本命名与升级规则](../../../../deploy/README.md#版本命名与升级规则),实验版另读[实验流程](experimental.md#分支与版本)
## 发布说明
读取当前 [notes.md](../../../../release/notes.md)与[项目约定](../../../../CONTRIBUTING.md#项目约定)中的发布说明条目
- 只在发布说明任务中修改 `release/notes.md`,替换为本次版本内容,首行为目标 tag,正文使用中文
- 按用户可感知的最终变化归并,省略空章节,不逐条翻译提交,不写排查过程或未经验证的承诺
- 剔除回退、重复及未进入发行范围的内容,升级事项须能追溯到实际迁移、配置或兼容变化
- 只翻译已有准确说明时保持事实与范围,不混入其他变更
- 常规文档仍只描述当前状态;必要修订执行[文档检查](../../cpr-dev-guide/references/documentation.md),不复制发布说明中的版本变化列表
实际发布由 `release/publish` 提交说明、版本文件及[插件接口弃用清单](../../../../backend/crates/gateway-plugin/sdk/docs/manifest.md#接口弃用)的必要更新,准备说明时不要提前单独提交或把版本文件改成目标版本
用户只要求准备时保留本地改动,纯说明修改无需重建前后端
## 发行准备检查
进入实际发行准备后,读取目标分支的 [release/publish](../../../../release/publish)、[工作流](../../../../.github/workflows/release.yml)与[平台清单](../../../../release/platforms.yaml),按当前入口执行,不另写脚本或绕过检查
发布说明准备好、其他代码已提交后,执行 `release/publish --check <version>`,检查版本、说明、工作区、远端分支与 tag、工具链和插件兼容窗口。
预检不修改文件、Git 引用或远端状态;远端存在本地未知提交时先同步再检查。它不替代 CI、发行物验证或实际发布授权
按[验证要求](../../../../CONTRIBUTING.md#验证)核对目标提交的证据,区分通过、失败、跳过与未执行
数据库变更同时读取[迁移冻结规则](../../../../backend/migrations/README.md#冻结规则):
- 对照同一大版本的上一正式版和本轮预发行/实验线的已发布版本,验证迁移未改写及升级路径
- 区分未发布迁移、已执行迁移的源码部署实例与 CI 冻结基线,未发版不代表未执行,也不授权放宽冻结规则
- 兼容性与 CI 检查分别报告,跨大版本不默认支持在线升级
正式执行时再读取[通道核对](operations.md#发行通道核对)与[执行发版](operations.md#执行发版),触发工作流不等于发布成功
+3
View File
@@ -0,0 +1,3 @@
# SQLx migration checksums are byte-sensitive; keep migration inputs portable across hosts.
backend/migrations/*.sql text eol=lf
backend/migrations/.frozen-sha256 text eol=lf
@@ -0,0 +1,33 @@
body:
- type: markdown
attributes:
value: |
欢迎提问。提交前可以搜索已有讨论,并查看[部署文档](https://github.com/zyycn/codex-proxy-rs/blob/main/deploy/README.md)。
明确的程序故障请使用 [Issue 模板](https://github.com/zyycn/codex-proxy-rs/issues/new/choose);不确定是不是 Bug,也可以先在这里交流。
请移除密钥、令牌、Cookie、代理认证和私人内容,不要上传完整配置或请求转储。解决后请标记帮助到你的答案。
- type: textarea
id: question
attributes:
label: 想解决什么问题
description: 说明希望完成的任务、当前遇到的困难,标题尽量具体。
validations:
required: true
- type: input
id: version
attributes:
label: 版本与部署方式
description: 涉及具体实例时,填写完整版本号或提交号,以及 Docker、二进制或源码部署方式。
- type: textarea
id: attempts
attributes:
label: 已尝试的方法与结果
description: 可以附看过的文档、相关讨论、脱敏错误片段或截图;未尝试可留空。
- type: textarea
id: environment
attributes:
label: 相关环境与补充信息
description: 按问题需要补充操作系统、客户端、反向代理或中转链路,无需提供无关信息。
@@ -0,0 +1,44 @@
body:
- type: markdown
attributes:
value: |
欢迎分享实验版本的使用体验,包括有效、无明显变化或遇到限制的情况。
请先查看对应[预发行页面](https://github.com/zyycn/codex-proxy-rs/releases)的部署与数据隔离说明,并搜索已有反馈。
明确的程序缺陷请使用 [Issue 模板](https://github.com/zyycn/codex-proxy-rs/issues/new/choose),在两处互相附链接即可,无需重复粘贴。
请移除密钥、令牌、Cookie、代理认证和私人内容,不要上传完整配置或请求转储。
- type: input
id: version
attributes:
label: 实验版本或提交号
description: 填写实际运行的完整版本号或提交号,不要只写 exp 或 latest。
validations:
required: true
- type: textarea
id: scenario
attributes:
label: 使用场景与环境
description: 说明启用的实验功能、部署方式、客户端及与结果相关的条件,不需要提供账号身份信息。
validations:
required: true
- type: textarea
id: observation
attributes:
label: 观察到的效果或限制
description: 描述实际现象、出现频率和观察时长;请区分观察结果与原因推测。
validations:
required: true
- type: textarea
id: comparison
attributes:
label: 对照与复现信息
description: 如已对比启用前后或不同配置,请说明条件和结果;没有对照也可以反馈,无需额外测试。
- type: textarea
id: references
attributes:
label: 相关链接与补充材料
description: 可附已有 Issue、讨论、脱敏日志片段或截图。
@@ -0,0 +1,29 @@
body:
- type: markdown
attributes:
value: |
这里适合交流尚在探索的想法、方案取舍和使用场景,不要求先给出完整实现。
已有明确场景与预期行为时,可以直接提交[功能建议 Issue](https://github.com/zyycn/codex-proxy-rs/issues/new?template=feature-request.yml),无需先开讨论。
请先搜索相关讨论,每个主题围绕一个问题展开。
- type: textarea
id: use-case
attributes:
label: 使用场景与当前不便
description: 希望完成什么任务,目前的做法有什么不足?
validations:
required: true
- type: textarea
id: idea
attributes:
label: 想法与希望讨论的方向
description: 可以说明期望体验、考虑过的替代方法,以及还不确定的地方。
validations:
required: true
- type: textarea
id: references
attributes:
label: 参考资料
description: 可附相关 Issue、讨论或其他项目的具体行为;示例和截图请先脱敏。
+173
View File
@@ -0,0 +1,173 @@
name: 接口问题反馈
description: 客户端调用模型、搜索等 API 时的报错、断流、超时或响应异常
body:
- type: markdown
attributes:
value: |
管理端页面、账号导入或服务部署的问题请选择“常规问题反馈”;新增能力请选择“功能建议”。
请提供最小复现信息。请求、日志和截图发布前请移除密钥、令牌、Cookie、代理认证及私人内容。
- type: textarea
id: description
attributes:
label: 问题描述
description: 说明发生了什么、实际结果以及出现的频率;已有正常账号、模型或客户端的对照结果也可说明。
validations:
required: true
- type: input
id: version
attributes:
label: 服务版本
description: 填写实际运行的完整版本号,不要只写“最新”或 latest;源码运行填写提交号。
placeholder: 完整版本号或提交号
validations:
required: true
- type: dropdown
id: deployment
attributes:
label: 部署方式
options:
- Docker Compose
- 其他容器部署
- 二进制运行
- 源码运行
- 其他方式(请在环境信息中说明)
validations:
required: true
- type: textarea
id: environment
attributes:
label: 环境信息
description: 填写服务端与客户端的操作系统及版本;性能或超时问题可补充 CPU、内存和容器资源限制。
placeholder: |
服务端操作系统与版本:
客户端操作系统与版本:
相关资源限制(如有):
validations:
required: true
- type: input
id: client-name
attributes:
label: 客户端名称
description: 区分 CLI、桌面端、编辑器扩展、SDK 或自编脚本。
placeholder: 例如 Codex CLI、Codex Desktop 或某个编辑器扩展
validations:
required: true
- type: input
id: client-version
attributes:
label: 客户端版本
description: 填写完整版本号;Codex CLI 可填写 codex --version 的输出。无法取得时说明原因。
placeholder: 完整版本号或相关 SDK 版本
validations:
required: true
- type: input
id: client-source
attributes:
label: 客户端安装来源与启动方式
description: 说明安装来源,以及是否通过包装脚本、启动器或第三方修改版运行;不确定时填写已知部分。
placeholder: 例如通过 npm 安装 @openai/codex,直接执行 codex
validations:
required: true
- type: input
id: endpoint
attributes:
label: 请求方法与端点
description: 填写发生异常的请求方法与路径,无需真实域名;不清楚端点时说明客户端当时执行的具体操作。
placeholder: 例如 POST /v1/responses、POST /v1/alpha/search 或 WebSocket /v1/responses
validations:
required: true
- type: dropdown
id: intermediary
attributes:
label: 是否经过中转或反向代理
description: |
指客户端到本服务之间的链路。API 中转包括 New API、One API、CCH 等;反向代理或 CDN 包括 Nginx、Caddy、Cloudflare Tunnel 等。
如有中转,请在下方填写链路详情;本服务到上游的出站代理请在补充信息中说明。
options:
- 否,直接连接本服务
- 是,仅经过 API 中转服务
- 是,仅经过反向代理或 CDN
- 是,同时经过 API 中转和反向代理或 CDN
- 不确定
validations:
required: true
- type: textarea
id: request-chain
attributes:
label: 请求链路
description: |
按调用顺序填写中间服务的名称与版本,例如:Codex → New API → Nginx → 本服务。不确定时填写已知部分。
如果已测试过直连本服务,请说明是否仍能复现;未测试可注明,不要求先完成对照测试。
placeholder: |
调用顺序与各中间服务版本:
是否启用请求参数或请求头改写(如已知):
直连本服务的对照结果(如已测试):
- type: textarea
id: account-model
attributes:
label: 平台、账号类型与模型
description: 填写与失败请求相关的信息,不要提供账号凭据;不涉及模型或无法确认的信息请注明。
placeholder: |
平台与认证类型:
账号套餐(如相关):
请求模型与映射后的上游模型(如已知):
- type: textarea
id: reproduction
attributes:
label: 复现步骤
description: 描述触发问题的操作;无法稳定复现时,说明已知触发条件。
placeholder: |
1. 使用什么配置或输入
2. 执行什么操作
3. 出现什么结果
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: 预期行为
validations:
required: true
- type: textarea
id: request-response
attributes:
label: 请求、响应与诊断附件
description: |
提供脱敏后的最小请求、网关实际状态/响应片段和客户端显示,区分上游、中转、网关与客户端的错误来源。
可在此直接上传管理端默认诊断包并检查其缺失说明;原始错误与日志片段需另行审阅脱敏,粘贴时请使用 Markdown 代码块。
按部署文档的“请求错误排查”检查关联 ID、错误详情与 application 日志,不默认开启完整 dump;无法取得的信息请注明。
placeholder: |
发生时间与时区:
传输(HTTP JSON / SSE / WebSocket):
脱敏后的最小请求:
网关关联 ID(x-gateway-request-id;配置的入口头请单独注明):
上游关联 ID(x-request-id / x-oai-request-id,注明来源):
网关实际状态、Content-Type 与响应片段:
中转服务的状态与错误(如有):
客户端显示的错误:
诊断包或无法取得的信息:
- type: textarea
id: additional-context
attributes:
label: 补充信息
description: 最近有升级时填写升级前后版本及前后端是否同步更新;网络问题补充出站代理、系统代理、TUN 和自定义证书等相关环境。
placeholder: |
服务或客户端升级前版本 → 当前版本(如有):
服务前后端是否同步更新(如有升级):
最近的配置变更:
其他相关环境:
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: true
contact_links:
- name: 使用问答
url: https://github.com/zyycn/codex-proxy-rs/discussions/categories/使用问答
about: 安装部署、配置和日常使用求助;明确的 Bug 请选择上方问题反馈模板。
- name: 想法与方案讨论
url: https://github.com/zyycn/codex-proxy-rs/discussions/categories/想法讨论
about: 探索尚未明确的需求与方案;具体功能需求也可以直接提交功能建议。
- name: 实验版体验反馈
url: https://github.com/zyycn/codex-proxy-rs/discussions/categories/实验反馈
about: 分享实验版本的使用效果、适用场景和限制。
@@ -0,0 +1,39 @@
name: 功能建议
description: 说明使用场景、当前不便和期望行为,提出新能力或体验改进
body:
- type: markdown
attributes:
value: |
如果已有功能无法正常使用,请选择对应的故障模板。提交前可搜索已有 Issues,相关讨论请附链接。
每个 Issue 尽量围绕一个使用场景;截图和示例请移除密钥、令牌、Cookie、代理认证及私人内容。
- type: textarea
id: use-case
attributes:
label: 使用场景与当前不便
description: 说明你希望完成什么任务、目前如何操作,以及现有能力有什么不足。
placeholder: |
希望完成的任务:
当前做法与不便:
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: 期望行为
description: 描述改进后希望如何使用,最好给出一个具体例子;不要求先设计技术方案。
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 已尝试的替代方案
description: 如果已有配置、操作方式或其他工具能部分满足需求,说明仍缺少什么;未尝试可留空。
- type: textarea
id: additional-context
attributes:
label: 补充信息
description: 可附当前使用的具体版本、相关 Issue、截图或参考链接;引用其他项目时说明希望借鉴的具体行为。
@@ -0,0 +1,142 @@
name: 常规问题反馈
description: 管理端页面、账号导入、安装部署、升级或其他非客户端 API 问题
body:
- type: markdown
attributes:
value: |
客户端调用模型、搜索等 API 的问题请选择“接口问题反馈”;新增能力或体验改进请选择“功能建议”。
请提供最小复现信息。请求、日志和截图发布前请移除密钥、令牌、Cookie、代理认证及私人内容。
- type: dropdown
id: issue-type
attributes:
label: 问题类型
options:
- 页面或交互异常
- 配置或部署异常
- 升级异常
- 其他问题
validations:
required: true
- type: textarea
id: description
attributes:
label: 问题描述
description: 说明发生了什么、实际结果以及出现的频率。
validations:
required: true
- type: input
id: version
attributes:
label: 服务版本
description: 填写实际运行的完整版本号,不要只写“最新”或 latest;源码运行填写提交号。服务无法启动时填写准备运行的版本。
placeholder: 完整版本号或提交号
validations:
required: true
- type: dropdown
id: deployment
attributes:
label: 部署方式
options:
- Docker Compose
- 其他容器部署
- 二进制运行
- 源码运行
- 其他方式(请在环境信息中说明)
validations:
required: true
- type: textarea
id: environment
attributes:
label: 环境信息
description: 填写操作系统与版本,并补充与问题相关的浏览器、访问方式或资源信息。
placeholder: |
操作系统与版本:
浏览器与版本(页面问题):
页面访问方式(HTTP / HTTPS,localhost / IP / 域名,无需真实地址):
CPU 核数与内存、容器资源限制(部署或性能问题):
Docker / Compose 版本(容器部署问题):
validations:
required: true
- type: dropdown
id: intermediary
attributes:
label: 是否经过中转或反向代理
description: |
指客户端或浏览器到本服务之间的链路。API 中转包括 New API、One API、CCH 等;反向代理或 CDN 包括 Nginx、Caddy、Cloudflare Tunnel 等。
如有中转,请在下方填写链路详情;本服务到上游的出站代理请在补充信息中说明。
options:
- 否,直接连接本服务
- 是,仅经过 API 中转服务
- 是,仅经过反向代理或 CDN
- 是,同时经过 API 中转和反向代理或 CDN
- 不确定
- 不适用(例如服务尚未启动)
validations:
required: true
- type: textarea
id: request-chain
attributes:
label: 请求链路
description: |
按调用顺序填写中间服务的名称与版本,例如:浏览器 → Cloudflare → Nginx → 本服务。不确定时填写已知部分。
如果已测试过直连本服务,请说明是否仍能复现;未测试可注明,不要求先完成对照测试。
placeholder: |
调用顺序与各中间服务版本:
是否启用请求参数或请求头改写(如已知):
直连本服务的对照结果(如已测试):
- type: textarea
id: reproduction
attributes:
label: 复现步骤
description: 描述触发问题的操作;批量操作补充数量,升级问题说明升级方式。无法稳定复现时,说明已知触发条件。
placeholder: |
1. 使用什么配置或输入
2. 执行什么操作
3. 出现什么结果
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: 预期行为
validations:
required: true
- type: textarea
id: logs
attributes:
label: 错误信息与日志
description: |
提供脱敏后的页面提示、浏览器控制台错误或相关启动、升级日志;如有失败请求,补充状态码与响应片段。
可以在此上传诊断包或日志附件;粘贴日志时请使用 Markdown 代码块。不要上传完整配置或请求转储。
placeholder: |
发生时间与时区:
页面提示或终端错误:
浏览器控制台 / 服务日志:
失败请求的状态码与响应片段(如有):
- type: textarea
id: screenshots
attributes:
label: 截图或录屏
description: 页面或交互问题可在此拖入相关截图、录屏,并说明对应的操作。
- type: textarea
id: additional-context
attributes:
label: 补充信息
description: 最近有升级时填写升级前后版本及前后端是否同步更新;网络问题补充出站代理、系统代理、TUN 和自定义证书等相关环境。
placeholder: |
升级前版本 → 当前版本(如有):
前后端是否同步更新(如有升级):
最近的配置变更:
其他相关环境:
+76
View File
@@ -0,0 +1,76 @@
name: Build binary
description: Build a native release binary with shared dependency caching and timings
inputs:
rust:
description: Pinned Rust toolchain
required: true
target:
description: Rust target triple
required: true
platform:
description: Release platform name, such as linux-amd64
required: true
binary:
description: Executable filename
default: codex-proxy-rs
rust-image:
description: Pinned Linux builder image; empty for native builds
default: ''
runs:
using: composite
steps:
- name: Setup Rust
uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master
with:
toolchain: ${{ inputs.rust }}
targets: ${{ inputs.target }}
- name: Configure Windows runtime linkage
if: runner.os == 'Windows'
shell: bash
# 发行包静态链接 MSVC 运行库,解压运行时无需安装 VC++ Redistributable
run: echo 'RUSTFLAGS=-C target-feature=+crt-static' >> "$GITHUB_ENV"
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
workspaces: backend -> target
shared-key: release-${{ inputs.target }}-${{ inputs.rust-image }}
cache-bin: false
# 只缓存依赖,避免随版本变化的 workspace 产物挤占缓存
save-if: ${{ github.event_name != 'pull_request' }}
- name: Build binary
shell: bash
working-directory: backend
env:
BUILD_TARGET: ${{ inputs.target }}
BUILD_PLATFORM: ${{ inputs.platform }}
BUILD_BINARY: ${{ inputs.binary }}
RUST_IMAGE: ${{ inputs.rust-image }}
run: |
set -euo pipefail
if [ -n "$RUST_IMAGE" ]; then
mkdir -p "$HOME/.cargo"
docker run --rm --user "$(id -u):$(id -g)" \
-e CARGO_HOME=/cargo -e HOME=/tmp \
-e CPR_VERSION -e CPR_GIT_SHA -e CPR_BUILD_TIME -e CPR_BUILD_TYPE \
-v "$HOME/.cargo:/cargo" -v "$PWD/..:/workspace" \
-w /workspace/backend "$RUST_IMAGE" \
cargo build --release --locked --timings --target "$BUILD_TARGET" --bin codex-proxy-rs
else
cargo build --release --locked --timings --target "$BUILD_TARGET" --bin codex-proxy-rs
fi
"target/$BUILD_TARGET/release/$BUILD_BINARY" --help
"target/$BUILD_TARGET/release/$BUILD_BINARY" --version
mkdir -p "../dist/binaries/$BUILD_PLATFORM"
cp "target/$BUILD_TARGET/release/$BUILD_BINARY" "../dist/binaries/$BUILD_PLATFORM/$BUILD_BINARY"
- name: Upload build timings
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: build-timings-${{ inputs.platform }}
path: backend/target/cargo-timings/
if-no-files-found: ignore
retention-days: 7
+46
View File
@@ -0,0 +1,46 @@
name: Build frontend
description: Validate and build the frontend, optionally uploading release assets
inputs:
upload-artifact:
description: Upload frontend-dist for downstream release jobs
default: 'false'
runs:
using: composite
steps:
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
package_json_file: frontend/package.json
- name: Read build toolchains
id: toolchains
shell: bash
run: ruby .github/scripts/toolchains.rb >> "$GITHUB_OUTPUT"
- name: Setup Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ steps.toolchains.outputs.node }}
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install frontend dependencies
shell: bash
run: pnpm --dir frontend install --frozen-lockfile
- name: Frontend format
shell: bash
run: pnpm --dir frontend format:check
- name: Frontend build
shell: bash
run: pnpm --dir frontend build
- name: Upload frontend artifact
if: inputs.upload-artifact == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: frontend-dist
path: frontend/dist
if-no-files-found: error
retention-days: 7
+258
View File
@@ -0,0 +1,258 @@
name: Verify container
description: Scan and validate a built image with the deployment Compose configuration
inputs:
image:
description: Local image tag or immutable registry digest to validate
required: true
platform:
description: Native runner image platform
required: true
artifact-name:
description: Unique diagnostics artifact name within the workflow run
required: true
runs:
using: composite
steps:
- name: Define isolated runtime values
shell: bash
env:
IMAGE: ${{ inputs.image }}
run: |
set -euo pipefail
admin_password="$(openssl rand -hex 24)"
postgres_password="$(openssl rand -hex 24)"
redis_password="$(openssl rand -hex 24)"
echo "::add-mask::$admin_password"
echo "::add-mask::$postgres_password"
echo "::add-mask::$redis_password"
{
echo "CPR_IMAGE=$IMAGE"
echo "COMPOSE_PROJECT_NAME=cpr-check-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${RUNNER_ARCH,,}"
echo "DIAGNOSTICS_DIR=$RUNNER_TEMP/codex-proxy-rs-diagnostics"
echo "CI_ADMIN_PASSWORD=$admin_password"
echo "CI_POSTGRES_PASSWORD=$postgres_password"
echo "CI_REDIS_PASSWORD=$redis_password"
} >> "$GITHUB_ENV"
- name: Scan container image
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
env:
TRIVY_PLATFORM: ${{ inputs.platform }}
TRIVY_QUIET: 'true'
TRIVY_SKIP_VERSION_CHECK: 'true'
with:
image-ref: ${{ inputs.image }}
version: v0.72.0
format: json
output: trivy-results.json
severity: HIGH,CRITICAL
vuln-type: os,library
scanners: vuln
ignore-unfixed: true
exit-code: '0'
- name: Generate Trivy SARIF
shell: bash
run: trivy convert --format sarif --output trivy-results.sarif trivy-results.json
- name: Upload Trivy SARIF
if: >-
${{ always() && hashFiles('trivy-results.sarif') != '' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository) }}
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
sarif_file: trivy-results.sarif
category: runtime-container-${{ inputs.platform }}
- name: Enforce Trivy vulnerability gate
shell: bash
run: trivy convert --format table --severity HIGH,CRITICAL --exit-code 1 trivy-results.json
- name: Prepare isolated runtime
shell: bash
run: |
set -euo pipefail
umask 0077
# Bind roots hand off ownership to the image-specific database users.
# Sticky directories keep that initialization compatible with user-namespaced runners.
install -d -m 1777 .runtime/postgres .runtime/redis
mkdir -p "$DIAGNOSTICS_DIR"
sudo install -d -m 0770 -o "$(id -u)" -g 10001 \
.runtime/data .runtime/logs
ruby <<'RUBY'
path = "deploy/config.example.yaml"
content = File.read(path)
replacements = {
"password: &postgres_password ''" => "password: &postgres_password '#{ENV.fetch("CI_POSTGRES_PASSWORD")}'",
"password: &redis_password ''" => "password: &redis_password '#{ENV.fetch("CI_REDIS_PASSWORD")}'",
"default_password: ''" => "default_password: '#{ENV.fetch("CI_ADMIN_PASSWORD")}'",
}
replacements.each do |placeholder, configured|
raise "missing config placeholder: #{placeholder}" unless content.sub!(placeholder, configured)
end
File.write("deploy/config.yaml", content)
RUBY
sudo chown "$(id -u):10001" deploy/config.yaml
chmod 0640 deploy/config.yaml
- name: Validate Docker Compose
shell: bash
run: |
set -euo pipefail
docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml config --quiet
- name: Configure Redis host memory overcommit
shell: bash
run: |
set -euo pipefail
sudo sysctl -w vm.overcommit_memory=1
test "$(sysctl -n vm.overcommit_memory)" = 1
- name: Verify PostgreSQL and Redis volume persistence
shell: bash
run: |
set -euo pipefail
compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml)
"${compose[@]}" up -d --no-build --wait postgres redis
# 基础设施使用 UTC,业务展示和调度时区由应用配置独立控制。
test "$("${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \
psql -At -U codex_proxy -d codex_proxy -c "SHOW TimeZone;" | tr -d '\r')" = UTC
"${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \
psql -v ON_ERROR_STOP=1 -U codex_proxy -d codex_proxy -c \
"CREATE TABLE cpr_ci_persistence_probe (value text PRIMARY KEY); INSERT INTO cpr_ci_persistence_probe VALUES ('persisted');"
"${compose[@]}" exec -T -e REDISCLI_AUTH="$CI_REDIS_PASSWORD" redis \
redis-cli SET cpr:ci:persistence-probe persisted
"${compose[@]}" up -d --no-build --wait --force-recreate postgres redis
test "$("${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \
psql -At -U codex_proxy -d codex_proxy -c \
"SELECT value FROM cpr_ci_persistence_probe;")" = persisted
test "$("${compose[@]}" exec -T -e REDISCLI_AUTH="$CI_REDIS_PASSWORD" redis \
redis-cli --raw GET cpr:ci:persistence-probe | tr -d '\r')" = persisted
"${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \
psql -v ON_ERROR_STOP=1 -U codex_proxy -d codex_proxy -c \
"DROP TABLE cpr_ci_persistence_probe;"
"${compose[@]}" exec -T -e REDISCLI_AUTH="$CI_REDIS_PASSWORD" redis \
redis-cli DEL cpr:ci:persistence-probe
- name: Start and verify application
shell: bash
run: |
set -euo pipefail
compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml)
"${compose[@]}" up -d --no-build --wait codex-proxy-rs
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
"http://127.0.0.1:8080/healthz")"
test "$status" = 204
test "$("${compose[@]}" exec -T codex-proxy-rs id -u | tr -d '\r')" = 10001
test "$("${compose[@]}" exec -T codex-proxy-rs id -g | tr -d '\r')" = 10001
test "$("${compose[@]}" exec -T codex-proxy-rs date +%z | tr -d '\r')" = +0000
test "$("${compose[@]}" exec -T codex-proxy-rs cat /etc/timezone | tr -d '\r')" = UTC
"${compose[@]}" exec -T codex-proxy-rs sh -c \
'touch /app/.runtime/data/.ci-write /app/.runtime/logs/.ci-write && rm /app/.runtime/data/.ci-write /app/.runtime/logs/.ci-write'
- name: Verify application restart after installer permissions
shell: bash
run: |
set -euo pipefail
compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml)
"${compose[@]}" stop --timeout 30 codex-proxy-rs
# 使用安装器原函数,验证已有备份暂存目录与日志不会因所有权变更而阻止启动
(
export INSTALL_DIR="$PWD"
source deploy/install.sh
fix_runtime_permissions
)
"${compose[@]}" up -d --no-build --wait codex-proxy-rs
- name: Verify clean logs and graceful shutdown
shell: bash
run: |
set -euo pipefail
compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml)
"${compose[@]}" stop --timeout 30 codex-proxy-rs
container_id="$("${compose[@]}" ps -a -q codex-proxy-rs)"
test -n "$container_id"
test "$(docker inspect --format '{{.State.ExitCode}}' "$container_id")" = 0
"${compose[@]}" logs --no-color --timestamps codex-proxy-rs \
> "$DIAGNOSTICS_DIR/application.log"
"${compose[@]}" logs --no-color --timestamps postgres \
> "$DIAGNOSTICS_DIR/postgres.log"
"${compose[@]}" logs --no-color --timestamps redis \
> "$DIAGNOSTICS_DIR/redis.log"
if grep --extended-regexp --ignore-case \
'"level"[[:space:]]*:[[:space:]]*"(warn|error|fatal|panic)"|(^|[^[:alnum:]_])panic([^[:alnum:]_]|$)' \
"$DIAGNOSTICS_DIR/application.log" \
|| grep --extended-regexp --ignore-case \
'(^|[[:space:]])(warning|error|fatal|panic):' \
"$DIAGNOSTICS_DIR/postgres.log" \
|| grep --extended-regexp --ignore-case \
'[0-9]+:[A-Z][[:space:]].*[[:space:]]#[[:space:]]|(^|[|[:space:]])(warning|error|fatal|panic)(:|[[:space:]])' \
"$DIAGNOSTICS_DIR/redis.log"; then
echo "container logs contain a warning or error severity" >&2
exit 1
fi
- name: Collect container diagnostics
if: always()
shell: bash
run: |
set +e
mkdir -p "$DIAGNOSTICS_DIR"
raw_dir="$(mktemp -d)"
compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml)
"${compose[@]}" config > "$raw_dir/compose-config.yaml" 2>&1
"${compose[@]}" ps -a > "$raw_dir/compose-ps.txt" 2>&1
"${compose[@]}" logs --no-color --timestamps > "$raw_dir/compose.log" 2>&1
mapfile -t container_ids < <("${compose[@]}" ps -a -q)
if [ "${#container_ids[@]}" -gt 0 ]; then
docker inspect "${container_ids[@]}" > "$raw_dir/docker-inspect.json" 2>&1
fi
RAW_DIR="$raw_dir" OUTPUT_DIR="$DIAGNOSTICS_DIR" ruby <<'RUBY'
raw_dir = ENV.fetch("RAW_DIR")
output_dir = ENV.fetch("OUTPUT_DIR")
secrets = [
ENV.fetch("CI_ADMIN_PASSWORD"),
ENV.fetch("CI_POSTGRES_PASSWORD"),
ENV.fetch("CI_REDIS_PASSWORD"),
]
encoded_secrets = secrets.map do |secret|
secret.encode("UTF-8").bytes.map do |byte|
character = byte.chr
character.match?(/[A-Za-z0-9]/) ? character : format("%%%02X", byte)
end.join
end
Dir.children(raw_dir).each do |name|
content = File.binread(File.join(raw_dir, name))
(secrets + encoded_secrets).each do |secret|
content = content.gsub(secret, "<redacted>")
end
File.binwrite(File.join(output_dir, name), content)
end
RUBY
rm -rf "$raw_dir"
cp trivy-results.json trivy-results.sarif "$DIAGNOSTICS_DIR/" 2>/dev/null || true
- name: Upload container diagnostics
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ inputs.artifact-name }}
path: ${{ runner.temp }}/codex-proxy-rs-diagnostics
if-no-files-found: warn
retention-days: 7
- name: Remove isolated runtime
if: always()
shell: bash
run: |
docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml down --remove-orphans || true
sudo rm -rf "$GITHUB_WORKSPACE/.runtime" "$GITHUB_WORKSPACE/deploy/config.yaml"
+28 -1
View File
@@ -1,8 +1,10 @@
version: 2
# 标签由维护者手动添加,各生态关闭默认打标,避免重新创建旧标签。
updates:
- package-ecosystem: github-actions
directory: /
labels: []
schedule:
interval: weekly
day: monday
@@ -16,11 +18,33 @@ updates:
- package-ecosystem: cargo
directory: /backend
labels: []
schedule:
interval: weekly
day: monday
time: '09:10'
timezone: Asia/Shanghai
# 按官方 Codex 锁文件、协议指纹或已审计 fork 固定;完成对应核对与实测后再手动升级。
ignore:
- dependency-name: aws-lc-rs
- dependency-name: hyper
- dependency-name: hyper-rustls
- dependency-name: hyper-tls
- dependency-name: hyper-util
- dependency-name: native-tls
- dependency-name: openssl
- dependency-name: openssl-src
- dependency-name: openssl-sys
- dependency-name: reqwest
- dependency-name: rustls
- dependency-name: rustls-native-certs
- dependency-name: rustls-pki-types
- dependency-name: tokio
- dependency-name: tokio-native-tls
- dependency-name: tokio-rustls
- dependency-name: tokio-tungstenite
- dependency-name: tungstenite
- dependency-name: zstd
groups:
minor-and-patch:
update-types:
@@ -29,6 +53,7 @@ updates:
- package-ecosystem: npm
directory: /frontend
labels: []
schedule:
interval: weekly
day: monday
@@ -43,7 +68,9 @@ updates:
- patch
- package-ecosystem: docker
directory: /deploy
directories:
- /deploy
labels: []
schedule:
interval: weekly
day: monday
+20
View File
@@ -0,0 +1,20 @@
## 问题与改动
<!-- 说明触发场景、问题依据和最终行为;涉及上游时注明官方源码版本与位置。 -->
## 自审与验证
<!-- 简述实际执行的检查、结果与缺口,不把跳过写成通过。
后端说明相关职责归属、复用与边界自审结果;页面说明沿用的设计及必要调整。
流程见 CONTRIBUTING.md;AI 开发执行 cpr-dev-guide,准备或更新 PR 执行 cpr-github-pr。 -->
## 页面截图
<!-- 无页面变更时删除本节。有页面变更必须嵌入或链接审查者可访问的实际截图:
现有页面提供前后对比,新增页面提供完成态;补充受影响状态,浮层展示展开状态。
注明入口、状态和对应提交;预览步骤、录屏、本机路径不能替代截图。截图待补时保留 Draft。 -->
## AI 使用
<!-- 写明未使用 AI;使用时列出工具名称与实际模型型号,多模型分别列出,可补充参与范围。
不能只填 Codex、ChatGPT 等工具或产品名。型号不可得时如实标为待补,不猜测,补齐前保留 Draft。 -->
+79
View File
@@ -0,0 +1,79 @@
require 'digest'
require 'fileutils'
require_relative 'release-metadata'
# 归档与镜像消费相同二进制和封口清单,不在打包时重新解释插件分发策略
module ReleasePackage
APP_NAME = ReleaseMetadata::APP_NAME
MANIFEST = 'dist/plugin-release-assets/plugin-release-manifest.json'
def self.install_manifest(directory)
manifest = JSON.parse(File.read(MANIFEST))
unless manifest['schema_version'] == 1 && manifest['sealed'] == true && manifest['plugins'] == []
raise 'Expected a sealed host manifest without bundled plugins'
end
FileUtils.mkdir_p(directory)
destination = File.join(directory, File.basename(MANIFEST))
FileUtils.cp(MANIFEST, destination)
File.chmod(0o444, destination)
File.chmod(0o555, directory)
end
def self.archives
platforms = JSON.parse(ENV.fetch('PLATFORM_MATRIX')).fetch('include')
release_dir = 'dist/release'
FileUtils.rm_rf(release_dir)
FileUtils.mkdir_p(release_dir)
# 上一次打包生成的只读清单目录需要先恢复写权限
FileUtils.chmod_R('u+w', 'package') if File.directory?('package')
FileUtils.rm_rf('package')
config = 'deploy/config.example.yaml'
FileUtils.cp(config, release_dir)
compose = File.read('deploy/compose.yaml')
image = "#{ENV.fetch('REGISTRY')}/#{ENV.fetch('IMAGE_NAME')}"
default_image = "${CPR_IMAGE:-#{image}:latest}"
raise 'Expected exactly one default application image in deploy/compose.yaml' unless compose.scan(default_image).length == 1
File.write("#{release_dir}/compose.yaml", compose.sub(default_image, "${CPR_IMAGE:-#{image}:#{ENV.fetch('DOCKER_VERSION')}}"))
FileUtils.cp(MANIFEST, release_dir)
platforms.each do |platform|
raise "Unsupported archive format: #{platform['archive']}" unless platform['archive'] == 'tar.gz'
name = "#{platform.fetch('os')}-#{platform.fetch('arch')}"
binary = platform.fetch('binary')
root = "package/#{name}/#{APP_NAME}"
FileUtils.mkdir_p(["#{root}/web/dist", "#{root}/deploy"])
FileUtils.cp("dist/binaries/#{APP_NAME}-#{name}/#{binary}", "#{root}/#{binary}")
File.chmod(0o755, "#{root}/#{binary}")
FileUtils.cp_r('dist/frontend/.', "#{root}/web/dist")
FileUtils.cp(config, "#{root}/deploy")
install_manifest("#{root}/plugins/official")
archive = File.join(release_dir, ReleaseMetadata.archive_name(ENV.fetch('VERSION'), platform))
raise "Failed to create #{archive}" unless system('tar', '-czf', archive, '-C', File.dirname(root), APP_NAME)
end
checksums = Dir.glob("#{release_dir}/*").sort.map { |path| "#{Digest::SHA256.file(path).hexdigest} #{File.basename(path)}\n" }
File.write("#{release_dir}/checksums.txt", checksums.join)
end
def self.image
platform = "#{ENV.fetch('MATRIX_OS')}-#{ENV.fetch('MATRIX_ARCH')}"
target = "dist/docker/#{platform}"
FileUtils.mkdir_p(target)
FileUtils.cp("dist/docker-artifacts/#{APP_NAME}-#{platform}/#{ENV.fetch('MATRIX_BINARY')}", "#{target}/#{APP_NAME}")
File.chmod(0o755, "#{target}/#{APP_NAME}")
install_manifest("#{target}/plugins/official")
end
end
if $PROGRAM_NAME == __FILE__
begin
case ARGV
when ['archives'] then ReleasePackage.archives
when ['image'] then ReleasePackage.image
else raise 'usage: package-release.rb archives|image'
end
rescue StandardError => error
warn "error: #{error.message}"
exit 1
end
end
+84
View File
@@ -0,0 +1,84 @@
require_relative 'release-metadata'
# 构建和验证可以并行;调用方必须在仓库级发布锁内执行本脚本
module ReleasePublication
def self.run(*command)
output, error, status = Open3.capture3(*command)
raise "#{command.first} failed: #{error.strip}" unless status.success?
output.strip
end
def self.latest_release(repository)
output, error, status = Open3.capture3('gh', 'api', '--include', "repos/#{repository}/releases/latest")
code = output.lines.first.to_s.split[1]
return nil if code == '404'
raise "Cannot read latest GitHub release: #{error.strip}" unless status.success? && code == '200'
JSON.parse(output.split(/\r?\n\r?\n/, 2).fetch(1)).fetch('tag_name')
end
def self.latest_image(image)
ref = "#{image}:latest"
output, error, status = Open3.capture3('docker', 'buildx', 'imagetools', 'inspect', ref, '--format', '{{json .Image}}')
# 只把明确的缺失标签当作首次发布,鉴权和网络错误必须终止提升
return nil if !status.success? && error.include?("#{ref}: not found")
raise "Cannot read latest image: #{error.strip}" unless status.success?
versions = JSON.parse(output).values.map do |config|
config.fetch('config').fetch('Labels').fetch('org.opencontainers.image.version')
end.uniq
raise 'Latest image must have one consistent version across platforms' unless versions.length == 1
versions.first
end
def self.can_promote?(version, current_versions)
return false if ReleaseMetadata.prerelease?(version)
current_versions.compact.all? do |current|
raise "Latest points to a prerelease: #{current}" if ReleaseMetadata.prerelease?(current)
comparison = ReleaseMetadata.sort_key(version) <=> ReleaseMetadata.sort_key(current)
# 构建元数据不决定版本先后;同优先级只允许相同版本重试
comparison.positive? || (comparison.zero? && ReleaseMetadata.version(current) == version)
end
end
def self.tag(image, digest, name)
ref = "#{image}:#{name}"
run('docker', 'buildx', 'imagetools', 'create', '-t', ref, "#{image}@#{digest}")
actual = run('docker', 'buildx', 'imagetools', 'inspect', ref, '--format', '{{.Manifest.Digest}}')
raise "Promoted image digest changed: #{ref}" unless actual == digest
end
def self.publish(version:, image:, digest:, repository:, release_id:)
version = ReleaseMetadata.version(version)
raise 'Invalid image digest' unless digest.match?(/\Asha256:[a-f0-9]{64}\z/)
raise 'Invalid GitHub release ID' unless release_id.match?(/\A\d+\z/)
endpoint = "repos/#{repository}/releases/#{release_id}"
# 附件上传及镜像证明、签名已完成,再公开版本入口;失败可从本 job 重跑
tag(image, digest, version.tr('+', '-'))
run('gh', 'api', '--method', 'PATCH', endpoint, '-F', 'draft=false', '-f', 'make_latest=false')
return if ReleaseMetadata.prerelease?(version)
current_versions = [latest_release(repository), latest_image(image)]
unless can_promote?(version, current_versions)
puts "Published #{version}; latest remains at #{current_versions.compact.join(', ')}"
return
end
# 两个服务不能原子更新;同时检查两侧版本,让失败重试可以补齐而旧版不能回退
tag(image, digest, 'latest')
run('gh', 'api', '--method', 'PATCH', endpoint, '-f', 'make_latest=true')
puts "Published #{version} and promoted latest"
end
end
if $PROGRAM_NAME == __FILE__
begin
ReleasePublication.publish(
version: ENV.fetch('VERSION'), image: "#{ENV.fetch('REGISTRY')}/#{ENV.fetch('IMAGE_NAME')}",
digest: ENV.fetch('DIGEST'), repository: ENV.fetch('GITHUB_REPOSITORY'),
release_id: ENV.fetch('RELEASE_ID')
)
rescue StandardError => error
warn "error: #{error.message}"
exit 1
end
end
+170
View File
@@ -0,0 +1,170 @@
require 'base64'
require 'digest'
require 'uri'
require_relative 'release-metadata'
# 通道快照随版本封存,固定 Release 只记录已公开快照和可恢复的待发布摘要
module PublishedChannels
POINTER = 'update-index'
LIMIT = 128 * 1024
def self.advance(document, version)
version = ReleaseMetadata.version(version)
major, = version.split('.', 2)
core, stage = version.split('+', 2).first.split('-', 2)
channels = (document.fetch('published')[major] ||= {})
if stage&.start_with?('exp.')
channels = (channels['exp'] ||= {})
keys = [core]
else
stages = %w[stable rc beta alpha]
keys = stages.drop(stages.index(stage ? stage.split('.').first : 'stable'))
end
incoming = ReleaseMetadata.sort_key(version)
changed = false
keys.each do |key|
current = channels[key]
if current
comparison = incoming <=> ReleaseMetadata.sort_key(current)
raise "Conflicting release identities: #{current}, #{version}" if comparison.zero? && current != version
next unless comparison.positive?
end
channels[key] = version
changed = true
end
changed
end
class Publisher
def api(endpoint, method: 'GET', data: nil, optional: false)
args = ['gh', 'api', endpoint, '--method', method]
args.concat(%w[--input -]) if data
out, error, status = Open3.capture3(*args, stdin_data: data ? JSON.generate(data) : '')
return nil if optional && !status.success? && error.include?('(HTTP 404)')
raise "Cannot access #{endpoint}: #{error.strip}" unless status.success?
JSON.parse(out)
end
def initialize(repository)
@repository = repository
@base = "repos/#{repository}"
release = api("#{@base}/releases/tags/#{POINTER}", optional: true)
if release
raise 'Invalid update-index release' unless release.fetch('tag_name') == POINTER &&
release.fetch('prerelease') && !release.fetch('draft')
@id = release.fetch('id')
@state = JSON.parse(release.fetch('body'))
@state.fetch('current')
else
@state = { 'current' => nil }
end
end
def save(current:, pending: nil)
state = { 'current' => current }
state['pending'] = pending if pending
body = { body: JSON.generate(state), make_latest: 'false' }
if @id
api("#{@base}/releases/#{@id}", method: 'PATCH', data: body)
else
release = api("#{@base}/releases", method: 'POST', data: body.merge(
tag_name: POINTER, name: '更新通道索引', target_commitish: 'main', prerelease: true
))
@id = release.fetch('id')
end
@state = state
end
def release(version)
api("#{@base}/releases/tags/v#{ReleaseMetadata.version(version)}", optional: true)
end
def verify_assets(version, release, index:)
tag = "v#{version}"
raise "Release is not public or tag differs: #{tag}" unless release && release.fetch('tag_name') == tag &&
!release.fetch('draft') && release.fetch('prerelease') == ReleaseMetadata.prerelease?(version)
data = api("#{@base}/contents/release/platforms.yaml?ref=#{URI.encode_www_form_component(tag)}")
platforms = YAML.safe_load(Base64.decode64(data.fetch('content'))).fetch('platforms')
raise "Missing platforms for #{tag}" if platforms.empty?
expected = platforms.map { |p| ReleaseMetadata.archive_name(version, p) }
expected.concat(%w[checksums.txt compose.yaml config.example.yaml plugin-release-manifest.json])
expected << 'version.yaml' if index
assets = release.fetch('assets').select { |a| a.fetch('state') == 'uploaded' && a.fetch('size').positive? }.to_h { |a| [a.fetch('name'), a] }
missing = expected - assets.keys
raise "Incomplete release #{tag}: #{missing.join(', ')}" unless missing.empty?
assets
end
def download(version)
content, error, status = Open3.capture3('gh', 'release', 'download', "v#{version}", '--repo', @repository,
'--pattern', 'version.yaml', '--output', '-')
raise "Cannot download v#{version} snapshot: #{error.strip}" unless status.success?
content
end
def read_snapshot(version, asset)
raise 'Invalid version.yaml size' unless (1..LIMIT).cover?(asset.fetch('size'))
content = download(version)
digest = Digest::SHA256.hexdigest(content)
raise "Snapshot size or digest differs for v#{version}" unless content.bytesize == asset.fetch('size') && asset.fetch('digest') == "sha256:#{digest}"
doc = YAML.safe_load(content)
raise 'Invalid channel snapshot' unless doc.fetch('version') == version && doc.fetch('published').is_a?(Hash)
[doc, digest]
end
def previous
if (current = @state.fetch('current'))
assets = verify_assets(current, release(current), index: true)
return read_snapshot(current, assets.fetch('version.yaml')).first
end
# 首次接入时从实际公开的稳定版引导;没有历史 Release 的仓库从空表开始
doc = { 'published' => {} }
latest = api("#{@base}/releases/latest", optional: true)
if latest
version = ReleaseMetadata.version(latest.fetch('tag_name'))
verify_assets(version, latest, index: false)
PublishedChannels.advance(doc, version)
end
doc
end
# 调用方持有同一仓库的发布锁;快照必须在取锁后生成,不能在并行构建中预先合并
def prepare(version, directory)
if (pending = @state.dig('pending', 'version')) && (old = release(pending)) && !old.fetch('draft')
finish(pending)
end
existing = release(version)
raise "Release v#{version} already public; use index-only recovery" if existing && !existing.fetch('draft')
doc = previous
doc['version'] = version
PublishedChannels.advance(doc, version)
content = YAML.dump(doc)
raise 'Published index exceeds the client size limit' if content.bytesize > LIMIT
digest = Digest::SHA256.hexdigest(content)
File.write(File.join(directory, 'version.yaml'), content)
File.open(File.join(directory, 'checksums.txt'), 'a') { |file| file.puts "#{digest} version.yaml" }
save(current: @state.fetch('current'), pending: { 'version' => version, 'sha256' => digest })
end
def finish(version)
assets = verify_assets(version, release(version), index: true)
_, digest = read_snapshot(version, assets.fetch('version.yaml'))
return if @state.fetch('current') == version
unless @state['pending'] == { 'version' => version, 'sha256' => digest }
raise "v#{version} is not the reserved snapshot; refusing to overwrite another publication"
end
save(current: version)
end
end
end
if $PROGRAM_NAME == __FILE__
mode, raw = ARGV
version = ReleaseMetadata.version(raw || '')
publisher = PublishedChannels::Publisher.new(ENV.fetch('GITHUB_REPOSITORY'))
case mode
when 'prepare' then publisher.prepare(version, 'dist/release')
when 'finish' then publisher.finish(version)
else abort 'usage: published-channels.rb prepare|finish VERSION'
end
end
+134
View File
@@ -0,0 +1,134 @@
require 'fileutils'
require 'json'
require 'open3'
require 'rubygems'
require 'time'
require 'yaml'
# 本地入口与发行工作流共用版本、说明和平台合同
module ReleaseMetadata
APP_NAME = 'codex-proxy-rs'
SEMVER = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:alpha|beta|rc|exp)\.[1-9]\d*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z/
def self.version(raw)
value = raw.delete_prefix('v')
raise "Invalid release version: #{raw}" unless value.match?(SEMVER)
value
end
def self.prerelease?(value)
version(value).split('+', 2).first.include?('-')
end
def self.sort_key(value)
Gem::Version.new(version(value).split('+', 2).first)
end
def self.validate_notes(value)
tag = "v#{version(value)}"
lines = File.readlines('release/notes.md')
unless lines.first&.strip == "# #{tag}" && !lines.drop(1).join.strip.empty?
raise "release/notes.md: expected non-empty notes starting with '# #{tag}'"
end
end
def self.git(*arguments)
output, status = Open3.capture2('git', *arguments)
raise "git #{arguments.join(' ')} failed" unless status.success?
output.strip
end
def self.platforms
entries = YAML.load_file('release/platforms.yaml').fetch('platforms')
raise 'release/platforms.yaml: platforms must be a non-empty array' unless entries.is_a?(Array) && !entries.empty?
seen = {}
entries.each do |entry|
raise 'Invalid release platform' unless entry.is_a?(Hash)
%w[os arch runner target binary archive].each do |key|
raise "Invalid platform #{key}" unless entry[key].is_a?(String) && !entry[key].strip.empty?
end
key = "#{entry['os']}-#{entry['arch']}"
raise "Duplicate release platform: #{key}" if seen[key]
seen[key] = true
raise "Unsupported archive: #{entry['archive']}" unless entry['archive'] == 'tar.gz'
if entry['docker'] == true
platform = entry['dockerPlatform']
raise "Missing Docker platform: #{key}" unless platform.is_a?(String) && !platform.strip.empty?
entry['dockerPlatform'] = platform.strip
end
entry['artifact'] = "#{APP_NAME}-#{key}"
end
raise 'At least one Docker platform is required' unless entries.any? { |entry| entry['docker'] == true }
entries
end
def self.archive_name(version, platform)
"#{APP_NAME}_#{version}_#{platform.fetch('os')}_#{platform.fetch('arch')}.#{platform.fetch('archive')}"
end
def self.outputs(tag)
value = version(YAML.load_file('release/version.yaml').fetch('version'))
raise "Release tag #{tag} does not match v#{value}" unless tag == "v#{value}"
validate_notes(value)
entries = platforms
docker, native = entries.partition { |entry| entry['docker'] == true }
previous = git('tag', '--list', 'v*', '--merged', 'HEAD').lines.map(&:strip).filter_map do |candidate|
next unless candidate.delete_prefix('v').match?(SEMVER)
key = sort_key(candidate)
[key, candidate] if key < sort_key(value)
end.max_by(&:first)&.last
{
'tag' => tag,
'version' => value,
'docker_version' => value.tr('+', '-'),
'build_time' => Time.now.utc.iso8601,
'git_sha' => git('rev-parse', 'HEAD'),
'platform_matrix' => JSON.generate('include' => entries),
'docker_matrix' => JSON.generate('include' => docker),
'non_docker_matrix' => JSON.generate('include' => native),
'windows_matrix' => JSON.generate('include' => entries.select { |entry| entry['os'] == 'windows' }),
'docker_platforms' => docker.map { |entry| entry['dockerPlatform'] }.join(','),
'previous_tag' => previous,
'prerelease' => prerelease?(value),
'build_type' => value.split('+', 2).first.include?('-exp.') ? 'experimental' : 'release'
}
end
def self.seal_manifest(metadata)
path = 'backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json'
host = JSON.parse(File.read(path))
raise "Invalid plugin host compatibility declaration: #{path}" unless host['schema_version'] == 2
manifest = {
'schema_version' => 1,
'sealed' => true,
'gateway_version' => metadata.fetch('version'),
'gateway_git_sha' => metadata.fetch('git_sha'),
'plugin_host' => host,
'plugins' => []
}
FileUtils.mkdir_p('dist/plugin-release-assets')
File.write('dist/plugin-release-assets/plugin-release-manifest.json', JSON.pretty_generate(manifest) + "\n")
end
end
if $PROGRAM_NAME == __FILE__
begin
command, value = ARGV
raise 'usage: release-metadata.rb validate VERSION | outputs TAG' unless ARGV.length == 2
case command
when 'validate'
value = ReleaseMetadata.version(value)
ReleaseMetadata.validate_notes(value)
puts value
when 'outputs'
metadata = ReleaseMetadata.outputs(value)
ReleaseMetadata.seal_manifest(metadata)
metadata.each { |key, entry| puts "#{key}=#{entry}" }
else
raise "Unknown command: #{command}"
end
rescue StandardError => error
warn "error: #{error.message}"
exit 1
end
end
+44
View File
@@ -0,0 +1,44 @@
require 'json'
require 'open3'
# 仅复用同一源码的主分支 CI,缺少证据时完整检查发行提交
module ReleaseQuality
def self.capture(*command)
output, error, status = Open3.capture3(*command)
raise "#{command.first} failed: #{error.strip}" unless status.success?
output
end
def self.evidence(repository)
parents = capture('git', 'rev-list', '--parents', '-n', '1', 'HEAD').split
return nil unless parents.length == 2
parent = parents.last
changes = capture('git', 'diff', '--name-only', '-z', parent, 'HEAD').split("\0")
return nil if changes.empty? || (changes - %w[release/version.yaml release/notes.md]).any?
# 按最新一次运行判断,失败或仍在执行时不回退到旧的成功记录
endpoint = "repos/#{repository}/actions/workflows/ci.yml/runs?event=push&branch=main&head_sha=#{parent}&per_page=1"
run = JSON.parse(capture('gh', 'api', endpoint)).fetch('workflow_runs').first
return nil unless run && run.fetch('head_sha') == parent && run.fetch('conclusion') == 'success'
return nil unless run.fetch('head_repository').fetch('full_name') == repository
rows = capture('gh', 'api', '--paginate', "repos/#{repository}/actions/runs/#{run.fetch('id')}/jobs?per_page=100", '--jq', '.jobs[] | @json')
job = rows.lines.map { |line| JSON.parse(line) }.find { |entry| entry.fetch('name') == 'quality / backend' }
return nil unless job && job.fetch('conclusion') == 'success'
job.fetch('html_url')
end
end
if $PROGRAM_NAME == __FILE__
begin
evidence = ReleaseQuality.evidence(ENV.fetch('GITHUB_REPOSITORY'))
rescue StandardError => error
warn "CI evidence unavailable; running full release checks: #{error.message}"
evidence = nil
end
puts "run_backend=#{evidence.nil?}"
if ENV['GITHUB_STEP_SUMMARY']
message = evidence ? "后端质量检查复用版本提交父提交的 [CI 结果](#{evidence});仅版本与发布说明不同" : '未找到可复用的后端 CI 结果,完整检查发行提交'
File.open(ENV.fetch('GITHUB_STEP_SUMMARY'), 'a') { |file| file.puts(message) }
end
end
@@ -0,0 +1,115 @@
require 'tmpdir'
require_relative '../published-channels'
# 测试只替换 GitHub 读写边界,执行真实的发布状态转换
class PublishedChannelsTest
def assert_equal(expected, actual)
raise "expected #{expected.inspect}, got #{actual.inspect}" unless expected == actual
end
def assert_rejected
yield
rescue RuntimeError
return
else
raise 'expected publication to be rejected'
end
def initialize
@pointer = nil
@releases = {}
@files = {}
@calls = []
@platforms = [{ 'os' => 'linux', 'arch' => 'amd64', 'archive' => 'tar.gz' }]
end
def api(endpoint, method: 'GET', data: nil, optional: false)
path = endpoint.delete_prefix('repos/test/repo/')
@calls << [method, path]
case [method, path]
when ['GET', 'releases/tags/update-index'] then @pointer
when ['POST', 'releases']
raise 'duplicate pointer' if @pointer
@pointer = data.transform_keys(&:to_s).merge('id' => 12, 'draft' => false)
when ['PATCH', 'releases/12']
@pointer.merge!(data.transform_keys(&:to_s))
when ['GET', 'releases/latest'] then @releases['3.41.2']
else
return @releases[path.delete_prefix('releases/tags/v')] if method == 'GET' && path.start_with?('releases/tags/v')
return { 'content' => Base64.encode64(YAML.dump({ 'platforms' => @platforms })) } if path.start_with?('contents/release/platforms.yaml?ref=v')
raise "unexpected #{method} #{path}"
end
end
def publish_assets(version, content = nil)
names = %w[checksums.txt compose.yaml config.example.yaml plugin-release-manifest.json]
names << ReleaseMetadata.archive_name(version, @platforms.first)
@files[version] = content if content
assets = names.map { |name| { 'name' => name, 'size' => 100, 'state' => 'uploaded' } }
assets << { 'name' => 'version.yaml', 'size' => content.bytesize, 'digest' => "sha256:#{Digest::SHA256.hexdigest(content)}", 'state' => 'uploaded' } if content
@releases[version] = { 'tag_name' => "v#{version}", 'draft' => false, 'prerelease' => ReleaseMetadata.prerelease?(version), 'assets' => assets }
end
def publisher
github = self
files = @files
Class.new(PublishedChannels::Publisher) do
define_method(:api) { |endpoint, **args| github.api(endpoint, **args) }
define_method(:download) { |version| files.fetch(version) }
end.new('test/repo')
end
def prepare(version)
Dir.mktmpdir do |dir|
File.write(File.join(dir, 'checksums.txt'), '')
publisher.prepare(version, dir)
content = File.read(File.join(dir, 'version.yaml'))
assert_equal "#{Digest::SHA256.hexdigest(content)} version.yaml\n", File.read(File.join(dir, 'checksums.txt'))
content
end
end
def current = JSON.parse(@pointer.fetch('body')).fetch('current')
def test_interrupted_publication_recovers_without_losing_another_channel
publish_assets('3.41.2')
stable = prepare('3.42.0')
assert_equal nil, current
assert_rejected { publisher.finish('3.42.0') }
publish_assets('3.42.0', stable)
# 模拟公开后更新指针中断,下一次发布在同一把锁内补写,再合并自己的通道
beta = prepare('3.43.0-beta.1')
assert_equal '3.42.0', current
assert_equal({ 'stable' => '3.42.0', 'rc' => '3.42.0', 'beta' => '3.43.0-beta.1', 'alpha' => '3.43.0-beta.1' },
YAML.safe_load(beta).dig('published', '3'))
publish_assets('3.43.0-beta.1', beta)
publisher.finish('3.43.0-beta.1')
publisher.finish('3.43.0-beta.1')
assert_rejected { publisher.finish('3.42.0') }
assert_equal '3.43.0-beta.1', current
experiment = YAML.safe_load(prepare('3.44.0-exp.1')).dig('published', '3')
assert_equal({ '3.44.0' => '3.44.0-exp.1' }, experiment.fetch('exp'))
assert_equal '3.43.0-beta.1', experiment.fetch('beta')
assert_equal false, @calls.include?(['GET', 'releases'])
end
def test_incomplete_corrupt_or_draft_release_keeps_last_public_snapshot
publish_assets('3.42.0', prepare('3.42.0'))
publisher.finish('3.42.0')
beta = prepare('3.43.0-beta.1')
publish_assets('3.43.0-beta.1', beta)
release = @releases.fetch('3.43.0-beta.1')
release['draft'] = true
assert_rejected { publisher.finish('3.43.0-beta.1') }
release['draft'] = false
missing = release.fetch('assets').shift
assert_rejected { publisher.finish('3.43.0-beta.1') }
release.fetch('assets') << missing
publish_assets('3.43.0-beta.1', beta.sub('3.42.0', '3.10.0'))
assert_rejected { publisher.finish('3.43.0-beta.1') }
assert_equal '3.42.0', current
end
end
PublishedChannelsTest.new.test_interrupted_publication_recovers_without_losing_another_channel
PublishedChannelsTest.new.test_incomplete_corrupt_or_draft_release_keeps_last_public_snapshot
puts 'Published channel recovery and integrity checks passed'
+27
View File
@@ -0,0 +1,27 @@
# 从构建声明读取 CI 工具链,阻止 Docker 与本地 Rust 版本漂移
module BuildToolchains
def self.read(root = '.')
declaration = File.read(File.join(root, 'backend/rust-toolchain.toml'))
rust = declaration[/^\s*channel\s*=\s*["'](\d+\.\d+(?:\.\d+)?)["']\s*(?:#.*)?$/, 1]
raise 'Expected a pinned Rust channel in backend/rust-toolchain.toml' unless rust
dockerfile = File.read(File.join(root, 'deploy/Dockerfile'))
image = lambda do |stage, name|
match = dockerfile.match(/^FROM (#{name}:([^\s@]+)@sha256:[a-f0-9]{64}) AS #{stage}$/)
raise "Missing pinned #{stage} image" unless match
[match[1], match[2].split('-', 2).first]
end
rust_image, image_rust = image.call('backend-builder', 'rust')
_, node = image.call('frontend-builder', 'node')
raise "Rust toolchain #{rust} differs from Docker builder #{image_rust}" unless rust == image_rust
{ 'rust' => rust, 'rust_image' => rust_image, 'node' => node }
end
end
if $PROGRAM_NAME == __FILE__
begin
BuildToolchains.read.each { |key, value| puts "#{key}=#{value}" }
rescue StandardError => error
warn "error: #{error.message}"
exit 1
end
end
+100 -229
View File
@@ -1,4 +1,4 @@
name: Reusable Container
name: Container
on:
workflow_call:
@@ -7,37 +7,72 @@ on:
description: Exact commit SHA to build and validate
required: true
type: string
source_build:
description: Validate the complete Docker source build
type: boolean
default: true
permissions:
contents: read
security-events: write
jobs:
frontend:
if: ${{ !inputs.source_build }}
runs-on: ubuntu-26.04
timeout-minutes: 15
env:
CI: 'true'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
- uses: ./.github/actions/build-frontend
with:
upload-artifact: 'true'
binary:
if: ${{ !inputs.source_build }}
runs-on: ubuntu-26.04
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
- id: toolchains
run: ruby .github/scripts/toolchains.rb >> "$GITHUB_OUTPUT"
- uses: ./.github/actions/build-binary
with:
rust: ${{ steps.toolchains.outputs.rust }}
target: x86_64-unknown-linux-gnu
platform: linux-amd64
rust-image: ${{ steps.toolchains.outputs.rust_image }}
env:
CPR_GIT_SHA: ${{ inputs.ref }}
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: codex-proxy-rs-linux-amd64
path: dist/binaries/linux-amd64/
if-no-files-found: error
retention-days: 7
container:
runs-on: ubuntu-latest
needs: [frontend, binary]
if: always() && !cancelled()
runs-on: ubuntu-26.04
timeout-minutes: 45
env:
COMPOSE_PROJECT_NAME: cpr-ci-${{ github.run_id }}-${{ github.run_attempt }}
CPR_IMAGE: codex-proxy-rs:ci-${{ inputs.ref }}
TRIVY_QUIET: 'true'
TRIVY_SKIP_VERSION_CHECK: 'true'
steps:
- name: Define isolated runtime values
shell: bash
run: |
set -euo pipefail
admin_password="$(openssl rand -hex 24)"
postgres_password="$(openssl rand -hex 24)"
redis_password="$(openssl rand -hex 24)"
echo "::add-mask::$admin_password"
echo "::add-mask::$postgres_password"
echo "::add-mask::$redis_password"
{
echo "DIAGNOSTICS_DIR=$RUNNER_TEMP/codex-proxy-rs-diagnostics"
echo "CI_ADMIN_PASSWORD=$admin_password"
echo "CI_POSTGRES_PASSWORD=$postgres_password"
echo "CI_REDIS_PASSWORD=$redis_password"
} >> "$GITHUB_ENV"
# 上游失败必须传递给已有的容器检查,不能让必需状态变成 skipped
- name: Require prebuilt inputs
if: ${{ !inputs.source_build }}
env:
FRONTEND_RESULT: ${{ needs.frontend.result }}
BINARY_RESULT: ${{ needs.binary.result }}
run: test "$FRONTEND_RESULT" = success && test "$BINARY_RESULT" = success
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -45,222 +80,58 @@ jobs:
ref: ${{ inputs.ref }}
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
id: buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Download frontend
if: ${{ !inputs.source_build }}
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: frontend-dist
path: dist/docker/web/dist
- name: Download binary
if: ${{ !inputs.source_build }}
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: codex-proxy-rs-linux-amd64
path: dist/docker-artifacts/codex-proxy-rs-linux-amd64
- name: Prepare prebuilt image
if: ${{ !inputs.source_build }}
env:
MATRIX_OS: linux
MATRIX_ARCH: amd64
MATRIX_BINARY: codex-proxy-rs
run: |
ruby -r './.github/scripts/release-metadata' -e '
ReleaseMetadata.seal_manifest(
"version" => ReleaseMetadata.version(YAML.load_file("release/version.yaml").fetch("version")),
"git_sha" => ReleaseMetadata.git("rev-parse", "HEAD")
)'
ruby .github/scripts/package-release.rb image
- name: Build runtime image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: deploy/Dockerfile
target: runtime
target: ${{ inputs.source_build && 'runtime' || 'runtime-prebuilt' }}
build-args: |
CPR_PREBUILT_DIR=dist/docker
CPR_GIT_SHA=${{ inputs.ref }}
load: true
tags: ${{ env.CPR_IMAGE }}
cache-from: type=gha,scope=runtime-ci
cache-to: type=gha,mode=max,scope=runtime-ci
no-cache-filters: |
runtime-base
runtime
runtime-prebuilt
cache-from: type=gha,scope=runtime-${{ inputs.source_build && 'source' || 'prebuilt' }}
cache-to: ${{ (github.event_name == 'push' || github.event_name == 'schedule') && github.ref == 'refs/heads/main' && format('type=gha,mode=max,scope=runtime-{0}', inputs.source_build && 'source' || 'prebuilt') || '' }}
- name: Generate Trivy SARIF
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
- name: Verify runtime image
uses: ./.github/actions/verify-container
with:
image-ref: ${{ env.CPR_IMAGE }}
version: v0.72.0
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
vuln-type: os,library
scanners: vuln
ignore-unfixed: true
exit-code: '0'
- name: Upload Trivy SARIF
if: >-
${{ hashFiles('trivy-results.sarif') != '' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository) }}
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
sarif_file: trivy-results.sarif
category: runtime-container
- name: Enforce Trivy vulnerability gate
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.CPR_IMAGE }}
version: v0.72.0
format: table
severity: HIGH,CRITICAL
vuln-type: os,library
scanners: vuln
ignore-unfixed: true
exit-code: '1'
- name: Prepare isolated runtime
shell: bash
run: |
set -euo pipefail
umask 0077
# Bind roots hand off ownership to the image-specific database users.
# Sticky directories keep that initialization compatible with user-namespaced runners.
install -d -m 1777 .runtime/postgres .runtime/redis
mkdir -p "$DIAGNOSTICS_DIR"
sudo install -d -m 0770 -o "$(id -u)" -g 10001 \
.runtime/data .runtime/logs
ruby <<'RUBY'
path = "deploy/config.example.yaml"
content = File.read(path)
replacements = {
"password: &postgres_password ''" => "password: &postgres_password '#{ENV.fetch("CI_POSTGRES_PASSWORD")}'",
"password: &redis_password ''" => "password: &redis_password '#{ENV.fetch("CI_REDIS_PASSWORD")}'",
"default_password: ''" => "default_password: '#{ENV.fetch("CI_ADMIN_PASSWORD")}'",
}
replacements.each do |placeholder, configured|
raise "missing config placeholder: #{placeholder}" unless content.sub!(placeholder, configured)
end
File.write("deploy/config.yaml", content)
RUBY
sudo chown "$(id -u):10001" deploy/config.yaml
chmod 0640 deploy/config.yaml
- name: Validate Docker Compose
shell: bash
run: |
set -euo pipefail
docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml config --quiet
- name: Configure Redis host memory overcommit
shell: bash
run: |
set -euo pipefail
sudo sysctl -w vm.overcommit_memory=1
test "$(sysctl -n vm.overcommit_memory)" = 1
- name: Verify PostgreSQL and Redis volume persistence
shell: bash
run: |
set -euo pipefail
compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml)
"${compose[@]}" up -d --no-build --wait postgres redis
test "$("${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \
psql -At -U codex_proxy -d codex_proxy -c "SHOW TimeZone;" | tr -d '\r')" = Asia/Shanghai
"${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \
psql -v ON_ERROR_STOP=1 -U codex_proxy -d codex_proxy -c \
"CREATE TABLE cpr_ci_persistence_probe (value text PRIMARY KEY); INSERT INTO cpr_ci_persistence_probe VALUES ('persisted');"
"${compose[@]}" exec -T -e REDISCLI_AUTH="$CI_REDIS_PASSWORD" redis \
redis-cli SET cpr:ci:persistence-probe persisted
"${compose[@]}" up -d --no-build --wait --force-recreate postgres redis
test "$("${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \
psql -At -U codex_proxy -d codex_proxy -c \
"SELECT value FROM cpr_ci_persistence_probe;")" = persisted
test "$("${compose[@]}" exec -T -e REDISCLI_AUTH="$CI_REDIS_PASSWORD" redis \
redis-cli --raw GET cpr:ci:persistence-probe | tr -d '\r')" = persisted
"${compose[@]}" exec -T -e PGPASSWORD="$CI_POSTGRES_PASSWORD" postgres \
psql -v ON_ERROR_STOP=1 -U codex_proxy -d codex_proxy -c \
"DROP TABLE cpr_ci_persistence_probe;"
"${compose[@]}" exec -T -e REDISCLI_AUTH="$CI_REDIS_PASSWORD" redis \
redis-cli DEL cpr:ci:persistence-probe
- name: Start and verify application
shell: bash
run: |
set -euo pipefail
compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml)
"${compose[@]}" up -d --no-build --wait codex-proxy-rs
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
"http://127.0.0.1:8080/healthz")"
test "$status" = 204
test "$("${compose[@]}" exec -T codex-proxy-rs id -u | tr -d '\r')" = 10001
test "$("${compose[@]}" exec -T codex-proxy-rs id -g | tr -d '\r')" = 10001
test "$("${compose[@]}" exec -T codex-proxy-rs date +%z | tr -d '\r')" = +0800
test "$("${compose[@]}" exec -T codex-proxy-rs cat /etc/timezone | tr -d '\r')" = Asia/Shanghai
"${compose[@]}" exec -T codex-proxy-rs sh -c \
'touch /app/.runtime/data/.ci-write /app/.runtime/logs/.ci-write && rm /app/.runtime/data/.ci-write /app/.runtime/logs/.ci-write'
- name: Verify clean logs and graceful shutdown
shell: bash
run: |
set -euo pipefail
compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml)
"${compose[@]}" stop --timeout 30 codex-proxy-rs
container_id="$("${compose[@]}" ps -a -q codex-proxy-rs)"
test -n "$container_id"
test "$(docker inspect --format '{{.State.ExitCode}}' "$container_id")" = 0
"${compose[@]}" logs --no-color --timestamps codex-proxy-rs \
> "$DIAGNOSTICS_DIR/application.log"
"${compose[@]}" logs --no-color --timestamps postgres \
> "$DIAGNOSTICS_DIR/postgres.log"
"${compose[@]}" logs --no-color --timestamps redis \
> "$DIAGNOSTICS_DIR/redis.log"
if grep --extended-regexp --ignore-case \
'"level"[[:space:]]*:[[:space:]]*"(warn|error|fatal|panic)"|(^|[^[:alnum:]_])panic([^[:alnum:]_]|$)' \
"$DIAGNOSTICS_DIR/application.log" \
|| grep --extended-regexp --ignore-case \
'(^|[[:space:]])(warning|error|fatal|panic):' \
"$DIAGNOSTICS_DIR/postgres.log" \
|| grep --extended-regexp --ignore-case \
'[0-9]+:[A-Z][[:space:]].*[[:space:]]#[[:space:]]|(^|[|[:space:]])(warning|error|fatal|panic)(:|[[:space:]])' \
"$DIAGNOSTICS_DIR/redis.log"; then
echo "container logs contain a warning or error severity" >&2
exit 1
fi
- name: Collect container diagnostics
if: always()
shell: bash
run: |
set +e
mkdir -p "$DIAGNOSTICS_DIR"
raw_dir="$(mktemp -d)"
compose=(docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml)
"${compose[@]}" config > "$raw_dir/compose-config.yaml" 2>&1
"${compose[@]}" ps -a > "$raw_dir/compose-ps.txt" 2>&1
"${compose[@]}" logs --no-color --timestamps > "$raw_dir/compose.log" 2>&1
mapfile -t container_ids < <("${compose[@]}" ps -a -q)
if [ "${#container_ids[@]}" -gt 0 ]; then
docker inspect "${container_ids[@]}" > "$raw_dir/docker-inspect.json" 2>&1
fi
RAW_DIR="$raw_dir" OUTPUT_DIR="$DIAGNOSTICS_DIR" ruby <<'RUBY'
raw_dir = ENV.fetch("RAW_DIR")
output_dir = ENV.fetch("OUTPUT_DIR")
secrets = [
ENV.fetch("CI_ADMIN_PASSWORD"),
ENV.fetch("CI_POSTGRES_PASSWORD"),
ENV.fetch("CI_REDIS_PASSWORD"),
]
encoded_secrets = secrets.map do |secret|
secret.encode("UTF-8").bytes.map do |byte|
character = byte.chr
character.match?(/[A-Za-z0-9]/) ? character : format("%%%02X", byte)
end.join
end
Dir.children(raw_dir).each do |name|
content = File.binread(File.join(raw_dir, name))
(secrets + encoded_secrets).each do |secret|
content = content.gsub(secret, "<redacted>")
end
File.binwrite(File.join(output_dir, name), content)
end
RUBY
rm -rf "$raw_dir"
cp trivy-results.sarif "$DIAGNOSTICS_DIR/" 2>/dev/null || true
- name: Upload container diagnostics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: container-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/codex-proxy-rs-diagnostics
if-no-files-found: warn
retention-days: 7
- name: Remove isolated runtime
if: always()
shell: bash
run: |
docker compose --project-name "$COMPOSE_PROJECT_NAME" \
-f deploy/compose.yaml down --remove-orphans || true
sudo rm -rf "$GITHUB_WORKSPACE/.runtime" "$GITHUB_WORKSPACE/deploy/config.yaml"
image: ${{ env.CPR_IMAGE }}
platform: linux/amd64
artifact-name: container-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}-amd64
+100 -43
View File
@@ -1,4 +1,4 @@
name: Reusable Quality
name: Quality
on:
workflow_call:
@@ -7,11 +7,6 @@ on:
description: Exact commit SHA to validate
required: true
type: string
upload_frontend:
description: Upload the built frontend for release jobs
required: false
default: false
type: boolean
run_backend:
description: Run backend quality checks
required: false
@@ -29,15 +24,18 @@ permissions:
jobs:
backend:
if: inputs.run_backend
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 30
env:
CARGO_TERM_COLOR: always
CARGO_PROFILE_DEV_DEBUG: '1'
# 编译 AWS SDK 调试信息需要更大的 codegen 线程栈
RUST_MIN_STACK: 16777216
CPR_TEST_DATABASE_URL: postgres://codex_proxy:codex_proxy@127.0.0.1:5432/codex_proxy
CPR_TEST_REDIS_URL: redis://127.0.0.1:6379
services:
postgres:
image: postgres:18-bookworm@sha256:d9c83446333daec3f0588cc709adb80c26090b7f9f0f7ec8d43c243385d79818
image: postgres:18-bookworm@sha256:bbf83594798e144bff365a9c191c5d779f181fb3135902064ab8ff11918020fc
env:
POSTGRES_DB: codex_proxy
POSTGRES_USER: codex_proxy
@@ -51,7 +49,7 @@ jobs:
--health-timeout 5s
--health-retries 10
redis:
image: redis:8-alpine@sha256:9d317178eceac8454a2284a9e6df2466b93c745529947f0cd42a0fa9609d7005
image: redis:8-alpine@sha256:0bf6577d7774e592b29d093e3ac9390eb91e13d72c21e506afce736de5e0eeb0
ports:
- 6379:6379
options: >-
@@ -100,15 +98,23 @@ jobs:
fi
fi
- name: Read build toolchains
id: toolchains
run: ruby .github/scripts/toolchains.rb >> "$GITHUB_OUTPUT"
- name: Setup Rust
uses: dtolnay/rust-toolchain@1a3a6d54512beeaffd394f8d516ca16f2c506a20 # 1.97
uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master
with:
toolchain: ${{ steps.toolchains.outputs.rust }}
components: rustfmt,clippy
- name: Cache Rust
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
workspaces: backend -> target
shared-key: quality
cache-bin: false
save-if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
- name: Rust format
working-directory: backend
@@ -118,6 +124,19 @@ jobs:
working-directory: backend
run: cargo clippy --all-targets --all-features --locked -- -D warnings
- name: Setup nextest
uses: taiki-e/install-action@682ce6c3c696a4cd5168a05f98e51aa02b877941 # v2.89.1
with:
tool: cargo-nextest@0.9.143
fallback: none
- name: Build Rust tests once
working-directory: backend
run: |
cargo metadata --format-version 1 --locked > "$RUNNER_TEMP/nextest-cargo-metadata.json"
cargo nextest list --test main --locked --timings --list-type binaries-only --message-format json \
> "$RUNNER_TEMP/nextest-binaries.json"
- name: Configure PostgreSQL and Redis test authentication
shell: bash
run: |
@@ -140,13 +159,62 @@ jobs:
echo "CPR_TEST_REDIS_URL=redis://:${redis_password}@127.0.0.1:6379"
} >> "$GITHUB_ENV"
- name: Expose matching PostgreSQL backup and restore tools
shell: bash
env:
CPR_TEST_POSTGRES_CONTAINER: ${{ job.services.postgres.id }}
run: |
set -euo pipefail
client_dir="$RUNNER_TEMP/pg-client"
mkdir -p "$client_dir"
for tool in pg_dump psql; do
printf '#!/bin/sh\nexec docker exec -i --env PGPASSWORD "%s" %s "$@"\n' "$CPR_TEST_POSTGRES_CONTAINER" "$tool" > "$client_dir/$tool"
chmod +x "$client_dir/$tool"
done
echo "$client_dir" >> "$GITHUB_PATH"
- name: Rust tests
working-directory: backend
run: cargo test --test main --locked
run: |
printf '[profile.ci.junit]\npath = "junit.xml"\n' > "$RUNNER_TEMP/nextest-ci.toml"
cargo nextest run --config-file "$RUNNER_TEMP/nextest-ci.toml" \
--cargo-metadata "$RUNNER_TEMP/nextest-cargo-metadata.json" \
--binaries-metadata "$RUNNER_TEMP/nextest-binaries.json" \
--test-threads 8 --no-tests fail --profile ci
- name: Upload test and build reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: backend-quality-reports
path: |
backend/target/nextest/ci/junit.xml
backend/target/cargo-timings/
if-no-files-found: ignore
retention-days: 7
- name: Store tests with Redis ACL authentication
working-directory: backend
shell: bash
run: |
set -euo pipefail
redis_acl_password="$(openssl rand -hex 24)"
echo "::add-mask::$redis_acl_password"
docker exec "${{ job.services.redis.id }}" redis-cli -e --no-auth-warning \
-u "$CPR_TEST_REDIS_URL" --user default ACL SETUSER cpr_acl reset on \
">$redis_acl_password" '~*' '&*' '+@all' >/dev/null
docker exec --env REDISCLI_AUTH="$redis_acl_password" "${{ job.services.redis.id }}" \
redis-cli -e --user cpr_acl ACL SETUSER default off >/dev/null
export CPR_TEST_REDIS_URL="redis://cpr_acl:${redis_acl_password}@127.0.0.1:6379"
# 从同一份构建清单选择 Redis 测试,保留 workspace 特性图且不重复运行 PostgreSQL
cargo nextest run \
--cargo-metadata "$RUNNER_TEMP/nextest-cargo-metadata.json" \
--binaries-metadata "$RUNNER_TEMP/nextest-binaries.json" \
--test-threads 8 --no-tests fail -E 'package(=gateway-store) & test(/^redis::/)'
frontend:
if: inputs.run_frontend
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 15
env:
CI: 'true'
@@ -156,38 +224,11 @@ jobs:
with:
ref: ${{ inputs.ref }}
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 11.7.0
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install frontend dependencies
run: pnpm --dir frontend install --frozen-lockfile
- name: Frontend format
run: pnpm --dir frontend format:check
- name: Frontend build
run: pnpm --dir frontend build
- name: Upload frontend artifact
if: inputs.upload_frontend
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: frontend-dist
path: frontend/dist
if-no-files-found: error
retention-days: 1
- name: Validate and build frontend
uses: ./.github/actions/build-frontend
workflow-lint:
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- name: Checkout
@@ -195,6 +236,10 @@ jobs:
with:
ref: ${{ inputs.ref }}
# GitHub 已支持 queue: max,actionlint 尚未支持该字段
# https://github.com/rhysd/actionlint/issues/657
# GitHub 已提供 ubuntu-26.04,actionlint 尚未收录该标签
# https://github.com/actions/runner-images#available-images
- name: Lint GitHub Actions workflows
run: >-
docker run --rm
@@ -202,4 +247,16 @@ jobs:
--workdir /repo
rhysd/actionlint:1.7.7@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9
-color
-ignore '^unexpected key "queue" for "concurrency" section\. expected one of "cancel-in-progress", "group"$'
-ignore '^label "ubuntu-26\.04" is unknown\.'
.github/workflows/*.yml
- name: Check workflow script syntax
run: |
bash -n release/publish
shopt -s globstar nullglob
for script in .github/**/*.sh; do bash -n "$script"; done
for script in .github/scripts/*.rb; do ruby -c "$script"; done
ruby .github/scripts/toolchains.rb
ruby .github/scripts/tests/published-channels-test.rb
ruby -c backend/crates/gateway-plugin/runtime/plugin-api-lifecycle.rb
+49 -5
View File
@@ -4,6 +4,9 @@ on:
push:
branches:
- main
paths-ignore:
- 'release/version.yaml'
- 'release/notes.md'
pull_request:
branches:
- main
@@ -17,12 +20,16 @@ concurrency:
jobs:
changes:
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
container: ${{ steps.filter.outputs.container }}
source_container: ${{ steps.filter.outputs.source_container }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -32,22 +39,57 @@ jobs:
- name: Detect affected checks
id: filter
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
with:
# 目录规则取并集,再统一排除说明文档,保留隐藏配置
predicate-quantifier: some-with-excludes
filters: |
backend:
- '.github/scripts/toolchains.rb'
- 'deploy/Dockerfile'
- 'backend/**'
- 'release/version.yaml'
- '.github/workflows/**'
- '.github/workflows/ci.yml'
- '.github/workflows/_quality.yml'
- '!**/*.md'
frontend:
- '.github/scripts/toolchains.rb'
- 'backend/rust-toolchain.toml'
- 'deploy/Dockerfile'
- 'frontend/**'
- '.github/workflows/**'
- '.github/actions/build-frontend/**'
- '.github/workflows/ci.yml'
- '.github/workflows/_quality.yml'
- '!**/*.md'
container:
- 'backend/**'
- 'frontend/**'
- 'deploy/**'
- '.dockerignore'
- 'release/version.yaml'
- '.github/actions/verify-container/**'
- '.github/actions/build-binary/**'
- '.github/actions/build-frontend/**'
- '.github/scripts/toolchains.rb'
- '.github/scripts/package-release.rb'
- '.github/scripts/release-metadata.rb'
- '.github/workflows/_container.yml'
- '.github/workflows/ci.yml'
- '!**/*.md'
source_container:
- 'deploy/Dockerfile'
- 'deploy/Dockerfile.dockerignore'
- '.dockerignore'
- 'backend/**/Cargo.toml'
- 'backend/Cargo.lock'
- 'backend/rust-toolchain.toml'
- 'backend/**/build.rs'
- 'backend/.cargo/**'
- 'frontend/package.json'
- 'frontend/pnpm-lock.yaml'
- 'frontend/pnpm-workspace.yaml'
- 'frontend/.npmrc'
- 'frontend/patches/**'
- '.github/workflows/_container.yml'
- '.github/workflows/ci.yml'
@@ -57,7 +99,8 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
run_backend: ${{ needs.changes.outputs.backend == 'true' }}
run_frontend: ${{ needs.changes.outputs.frontend == 'true' }}
# 预构建容器复用同一次前端检查产物,源码 Docker 构建仍独立验证
run_frontend: ${{ needs.changes.outputs.frontend == 'true' && (needs.changes.outputs.container != 'true' || needs.changes.outputs.source_container == 'true') }}
container:
needs: changes
@@ -68,3 +111,4 @@ jobs:
uses: ./.github/workflows/_container.yml
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
source_build: ${{ needs.changes.outputs.source_container == 'true' }}
+301 -355
View File
@@ -1,21 +1,22 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release, for example v0.1.0'
required: true
type: string
index_only:
description: 'Recover the index for an already public release without rebuilding'
type: boolean
default: false
permissions:
contents: read
concurrency:
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
group: release-${{ inputs.tag }}
cancel-in-progress: false
env:
@@ -24,8 +25,12 @@ env:
jobs:
release-info:
runs-on: ubuntu-latest
if: ${{ !inputs.index_only }}
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
actions: read
outputs:
tag: ${{ steps.version.outputs.tag }}
version: ${{ steps.version.outputs.version }}
@@ -35,136 +40,130 @@ jobs:
platform_matrix: ${{ steps.version.outputs.platform_matrix }}
docker_matrix: ${{ steps.version.outputs.docker_matrix }}
non_docker_matrix: ${{ steps.version.outputs.non_docker_matrix }}
windows_matrix: ${{ steps.version.outputs.windows_matrix }}
docker_platforms: ${{ steps.version.outputs.docker_platforms }}
previous_tag: ${{ steps.version.outputs.previous_tag }}
prerelease: ${{ steps.version.outputs.prerelease }}
build_type: ${{ steps.version.outputs.build_type }}
rust: ${{ steps.toolchains.outputs.rust }}
rust_image: ${{ steps.toolchains.outputs.rust_image }}
run_backend: ${{ steps.quality.outputs.run_backend }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
ref: ${{ inputs.tag }}
fetch-depth: 0
- name: Read release metadata
- name: Read release metadata and seal plugin manifest
id: version
shell: bash
env:
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
run: |
ruby <<'RUBY' >> "$GITHUB_OUTPUT"
require "json"
require "open3"
require "rubygems"
require "time"
require "yaml"
RELEASE_TAG: ${{ inputs.tag }}
run: ruby .github/scripts/release-metadata.rb outputs "$RELEASE_TAG" >> "$GITHUB_OUTPUT"
APP_NAME = "codex-proxy-rs"
SEMVER = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\z/
- name: Read build toolchains
id: toolchains
run: ruby .github/scripts/toolchains.rb >> "$GITHUB_OUTPUT"
def fail(message)
warn(message)
exit(1)
end
- name: Check reusable backend CI evidence
id: quality
env:
GH_TOKEN: ${{ github.token }}
run: ruby .github/scripts/release-quality.rb >> "$GITHUB_OUTPUT"
def require_string(data, key, path)
value = data[key]
fail("#{path}: #{key} must be a non-empty string") unless value.is_a?(String) && !value.strip.empty?
value.strip
end
def version_sort_key(version)
Gem::Version.new(version.split("+", 2).first)
rescue ArgumentError
nil
end
version_path = "release/version.yaml"
platforms_path = "release/platforms.yaml"
version_data = YAML.load_file(version_path)
platforms_data = YAML.load_file(platforms_path)
version = require_string(version_data, "version", version_path).delete_prefix("v")
tag_prefix = "v"
expected_tag = "#{tag_prefix}#{version}"
release_tag = ENV.fetch("RELEASE_TAG")
fail("Release tag #{release_tag} does not match #{version_path} tag #{expected_tag}") unless release_tag == expected_tag
fail("#{version_path}: version #{version} is not a valid semver") unless version.match?(SEMVER)
platforms = platforms_data["platforms"]
fail("#{platforms_path}: platforms must be a non-empty array") unless platforms.is_a?(Array) && !platforms.empty?
required_platform_keys = %w[os arch runner target binary archive]
seen_platforms = {}
docker_platform_jobs = []
non_docker_platform_jobs = []
docker_platforms = []
platforms.each_with_index do |platform, index|
fail("#{platforms_path}: platforms[#{index}] must be a table") unless platform.is_a?(Hash)
required_platform_keys.each do |key|
require_string(platform, key, "#{platforms_path}: platforms[#{index}]")
end
archive = platform["archive"]
fail("#{platforms_path}: platforms[#{index}].archive must be tar.gz") unless archive == "tar.gz"
platform_key = "#{platform["os"]}-#{platform["arch"]}"
fail("#{platforms_path}: duplicate platform #{platform_key}") if seen_platforms[platform_key]
seen_platforms[platform_key] = true
platform["artifact"] = "#{APP_NAME}-#{platform_key}"
if platform["docker"] == true
docker_platform = require_string(platform, "dockerPlatform", "#{platforms_path}: platforms[#{index}]")
platform["dockerPlatform"] = docker_platform
docker_platform_jobs << platform
docker_platforms << docker_platform
else
non_docker_platform_jobs << platform
end
end
fail("#{platforms_path}: at least one platform must set docker = true") if docker_platforms.empty?
git_sha, status = Open3.capture2("git", "rev-parse", "HEAD")
fail("failed to resolve git sha") unless status.success?
matrix = JSON.generate({ "include" => platforms })
docker_matrix = JSON.generate({ "include" => docker_platform_jobs })
non_docker_matrix = JSON.generate({ "include" => non_docker_platform_jobs })
current_sort_key = version_sort_key(version)
tags, status = Open3.capture2("git", "tag", "--list", "#{tag_prefix}*", "--merged", "HEAD")
fail("failed to list git tags") unless status.success?
previous_tag = tags
.lines
.map(&:strip)
.reject(&:empty?)
.reject { |tag| tag == release_tag }
.filter_map do |tag|
tag_version = tag.delete_prefix(tag_prefix)
next unless tag_version.match?(SEMVER)
sort_key = version_sort_key(tag_version)
next unless sort_key && current_sort_key && sort_key < current_sort_key
[sort_key, tag]
end
.max_by(&:first)
&.last
puts "tag=#{release_tag}"
puts "version=#{version}"
# Docker tags forbid "+", so semver build metadata maps to "-" in image tags.
puts "docker_version=#{version.tr("+", "-")}"
puts "build_time=#{Time.now.utc.iso8601}"
puts "git_sha=#{git_sha.strip}"
puts "platform_matrix=#{matrix}"
puts "docker_matrix=#{docker_matrix}"
puts "non_docker_matrix=#{non_docker_matrix}"
puts "docker_platforms=#{docker_platforms.join(",")}"
puts "previous_tag=#{previous_tag}"
puts "prerelease=#{version.split("+", 2).first.include?("-")}"
RUBY
- name: Upload sealed plugin assets
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: plugin-release-assets
path: dist/plugin-release-assets
if-no-files-found: error
retention-days: 7
quality:
needs: [release-info]
uses: ./.github/workflows/_quality.yml
with:
ref: ${{ needs.release-info.outputs.git_sha }}
upload_frontend: true
run_backend: ${{ needs.release-info.outputs.run_backend == 'true' }}
run_frontend: false
windows:
needs: release-info
name: windows-tests (${{ matrix.arch }})
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.release-info.outputs.windows_matrix) }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
env:
RUST_MIN_STACK: '16777216'
RUSTFLAGS: '-C target-feature=+crt-static'
CARGO_PROFILE_DEV_DEBUG: '1'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.release-info.outputs.git_sha }}
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master
with:
toolchain: ${{ needs.release-info.outputs.rust }}
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
workspaces: backend -> target
shared-key: platform-tests-${{ matrix.target }}
cache-bin: false
save-if: ${{ github.event_name != 'pull_request' }}
- name: Setup nextest
uses: taiki-e/install-action@682ce6c3c696a4cd5168a05f98e51aa02b877941 # v2.89.1
with:
tool: cargo-nextest@0.9.143
fallback: none
- name: Verify Windows platform behavior
shell: bash
working-directory: backend
env:
BUILD_TARGET: ${{ matrix.target }}
# 同一次构建统一两个 package 的依赖特性,保留原有平台用例和静态运行库
run: |
printf '[profile.ci.junit]\npath = "junit.xml"\n' > "$RUNNER_TEMP/nextest-ci.toml"
cargo nextest run --config-file "$RUNNER_TEMP/nextest-ci.toml" --locked --target "$BUILD_TARGET" \
-p gateway-admin -p gateway-host --test main --no-tests fail --profile ci \
-E '(package(=gateway-admin) & test(/^use_case::plugins::official::/)) | (package(=gateway-host) & test(/^system_update::/))'
- name: Upload test report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: windows-tests-${{ matrix.arch }}
path: backend/target/nextest/ci/junit.xml
if-no-files-found: ignore
retention-days: 7
security:
needs: release-info
uses: ./.github/workflows/security.yml
permissions:
contents: read
security-events: write
with:
ref: ${{ needs.release-info.outputs.git_sha }}
frontend:
needs: release-info
runs-on: ubuntu-26.04
timeout-minutes: 15
env:
CI: 'true'
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.release-info.outputs.git_sha }}
- name: Validate and build frontend
uses: ./.github/actions/build-frontend
with:
upload-artifact: 'true'
build-docker-binary:
name: build-docker-binary (${{ matrix.os }}-${{ matrix.arch }})
@@ -181,50 +180,27 @@ jobs:
with:
ref: ${{ needs.release-info.outputs.git_sha }}
- name: Cache Cargo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
backend/target
key: cargo-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock') }}
restore-keys: |
cargo-${{ runner.os }}-${{ matrix.target }}-
- name: Build binary
shell: bash
run: |
mkdir -p "$HOME/.cargo" "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}"
docker run --rm \
--platform "${{ matrix.dockerPlatform }}" \
--user "$(id -u):$(id -g)" \
-e CARGO_HOME=/cargo \
-e HOME=/tmp \
-e CPR_VERSION="${CPR_VERSION}" \
-e CPR_GIT_SHA="${CPR_GIT_SHA}" \
-e CPR_BUILD_TIME="${CPR_BUILD_TIME}" \
-e CPR_BUILD_TYPE="${CPR_BUILD_TYPE}" \
-v "$HOME/.cargo:/cargo" \
-v "$PWD/..:/workspace" \
-w /workspace/backend \
rust:1.97-bookworm@sha256:77fac8b98f9f46062bb680b6d25d5bcaabfc400143952ebc572e924bcbedc3fa \
cargo build --release --locked --target "${{ matrix.target }}" --bin codex-proxy-rs
cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/${{ matrix.binary }}"
working-directory: backend
uses: ./.github/actions/build-binary
with:
rust: ${{ needs.release-info.outputs.rust }}
target: ${{ matrix.target }}
platform: ${{ matrix.os }}-${{ matrix.arch }}
binary: ${{ matrix.binary }}
rust-image: ${{ needs.release-info.outputs.rust_image }}
env:
CPR_VERSION: ${{ needs.release-info.outputs.version }}
CPR_GIT_SHA: ${{ needs.release-info.outputs.git_sha }}
CPR_BUILD_TIME: ${{ needs.release-info.outputs.build_time }}
CPR_BUILD_TYPE: release
CPR_BUILD_TYPE: ${{ needs.release-info.outputs.build_type }}
- name: Upload binary artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: codex-proxy-rs-${{ matrix.os }}-${{ matrix.arch }}
path: dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/
if-no-files-found: error
retention-days: 1
retention-days: 7
build-binary:
name: build-binary (${{ matrix.os }}-${{ matrix.arch }})
@@ -241,51 +217,33 @@ jobs:
with:
ref: ${{ needs.release-info.outputs.git_sha }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@1a3a6d54512beeaffd394f8d516ca16f2c506a20 # 1.97
with:
targets: ${{ matrix.target }}
- name: Cache Cargo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
backend/target
key: cargo-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock') }}
restore-keys: |
cargo-${{ runner.os }}-${{ matrix.target }}-
- name: Build binary
shell: bash
run: |
cargo build --release --locked --target "${{ matrix.target }}" --bin codex-proxy-rs
mkdir -p "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}"
cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/${{ matrix.binary }}"
working-directory: backend
uses: ./.github/actions/build-binary
with:
rust: ${{ needs.release-info.outputs.rust }}
target: ${{ matrix.target }}
platform: ${{ matrix.os }}-${{ matrix.arch }}
binary: ${{ matrix.binary }}
env:
CPR_VERSION: ${{ needs.release-info.outputs.version }}
CPR_GIT_SHA: ${{ needs.release-info.outputs.git_sha }}
CPR_BUILD_TIME: ${{ needs.release-info.outputs.build_time }}
CPR_BUILD_TYPE: release
CPR_BUILD_TYPE: ${{ needs.release-info.outputs.build_type }}
- name: Upload binary artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: codex-proxy-rs-${{ matrix.os }}-${{ matrix.arch }}
path: dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/
if-no-files-found: error
retention-days: 1
retention-days: 7
package-assets:
needs: [release-info, quality, build-docker-binary, build-binary]
runs-on: ubuntu-latest
needs: [release-info, frontend, build-docker-binary, build-binary]
runs-on: ubuntu-26.04
timeout-minutes: 20
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Checkout
@@ -294,105 +252,44 @@ jobs:
ref: ${{ needs.release-info.outputs.git_sha }}
- name: Download frontend artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: frontend-dist
path: dist/frontend
- name: Download binary artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: codex-proxy-rs-*
path: dist/binaries
- name: Download sealed plugin assets
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: plugin-release-assets
path: dist/plugin-release-assets
- name: Package release assets
shell: bash
env:
VERSION: ${{ needs.release-info.outputs.version }}
run: |
set -euo pipefail
ruby <<'RUBY' > release-platforms.json
require "json"
require "yaml"
print JSON.generate(YAML.load_file("release/platforms.yaml"))
RUBY
python3 - <<'PY'
import hashlib
import json
import os
import pathlib
import shutil
import tarfile
APP_NAME = "codex-proxy-rs"
def sha256(path: pathlib.Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
version = os.environ["VERSION"]
platforms = json.loads(pathlib.Path("release-platforms.json").read_text())["platforms"]
frontend_dir = pathlib.Path("dist/frontend")
binary_artifacts_dir = pathlib.Path("dist/binaries")
release_dir = pathlib.Path("dist/release")
package_dir = pathlib.Path("package")
shutil.rmtree(release_dir, ignore_errors=True)
release_dir.mkdir(parents=True)
archives = []
for platform in platforms:
platform_name = f"{platform['os']}-{platform['arch']}"
artifact_name = f"{APP_NAME}-{platform_name}"
binary_src = binary_artifacts_dir / artifact_name / platform["binary"]
if not binary_src.is_file():
raise SystemExit(f"Missing binary artifact: {binary_src}")
shutil.rmtree(package_dir, ignore_errors=True)
package_root = package_dir / APP_NAME
web_dist = package_root / "web" / "dist"
web_dist.mkdir(parents=True)
binary_dest = package_root / platform["binary"]
shutil.copy2(binary_src, binary_dest)
binary_dest.chmod(0o755)
shutil.copytree(frontend_dir, web_dist, dirs_exist_ok=True)
archive_name = f"{APP_NAME}_{version}_{platform['os']}_{platform['arch']}.{platform['archive']}"
archive_path = release_dir / archive_name
if platform["archive"] == "tar.gz":
with tarfile.open(archive_path, "w:gz") as archive:
archive.add(package_root, arcname=APP_NAME)
else:
raise SystemExit(f"Unsupported archive format: {platform['archive']}")
archives.append(archive_path)
checksum_path = release_dir / "checksums.txt"
with checksum_path.open("w", encoding="utf-8") as file:
for archive in archives:
file.write(f"{sha256(archive)} {archive.name}\n")
PY
- name: Generate release asset provenance
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-path: dist/release/*
DOCKER_VERSION: ${{ needs.release-info.outputs.docker_version }}
PLATFORM_MATRIX: ${{ needs.release-info.outputs.platform_matrix }}
run: ruby .github/scripts/package-release.rb archives
- name: Upload release assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-assets
path: dist/release
if-no-files-found: error
retention-days: 1
retention-days: 7
# tar.gz 已压缩,Actions 只负责传递原始发行字节。
compression-level: 0
build-docker-image:
name: build-docker-image (${{ matrix.dockerPlatform }})
needs: [release-info, quality, build-docker-binary]
needs: [release-info, frontend, build-docker-binary]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.release-info.outputs.docker_matrix) }}
@@ -402,6 +299,7 @@ jobs:
contents: read
packages: write
id-token: write
security-events: write
env:
TRIVY_QUIET: 'true'
TRIVY_SKIP_VERSION_CHECK: 'true'
@@ -413,48 +311,36 @@ jobs:
ref: ${{ needs.release-info.outputs.git_sha }}
- name: Download frontend artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: frontend-dist
path: dist/docker/web/dist
- name: Download Docker binary artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: codex-proxy-rs-${{ matrix.os }}-${{ matrix.arch }}
path: dist/docker-artifacts/codex-proxy-rs-${{ matrix.os }}-${{ matrix.arch }}
- name: Download sealed plugin assets
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: plugin-release-assets
path: dist/plugin-release-assets
- name: Prepare Docker prebuilt assets
shell: bash
env:
MATRIX_OS: ${{ matrix.os }}
MATRIX_ARCH: ${{ matrix.arch }}
MATRIX_BINARY: ${{ matrix.binary }}
run: |
set -euo pipefail
python3 - <<'PY'
import shutil
import os
import pathlib
app_name = "codex-proxy-rs"
platform_name = f"{os.environ['MATRIX_OS']}-{os.environ['MATRIX_ARCH']}"
binary_src = pathlib.Path("dist/docker-artifacts") / f"{app_name}-{platform_name}" / os.environ["MATRIX_BINARY"]
if not binary_src.is_file():
raise SystemExit(f"Missing Docker binary artifact: {binary_src}")
target_dir = pathlib.Path("dist/docker") / platform_name
target_dir.mkdir(parents=True, exist_ok=True)
binary_dest = target_dir / app_name
shutil.copy2(binary_src, binary_dest)
binary_dest.chmod(0o755)
PY
run: ruby .github/scripts/package-release.rb image
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GHCR
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -462,7 +348,7 @@ jobs:
- name: Build and push app image
id: build-and-push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: deploy/Dockerfile
@@ -471,8 +357,11 @@ jobs:
push: true
build-args: |
CPR_PREBUILT_DIR=dist/docker
cache-from: type=gha,scope=runtime-prebuilt-${{ matrix.os }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=runtime-prebuilt-${{ matrix.os }}-${{ matrix.arch }}
no-cache-filters: |
runtime-base
runtime-prebuilt
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.os }}-${{ matrix.arch }}
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.os }}-${{ matrix.arch }},mode=min
provenance: mode=max
sbom: true
labels: |
@@ -482,23 +371,39 @@ jobs:
org.opencontainers.image.source=https://github.com/zyycn/codex-proxy-rs
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ needs.release-info.outputs.git_sha }}-${{ matrix.arch }}
- name: Scan exact app image digest
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
env:
TRIVY_PLATFORM: ${{ matrix.dockerPlatform }}
- name: Verify exact app image digest
uses: ./.github/actions/verify-container
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
version: v0.72.0
format: table
severity: HIGH,CRITICAL
vuln-type: os,library
scanners: vuln
ignore-unfixed: true
exit-code: '1'
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
platform: ${{ matrix.dockerPlatform }}
artifact-name: container-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.arch }}
- name: Record verified image digest
shell: bash
env:
IMAGE_DIGEST: ${{ steps.build-and-push.outputs.digest }}
IMAGE_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
[[ "$IMAGE_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]
mkdir -p dist/digests
printf '%s\n' "$IMAGE_DIGEST" > "dist/digests/linux-${IMAGE_ARCH}.txt"
- name: Upload verified image digest
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: image-digest-${{ matrix.os }}-${{ matrix.arch }}
path: dist/digests/*.txt
if-no-files-found: error
retention-days: 7
publish:
needs: [release-info, package-assets, build-docker-image]
runs-on: ubuntu-latest
concurrency:
group: release-publication
queue: max
# 产物准备可与测试重叠,版本标签和 GitHub Release 必须等全部质量与镜像验证通过
needs: [release-info, quality, windows, security, package-assets, build-docker-image]
runs-on: ubuntu-26.04
timeout-minutes: 20
permissions:
contents: write
@@ -513,32 +418,37 @@ jobs:
ref: ${{ needs.release-info.outputs.git_sha }}
- name: Download release assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release-assets
path: dist/release
- name: Download verified image digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: image-digest-*
path: dist/digests
merge-multiple: true
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Login to GHCR
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create app image manifest
- name: Assemble candidate image manifest
id: manifest
shell: bash
env:
DOCKER_VERSION: ${{ needs.release-info.outputs.docker_version }}
GIT_SHA: ${{ needs.release-info.outputs.git_sha }}
DOCKER_PLATFORMS: ${{ needs.release-info.outputs.docker_platforms }}
PRERELEASE: ${{ needs.release-info.outputs.prerelease }}
run: |
set -euo pipefail
@@ -549,7 +459,12 @@ jobs:
platform="${platform//[[:space:]]/}"
[ -n "${platform}" ] || continue
arch="${platform##*/}"
sources+=("${image}:sha-${GIT_SHA}-${arch}")
digest="$(cat "dist/digests/linux-${arch}.txt")"
if [[ ! "$digest" =~ ^sha256:[a-f0-9]{64}$ ]]; then
echo "invalid verified image digest for ${platform}" >&2
exit 1
fi
sources+=("${image}@${digest}")
done
if [ "${#sources[@]}" -eq 0 ]; then
@@ -557,16 +472,10 @@ jobs:
exit 1
fi
target_args=(
-t "${image}:${DOCKER_VERSION}"
-t "${image}:sha-${GIT_SHA}"
)
if [ "$PRERELEASE" != true ]; then
target_args+=(-t "${image}:latest")
fi
docker buildx imagetools create "${target_args[@]}" "${sources[@]}"
candidate="${image}:sha-${GIT_SHA}"
docker buildx imagetools create -t "$candidate" "${sources[@]}"
inspect_output="$(docker buildx imagetools inspect "${image}:${DOCKER_VERSION}")"
inspect_output="$(docker buildx imagetools inspect "$candidate")"
platform_list="$(awk '$1 == "Platform:" { print $2 }' <<< "${inspect_output}")"
for platform in "${platforms[@]}"; do
if ! grep -Eq "^${platform}(/|$)" <<< "${platform_list}"; then
@@ -576,7 +485,7 @@ jobs:
fi
done
digest="$(docker buildx imagetools inspect "${image}:${DOCKER_VERSION}" --format '{{ .Manifest.Digest }}')"
digest="$(docker buildx imagetools inspect "$candidate" --format '{{ .Manifest.Digest }}')"
if [ -z "${digest}" ]; then
echo "missing image digest"
exit 1
@@ -584,67 +493,104 @@ jobs:
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
- name: Generate container provenance attestation
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.manifest.outputs.digest }}
push-to-registry: true
- name: Sign app image
shell: bash
- name: Sign app image digest
env:
DIGEST: ${{ steps.manifest.outputs.digest }}
DOCKER_VERSION: ${{ needs.release-info.outputs.docker_version }}
GIT_SHA: ${{ needs.release-info.outputs.git_sha }}
PRERELEASE: ${{ needs.release-info.outputs.prerelease }}
run: cosign sign --yes "${REGISTRY}/${IMAGE_NAME}@${DIGEST}"
- name: Prepare release notes
shell: bash
env:
INSTALLATION_NOTES: |
## 📥 安装
**Docker 部署:**
```bash
# 从 GitHub 容器镜像仓库拉取
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.release-info.outputs.docker_version }}
```
请从本版本附件下载 `compose.yaml` 和 `config.example.yaml`。Compose 默认固定到本版本镜像;升级前请对比模板与已有配置。
**手动下载:**
请从下方附件中下载适合当前平台的归档包。
附件包含与当前宿主版本及提交绑定的 `plugin-release-manifest.json`,当前没有默认内置插件。
教学示例由独立的 `codex-proxy-plugins` 仓库维护和发布,不随宿主发行物分发。
发布归档提供 GitHub 构建产物证明。Docker 镜像附带软件物料清单(SBOM)、构建来源证明和 cosign 无密钥签名。
## 📚 文档
- [GitHub 仓库](https://github.com/${{ github.repository }})
- [部署指南](https://github.com/${{ github.repository }}/blob/${{ needs.release-info.outputs.tag }}/deploy/README.md)
run: |
set -euo pipefail
if [ -z "${DIGEST}" ]; then
echo "missing image digest"
exit 1
fi
ruby <<'RUBY'
notes = File.readlines('release/notes.md').drop(1).join.strip
installation = ENV.fetch('INSTALLATION_NOTES').strip
output = File.join(ENV.fetch('RUNNER_TEMP'), 'release-notes.md')
File.write(output, "#{notes}\n\n#{installation}\n")
RUBY
image="${REGISTRY}/${IMAGE_NAME}"
tags=("${image}:${DOCKER_VERSION}" "${image}:sha-${GIT_SHA}")
if [ "$PRERELEASE" != true ]; then
tags+=("${image}:latest")
fi
images=()
for tag in "${tags[@]}"; do
images+=("${tag}@${DIGEST}")
done
- name: Prepare versioned channel snapshot under the publication lock
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.release-info.outputs.version }}
run: ruby .github/scripts/published-channels.rb prepare "$VERSION"
cosign sign --yes "${images[@]}"
- name: Generate release asset provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/release/*
- name: Publish GitHub Release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
- name: Upload GitHub Release assets
id: release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
tag_name: ${{ needs.release-info.outputs.tag }}
generate_release_notes: true
previous_tag: ${{ needs.release-info.outputs.previous_tag }}
prerelease: ${{ needs.release-info.outputs.prerelease == 'true' }}
make_latest: ${{ needs.release-info.outputs.prerelease == 'true' && 'false' || 'true' }}
body: |
> [!WARNING]
> **Upgrading across major versions?** Database schema and configuration are only compatible within a major version, and the one-click self-update refuses cross-major targets (releases before 3.0 lack this gate — do not use their built-in update to reach a newer major). Deploy the new major fresh (new `.runtime/` data directory) and re-import or re-authorize provider accounts and client keys. See the [upgrade notes](https://github.com/${{ github.repository }}/blob/main/deploy/README.md#镜像升级与源码构建).
## 📥 Installation
**Docker:**
```bash
# GitHub Container Registry
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.release-info.outputs.docker_version }}
```
**Manual download:**
Download the appropriate archive for your platform from the assets below.
Release archives are covered by GitHub artifact attestations. Docker images are published with SBOM/provenance attestations and keyless cosign signatures.
## 📚 Documentation
- [GitHub Repository](https://github.com/${{ github.repository }})
- [Installation Guide](https://github.com/${{ github.repository }}/blob/main/deploy/README.md)
# action 保留已公开 Release 的状态,新 Release 上传完成后仍保持草稿
draft: true
make_latest: 'false'
body_path: ${{ runner.temp }}/release-notes.md
fail_on_unmatched_files: true
files: |
dist/release/*
- name: Publish version and advance latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.release-info.outputs.version }}
DIGEST: ${{ steps.manifest.outputs.digest }}
RELEASE_ID: ${{ steps.release.outputs.id }}
run: ruby .github/scripts/publish-release.rb
- name: Advance update-index to the verified snapshot
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.release-info.outputs.version }}
run: ruby .github/scripts/published-channels.rb finish "$VERSION"
recover-index:
if: inputs.index_only
runs-on: ubuntu-26.04
concurrency:
group: release-publication
queue: max
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Verify public assets and finish pending publication
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
run: ruby .github/scripts/published-channels.rb finish "$TAG"
+91
View File
@@ -0,0 +1,91 @@
name: Rules Guard
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
- edited
permissions:
contents: read
pull-requests: write
concurrency:
group: rules-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
guard:
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
# 只查询 PR 元数据,禁止检出或执行贡献分支中的代码。
- name: Check protected files
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const params = { owner, repo, pull_number: pr.number };
const files = await github.paginate(github.rest.pulls.listFiles, {
...params,
per_page: 100,
});
// 按 PR 作者识别官方机器人,维护者重开或编辑 PR 时仍使用同一判断
const isDependabot = pr.user.login === 'dependabot[bot]'
&& pr.user.type === 'Bot'
&& pr.head.repo?.full_name === `${owner}/${repo}`;
const isDependencyUpdate = file => isDependabot
&& file.status === 'modified'
&& (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(file.filename)
|| /^\.github\/actions\/.+\/(?:action\.ya?ml|Dockerfile)$/.test(file.filename));
const isProtectedRule = path => typeof path === 'string'
&& (path === 'AGENTS.md' || path.endsWith('/AGENTS.md')
|| path === 'CONTRIBUTING.md'
|| path === '.agents/skills' || path.startsWith('.agents/skills/')
|| path === '.github' || path.startsWith('.github/'));
// 依赖更新只豁免已有文件的修改,重命名前后的受保护路径仍参与检查
if (!files.some(file => !isDependencyUpdate(file)
&& (isProtectedRule(file.filename) || isProtectedRule(file.previous_filename)))) {
core.info('No protected contribution rule changes detected.');
return;
}
// 旧事件不处理后续提交,也不重复处理已关闭的 PR。
const { data: current } = await github.rest.pulls.get(params);
if (current.state !== 'open' || current.head.sha !== pr.head.sha) {
core.info('Pull request state has changed; skipping this event.');
return;
}
// 通知标记保持稳定,以便复用机器人已有评论,避免重复提醒
const marker = '<!-- agents-md-guard -->';
const body = [
marker,
'我看到这个 PR 涉及受保护的协作规则文件,按仓库约定需要先关闭',
'',
'保护范围包括根目录或子目录的 `AGENTS.md`、根目录的 `CONTRIBUTING.md`,以及 `.agents/skills/` 和 `.github/` 中的所有文件,涵盖新增、修改、删除和重命名',
'',
'麻烦先撤销这些变更,再重新打开 PR;如果希望调整协作规则,可以先提个 Issue 说明原因和建议,交由维护者处理,感谢理解',
].join('\n');
try {
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100,
});
const notification = comments.find(comment => comment.user?.login === 'github-actions[bot]'
&& comment.body?.includes(marker));
if (!notification) {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
} else if (notification.body !== body) {
await github.rest.issues.updateComment({ owner, repo, comment_id: notification.id, body });
}
} catch (error) {
core.warning(`Unable to post policy explanation: ${error.message}`);
}
await github.rest.pulls.update({ ...params, state: 'closed' });
core.setFailed('Pull requests must not change protected contribution rules (AGENTS.md, CONTRIBUTING.md, .agents/skills/, .github/).');
-79
View File
@@ -1,79 +0,0 @@
name: Security Scan
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: '0 3 * * 1'
permissions:
contents: read
concurrency:
group: security-scan-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
jobs:
backend-security:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@1a3a6d54512beeaffd394f8d516ca16f2c506a20 # 1.97
- name: Cache Rust
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: backend -> target
- name: Install cargo-audit
run: cargo install cargo-audit --version 0.22.2 --locked
- name: Run cargo audit
working-directory: backend
run: cargo audit
frontend-security:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 11.7.0
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install frontend dependencies
run: pnpm --dir frontend install --frozen-lockfile
- name: Run pnpm audit
run: pnpm --dir frontend audit --prod --audit-level=high
container-security:
if: github.event_name == 'schedule'
permissions:
contents: read
security-events: write
uses: ./.github/workflows/_container.yml
with:
ref: ${{ github.sha }}
+84
View File
@@ -0,0 +1,84 @@
name: Security
on:
workflow_call:
inputs:
ref:
description: Exact commit SHA to audit
required: true
type: string
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: '0 3 * * 1'
permissions:
contents: read
concurrency:
group: security-${{ github.workflow }}-${{ inputs.ref || github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
jobs:
backend-security:
runs-on: ubuntu-26.04
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.sha }}
- name: Install cargo-audit
uses: taiki-e/install-action@682ce6c3c696a4cd5168a05f98e51aa02b877941 # v2.89.1
with:
tool: cargo-audit@0.22.2
fallback: none
- name: Run Cargo audits
run: |
cargo-audit audit --file backend/Cargo.lock
frontend-security:
runs-on: ubuntu-26.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.sha }}
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
package_json_file: frontend/package.json
- name: Read build toolchains
id: toolchains
run: ruby .github/scripts/toolchains.rb >> "$GITHUB_OUTPUT"
- name: Setup Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ steps.toolchains.outputs.node }}
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install frontend dependencies
run: pnpm --dir frontend install --frozen-lockfile
- name: Run pnpm audit
run: pnpm --dir frontend audit --prod --audit-level=high
container-security:
if: github.event_name == 'schedule'
permissions:
contents: read
security-events: write
uses: ./.github/workflows/_container.yml
with:
ref: ${{ github.sha }}
+6 -3
View File
@@ -6,9 +6,8 @@
# Frontend
/frontend/node_modules/
/frontend/dist/
/frontend/.vite/
/.pnpm-store/
/frontend/.pnpm-store/
/frontend/.pnpm-cache/
*.tsbuildinfo
# Release artifacts
@@ -20,6 +19,9 @@
# Local configuration and secrets
/deploy/config.yaml
.env
.env.*
!.env.example
# Editors and operating systems
/.idea/
@@ -32,9 +34,10 @@ Thumbs.db
# Local tools and worktrees
/.codegraph/
/.playwright-cli/
/.codex-ws-audit/
/.agents/
/.reasonix/
/.claude
/.worktrees/
/worktrees/
/.cargo/
+6
View File
@@ -0,0 +1,6 @@
[submodule "modules/ui"]
path = modules/ui
url = https://github.com/zyycn/codex-proxy-ui.git
[submodule "modules/plugins"]
path = modules/plugins
url = https://github.com/zyycn/codex-proxy-plugins.git
+78
View File
@@ -0,0 +1,78 @@
# 项目协作约定
按用户意图和当前会话主动加载并执行仓库技能,不要求用户写出 `$技能名`:
| 任务意图与常见说法 | 必用技能 |
| --- | --- |
| 开发、修复、排障、代码审查、改文档;包括“改弹窗/样式/文案”“修接口/调度/数据库”等省略请求 | [cpr-dev-guide](.agents/skills/cpr-dev-guide/SKILL.md),按实际操作和文件匹配规范,跨前后端同时执行 |
| 提交/提个 PR、准备或更新 PR、看下 PR、回复 PR 讨论、处理 PR 审查意见 | [cpr-github-pr](.agents/skills/cpr-github-pr/SKILL.md);实现自审、审查或修改同时执行 cpr-dev-guide |
| 提 Issue、反馈到仓库、把问题或建议整理成 Issue、补充或回复 Issue | [cpr-github-issue](.agents/skills/cpr-github-issue/SKILL.md) |
| 写、修或审查独立网关插件,排查插件接入、打包插件 | [cpr-plugin-dev](.agents/skills/cpr-plugin-dev/SKILL.md),同时执行 cpr-dev-guide;仅改宿主能力时由 cpr-dev-guide 加载插件边界 |
| 发版、准备版本、发布 beta/实验版、整理发布说明、打发行 tag | [cpr-release](.agents/skills/cpr-release/SKILL.md) |
结合上下文识别“提交吧”“看看这个问题”等省略说法:普通 Git 提交不自动变成 PR,代码排查不自动变成 Issue;审查与发版准备不代表获准远程写入
首次命中技能时完整读取其 `SKILL.md`,再读取领域必读规范,之后按当前步骤补充实现、命令和示例。技能名称、描述或入口摘要不能代替首次读取的正文与必读引用;引用中明确要求“先读”“必须读取”或“执行”的规范也要落实,其他资料按需读取
按用户意图、行为影响和实际文件共同匹配,跨领域叠加;确定方案或给出审查结论前完成对应领域必读,不能因改动小、路径未落在该模块或自认为熟悉而跳过
执行所选流程中的问题核实、自审、验证与交付要求,协作流程和审查标准见 [CONTRIBUTING.md](CONTRIBUTING.md)
Issue 和 PR 回复统一按 [GitHub 回复口吻](CONTRIBUTING.md#github-回复口吻)执行
模块职责以 [系统架构](docs/architecture.md) 为准,界面约定以 [管理端主题](docs/theme.md) 为准;
没有明确约定时保持与相邻实现一致,不把单次页面反馈扩展成全仓规则
### 任务延续与收尾
- 在任务上下文和压缩摘要中保留目标、授权、当前阶段、已读规范的路径与关键约束、验证命令与结果及对应代码状态、剩余事项
- 同一任务可复用实际完成的读取与验证记录;仅在规范变更、进入未覆盖领域或具体决策缺少条款时补读对应内容。输出截断只补缺失部分,上下文压缩与阶段切换本身不触发重读或重验
- 远端更新或合并冲突后,按实际变更及其依赖影响补充自审和验证,复用未受影响的证据
- 实现、自审与必要验证完成后,按[验证停止条件](CONTRIBUTING.md#验证)停止扩围;完成已获授权的剩余交付步骤并确认结果后结束
<!-- CODEGRAPH_START -->
## CodeGraph
In repositories indexed by CodeGraph (a `.codegraph/` directory exists at the repo root), reach for it BEFORE grep/find or reading files when you need to understand or locate code:
- **Index freshness**: Run `codegraph sync` from the repository root before the first code query in a task. After pulling updates, switching branches, or changing code, run it again before the next query so symbols and call paths match the working tree. If synchronization fails, report the failure and verify against current source rather than treating the index as current.
- **MCP tools** (when available): `codegraph_explore` answers most code questions in one call — the relevant symbols' verbatim source plus the call paths between them. `codegraph_node` returns one symbol's source + callers, or reads a whole file with line numbers. If the tools are listed but deferred, load them by name via tool search.
- **Shell** (always works): `codegraph explore "<symbol names or question>"` and `codegraph node <symbol-or-file>` print the same output.
If there is no `.codegraph/` directory, skip CodeGraph entirely — indexing is the user's decision.
<!-- CODEGRAPH_END -->
## 工作方式
- 开发和审查按 [问题与方案依据](CONTRIBUTING.md#问题与方案依据) 先核实问题及方案必要性;涉及 Codex 行为、协议或兼容性时,以官方对应版本源码为第一手资料,三方仓库仅作参考
- 先定位变更所属模块、同类实现和上下游调用关系,再选择最小合理改动。复用已有能力,也避免为尚不存在的需求增加抽象。
- 提交前自审最终差异,先处理本次引入的职责混淆、重复规则、冗余状态和无依据的兼容分支;后端按 [后端自审](docs/architecture.md#后端自审) 追踪状态与资源生命周期,不能把自审留给 CI 或 PR 审查者。
- 前端开发与审查执行[前端流程](.agents/skills/cpr-dev-guide/references/frontend.md),先复用已有页面设计与组件,不因个人偏好重排页面或另建交互;页面 PR 必须提供 [实际截图](CONTRIBUTING.md#界面验证)。
- 代码注释使用中文,解释原因与边界;提交信息使用英文,沿用历史中的 Conventional Commits 格式。
- 常规文档只描述当前状态,不记录变更历史;按[文档职责与更新条件](CONTRIBUTING.md#文档职责与更新条件)在所属章节就地修订,分清层次与详略,短段和列表少用句号,交付前逐段检查必要性、归属、重复与当前状态表达
- 按变更范围执行验证,记录命令、结果和缺口。跳过的测试不算通过;界面与集成行为需要对应运行证据,构建通过不能替代实际验收。
- 审查请求默认只读。修复、提交、推送和合并按用户当前授权执行;“本地验证通过”不等于用户已经审阅批准。
- 不读取或输出无关凭据,不把真实密钥、代理认证、账号令牌或请求转储放入提交、截图及审查报告。
- 使用 AI 的 PR 按 [AI 使用披露](CONTRIBUTING.md#ai-使用披露) 写明实际模型型号,不以工具名称代替模型。
## Code Review Rules
### 审查范围
- 确认目标分支与当前变更范围,结合调用方、被调用方及已有测试审查;结论对应实际审查的提交或工作区状态。
- 按[审查标准](CONTRIBUTING.md#审查标准)检查设计、正确性、安全、复杂度、性能、可读性、验证、界面和文档,深度与变更风险相称
- 重点核对现有架构和数据合同:共享业务事实是否重复解释,异步与并发操作是否破坏状态,权限和敏感数据是否越界,协议适配是否影响兼容性。
- 修改既有特殊分支时先查场景和历史;放宽检查、修改测试或新增例外需要业务依据,不能仅以让检查通过为理由。
### 代码与界面一致性
- 对重复逻辑、冗余状态和多余抽象给出具体位置及影响;建议复用时指出已有实现,不能仅凭行数或语法相似判定质量。
- UI 变更对照原页面、项目基础组件、主题和同类页面,检查布局变化是否服务本次需求、是否堆叠重复文案,以及交互、状态、响应式与可访问性。局部问题是否应在共用组件修复,按职责判断,不一律禁止局部样式。
- 核对页面 PR 按 [界面验证](CONTRIBUTING.md#界面验证) 提供的截图与实际交互证据;缺少必需截图时明确要求补齐,未检查的状态列为验证缺口,不推断视觉验收已经通过。
### 审查输出
- 用中文先回答原问题是否成立、解决方向是否合理,依据与判断标准见 [贡献与审查](CONTRIBUTING.md#审查标准),再报告实现问题和验证缺口。
- 实现问题聚焦本次变更新引入或加重的、可定位的问题,包含文件位置、触发场景或规范依据、影响和最小修复方向。
- 区分必须修复的问题、可选建议和验证缺口。个人偏好标为建议,不作为必须修复项;没有问题时不要凑数。
- 严重程度按实际影响判断,不为绕过工具的优先级过滤而升级普通规范问题。说明审查和验证的实际覆盖范围,AI 未报告问题不等于维护者批准。
+244
View File
@@ -0,0 +1,244 @@
# 贡献与审查
贡献应解决明确的问题,并保持或改善项目的可维护性。提交者、自动检查和审查者使用同一套标准,让重复问题在提交和初审阶段得到处理
本文定义协作流程与审查标准;模块职责以 [系统架构](docs/architecture.md) 为准,视觉规则以 [管理端主题](docs/theme.md) 为准。没有明确约定时遵循相邻实现;具体页面的布局决定不自动成为全项目限制
## 交流与反馈
[讨论区](https://github.com/zyycn/codex-proxy-rs/discussions) 用于使用问答、想法讨论、实验反馈和经验分享。
提问时按需补充版本、部署方式、已尝试的方法及脱敏信息;问题解决后,请在「使用问答」中标记有帮助的答案
明确的 Bug、具体功能需求或协作规则建议使用 Issue,无需先开讨论。
讨论形成可执行事项后,可以新建 Issue 并互相附链接;已有 Issue 的后续信息继续补充在原帖,避免重复记录。
围绕问题友善交流,欢迎不同使用场景和方案观点
### 提交 Issue
按场景选择 [接口问题反馈](.github/ISSUE_TEMPLATE/api-bug-report.yml)、[常规问题反馈](.github/ISSUE_TEMPLATE/general-bug-report.yml) 或 [功能建议](.github/ISSUE_TEMPLATE/feature-request.yml) 表单,按对应提示提供信息。故障反馈填写实际运行版本和中转链路;接口问题区分客户端显示与网关实际响应,页面问题附相关截图或录屏
日志和配置只提供定位所需片段,不上传真实凭据或完整请求转储。功能建议说明使用场景、当前不便和期望行为;协作规则调整可直接提交普通 Issue,说明原因和建议方案
### GitHub 回复口吻
AI 草拟、发布或修改 Issue、PR 讨论及审查意见的回复时,以一起排查问题、讨论方案的口吻交流:
- 围绕问题、事实和结论展开,能自然省略主语时省略;需要说明个人判断或行动归属时用“我”,避免反复以“我”起句或强调自己,不用“我们”“本助手”代称自己
- 普通回复直接回应问题,不加问候语或称呼;向对方提问时称呼“佬”,同一条回复不重复称呼
- 先接住对方想完成的事和遇到的困难,再给出核实结果与建议;回复让对方感到问题被理解、有下一步可做,避免一上来归类、纠正或划定责任
- 给出操作建议时说明它能帮助判断什么,措辞自然、委婉,不靠堆叠缓和词修饰命令
- 回应已有能力、暂不支持或未采纳的建议时,结合场景说明可用入口或具体原因,有可行替代办法时一并说明,再交代必要边界;避免只回复“已有功能”“重复问题”“不在范围内”等结论
- 需要补充材料或配合操作时,先利用已有信息,只询问仍缺少的部分,说明用途和最小范围;例如“佬,方便再观察几次吗?如果还出现,可以附一下对应请求的诊断包,便于对照终态用量”
- 澄清误解或提出不同意见时,从观察和依据说起,照顾对方的使用环境与理解路径,避免责备、反问或居高临下的判断;已确认的事实保持明确,尚未确认的原因保留余地
- 使用自然短句,简单回复用短段,并列信息较多时再分点;少用句号,避免客服套话、机械模板和空泛保证
- 排查、复现、修改和验证的完成状态与实际一致;未采纳的意见说明依据,未确认、待处理或未验证的内容如实交代,说清结论、限制和下一步
## 问题与方案依据
开发、创建 PR 和审查都先核实待解决的问题、触发条件与预期行为,再确定或评估实现。区分问题发生版本、目标分支现状和本次变更后的行为,不把需求描述、PR 描述或第三方项目的做法直接当成事实。目标分支已修复的问题,应说明当前仍需补齐的差异,不能据此否定旧版本中的问题
涉及上游协议、客户端行为或兼容性时,遵守以下依据与行为约束;无上游依赖的内部变更以本项目代码、合同和运行证据为依据
- **资料来源**:以对应版本的官方源码与调用链为首要资料,Codex 对照 [openai/codex](https://github.com/openai/codex)。先查用户提供或本机已有的仓库,核对来源、版本或提交;不足时再查官方对应版本源码,结合测试、文档、发布说明和运行证据。三方资料只作线索或实现参考,不能替代官方依据
- **证据表达**:引用具体文件、符号或行号,对外优先使用固定提交的源码链接;注明适用版本,区分官方事实、三方参考与本项目推断。源码不可得或版本不匹配时报告缺口,不把最新实现当作部署行为,也不把自定义策略写成官方要求
- **默认方案**:Codex 兼容性优先保持官方行为,以透明转发和最小必要适配为默认方案。沿用官方协议语义、职责归属和解决路径,优先复用已有能力或正确传递合同,不额外解释请求意图、扩展工具权限或改变续接语义
- **问题核实**:对照官方与 CPR 的实际请求、响应及状态生命周期,检查本项目是否误解、改写、遗漏或破坏合同;分别说明源码推断、代理链路复现与官方直连结果。真实上游报错不自动等于 CPR 缺陷
- **审查官方对接**:审查 OpenAI 官方上游与 Codex 的对接时,默认二者兼容、直接对接正常。不得仅凭自行构造的异常上游数据揣测官方不兼容,据此报告缺陷、提出改进建议或要求防御分支与测试;特定版本或场景的例外须有真实响应或实际复现证据
- **偏离依据**:新增覆盖配置、补偿逻辑或三方兼容方案前,说明官方路径为何不足、本项目实际约束、最小改动、行为代价及验证。补偿后成功、测试通过或三方采用同一做法不能单独证明必要性;依据不足时保持现有合同并报告缺口,不先加分支再倒推理由。用户明确要求的行为变更限定在授权范围内
- **网关职责**:调度、隔离和资源管理按网关职责评估,不机械照搬单客户端实现,不因官方客户端没有同类池策略就判错,也不任意改变客户端可观察行为。新增默认限制或回收阈值须有容量依据,并验证续接、重试与延迟影响
反馈 Issue 时区分症状、根因假设和方案建议,关联已有 Issue、PR 及包含修复的发布版本。症状反馈不以找到官方源码或证明根因为前提;引用上游行为或提出方案时注明上述依据与缺口
## PR 流程
1. **准备变更**:按上述依据核实问题与方案,找到所属模块、相关调用链和最接近的现有实现,在功能分支完成修改。提交前自审完整差异,清理本次引入的重复逻辑、冗余状态与职责越界;后端按 [后端自审](docs/architecture.md#后端自审) 执行。涉及架构、公开合同或较大交互变化时,先说明方案与取舍
2. **发起 PR**:目标分支使用 `main`,按 [PR 模板](.github/pull_request_template.md) 说明问题依据、触发场景、最终行为、自审和实际验证结果;涉及官方行为时注明源码位置、版本和方案关系。页面变更必须按 [界面验证](#界面验证) 附实际截图,使用 AI 时按 [AI 使用披露](#ai-使用披露) 写明模型。一个 PR 围绕一个明确目标,必要重构说明关联原因
3. **自动检查与初审**:CI 执行适用的格式、静态分析、测试和构建检查;启用 AI 审查后,由它按本规范辅助检查语义、复用和一致性。提交者先处理明确问题,维护者重点审阅方案、风险与用户体验
4. **修改与合并**:更新代码后重新执行受影响的检查,确认审查意见已处理,验证证据对应当前变更。维护者审阅批准后再合并;需要升级说明的变更同步相关文档
AI 开发和审查入口是根目录的 [AGENTS.md](AGENTS.md)。在仓库上下文中要求“审查 PR #编号”时,应按下列标准执行,无需再次列举要求
### 项目技能
仓库技能位于 `.agents/skills/`,随代码共享。技能提供执行流程,协作与审查标准仍以本文和 `AGENTS.md` 为准。
AI 开发、排查、代码审查和文档修改必须执行 `cpr-dev-guide`;准备、创建、更新及审查 PR 必须执行 `cpr-github-pr`,不以用户是否显式点名技能为条件。PR 内修复仍须经过开发流程,不能用填写 PR 正文代替实现自审与验证
| 技能 | 用途 | 调用示例 |
| --- | --- | --- |
| [cpr-dev-guide](.agents/skills/cpr-dev-guide/SKILL.md) | 项目开发与文档导航 | `$cpr-dev-guide 修复当前问题` |
| [cpr-plugin-dev](.agents/skills/cpr-plugin-dev/SKILL.md) | 网关插件开发、打包与验证 | `$cpr-plugin-dev 开发一个添加响应头的插件` |
| [cpr-github-pr](.agents/skills/cpr-github-pr/SKILL.md) | 准备、创建、更新或审查 PR | `$cpr-github-pr 创建当前分支的 PR` / `$cpr-github-pr 审查 PR #编号` |
| [cpr-github-issue](.agents/skills/cpr-github-issue/SKILL.md) | 整理或提交 Issue | `$cpr-github-issue 提交当前问题` |
| [cpr-release](.agents/skills/cpr-release/SKILL.md) | 准备发布说明或正式发版 | `$cpr-release 整理本次发布说明` / `$cpr-release 发布 vX.Y.Z` |
审查请求默认在当前会话报告;明确要求创建 PR、提交 Issue 或正式发版时才执行对应远程操作。技能安装不等于启用 GitHub 自动审查
### AI 使用披露
PR 正文说明提交者是否使用 AI。使用 AI 参与方案、代码、文档、测试或自审时,写明工具名称和实际模型型号;使用多个模型时分别列出,可简述各自参与范围。
`Codex`、`ChatGPT`、`Claude Code` 等工具或产品名称不能代替模型型号,优先填写工具显示或调用配置中的具体模型标识。
无法获知型号时如实标记「型号未提供,待补充」,不能猜测或伪造,也不能把待补信息称为已完成披露;补齐前保留 Draft。
更新 PR 时同步新增的模型信息;这一披露不替代提交者自审、验证责任或已有提交署名约定
### 规则文件保护
PR 不接受以下协作规则文件的新增、修改、删除或重命名:
- 根目录及任意子目录的 `AGENTS.md`
- 根目录的 `CONTRIBUTING.md`
- `.agents/skills/` 中的所有文件,包括技能入口、引用文档、配置和脚本
- `.github/` 中的所有文件,包括工作流、Actions、Issue/PR 模板和依赖更新配置
官方 `dependabot[bot]` 从本仓库分支发起的依赖更新可修改 `.github/workflows/` 中已有的 YAML 工作流,以及 `.github/actions/` 下已有的 `action.yml`、`action.yaml` 和 `Dockerfile`,继续执行正常审查与 CI;其他受保护文件以及新增、删除、重命名不在例外范围内
除此之外,调整这些文件时先提交 Issue 说明原因和建议,由维护者处理;[规则保护工作流](.github/workflows/rules.yml) 会提示并关闭包含此类变更的 PR。撤销相关变更后可以重新打开 PR,继续正常检查
源码变更遵守现有架构边界与迁移冻结规则,由对应 CI 检查执行;不额外设立整块业务目录的 PR 禁改清单。构建、测试和安全扫描复用现有工作流
## 审查标准
按改动涉及的范围选择检查深度,无需为每个 PR 填写所有维度的报告
结论先回答原问题是否成立(成立、部分成立、不成立或证据不足),说明触发条件和依据;再判断解决方向是否合理,交代适用的官方方案、本次实现与它的关系及必要调整,最后报告实现问题与验证缺口。原问题是否成立与 PR 实现是否正确分别判断。源码推断、实际复现和检查结果分别说明,CI 通过不能代替问题与方案的论证
| 维度 | 检查重点 |
| --- | --- |
| 设计与架构 | 职责归属、依赖方向、公开接口是否合理;是否融入已有体系,有无绕过边界的旁路 |
| 正确性与兼容性 | 正常、异常和边界输入;并发、取消、重试与副作用;是否改变既有 API、协议或数据合同 |
| 安全与数据 | 权限、输入校验、敏感信息、状态隔离和持久化一致性;日志与诊断是否符合脱敏边界 |
| 简洁性与复用 | 重复业务规则、冗余状态、无效代码和过早抽象;能否用更直接的实现解决当前问题 |
| 性能与资源 | 实际数据规模下的查询、请求、内存与连接成本;资源释放、背压和异常时的生命周期 |
| 可读性与风格 | 命名是否准确,控制流是否易懂,格式是否一致;注释解释约束与原因而非复述代码 |
| 测试与验证 | 是否覆盖真实触发条件和结果,能否发现回归;证据是否对应本次变更,有无跳过或缺失项 |
| 界面与交互 | 共用组件和主题、信息层级、状态反馈、响应式、键盘操作及可访问性;是否有实际页面证据 |
| 文档与升级 | 行为、配置、API 和部署说明是否同步;数据迁移和兼容影响是否交代清楚 |
审查意见区分三类:
- **必须修复:** 有具体触发条件的缺陷、违反现有合同或明确项目规范的问题。说明位置、依据、影响和最小修复方向
- **建议:** 不影响合同或规范的替代实现、命名偏好和局部优化。个人偏好不能单独阻止合并,也不应强求与本次目标无关的重构
- **验证缺口:** 尚未检查的行为、状态或运行环境。说明缺少什么证据,由维护者评估是否需要补齐,不能把未验证当作已通过或已发现缺陷
复杂度不按行数判断,复用不只看语法相似。建议合并重复逻辑时应指出共享的业务含义;建议保留特殊分支时应能说明它服务的场景。例外应有依据,不能仅为绕过检查而放宽规则
## 项目约定
- AI 进行 Rust 开发时,加载并使用 `$rust-best-practices` 技能。模块、可见性、依赖方向和测试目录按架构文档执行,使用现有 Rustfmt 与 Clippy 配置
- Vue 使用 TypeScript、`<script setup>` 和现有 ESLint 配置;API 请求通过已有模块及 `request` 封装,沿用逐行请求配置格式
- 业务数据的解析、校验和展示优先复用已有逻辑。新增抽象放在拥有该职责的模块,避免重复事实源或无实际需求的通用层
- 新增代码注释使用中文。提交信息使用英文 Conventional Commits,参考近期历史,按完整功能归并
- AI 参与的提交按实际助手添加 `Co-authored-by: 名称 <邮箱>`,名称与邮箱采用该助手已知的署名身份,并保留其他实际共同作者署名;PR 的 AI 使用披露不替代提交署名
- 发布说明写入 `release/notes.md`,标题首行为目标版本的 `# vX.Y.Z`,章节标题和正文使用中文;命令、配置键、协议名等保留原文。只说明本次版本的实际变化,按影响交代升级事项
- PR 聚焦目标;无关格式化、重构和依赖升级单独处理
## 文档职责与更新条件
**文档只描述当前有效状态,不记录变更历史**,在所属章节就地修订,不追加“本次新增”“此前行为”或修复经过
只有用法、合同、职责、约束或读者需要执行的操作发生变化时才更新,内部重构和界面微调不默认附带文档改动
### 内容归属
| 文档 | 回答的问题 | 详略边界 |
| --- | --- | --- |
| [README](README.md) | 项目能做什么,如何开始 | 简短介绍、最短可用路径和文档导航 |
| [部署与运维](deploy/README.md) | 如何安装、接入、排障和恢复 | 前置条件、配置、操作步骤、结果判断与风险 |
| [开发与源码联调](docs/development.md) | 如何准备环境、运行源码和联调独立仓库 | 可执行命令、目录关系和依赖维护 |
| [API 参考](docs/api.md) | 调用者如何正确请求和解释结果 | 鉴权、字段、默认值、错误、限制与幂等语义,不展开内部实现或界面布局 |
| [系统架构](docs/architecture.md) | 业务事实归谁,调用如何协作 | 模块边界、状态所有权、生命周期和不变量,不重复字段清单 |
| [管理端主题](docs/theme.md) | 界面如何保持一致,主题如何扩展 | 文案与视觉约束、Token 合同、实现入口和验收范围 |
| [插件使用](docs/plugins.md) | 如何安装、授权、配置和使用插件 | 管理员操作与影响,不展开 SDK 调用或宿主事务实现 |
| [插件 SDK](backend/crates/gateway-plugin/sdk/README.md) / [CLI](backend/apps/plugin-cli/README.md) | 如何编写和打包插件 | 作者合同、权限边界、示例与打包验证 |
| [数据库迁移](backend/migrations/README.md) | 如何维护 schema 和准备测试库 | 冻结规则、升级约束和测试环境,不罗列迁移历史 |
| [共用组件](modules/ui/README.md)及其组件参考 | 如何安装、使用和开发独立 UI 库 | 组件公开 API、主题能力与库内开发;宿主页面约定归管理端主题,跨仓库联调归开发文档 |
| [官方插件示例](modules/plugins/README.md)及其示例说明 | 如何构建、使用和维护示例插件 | 示例自身的功能、配置与验证;通用协议归 SDK,宿主安装操作归插件使用文档 |
| [AGENTS.md](AGENTS.md) 与[项目技能](.agents/skills/cpr-dev-guide/SKILL.md) | AI 如何选择并执行任务流程 | 任务匹配、执行步骤和规范入口,不另建模块合同、版本表或协作标准 |
| 本文 | 如何协作、审查和交付 | 流程、判断标准与验证要求,任务入口由 [开发技能](.agents/skills/cpr-dev-guide/SKILL.md) 串联 |
- 同一事实只在所属文档完整说明,其他位置按读者任务保留必要摘要和链接
- 改动原因、前后对比、排查经过和验证结果放在 PR、Issue 或任务报告;`release/notes.md` 是发版时使用的发布说明,不作为常规文档的历史章节,也不随普通开发任务自动追加
- 仍影响升级、恢复或兼容使用的内容,写成明确的适用条件与操作,保留必要限制,不叙述演变过程
### 层次与行文
- 按读者任务组织章节,先给入口和操作,再解释限制;长篇参考文档提供导航,独立主题使用小标题
- 快速开始只保留走通流程的必要步骤;API、配置、恢复和权限等容易误用的内容写清输入、默认值、边界与失败处理
- 短说明直接成段,并列信息用列表,可比较的字段或职责用表格,避免一段堆叠多个主题
- 标题、表格单元格、短列表项和独立短段通常不加句号;长段优先拆分,确需区分完整句意时保留句号,不机械替换成逗号或分号
- 命令、URL、版本号、字段名和原始输出保持准确,不为统一标点改写代码示例
审查时逐段核对:是否属于当前状态、是否回答读者问题、是否放在正确文档、是否重复、脱离变更背景能否独立理解
没有文档改动本身不构成验证缺口,判断依据是当前说明是否仍然准确、完整
## 界面验证
AI 进行前端开发或审查时,执行[前端流程](.agents/skills/cpr-dev-guide/references/frontend.md),覆盖页面、组件、弹窗、表单及局部文案和样式。用户指定设计技能时使用该技能;未指定时,只有任务明确需要建立或重塑视觉方向才使用 `$frontend-design`。新增页面也优先沿用 [管理端主题](docs/theme.md) 和已有设计,设计技能不构成重做页面的理由
开发前查找原页面、同类页面及 `modules/ui/src/components` 中的基础组件,明确参考实现和可复用部分,再确定最小改动。只有原设计不能满足本次操作或存在具体问题时才调整结构,并在 PR 中说明原因;不顺带换风格、重排无关区域或增加重复的模式入口。需要扩展时在对应职责层修改;局部样式的适用范围遵循主题文档,不一律禁止,也不靠页面覆盖掩盖共用组件的问题
列表列宽、序号、按钮、分页和弹窗尺寸由实际任务与数据规模决定,以同类页面为参照。新增或调整交互时说明用户需要完成什么操作,不把某个页面的取舍推广成全局禁令
提交者在浏览器中验证受影响路径,页面变更的 PR 必须提供实际运行截图,包括布局、文案、样式和交互状态的变化:
- 修改现有页面提供变更前后截图,尽量保持相同数据、主题和窗口尺寸;新增页面提供完成态截图。截图保留必要上下文,让审查者能判断布局和信息密度,不只截取孤立控件
- 按影响范围补充浅色/深色、窄窗口及空/加载/错误等受影响状态;共用主题或基础组件变更按主题文档扩大验证范围。弹出层中的内容需展示展开状态,不能只截关闭后的页面
- 在 PR 正文嵌入或链接审查者可访问的图片,注明页面入口、状态和对应提交。录屏、预览地址和复现步骤用于补充,不能替代截图;本机绝对路径不算可供远端审阅的附件
- 视觉或交互继续修改后更新受影响截图,使证据对应待审实现。截图与模拟数据不得包含真实凭据,不用设计稿、生成图片或旧版截图冒充运行结果
- 环境不足时说明缺少的截图和交互验证,不宣称验收通过;页面 PR 在截图补齐前保留 Draft,不标为可审阅或建议合并
截图用于视觉审阅,保存、切换、错误恢复和键盘操作等行为仍需实际验证并记录覆盖范围,不能仅凭静态图片推断交互正确
当前用户明确接管或确认本轮视觉验收时,沿用约定分工和结果;明确免除本次截图时,该项不再阻塞交付或要求 Draft。报告区分用户确认、AI 实测和未验证,其他验证仍执行;后续修改只复核受影响部分
涉及浏览器能力或访问环境差异时,按项目支持的部署方式核对相关 HTTP/HTTPS、localhost/实际部署地址与浏览器版本,记录实际覆盖的条件;本地预览正常不能替代部署环境中的交互验收
## 验证
按变更风险明确要验证的行为与通过条件,优先复用现有检查;缺少有效回归覆盖时补充行为测试,不为表名、字段清单或 SQL 文本形状单独保留测试,也不增加仅复述实现的断言。纯文档修改无需重建前后端
必要验证通过且没有新改动、失败或未解疑点后停止扩围与重复运行。真实账号验证满足约定场景后即收尾,不无目的重复请求;用户限定验证范围或次数时按其要求执行,未覆盖项如实列出
临时预览、模拟数据、诊断脚本与压测产物放在 `.runtime/` 或系统临时目录,使用后按需清理;只有持续维护的工具和回归测试进入源码目录,不把一次性验收产物混进 `src/`、`examples/` 或正式提交
| 范围 | 现有检查 | 仍需提供的证据 |
| --- | --- | --- |
| Rust | Rustfmt、严格 Clippy、后端测试、架构边界检查 | 相关失败场景和集成行为 |
| 前端 | ESLint、类型检查与构建 | 页面一致性、交互和实际数据状态 |
| 数据库迁移 | 冻结清单校验与 PR 基线比对 | 升级行为及兼容说明 |
| 依赖安全 | Cargo Audit、pnpm Audit | 本次变更的权限、数据与信任边界 |
| 工作流与容器 | Actionlint、相关容器检查 | 改动涉及的运行行为 |
环境准备、依赖安装和服务启动见 [开发与源码联调](docs/development.md),联合修改组件库或插件时使用其中的子模块入口
正式依赖固定到标签或提交,联调只临时替换解析路径,不改写正式依赖或锁文件
前端从仓库根目录执行:
```bash
pnpm --dir frontend format:check
pnpm --dir frontend build
git diff --check
```
`build` 已包含类型检查;不维护独立前端测试代码。后端的工具链、命令与线程栈设置统一见 [修改与验收](docs/architecture.md#12-修改与验收)
依赖 PostgreSQL / Redis 的测试使用 [专用测试环境](backend/migrations/README.md#本地测试库),未配置导致跳过时明确记录。CI 中已有对应服务和检查,见 [质量工作流](.github/workflows/_quality.yml) 与 [安全扫描](.github/workflows/security.yml)
集成变更应验证实际业务路径及结果,不能只凭健康检查或探测成功认定全链路正常。协议和网络改动按需核对认证、证书、代理、流式交付及客户端结果;记录影响复现的环境条件。缺少运行条件时说明缺口,不用模拟结果代替真实集成结论
## 自动检查与合并条件
[CI](.github/workflows/ci.yml) 按变更范围调用质量检查,Markdown 不触发后端、前端和容器任务,工作流语法检查独立执行
后端 CI 使用 nextest 执行 `main` 集成测试,默认 Redis 密码模式执行完整测试集,ACL 用户模式复用同一批构建产物执行 Store 的 Redis 测试;本地验证命令见 [架构文档](docs/architecture.md#验证命令)
常规容器检查与发行构建共用二进制构建入口,并复用已检查的前端产物;Dockerfile、工具链、依赖或构建配置变化时执行完整源码镜像构建,定期安全检查也覆盖源码构建。两条路径均验证容器运行、安全扫描与关闭行为
发行构建与质量检查并行,Windows 两种架构的插件与系统更新测试独立运行。仅当发行提交只修改版本与发布说明,且其父提交有本仓库主分支成功的后端 CI 时复用该结果;缺少证据则完整重跑。对外发布等待质量、Windows 测试、依赖审计和镜像验证全部成功,构建耗时与测试报告保留 7 天
新增自动规则优先使用现有工具,要求依据明确、结果稳定、误报可控;无法可靠判定的设计与视觉问题保留审查判断,避免把具体源码写法锁成测试
AI 自动审查需要在所用工具中单独启用。其支持的检查范围和优先级过滤以工具说明为准;即使能读取 `AGENTS.md`,也不能保证所有可维护性和视觉问题都会被报告。配置参考 [Codex GitHub 审查文档](https://learn.chatgpt.com/docs/third-party/github)
要由 GitHub 强制执行合并条件,维护者需配置对应分支的必需状态检查、审阅批准和讨论解决要求,并按需要设置新提交后的重新审阅。配置前核对实际检查名称与触发范围,见 [GitHub 分支保护](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches)。规则文档本身不会启用 AI 服务或更改分支设置
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+87 -148
View File
@@ -5,193 +5,132 @@
# Codex Proxy RS
**基于 Rust 的多 Provider 透明 AI 网关**
面向 Codex 的自托管多账号 AI 网关
[![CI](https://github.com/zyycn/codex-proxy-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/zyycn/codex-proxy-rs/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/zyycn/codex-proxy-rs?display_name=tag&sort=semver&style=flat-square)](https://github.com/zyycn/codex-proxy-rs/releases)
[![GHCR](https://img.shields.io/badge/GHCR-codex--proxy--rs-2496ED?logo=docker&logoColor=white&style=flat-square)](https://github.com/zyycn/codex-proxy-rs/pkgs/container/codex-proxy-rs)
[![MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/license/mit)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square)](LICENSE)
[快速开始](#快速开始) · [客户端接入](#客户端接入) · [运维](#运维) · [部署](deploy/README.md) · [接口](docs/api.md) · [架构](docs/architecture.md)
[快速预览](#快速预览) · [快速开始](#快速开始) · [客户端接入](#客户端接入) · [文档](#文档) · [社区](#社区) · [许可证](#许可证)
</div>
> [!NOTE]
> 只支持 OpenAI Responses API,不提供 `/v1/chat/completions`。
> 本项目提供 Responses API 和 Chat Completions API;Chat Completions 请求通过 Responses 链路执行,支持范围见 [API 参考](docs/api.md#chat-completions-请求)
## 能力
## 快速预览
| 领域 | 实现 |
| -------- | ----------------------------------------------------------------------------------------------------- |
| 协议 | OpenAI Responses JSON、SSE、WebSocket 与模型目录 |
| Provider | 固定 OpenAI、xAI 两个 Provider;各自管理 OAuth、账号、额度与模型目录 |
| 路由 | Client Key 绑定 Provider、全局模型映射、会话亲和与同 Provider 安全 fallback |
| 透明边界 | OpenAI 请求保留未知字段和字段顺序,SSE 与 WebSocket 原样转发业务字节;xAI 在 Provider 内做协议转换 |
| 延续 | OpenAI native/replay continuation;xAI 使用客户端提交的完整历史 |
| 账号 | 导入、OAuth、credential 刷新、主动额度刷新,以及正常响应中的被动额度/状态观测 |
| 管理 | Client Key、账号、模型目录、设置、观测、系统更新与 OAuth |
| 计量 | 模型请求 Token、费用、延迟、账号与 Provider 归因,并记录响应确认的实际 `service_tier` |
无需部署即可体验管理端的系统概览、账号分组、代理管理与用量统计:
| 登录信息 | 默认值 |
| --- | --- |
| 地址 | <https://codex-proxy-rs.ainz.cc> |
| 登录身份 | 管理员 |
| 账号 | `admin@cpr.local` |
| 密码 | `039c18de2aeac46d23ead7766bb07bbbe3747233c66a128e` |
预览服务运行已发布版本,展示的账号、代理与使用记录均为模拟数据,每天北京时间 `00:00` 自动生成当天数据
这是公开共享的功能预览环境,不提供真实模型调用;请勿导入真实账号、密钥或其他敏感信息
## 快速开始
需要 Docker Engine 与 Docker Compose Plugin。
使用 Docker Compose 部署版本固定的发布镜像,同时启动 PostgreSQL 和 Redis
### 1. 准备
以下命令适用于 Linux amd64/arm64,需要 Docker Engine、Docker Compose Plugin、curl 和 OpenSSL
Windows x64 / ARM64 使用发行包部署,步骤见 [Windows 二进制部署](deploy/README.md#windows-二进制部署)
已有部署请先看 [升级说明](deploy/README.md#镜像升级与源码构建),不要覆盖原配置
### 一键安装
请确保当前用户能访问 Docker,并可通过 `sudo` 或 root 设置目录权限
```bash
git clone https://github.com/zyycn/codex-proxy-rs.git
cd codex-proxy-rs
mkdir -p .runtime/data .runtime/logs
install -d -m 0750 .runtime/postgres .runtime/redis
cp deploy/config.example.yaml deploy/config.yaml
sudo chown "$(id -u):10001" deploy/config.yaml
chmod 0640 deploy/config.yaml
curl -fsSL https://raw.githubusercontent.com/zyycn/codex-proxy-rs/main/deploy/install.sh -o install.sh && bash install.sh
```
分别生成 PostgreSQL 与 Redis 密码:
[安装脚本](deploy/install.sh) 默认安装到当前目录下的 `codex-proxy-rs/`,下载同一正式 Release 的部署文件,
自动生成密码、设置目录权限并启动服务
完成后保存显示的访问地址和管理员密码,再[登录管理端](#登录管理端)并[添加账号与客户端密钥](#添加账号与客户端密钥)
可在执行 `bash install.sh` 时传入环境变量:
| 变量 | 用途 | 默认值 |
| --- | --- | --- |
| `INSTALL_DIR` | 安装目录,建议使用绝对路径 | 当前目录下的 `codex-proxy-rs/` |
| `CPR_RELEASE_TAG` | 指定发布标签 | 最新正式版本 |
| `ADMIN_PASSWORD` | 管理员初始密码,至少 12 位,不能包含 `$`,不能使用常见弱口令 | 随机生成 |
例如,自定义安装目录:
```bash
openssl rand -hex 24
openssl rand -hex 24
INSTALL_DIR="$HOME/services/codex-proxy-rs" bash install.sh
```
把两个结果写入 `deploy/config.yaml`,并单独设置管理员初始密码:
重复运行时请使用同一安装目录,检测到 `deploy/config.yaml` 后,脚本保留现有配置和部署文件,
忽略传入的管理员密码,也不执行版本升级
| 配置 | 约束 |
| -------------------------- | ---------------------------- |
| `store.database.password` | 48 位十六进制 |
| `store.redis.password` | 48 位十六进制 |
| `admin.default_password` | 至少 12 位,不能包含 `$` |
### 手动安装
PostgreSQL、Redis 容器和应用通过同一份 `config.yaml` 使用这两个基础设施密码,不需要额外
导出环境变量。管理员密码只在首次创建管理员时生效。
自行下载部署文件、配置密码和启动服务的完整步骤见 [部署文档](deploy/README.md#手动安装)
Linux 需要允许容器组写入运行目录:
### 登录管理端
```bash
sudo chown -R "$(id -u):10001" .runtime/data .runtime/logs
chmod 0770 .runtime/data .runtime/logs
```
部署完成后,打开 `http://127.0.0.1:8080`,使用 `admin@cpr.local` 和管理员密码登录
### 2. 启动
API Key 持有者可在登录页切换身份,查看自己的用量与额度,并通过「密钥配置」复制 Codex 配置或导入 CC Switch
```bash
docker compose -f deploy/compose.yaml config --quiet
docker compose -f deploy/compose.yaml pull
docker compose -f deploy/compose.yaml up -d --no-build
curl -i http://127.0.0.1:8080/healthz
```
默认地址只能在服务器本机访问,从其他设备使用时,需要配置
[HTTPS 反向代理](deploy/README.md#公网访问)
`204 No Content` 表示应用、PostgreSQL 与 Redis 均可用。管理端地址:
`http://127.0.0.1:8080`。
### 添加账号与客户端密钥
### 3. 初始化
1. 使用 `admin@cpr.local` 与初始密码登录。
2. 在账号页选择对应 Provider:OpenAI 支持 OAuth、账号文件与 Agent Identity;xAI 支持 OAuth
和兼容账号文件导入。
3. 按需配置客户端模型到上游模型的全局映射;未命中映射的模型名原样透传。
4. 创建 `sk_...` Client Key,绑定 `openai` 或 `xai`,并设置速率与并发限制。
OpenAI 账号导入和首次 OAuth 创建在 credential 提交后会立即尝试一次额度观测;额度接口临时失败不会
回滚已提交的账号。Free、K12 等套餐共用同一套额度与状态转换逻辑,套餐字段只作为账号事实并隔离
对应的模型目录 cache。credential 刷新、额度刷新和正常请求中的被动额度观测是三条独立链路,详见
[账号接口](docs/api.md#5-账号)。
> [!IMPORTANT]
> xAI 使用 OAuth session,不支持把 xAI API Key 作为上游 credential。
> [!NOTE]
> `admin.default_password` 只在首次创建管理员时生效。
完整部署、权限和密码轮换规则见 [deploy/README.md](deploy/README.md)。
1. 在「账号」中添加账号,完成授权或导入
2. 按需建立账号分组,再创建客户端密钥并选择可用分组,**不选分组表示可使用全部账号**
3. [连接客户端](#客户端接入)
## 客户端接入
| 配置 | 值 |
| -------- | ---------------------------------------- |
| Base URL | `http://127.0.0.1:8080/v1` |
| API Key | 管理端创建的 `sk_...` |
| 鉴权 | `Authorization: Bearer <client-api-key>` |
**Codex CLI / 桌面端**:在「使用密钥」中按操作系统复制配置,合并配置后重启 Codex;也可通过「导入到」把服务连接与密钥添加到 CC Switch
完整步骤、生图配置与排障见[客户端配置](deploy/README.md#客户端配置)
**其他 Responses / Chat Completions API 客户端**:填写以下信息:
| 配置 | 值 |
| --- | --- |
| Base URL | `http://127.0.0.1:8080/v1`;远程接入使用服务器的 HTTPS 地址 |
| API Key | 管理端创建的客户端密钥 |
查询该密钥可见的模型目录:
```bash
curl http://127.0.0.1:8080/v1/models \
-H 'Authorization: Bearer <client-api-key>'
curl http://127.0.0.1:8080/v1/responses \
-H 'Authorization: Bearer <client-api-key>' \
-H 'Content-Type: application/json' \
-d '{
"model": "<model-id>",
"input": "Reply with pong.",
"stream": false
}'
```
<details>
<summary>API 路由</summary>
| 路由 | 用途 |
| ---------------------------------- | ------------------------------ |
| `POST /v1/responses` | JSON 响应或 SSE Responses 流 |
| `GET /v1/responses` | Responses WebSocket 升级 |
| `POST /v1/responses/review` | review 子代理请求 |
| `GET /v1/models` | 当前 Client Key 可用的模型列表 |
| `GET /v1/models/catalog` | Codex 客户端展示用模型目录 |
| `GET /v1/models/{model_id}/info` | Codex 客户端展示用模型详情 |
| `GET /v1/models/{model_id}` | OpenAI 兼容模型详情 |
所有 `/v1/*` 路由都需要客户端 API Key。
</details>
完整的客户端与管理端路由、鉴权和 mutation 规则见 [接口文档](docs/api.md)。
## 运维
```bash
# 实例升级
docker compose -f deploy/compose.yaml pull codex-proxy-rs
docker compose -f deploy/compose.yaml up -d --no-build
# 日志
docker compose -f deploy/compose.yaml logs -f codex-proxy-rs
# 从源码构建
docker compose -f deploy/compose.yaml build codex-proxy-rs
docker compose -f deploy/compose.yaml up -d
```
> [!IMPORTANT]
> `.runtime/` 保存 PostgreSQL、Redis、日志、OpenAI 会话锚点密钥和自更新状态。删除该目录会永久清除
> 运行状态。
> [!WARNING]
> 跨大版本不是原地升级:数据库 schema 与配置只在同一大版本内兼容,内置一键自更新
> 会拒绝跨大版本目标(3.0 之前的旧版没有这道门禁,不要用旧版界面的一键更新跨大版本)。
> 升级到新的大版本需要全新部署(全新 `.runtime/` 数据目录),并重新导入或重新授权
> Provider 账号与客户端 Key。
Compose 默认只绑定 `127.0.0.1`。公网接入应使用 HTTPS 反向代理,转发 WebSocket
upgrade 与真实客户端 IP;不要暴露 PostgreSQL 或 Redis。
## 维护与发布
发布仓库版本必须从干净、已同步上游的发布分支运行:
```bash
release/publish <version>
```
脚本会更新 `release/version.yaml`、创建约定提交与带注释的 `v<version>` tag,并原子推送分支和 tag。
不要手工修改版本后单独打 tag。该脚本只发布仓库版本和触发 Release 构建,不会登录服务器,也不会
改变任何运行实例;源码提交、仓库发版和实例升级是三个独立状态。
## 文档
- [部署](deploy/README.md)
- [客户端与管理端接口](docs/api.md)
- [架构与数据边界](docs/architecture.md)
- [Release](https://github.com/zyycn/codex-proxy-rs/releases)
- [容器镜像](https://github.com/zyycn/codex-proxy-rs/pkgs/container/codex-proxy-rs)
| 任务 | 文档 |
| --- | --- |
| 部署与使用 | [部署、备份与恢复](deploy/README.md) · [客户端接入与生图](deploy/README.md#客户端配置) |
| 接口集成 | [API 参考](docs/api.md) · [模型定价](docs/api.md#模型定价) |
| 使用插件 | [安装、配置与使用](docs/plugins.md) |
| 开发插件 | [SDK 与合同](backend/crates/gateway-plugin/sdk/README.md) · [打包工具](backend/apps/plugin-cli/README.md) · [官方示例](https://github.com/zyycn/codex-proxy-plugins/blob/main/README.md) |
| 开发共用组件 | [组件库说明](https://github.com/zyycn/codex-proxy-ui/blob/main/README.md) · [组件参考](https://github.com/zyycn/codex-proxy-ui/blob/main/playground/components/index.md) · [Playground](https://zyycn.github.io/codex-proxy-ui/) |
| 开发宿主 | [开发与源码联调](docs/development.md) · [系统架构](docs/architecture.md) · [管理端主题](docs/theme.md) · [数据库迁移](backend/migrations/README.md) |
| 参与协作 | [贡献、审查与验证](CONTRIBUTING.md) |
## 社区
问题反馈与功能建议请使用 [GitHub Issues](https://github.com/zyycn/codex-proxy-rs/issues),填写要求见 [贡献说明](CONTRIBUTING.md#提交-issue)
感谢 [LINUX DO](https://linux.do) 社区提供开放、友善的技术交流平台
## 许可证
本项目基于 [Apache License 2.0](LICENSE) 开源
+2030 -502
View File
File diff suppressed because it is too large Load Diff
+27 -2
View File
@@ -1,10 +1,13 @@
[workspace]
members = [
"apps/gateway",
"apps/plugin-cli",
"crates/gateway-admin",
"crates/gateway-api",
"crates/gateway-core",
"crates/gateway-host",
"crates/gateway-plugin/runtime",
"crates/gateway-plugin/sdk",
"crates/gateway-protocol",
"crates/gateway-store",
"crates/providers/openai",
@@ -15,9 +18,9 @@ resolver = "3"
[workspace.package]
version = "0.0.0"
edition = "2024"
license = "MIT"
license = "Apache-2.0"
publish = false
rust-version = "1.97"
rust-version = "1.99.1"
[workspace.lints.rust]
warnings = "deny"
@@ -32,9 +35,31 @@ needless_collect = { level = "deny", priority = 7 }
[profile.dev]
debug = true
# 测试保留真实密码参数和压缩数据量,优化热点依赖以减少 fixture 的计算开销。
# test 继承 dev;workspace 代码仍保留默认调试与溢出检查。
[profile.dev.package.argon2]
opt-level = 2
[profile.dev.package.blake2]
opt-level = 2
[profile.dev.package."sha2@0.11.1"]
opt-level = 2
[profile.dev.package."miniz_oxide@0.9.1"]
opt-level = 2
# workspace 的 WebSocket deflate 特性会让 flate2 选用 zlib-rs
[profile.dev.package.zlib-rs]
opt-level = 2
[profile.release]
debug = false
strip = "symbols"
lto = "thin"
codegen-units = 1
panic = "abort"
[patch.crates-io]
# 固定上游 BEGIN 取消回滚修复,避免未结束事务复用;见 transact-rs/sqlx#4394
sqlx = { git = "https://github.com/transact-rs/sqlx", rev = "f1e94ec34dd5f11ad6488a3a8ac7988439a3f757" }
+4 -1
View File
@@ -16,6 +16,7 @@ gateway-admin = { path = "../../crates/gateway-admin" }
gateway-api = { path = "../../crates/gateway-api" }
gateway-core = { path = "../../crates/gateway-core" }
gateway-host = { path = "../../crates/gateway-host" }
gateway-plugin-runtime = { path = "../../crates/gateway-plugin/runtime" }
gateway-store = { path = "../../crates/gateway-store" }
provider-openai = { path = "../../crates/providers/openai" }
provider-xai = { path = "../../crates/providers/xai" }
@@ -25,8 +26,10 @@ tokio = { version = "1.52.3", default-features = false, features = ["rt-multi-th
[dev-dependencies]
config = { version = "0.15.23", default-features = false, features = ["yaml"] }
syn = { version = "3.0.3", features = ["full"] }
serde_json = "1"
syn = { version = "3.0.3", features = ["full", "visit"] }
tempfile = "3.27.0"
tokio-rustls = { version = "=0.26.4", default-features = false }
[lints]
workspace = true
-129
View File
@@ -1,129 +0,0 @@
//! 网关唯一组合根:加载包级配置并连接各 Bundle。
use gateway_core::engine::provider::{ProviderRegistry, RegistryError};
use gateway_host::{ConfigError, HostConfig, LoadableConfig};
use serde::Deserialize;
const CONFIG_SCHEMA_VERSION: u32 = 1;
/// 顶层配置只组合各包拥有的配置段,不解释任何业务字段。
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GatewayConfig {
schema_version: u32,
host: HostConfig,
store: gateway_store::StoreConfig,
admin: gateway_admin::AdminConfig,
api: gateway_api::ApiConfig,
openai: provider_openai::OpenAiConfig,
xai: provider_xai::XaiConfig,
#[serde(default, rename = "services")]
_compose_services: Option<serde::de::IgnoredAny>,
}
impl LoadableConfig for GatewayConfig {
fn resolve_and_validate(&mut self, source_dir: &std::path::Path) -> Result<(), ConfigError> {
if self.schema_version != CONFIG_SCHEMA_VERSION {
return Err(ConfigError::InvalidField("schema_version"));
}
self.host.resolve_and_validate(source_dir)?;
self.store
.resolve_and_validate(source_dir)
.map_err(|_| ConfigError::InvalidField("store"))?;
self.admin
.resolve_and_validate(source_dir)
.map_err(|_| ConfigError::InvalidField("admin"))?;
self.api
.resolve_and_validate(source_dir)
.map_err(|_| ConfigError::InvalidField("api"))?;
self.openai
.resolve_and_validate(source_dir)
.map_err(|_| ConfigError::InvalidField("openai"))?;
self.xai
.resolve_and_validate(source_dir)
.map_err(|_| ConfigError::InvalidField("xai"))?;
Ok(())
}
}
/// 按冻结顺序初始化全部 Bundle,并把进程阻塞权交给 Host。
pub async fn run() -> Result<(), BootstrapError> {
let config = gateway_host::load_config::<GatewayConfig>()?;
let GatewayConfig {
schema_version: _,
host,
store,
admin,
api,
openai,
xai,
_compose_services: _,
} = config;
let host = gateway_host::initialize(host).await?;
host.report_startup_ready("Host");
let mut store = gateway_store::initialize(store).await?;
host.report_startup_ready("Store");
let provider_ports = store.provider_ports();
let mut openai = provider_openai::initialize(openai, provider_ports.clone()).await?;
host.report_startup_ready("OpenAI Provider");
let mut xai = provider_xai::initialize(xai, provider_ports).await?;
host.report_startup_ready("xAI Provider");
let providers = ProviderRegistry::new([openai.core_provider(), xai.core_provider()])?;
let mut core = gateway_core::initialize(store.core_ports(), providers).await?;
host.report_startup_ready("Core");
let admin = gateway_admin::initialize(
admin,
store.admin_ports(),
vec![openai.admin_provider(), xai.admin_provider()],
core.snapshot_control(),
core.account_probe(),
host.system_operations(),
)
.await?;
host.report_startup_ready("Admin");
let mut probes = store.health_probes();
probes.extend(core.health_probes());
let api = gateway_api::initialize(
api,
core.execution_service(),
admin.services(),
probes,
host.worker_health(),
host.connection_lifecycle(),
)?;
host.report_startup_ready("API");
let mut plan = store.take_worker_contributions();
plan.extend(core.take_worker_contributions());
plan.extend(openai.take_worker_contributions());
plan.extend(xai.take_worker_contributions());
host.start_workers(plan, store.worker_leader_lease())?;
host.report_startup_ready("Workers");
host.serve(api.router()).await?;
Ok(())
}
/// 组合根只保留包级错误分类,不展开内部实现或敏感配置。
#[derive(Debug, thiserror::Error)]
pub enum BootstrapError {
#[error(transparent)]
Config(#[from] gateway_host::ConfigError),
#[error(transparent)]
Host(#[from] gateway_host::HostError),
#[error(transparent)]
Store(#[from] gateway_store::StoreError),
#[error(transparent)]
OpenAi(#[from] provider_openai::OpenAiInitializeError),
#[error(transparent)]
Xai(#[from] provider_xai::XaiInitializeError),
#[error(transparent)]
Registry(#[from] RegistryError),
#[error(transparent)]
Core(#[from] gateway_core::CoreError),
#[error(transparent)]
Admin(#[from] gateway_admin::model::AdminError),
#[error(transparent)]
Api(#[from] gateway_api::ApiError),
}
@@ -0,0 +1,169 @@
//! 插件帮助只读启动;命令仅组装命令平面和必要写泵
use super::{
BootstrapError, GatewayConfig,
plugins::{ManagementPorts, or_shutdown, prepare_plugins, service_middleware},
startup::{Environment, Mode, Providers},
};
/// 这里只接收插件命名空间内的参数,不转交宿主配置或其他启动 secret
pub struct PluginCommand {
pub instance_id: Option<String>,
pub name: Option<String>,
pub arguments: Vec<String>,
}
impl PluginCommand {
fn is_help(&self) -> bool {
self.name.is_none()
|| self
.arguments
.iter()
.any(|value| matches!(value.as_str(), "--help" | "-h"))
}
}
pub async fn plugin_command(
command: PluginCommand,
) -> Result<gateway_plugin_runtime::PluginCommandOutput, BootstrapError> {
let config = gateway_host::load_config::<GatewayConfig>()?;
if command.is_help() {
let environment = Environment::initialize(config.host, config.store, Mode::Help).await?;
help(command, environment).await
} else {
let environment = Environment::initialize(config.host, config.store, Mode::Command).await?;
let providers = Providers::initialize(config.openai, &environment).await?;
execute(command, environment, providers).await
}
}
async fn help(
command: PluginCommand,
environment: Environment,
) -> Result<gateway_plugin_runtime::PluginCommandOutput, BootstrapError> {
let runtime = prepare_plugins(
&environment.store,
environment.plugin_cache,
&environment.version,
environment.plugin_http,
)?;
let commands = or_shutdown!(runtime, runtime.prepare_command_line().await);
let stdout = commands.help(command.instance_id.as_deref(), command.name.as_deref());
commands.shutdown().await;
runtime.shutdown().await;
Ok(gateway_plugin_runtime::PluginCommandOutput {
stdout: stdout?,
stderr: String::new(),
exit_code: 0,
saved_accounts: 0,
})
}
async fn execute(
command: PluginCommand,
environment: Environment,
providers: Providers,
) -> Result<gateway_plugin_runtime::PluginCommandOutput, BootstrapError> {
let Environment {
host,
mut store,
timezone,
plugin_cache,
version,
plugin_http,
..
} = environment;
let plugin_runtime = prepare_plugins(&store, plugin_cache, &version, plugin_http)?;
let instance_id = or_shutdown!(
plugin_runtime,
command
.instance_id
.as_deref()
.ok_or_else(|| gateway_admin::model::AdminError::invalid("插件实例 ID 缺失"))
);
let name = or_shutdown!(
plugin_runtime,
command
.name
.as_deref()
.ok_or_else(|| gateway_admin::model::AdminError::invalid("插件命令名缺失"))
);
let core = gateway_core::prepare_command_plane(
store.core_ports(),
providers.core,
Some(plugin_runtime.clone()),
Some(plugin_runtime.observer_registry()),
Some(plugin_runtime.policy_registry()),
Some(plugin_runtime.execution_registry()),
);
or_shutdown!(
plugin_runtime,
plugin_runtime.bind_snapshots(&core.snapshots())
);
let management = ManagementPorts::new(
&store,
providers.admin.clone(),
core.snapshot_control(),
providers.peer.service(),
);
or_shutdown!(plugin_runtime, management.bind(&plugin_runtime));
let core = or_shutdown!(plugin_runtime, core.activate().await);
or_shutdown!(
plugin_runtime,
plugin_runtime.bind_client_identity_port(&core.client_identity_port())
);
let nested_models = core.nested_model_execution_port();
let affinity_lookup = core.affinity_lookup_port();
or_shutdown!(
plugin_runtime,
plugin_runtime.bind_model_ports(&nested_models, &affinity_lookup)
);
let settings = gateway_admin::initialize_settings(
store.admin_ports().settings(),
core.snapshot_control(),
providers.admin.clone(),
std::sync::Arc::new(gateway_host::pricing::ModelsDevPricing),
);
let mut services = gateway_admin::public_service::Registry::new(service_middleware(
core.snapshots(),
&plugin_runtime,
));
or_shutdown!(plugin_runtime, services.register_settings(&settings));
let accounts = gateway_admin::initialize_accounts(
store.admin_ports().accounts(),
store.admin_ports().account_runtime(),
store.admin_ports().settings(),
timezone,
providers.admin.clone(),
core.snapshot_control(),
core.account_probe(),
Some(providers.peer.service()),
);
or_shutdown!(plugin_runtime, services.register_accounts(&accounts));
let usage_data = gateway_admin::initialize_usage_data(
store.admin_ports().usage_data(),
store.admin_ports().settings(),
providers.admin,
);
or_shutdown!(plugin_runtime, services.register_usage_data(&usage_data));
or_shutdown!(
plugin_runtime,
plugin_runtime.bind_usage_queries(&usage_data)
);
let services = std::sync::Arc::new(services);
or_shutdown!(plugin_runtime, plugin_runtime.bind_services(&services));
let commands = or_shutdown!(plugin_runtime, plugin_runtime.prepare_command_line().await);
let result = async {
store.start_command_line_writes()?;
Ok::<_, BootstrapError>(
commands
.execute_cancellable(instance_id, name, &command.arguments, &host.cancellation())
.await?,
)
}
.await;
commands.shutdown().await;
plugin_runtime.shutdown().await;
store.shutdown_command_line_writes().await?;
result
}
@@ -0,0 +1,50 @@
//! 网关顶层配置的加载、路径解析与跨模块参数校验
use gateway_host::{ConfigError, HostConfig, LoadableConfig};
use serde::Deserialize;
const CONFIG_SCHEMA_VERSION: u32 = 1;
/// 顶层配置只组合各包拥有的配置段,不解释任何业务字段
/// 各启动配置忽略未知字段,允许升级时保留旧配置;已知字段仍按类型和业务约束校验
#[derive(Debug, Deserialize)]
pub struct GatewayConfig {
schema_version: u32,
pub(super) host: HostConfig,
pub(super) store: gateway_store::StoreConfig,
pub(super) admin: gateway_admin::AdminConfig,
#[serde(default)]
pub(super) client: gateway_admin::ClientConfig,
pub(super) api: gateway_api::ApiConfig,
#[serde(default)]
pub(super) openai: provider_openai::OpenAiConfig,
}
impl LoadableConfig for GatewayConfig {
const EXTERNAL_SECTIONS: &'static [&'static str] = &["services"];
fn resolve_and_validate(&mut self, source_dir: &std::path::Path) -> Result<(), ConfigError> {
if self.schema_version != CONFIG_SCHEMA_VERSION {
return Err(ConfigError::InvalidField("schema_version"));
}
self.api
.resolve_and_validate(source_dir)
.map_err(|_| ConfigError::InvalidField("api"))?;
self.host
.resolve_and_validate(source_dir, &self.api.asset_directory)?;
let runtime_data_dir = self.host.runtime_data_dir().to_path_buf();
self.store
.resolve_and_validate(&runtime_data_dir)
.map_err(|_| ConfigError::InvalidField("store"))?;
self.admin
.resolve_and_validate(source_dir)
.map_err(|_| ConfigError::InvalidField("admin"))?;
self.client
.resolve_and_validate(source_dir)
.map_err(|_| ConfigError::InvalidField("client"))?;
self.openai
.resolve_and_validate(&runtime_data_dir)
.map_err(|_| ConfigError::InvalidField("openai"))?;
Ok(())
}
}
+42
View File
@@ -0,0 +1,42 @@
//! 网关唯一组合根:按启动模式连接各 Bundle
mod command;
mod config;
mod plugins;
mod server;
mod startup;
pub use command::{PluginCommand, plugin_command};
pub use config::GatewayConfig;
pub use server::run;
/// 组合根只保留包级错误分类,不展开内部实现或敏感配置
#[derive(Debug, thiserror::Error)]
pub enum BootstrapError {
#[error(transparent)]
Worker(#[from] gateway_core::task::WorkerDefinitionError),
#[error(transparent)]
PluginCommand(#[from] gateway_plugin_runtime::PluginCommandError),
#[error(transparent)]
Config(#[from] gateway_host::ConfigError),
#[error(transparent)]
Host(#[from] gateway_host::HostError),
#[error(transparent)]
Outbound(#[from] gateway_host::outbound::HttpError),
#[error(transparent)]
Store(#[from] gateway_store::StoreError),
#[error(transparent)]
CommandStoreDrain(#[from] gateway_store::CommandStoreDrainError),
#[error(transparent)]
OpenAi(#[from] provider_openai::OpenAiInitializeError),
#[error(transparent)]
Xai(#[from] provider_xai::XaiInitializeError),
#[error(transparent)]
Registry(#[from] gateway_core::engine::provider::RegistryError),
#[error(transparent)]
Core(#[from] gateway_core::CoreError),
#[error(transparent)]
Admin(#[from] gateway_admin::model::AdminError),
#[error(transparent)]
Api(#[from] gateway_api::ApiError),
}
@@ -0,0 +1,110 @@
//! 插件 Runtime 的组装与回调资源持有;服务和命令模式复用同一组管理端口
use std::sync::Arc;
use gateway_admin::{
model::AdminError,
ports::{
plugin_accounts::PluginAccountAccess, plugin_client_keys::PluginClientKeyAccess,
plugin_resources::PluginResourceAccess, provider::ProviderAdminRegistry,
},
};
use gateway_core::runtime::{RuntimeSnapshotHandle, SnapshotControl};
use gateway_host::ConfigError;
use gateway_plugin_runtime::PluginRuntime;
use super::BootstrapError;
// 必须在调用方仍持有 Core 和回调资源时异步关闭,不能先退出该作用域再清理
macro_rules! or_shutdown {
($runtime:expr, $result:expr) => {
match $result {
Ok(value) => value,
Err(error) => {
$runtime.shutdown().await;
return Err(error.into());
}
}
};
}
pub(super) use or_shutdown;
pub(super) struct ManagementPorts {
// Runtime 保存 Weak;组合根持有这些 Arc,直到插件全部关闭
pub(super) accounts: Arc<dyn PluginAccountAccess>,
keys: Arc<dyn PluginClientKeyAccess>,
resources: Arc<dyn PluginResourceAccess>,
}
impl ManagementPorts {
pub(super) fn new(
store: &gateway_store::StoreBundle,
providers: ProviderAdminRegistry,
snapshot: Arc<dyn SnapshotControl>,
upstream_info: Arc<dyn gateway_admin::ports::peer::PeerUpstreamService>,
) -> Self {
let ports = store.admin_ports();
Self {
accounts: gateway_admin::initialize_plugin_accounts(
providers.clone(),
ports.accounts(),
snapshot.clone(),
Some(upstream_info),
),
keys: gateway_admin::initialize_plugin_client_keys(
providers,
ports.client_keys(),
snapshot.clone(),
),
resources: gateway_admin::initialize_plugin_resources(
ports.plugin_resources(),
snapshot,
),
}
}
pub(super) fn bind(&self, runtime: &PluginRuntime) -> Result<(), AdminError> {
runtime.bind_account_ports(&self.accounts)?;
runtime.bind_client_key_ports(&self.keys)?;
runtime.bind_resource_ports(&self.resources)
}
}
pub(super) fn service_middleware(
snapshots: RuntimeSnapshotHandle,
runtime: &PluginRuntime,
) -> gateway_admin::public_service::PlanSource {
let middleware = runtime.execution_registry();
Arc::new(move || {
let snapshot = snapshots.snapshot_for_diagnostics()?;
middleware.middleware(snapshot.extensions()?)
})
}
pub(super) fn prepare_plugins(
store: &gateway_store::StoreBundle,
plugin_cache: std::path::PathBuf,
version: &str,
http: Arc<gateway_host::outbound::HttpClient>,
) -> Result<Arc<gateway_plugin_runtime::PluginRuntime>, BootstrapError> {
Ok(Arc::new(
gateway_plugin_runtime::PluginRuntime::new(
store.admin_ports().plugins(),
store.admin_ports().plugin_state(),
gateway_plugin_runtime::PluginRuntimeConfig {
cache_directory: plugin_cache,
host_version: version
.trim_start_matches('v')
.parse()
.map_err(|_| ConfigError::InvalidField("host.version"))?,
package_limits: gateway_plugin_runtime::PackageLimits::default(),
rpc_limits: gateway_plugin_runtime::RpcLimits::default(),
restart_circuit: Default::default(),
},
http,
Arc::new(gateway_host::process::ProcessSupervisor::default()),
)
.with_oauth_pending(store.provider_ports().oauth_pending()),
))
}
@@ -0,0 +1,191 @@
//! 服务模式完成全部组装后交给 Host 监听,并在停止监听与 Worker 收尾后关闭插件
use gateway_host::ConfigError;
use super::{
BootstrapError, GatewayConfig,
plugins::{ManagementPorts, or_shutdown, prepare_plugins, service_middleware},
startup::{Environment, Mode, Providers},
};
/// 按冻结顺序初始化全部 Bundle,并把进程阻塞权交给 Host
pub async fn run() -> Result<(), BootstrapError> {
let config = gateway_host::load_config::<GatewayConfig>()?;
let environment = Environment::initialize(config.host, config.store, Mode::Server).await?;
let mut providers = Providers::initialize(config.openai, &environment).await?;
let Environment {
host,
mut store,
timezone,
plugin_cache,
version,
plugin_http,
} = environment;
let plugin_runtime = prepare_plugins(&store, plugin_cache, &version, plugin_http.clone())?;
let core = gateway_core::prepare(
store.core_ports(),
providers.core,
Some(plugin_runtime.clone()),
Some(plugin_runtime.observer_registry()),
Some(plugin_runtime.policy_registry()),
Some(plugin_runtime.execution_registry()),
Some(plugin_runtime.frontend_authentication_registry()),
);
or_shutdown!(
plugin_runtime,
plugin_runtime.bind_snapshots(&core.snapshots())
);
let management = ManagementPorts::new(
&store,
providers.admin.clone(),
core.snapshot_control(),
providers.peer.service(),
);
or_shutdown!(plugin_runtime, management.bind(&plugin_runtime));
let mut core = or_shutdown!(plugin_runtime, core.activate().await);
or_shutdown!(
plugin_runtime,
plugin_runtime.bind_client_identity_port(&core.client_identity_port())
);
let nested_models = core.nested_model_execution_port();
let affinity_lookup = core.affinity_lookup_port();
or_shutdown!(
plugin_runtime,
plugin_runtime.bind_model_ports(&nested_models, &affinity_lookup)
);
host.report_startup_ready("Core");
let host_version = or_shutdown!(
plugin_runtime,
version
.trim_start_matches('v')
.parse()
.map_err(|_| ConfigError::InvalidField("host.version"))
);
let plugin_inspector = std::sync::Arc::new(gateway_plugin_runtime::PackageInspector::new(
gateway_plugin_runtime::PackageLimits::default(),
host_version,
));
let plugin_distribution = std::sync::Arc::new(or_shutdown!(
plugin_runtime,
gateway_host::plugin_distribution::HttpPluginDistribution::new(plugin_http)
));
let analytics_worker = or_shutdown!(
plugin_runtime,
store.request_analytics_worker(std::sync::Arc::new(
gateway_admin::ProviderUsageBillingProjector::new(providers.admin.clone()),
))
);
let mut admin = or_shutdown!(
plugin_runtime,
gateway_admin::initialize_with_plugin_accounts(
config.admin,
config.client,
store.admin_ports(),
gateway_admin::AdminRuntimePorts {
upstream_info: Some(providers.peer.service()),
timezone,
service_middleware: service_middleware(core.snapshots(), &plugin_runtime),
providers: providers.admin,
plugin_preparation: plugin_runtime.clone(),
plugin_management: plugin_runtime.clone(),
published_snapshot: core.snapshots(),
plugin_inspector,
plugin_distribution,
pricing_source: std::sync::Arc::new(gateway_host::pricing::ModelsDevPricing),
snapshot: core.snapshot_control(),
account_probe: core.account_probe(),
proxy_probe: host.proxy_probe(provider_openai::build_reqwest_client_with_custom_ca),
client_distribution: host.client_distribution_resolver(),
system: host.system_operations(),
client_key_verifier: core.client_key_verifier(),
},
management.accounts.clone(),
)
.await
);
let official_files = host.official_plugin_release_files();
let official_identity = host.official_plugin_release_identity();
let official_import = or_shutdown!(
plugin_runtime,
admin
.services()
.plugins()
.import_official_release(
official_files.as_ref(),
&official_identity,
&gateway_admin::model::MutationContext {
actor: gateway_admin::model::MutationActor::System,
request_id: "startup-official-plugin-import".to_owned(),
},
)
.await
);
if official_import.artifacts > 0 {
host.report_startup_ready("Official plugins");
}
host.report_startup_ready("Admin");
let mut probes = store.health_probes();
probes.extend(core.health_probes());
probes.push(host.logging_health_probe());
let api = or_shutdown!(
plugin_runtime,
gateway_api::initialize(
config.api,
core.execution_service(),
admin.services(),
probes,
host.worker_health(),
host.connection_lifecycle(),
store.diagnostics(),
)
)
.with_middleware({
let snapshots = core.snapshots();
let middleware = plugin_runtime.execution_registry();
move |snapshot| {
if let Some(snapshot) = snapshot {
return middleware.middleware(snapshot.extensions()?);
}
// 数据面未就绪时,管理与诊断路由仍可使用发布候选中的插件
let diagnostic = snapshots.snapshot_for_diagnostics()?;
middleware.middleware(diagnostic.extensions()?)
}
});
let http_dispatcher = api.dispatcher();
or_shutdown!(plugin_runtime, plugin_runtime.bind_http(&http_dispatcher));
host.report_startup_ready("API");
or_shutdown!(
plugin_runtime,
plugin_runtime.bind_services(&admin.services().public_services())
);
or_shutdown!(
plugin_runtime,
plugin_runtime.bind_usage_queries(&admin.services().usage_data())
);
let mut plan = store.take_worker_contributions();
plan.push(analytics_worker);
plan.push(or_shutdown!(
plugin_runtime,
gateway_host::retention::worker(store.retention())
));
plan.extend(core.take_worker_contributions());
plan.extend(providers.openai.take_worker_contributions());
plan.extend(providers.xai.take_worker_contributions());
plan.extend(admin.take_worker_contributions());
plan.extend(providers.peer.take_worker_contributions());
plan.push(or_shutdown!(
plugin_runtime,
plugin_runtime.maintenance_worker(core.snapshots())
));
or_shutdown!(
plugin_runtime,
host.start_workers(plan, store.worker_leader_lease(), store.diagnostics())
);
host.report_startup_ready("Workers");
let served = host.serve(api.router()).await;
plugin_runtime.shutdown().await;
Ok(served?)
}
@@ -0,0 +1,107 @@
//! 启动模式共享的基础设施与 Provider 组装,不启动 HTTP 或 Worker
use std::{path::PathBuf, sync::Arc};
use gateway_admin::ports::provider::ProviderAdminRegistry;
use gateway_core::{engine::provider::ProviderRegistry, time::DeploymentTimeZone};
use gateway_host::{HostBundle, HostConfig, outbound::HttpClient};
use gateway_store::{StoreBundle, StoreConfig};
use super::BootstrapError;
pub(super) enum Mode {
Server,
Command,
Help,
}
pub(super) struct Environment {
pub(super) host: HostBundle,
pub(super) store: StoreBundle,
pub(super) timezone: DeploymentTimeZone,
pub(super) plugin_cache: PathBuf,
pub(super) version: String,
pub(super) plugin_http: Arc<HttpClient>,
}
impl Environment {
pub(super) async fn initialize(
host: HostConfig,
store: StoreConfig,
mode: Mode,
) -> Result<Self, BootstrapError> {
let timezone = host.timezone;
let store = store.with_timezone(timezone);
let plugin_cache = host.runtime_data_dir().join("plugins");
// 插件兼容检查只需本地运行版本,管理端版本查询还会等待远端发行服务
let version = host.system_update.version.clone();
let host = match mode {
Mode::Server => gateway_host::initialize(host).await?,
Mode::Command | Mode::Help => gateway_host::initialize_command_line(host).await?,
};
host.report_startup_ready("Host");
let store = match mode {
Mode::Server => gateway_store::initialize(store).await?,
Mode::Command => gateway_store::initialize_command_line(store).await?,
Mode::Help => gateway_store::initialize_read_only(store).await?,
};
host.report_startup_ready("Store");
let plugin_http = Arc::new(HttpClient::new()?);
Ok(Self {
host,
store,
timezone,
plugin_cache,
version,
plugin_http,
})
}
}
pub(super) struct Providers {
pub(super) openai: provider_openai::ProviderBundle,
pub(super) xai: provider_xai::ProviderBundle,
pub(super) core: ProviderRegistry,
pub(super) admin: ProviderAdminRegistry,
pub(super) peer: gateway_admin::PeerUpstreamBundle,
}
impl Providers {
pub(super) async fn initialize(
config: provider_openai::OpenAiConfig,
environment: &Environment,
) -> Result<Self, BootstrapError> {
let ports = environment.store.provider_ports();
let upstream_reader = Arc::new(gateway_core::account::UpstreamInfoReader::default());
let openai = provider_openai::initialize(
config.with_timezone(environment.timezone),
ports.clone(),
upstream_reader.clone(),
)
.await?;
environment.host.report_startup_ready("OpenAI Provider");
let xai = provider_xai::initialize(ports).await?;
environment.host.report_startup_ready("xAI Provider");
let core = ProviderRegistry::new([openai.core_provider(), xai.core_provider()])?;
let admin = ProviderAdminRegistry::new([openai.admin_provider(), xai.admin_provider()])
.map_err(|_| gateway_admin::model::AdminError::invalid("Provider 管理身份冲突"))?;
let peer = gateway_admin::initialize_peer_upstream(
environment.store.provider_ports().accounts(),
&admin,
Arc::new(gateway_host::peer::HttpPeerTransport::with_client_builder(
provider_openai::build_reqwest_client_with_custom_ca,
)),
)
.map_err(|_| gateway_admin::model::AdminError::invalid("互联后台任务配置无效"))?;
upstream_reader
.bind(&peer.scheduling_source())
.map_err(|_| gateway_admin::model::AdminError::invalid("互联观测端口重复绑定"))?;
Ok(Self {
peer,
openai,
xai,
core,
admin,
})
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
//! 网关进程组装根。
//! 网关进程组装根
pub mod bootstrap;
+77 -8
View File
@@ -1,16 +1,85 @@
use std::{env, error::Error, io};
//! 网关命令行入口,选择启动模式并创建异步运行时
fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let mut arguments = env::args_os().skip(1);
match arguments.next().as_deref().and_then(|value| value.to_str()) {
use std::{
error::Error,
io::{self, Write},
process::ExitCode,
};
use provider_openai::ensure_rustls_provider;
fn main() -> ExitCode {
match run() {
Ok(code) => code,
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<ExitCode, Box<dyn Error + Send + Sync>> {
ensure_rustls_provider();
let mut arguments = gateway_host::system_update::startup_arguments()?;
let command = arguments
.next()
.map(|value| {
value
.into_string()
.map_err(|_| invalid_cli("command must be UTF-8"))
})
.transpose()?;
match command.as_deref() {
None | Some("serve") => {
reject_extra_arguments(arguments)?;
runtime()?.block_on(codex_proxy_rs::bootstrap::run())?;
Ok(())
Ok(ExitCode::SUCCESS)
}
Some(command) => {
Err(invalid_cli(&format!("unknown command {command:?}; expected serve")).into())
Some("help" | "--help" | "-h") => {
reject_extra_arguments(arguments)?;
println!(
"Usage: codex-proxy-rs [serve]\n codex-proxy-rs plugin <instance-id> <command> [options]\n\n --help Show help without loading configuration\n --version Show version\n plugin --help List enabled plugin commands without starting HTTP"
);
Ok(ExitCode::SUCCESS)
}
Some("--version" | "-V") => {
reject_extra_arguments(arguments)?;
println!(
"{}",
option_env!("CPR_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
);
Ok(ExitCode::SUCCESS)
}
Some("plugin") => {
let arguments = arguments
.map(|value| {
value
.into_string()
.map_err(|_| invalid_cli("plugin arguments must be UTF-8"))
})
.collect::<Result<Vec<_>, _>>()?;
let mut arguments = arguments.into_iter();
let instance_id = arguments
.next()
.filter(|value| !matches!(value.as_str(), "--help" | "-h"));
let name = arguments
.next()
.filter(|value| !matches!(value.as_str(), "--help" | "-h"));
if instance_id.is_none() && name.is_some() {
return Err(invalid_cli("unexpected plugin help arguments").into());
}
let output = runtime()?.block_on(codex_proxy_rs::bootstrap::plugin_command(
codex_proxy_rs::bootstrap::PluginCommand {
instance_id,
name,
arguments: arguments.collect(),
},
))?;
io::stdout().lock().write_all(output.stdout.as_bytes())?;
io::stderr().lock().write_all(output.stderr.as_bytes())?;
Ok(ExitCode::from(output.exit_code))
}
Some(_) => Err(invalid_cli("unknown command; expected serve, plugin, or --help").into()),
}
}
@@ -22,7 +91,7 @@ fn runtime() -> Result<tokio::runtime::Runtime, io::Error> {
fn reject_extra_arguments(mut arguments: impl Iterator) -> Result<(), io::Error> {
if arguments.next().is_some() {
return Err(invalid_cli("unexpected extra command arguments"));
return Err(invalid_cli("too many arguments"));
}
Ok(())
}
+844 -49
View File
@@ -1,17 +1,24 @@
//! docs/architecture.md 依赖 DAG 与生产源码纪律的 workspace 级机器校验。
//! docs/architecture.md 依赖 DAG 与生产源码纪律的 workspace 级机器校验
use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::{Path, PathBuf},
sync::OnceLock,
};
/// workspace 成员冻结清单;新增 crate 必须同步扩展本文件的依赖规则。
const WORKSPACE_MEMBERS: &[&str] = &[
use syn::{Item, visit::Visit};
/// workspace 成员冻结清单;新增 crate 必须同步扩展本文件的依赖规则
pub(super) const WORKSPACE_MEMBERS: &[&str] = &[
"apps/gateway",
"apps/plugin-cli",
"crates/gateway-admin",
"crates/gateway-api",
"crates/gateway-core",
"crates/gateway-host",
"crates/gateway-plugin/runtime",
"crates/gateway-plugin/sdk",
"crates/gateway-protocol",
"crates/gateway-store",
"crates/providers/openai",
@@ -20,22 +27,24 @@ const WORKSPACE_MEMBERS: &[&str] = &[
#[test]
fn workspace_member_list_matches_the_frozen_dag_scope() {
let manifest =
fs::read_to_string(backend_root().join("Cargo.toml")).expect("read workspace manifest");
for member in WORKSPACE_MEMBERS {
assert!(
manifest.contains(&format!("\"{member}\"")),
"workspace must include {member}"
);
}
let member_count = manifest
.lines()
.map(str::trim)
.filter(|line| line.starts_with("\"apps/") || line.starts_with("\"crates/"))
.count();
let metadata = cargo_metadata_json();
let root = backend_root();
let actual: BTreeSet<_> = metadata["packages"]
.as_array()
.expect("Cargo packages")
.iter()
.map(|package| {
Path::new(package["manifest_path"].as_str().expect("package manifest"))
.parent()
.expect("package directory")
.strip_prefix(&root)
.expect("workspace member path")
.to_path_buf()
})
.collect();
assert_eq!(
member_count,
WORKSPACE_MEMBERS.len(),
actual,
WORKSPACE_MEMBERS.iter().map(PathBuf::from).collect(),
"new workspace members must extend the dependency DAG rules"
);
}
@@ -58,6 +67,348 @@ fn gateway_core_depends_on_no_http_db_redis_or_provider_crate() {
);
}
#[test]
fn core_value_owners_do_not_depend_on_execution_or_routing() {
let root = backend_root().join("crates/gateway-core/src");
let reexports = read_root_reexport_owners(&root);
for relative in super::rust_files(&root) {
let allowed: Option<&[&str]> = match relative.to_str().expect("source path") {
"validation.rs" => Some(&[]),
"identity.rs" => Some(&["validation"]),
"upstream.rs" => Some(&["validation"]),
"event.rs" => Some(&["metering", "operation", "upstream", "validation"]),
"account/selection.rs" => Some(&["account", "concurrency", "identity", "validation"]),
"concurrency.rs" => Some(&["error"]),
"account/store.rs" => Some(&["account", "error", "identity", "validation"]),
path if path.starts_with("policy/") => {
Some(&["account", "identity", "policy", "validation"])
}
path if path.starts_with("settings/") => Some(&[
"account",
"concurrency",
"identity",
"metering",
"policy",
"settings",
]),
path if path.starts_with("account/") => Some(&["account", "identity", "validation"]),
_ => None,
};
let Some(allowed) = allowed else { continue };
let source = fs::read_to_string(root.join(&relative)).expect("read core source");
let syntax = syn::parse_file(&source).expect("parse core source");
let mut references = CrateReferences::in_module(&relative).with_root_reexports(&reexports);
references.visit_file(&syntax);
// 相对路径中的 self 指向当前 owner,不是跨 owner 依赖
references.owners.remove(&references.module_path[0]);
for dependency in references.owners {
assert!(
allowed.contains(&dependency.as_str()),
"{} must not depend on crate::{dependency}",
relative.display()
);
}
}
}
#[test]
fn domain_owner_paths_form_a_dag() {
for member in ["crates/gateway-core", "crates/gateway-admin"] {
assert_owner_paths_form_a_dag(member);
}
}
fn assert_owner_paths_form_a_dag(member: &str) {
let root = backend_root().join(member).join("src");
let reexports = read_root_reexport_owners(&root);
let mut graph = BTreeMap::<String, BTreeSet<String>>::new();
for relative in super::rust_files(&root) {
if relative == Path::new("lib.rs") {
continue;
}
let mut references = CrateReferences::in_module(&relative).with_root_reexports(&reexports);
let owner = references.module_path[0].clone();
let source = fs::read_to_string(root.join(&relative)).expect("read owner source");
references.visit_file(&syn::parse_file(&source).expect("parse owner source"));
references.owners.remove(&owner);
graph.entry(owner).or_default().extend(references.owners);
}
for owner in graph.keys() {
let mut pending = graph[owner].iter().cloned().collect::<Vec<_>>();
let mut visited = BTreeSet::new();
while let Some(dependency) = pending.pop() {
assert_ne!(
&dependency, owner,
"{member} owner dependency cycle reaches {owner}"
);
if visited.insert(dependency.clone())
&& let Some(next) = graph.get(&dependency)
{
pending.extend(next.iter().cloned());
}
}
}
}
#[test]
fn shared_owners_do_not_depend_on_their_callers() {
let boundaries: &[(&str, &str, &[&str])] = &[
("crates/providers/xai", "transport", &["credential"]),
("crates/providers/openai", "transport", &["provider"]),
("crates/gateway-api", "time", &["admin"]),
(
"crates/gateway-api",
"client_error",
&["admin", "openai", "middleware"],
),
("crates/gateway-api", "middleware", &["openai"]),
("crates/gateway-host", "system_update", &["config"]),
("crates/gateway-plugin/runtime", "package", &["callback"]),
];
for &(member, owner, forbidden) in boundaries {
let root = backend_root().join(member).join("src");
let reexports = read_root_reexport_owners(&root);
for relative in super::rust_files(&root) {
let mut references =
CrateReferences::in_module(&relative).with_root_reexports(&reexports);
if references.module_path.first().map(String::as_str) != Some(owner) {
continue;
}
let source = fs::read_to_string(root.join(&relative)).expect("read owner source");
references.visit_file(&syn::parse_file(&source).expect("parse owner source"));
for dependency in forbidden {
assert!(
!references.owners.contains(*dependency),
"{member}/src/{} must not depend on crate::{dependency}",
relative.display()
);
}
}
}
}
#[test]
fn owner_reference_scan_resolves_groups_reexports_and_relative_paths() {
let syntax = syn::parse_file(
r#"
pub use crate::{engine::AttemptContext, settings::SettingsValues as Values};
use super::super::runtime as publication;
use self::compiled::Value;
// crate::ignored::Comment
const EXAMPLE: &str = "crate::ignored::Literal";
"#,
)
.expect("parse reference fixture");
let mut references = CrateReferences::in_module(Path::new("routing/snapshot.rs"));
references.visit_file(&syntax);
assert_eq!(
references.owners,
BTreeSet::from([
"engine".to_owned(),
"settings".to_owned(),
"runtime".to_owned(),
"routing".to_owned(),
])
);
}
#[test]
fn owner_reference_scan_resolves_crate_root_reexports() {
let root = syn::parse_file(
r#"
mod provider;
mod transport;
mod secret;
pub use provider::{OFFICIAL_URL, Options as ProviderOptions};
pub use self::transport::{self as network, Request};
pub use crate::secret::SecretValue;
pub use external::Unrelated;
"#,
)
.expect("parse root reexport fixture");
let syntax = syn::parse_file(
r#"
use crate::{OFFICIAL_URL, ProviderOptions, network::Client};
use super::super::SecretValue;
fn request() -> crate::Request { unimplemented!() }
"#,
)
.expect("parse reexport consumer fixture");
let reexports = root_reexport_owners(&root);
assert!(!reexports.contains_key("Unrelated"));
let mut references = CrateReferences::in_module(Path::new("consumer/request.rs"))
.with_root_reexports(&reexports);
references.visit_file(&syntax);
assert_eq!(
references.owners,
BTreeSet::from([
"provider".to_owned(),
"transport".to_owned(),
"secret".to_owned(),
])
);
}
fn read_root_reexport_owners(root: &Path) -> BTreeMap<String, String> {
let source = fs::read_to_string(root.join("lib.rs")).expect("read crate root");
root_reexport_owners(&syn::parse_file(&source).expect("parse crate root"))
}
// 只解析根模块的显式重导出,不推断 glob、宏展开或完整 Rust 名称解析
fn root_reexport_owners(syntax: &syn::File) -> BTreeMap<String, String> {
let modules = syntax
.items
.iter()
.filter_map(|item| match item {
Item::Mod(module) => Some(module.ident.to_string()),
_ => None,
})
.collect::<BTreeSet<_>>();
let mut reexports = BTreeMap::new();
for item in &syntax.items {
if let Item::Use(item) = item
&& !matches!(item.vis, syn::Visibility::Inherited)
{
visit_use_leaves(&item.tree, &mut Vec::new(), &mut |path, name| {
let owner = path
.iter()
.find(|segment| !matches!(segment.as_str(), "crate" | "self"));
if let (Some(owner), Some(name)) = (owner, name)
&& modules.contains(owner)
{
reexports.insert(name.to_owned(), owner.clone());
}
});
}
}
reexports
}
#[derive(Default)]
struct CrateReferences<'a> {
owners: BTreeSet<String>,
module_path: Vec<String>,
root_reexports: Option<&'a BTreeMap<String, String>>,
}
impl<'ast> Visit<'ast> for CrateReferences<'_> {
fn visit_path(&mut self, path: &'ast syn::Path) {
self.record(
&path
.segments
.iter()
.map(|part| part.ident.to_string())
.collect::<Vec<_>>(),
);
syn::visit::visit_path(self, path);
}
fn visit_item_use(&mut self, item: &'ast syn::ItemUse) {
visit_use_leaves(&item.tree, &mut Vec::new(), &mut |path, _| {
self.record(path);
});
}
}
impl<'a> CrateReferences<'a> {
fn in_module(relative: &Path) -> Self {
let mut module = relative
.with_extension("")
.components()
.map(|part| {
part.as_os_str()
.to_str()
.expect("UTF-8 module path")
.to_owned()
})
.collect::<Vec<_>>();
if module.last().is_some_and(|part| part == "mod") {
module.pop();
}
Self {
owners: BTreeSet::new(),
module_path: module,
root_reexports: None,
}
}
fn with_root_reexports(mut self, reexports: &'a BTreeMap<String, String>) -> Self {
self.root_reexports = Some(reexports);
self
}
fn root_owner(&self, name: &str) -> String {
self.root_reexports
.and_then(|reexports| reexports.get(name))
.cloned()
.unwrap_or_else(|| name.to_owned())
}
fn record(&mut self, path: &[String]) {
let Some(first) = path.first() else { return };
let owner = if first == "crate" {
path.get(1).map(|name| self.root_owner(name))
} else if first == "self" || first == "super" {
let mut module = self.module_path.clone();
let mut rest = path;
while rest
.first()
.is_some_and(|part| part == "super" || part == "self")
{
if rest[0] == "super" {
module.pop();
}
rest = &rest[1..];
}
module
.first()
.cloned()
.or_else(|| rest.first().map(|name| self.root_owner(name)))
} else {
None
};
if let Some(owner) = owner {
self.owners.insert(owner);
}
}
}
fn visit_use_leaves(
tree: &syn::UseTree,
prefix: &mut Vec<String>,
visit: &mut impl FnMut(&[String], Option<&str>),
) {
match tree {
syn::UseTree::Path(path) => {
prefix.push(path.ident.to_string());
visit_use_leaves(&path.tree, prefix, visit);
prefix.pop();
}
syn::UseTree::Name(name) => {
let name = name.ident.to_string();
let export = if name == "self" {
prefix.last().cloned()
} else {
Some(name.clone())
};
prefix.push(name);
visit(prefix, export.as_deref());
prefix.pop();
}
syn::UseTree::Rename(rename) => {
prefix.push(rename.ident.to_string());
visit(prefix, Some(&rename.rename.to_string()));
prefix.pop();
}
syn::UseTree::Group(group) => {
for item in &group.items {
visit_use_leaves(item, prefix, visit);
}
}
syn::UseTree::Glob(_) => visit(prefix, None),
}
}
#[test]
fn gateway_protocol_has_no_workspace_dependencies() {
for name in dependency_names("crates/gateway-protocol") {
@@ -68,6 +419,13 @@ fn gateway_protocol_has_no_workspace_dependencies() {
}
}
#[test]
fn plugin_sdk_has_no_workspace_dependencies() {
for name in dependency_names("crates/gateway-plugin/sdk") {
assert!(!name.starts_with("gateway-") && !name.starts_with("provider-"));
}
}
#[test]
fn provider_crates_do_not_depend_on_each_other() {
assert_no_dependency("crates/providers/openai", &["provider-xai"]);
@@ -82,51 +440,488 @@ fn gateway_admin_stays_free_of_infrastructure_dependencies() {
);
}
/// workspace 包名到冻结成员路径的映射
const PACKAGE_TO_MEMBER: &[(&str, &str)] = &[
("codex-proxy-rs", "apps/gateway"),
("codex-proxy-plugin-cli", "apps/plugin-cli"),
("gateway-admin", "crates/gateway-admin"),
("gateway-api", "crates/gateway-api"),
("gateway-core", "crates/gateway-core"),
("gateway-host", "crates/gateway-host"),
("gateway-plugin-runtime", "crates/gateway-plugin/runtime"),
("gateway-plugin-sdk", "crates/gateway-plugin/sdk"),
("gateway-protocol", "crates/gateway-protocol"),
("gateway-store", "crates/gateway-store"),
("provider-openai", "crates/providers/openai"),
("provider-xai", "crates/providers/xai"),
];
/// SDK 与 Adapter/provider 根门面的稳定合同模块;任何增减都必须同步完成边界审计
const ADAPTER_PUBLIC_MODULES: &[(&str, &[&str])] = &[
("crates/gateway-plugin/sdk", &["call", "client"]),
("crates/gateway-plugin/runtime", &[]),
("crates/gateway-api", &["admin", "auth", "openai"]),
(
"crates/gateway-host",
&[
"client_distribution",
"config",
"outbound",
"peer",
"official_plugins",
"pricing",
"plugin_distribution",
"process",
"proxy_probe",
"retention",
"serve",
"system_update",
"workers",
],
),
("crates/gateway-store", &["backup", "postgres", "redis"]),
(
"crates/providers/openai",
&["config", "credential", "transport"],
),
("crates/providers/xai", &["transport"]),
];
/// 不对应单一生产模块、而是校验 crate/workspace 整体契约的根级测试场景
const ROOT_TEST_SCENARIOS: &[(&str, &[&str])] = &[
("apps/gateway", &["architecture"]),
("crates/gateway-api", &["architecture"]),
];
/// 冻结的 workspace 内部运行时依赖边;新增/删除任何边都必须同步本表
const ALLOWED_INTERNAL_EDGES: &[(&str, &str)] = &[
("codex-proxy-plugin-cli", "gateway-plugin-sdk"),
("codex-proxy-rs", "gateway-admin"),
("codex-proxy-rs", "gateway-api"),
("codex-proxy-rs", "gateway-core"),
("codex-proxy-rs", "gateway-host"),
("codex-proxy-rs", "gateway-plugin-runtime"),
("codex-proxy-rs", "gateway-store"),
("codex-proxy-rs", "provider-openai"),
("codex-proxy-rs", "provider-xai"),
("gateway-admin", "gateway-core"),
("gateway-admin", "gateway-protocol"),
("gateway-api", "gateway-admin"),
("gateway-api", "gateway-core"),
("gateway-api", "gateway-protocol"),
("gateway-host", "gateway-admin"),
("gateway-host", "gateway-core"),
("gateway-host", "gateway-protocol"),
("gateway-plugin-runtime", "gateway-host"),
("gateway-plugin-runtime", "gateway-core"),
("gateway-plugin-runtime", "gateway-admin"),
("gateway-plugin-runtime", "gateway-plugin-sdk"),
("gateway-plugin-runtime", "gateway-protocol"),
("gateway-store", "gateway-admin"),
("gateway-store", "gateway-core"),
("provider-openai", "gateway-admin"),
("provider-openai", "gateway-core"),
("provider-openai", "gateway-protocol"),
("provider-xai", "gateway-admin"),
("provider-xai", "gateway-core"),
("provider-xai", "gateway-protocol"),
];
#[test]
fn production_sources_do_not_host_tests() {
fn workspace_internal_dependency_edges_match_frozen_dag() {
let metadata = cargo_metadata_json();
let actual = workspace_runtime_dependency_edges(metadata);
let mut expected: Vec<(String, String)> = ALLOWED_INTERNAL_EDGES
.iter()
.map(|(from, to)| ((*from).to_owned(), (*to).to_owned()))
.collect();
expected.sort();
assert_eq!(
actual, expected,
"workspace internal dependency edges diverged from the frozen DAG"
);
}
#[test]
fn runtime_dependency_edges_exclude_dev_build_and_external_dependencies() {
let metadata = serde_json::json!({
"packages": [
{
"name": "gateway-api",
"dependencies": [
{ "name": "gateway-core", "kind": null },
{ "name": "gateway-store", "kind": "dev" },
{ "name": "gateway-host", "kind": "build" },
{ "name": "serde", "kind": null }
]
},
{
"name": "external-package",
"dependencies": [{ "name": "gateway-core", "kind": null }]
}
]
});
assert_eq!(
workspace_runtime_dependency_edges(&metadata),
vec![("gateway-api".to_owned(), "gateway-core".to_owned())]
);
}
fn workspace_runtime_dependency_edges(metadata: &serde_json::Value) -> Vec<(String, String)> {
let packages = metadata["packages"]
.as_array()
.expect("cargo metadata packages");
let internal_names: std::collections::BTreeSet<&str> =
PACKAGE_TO_MEMBER.iter().map(|(name, _)| *name).collect();
let mut actual: Vec<(String, String)> = Vec::new();
for package in packages {
let name = package["name"].as_str().expect("package name");
if !internal_names.contains(name) {
continue;
}
for dependency in package["dependencies"].as_array().into_iter().flatten() {
let dep_name = dependency["name"].as_str().expect("dependency name");
if !internal_names.contains(dep_name) {
continue;
}
if dependency.get("kind").expect("dependency kind").is_null() {
actual.push((name.to_owned(), dep_name.to_owned()));
}
}
}
actual.sort();
actual
}
#[test]
fn adapter_public_module_surfaces_match_allowlist() {
for (member, allowed) in ADAPTER_PUBLIC_MODULES {
let root = backend_root().join(member).join("src/lib.rs");
let source = fs::read_to_string(&root).expect("read adapter crate root");
let syntax = syn::parse_file(&source).expect("parse adapter crate root");
let actual = syntax
.items
.iter()
.filter_map(|item| match item {
Item::Mod(module) if matches!(module.vis, syn::Visibility::Public(_)) => {
Some(module.ident.to_string())
}
_ => None,
})
.collect::<BTreeSet<_>>();
let expected = allowed
.iter()
.map(|module| (*module).to_owned())
.collect::<BTreeSet<_>>();
assert_eq!(
actual,
expected,
"{} public module surface diverged from its allowlist",
root.display()
);
}
}
#[test]
fn workspace_modules_follow_conventional_file_layout() {
let targets = test_source_roots();
for member in WORKSPACE_MEMBERS {
let src = backend_root().join(member).join("src");
let sources = super::rust_files(&src);
assert!(!sources.is_empty(), "{member} has no production sources");
for relative in sources {
let path = src.join(relative);
let source = fs::read_to_string(&path).expect("read production source");
assert!(
!source.contains("#[cfg(test)]"),
"{} hosts tests in production src",
path.display()
let member_root = backend_root().join(member);
let src = member_root.join("src");
assert_module_tree(&src, &["lib.rs", "main.rs"]);
assert_directory_modules_have_children(&src, &["lib.rs", "main.rs"]);
let tests = member_root.join("tests");
if tests.is_dir() {
let roots = targets.get(&tests).expect("Cargo test source roots");
assert_module_tree(
&tests,
&roots.iter().map(String::as_str).collect::<Vec<_>>(),
);
}
}
}
fn backend_root() -> PathBuf {
fn assert_directory_modules_have_children(root: &Path, crate_roots: &[&str]) {
let files = super::rust_files(root);
for relative in &files {
if crate_roots
.iter()
.any(|candidate| relative == Path::new(candidate))
|| relative.file_name().and_then(|value| value.to_str()) != Some("mod.rs")
{
continue;
}
let directory = relative.parent().expect("mod.rs parent");
assert!(
files
.iter()
.any(|candidate| candidate != relative && candidate.starts_with(directory)),
"{} is a leaf module and must use {}",
root.join(relative).display(),
root.join(directory).with_extension("rs").display(),
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ModuleLayout {
File,
Directory,
}
#[test]
fn integration_tests_mirror_production_module_tree() {
let targets = test_source_roots();
for member in WORKSPACE_MEMBERS {
let member_root = backend_root().join(member);
let tests_root = member_root.join("tests");
if !tests_root.is_dir() {
continue;
}
let production = module_layouts(&member_root.join("src"), &["lib.rs", "main.rs"]);
let roots = targets.get(&tests_root).expect("Cargo test source roots");
let tests = module_layouts(
&tests_root,
&roots.iter().map(String::as_str).collect::<Vec<_>>(),
);
for (module, test_layout) in tests {
if module.starts_with("support") {
continue;
}
if let Some(production_layout) = production.get(&module) {
assert_eq!(
test_layout,
*production_layout,
"{} must mirror the production module layout for {}",
tests_root.join(module.with_extension("rs")).display(),
member_root.join("src").join(&module).display(),
);
continue;
}
let has_production_owner = module
.parent()
.into_iter()
.flat_map(Path::ancestors)
.take_while(|ancestor| !ancestor.as_os_str().is_empty())
.any(|ancestor| production.contains_key(ancestor));
assert!(
has_production_owner || root_test_scenario_allowed(member, &module),
"{} has no mirrored production module owner",
tests_root.join(module.with_extension("rs")).display(),
);
}
}
}
/// Rust 子进程 fixture 是 Cargo 的独立 crate 根,不能要求它由测试模块再次声明
fn test_source_roots() -> BTreeMap<PathBuf, Vec<String>> {
let metadata = cargo_metadata_json();
let mut roots = BTreeMap::<PathBuf, Vec<String>>::new();
for package in metadata["packages"].as_array().expect("Cargo packages") {
let root = Path::new(package["manifest_path"].as_str().expect("package manifest"))
.parent()
.expect("package directory")
.join("tests");
for target in package["targets"].as_array().expect("Cargo targets") {
let source = Path::new(target["src_path"].as_str().expect("target source"));
if let Ok(relative) = source.strip_prefix(&root) {
roots
.entry(root.clone())
.or_default()
.push(relative.to_str().expect("UTF-8 source path").to_owned());
}
}
}
roots
}
fn module_layouts(root: &Path, crate_roots: &[&str]) -> BTreeMap<PathBuf, ModuleLayout> {
super::rust_files(root)
.into_iter()
.filter(|relative| {
!crate_roots
.iter()
.any(|candidate| relative == Path::new(candidate))
})
.map(|relative| {
if relative.file_name().and_then(|value| value.to_str()) == Some("mod.rs") {
(
relative.parent().expect("mod.rs parent").to_path_buf(),
ModuleLayout::Directory,
)
} else {
(relative.with_extension(""), ModuleLayout::File)
}
})
.collect()
}
fn root_test_scenario_allowed(member: &str, module: &Path) -> bool {
let Some(module) = module.to_str() else {
return false;
};
ROOT_TEST_SCENARIOS
.iter()
.any(|(candidate, scenarios)| *candidate == member && scenarios.contains(&module))
}
fn assert_module_tree(root: &Path, crate_roots: &[&str]) {
let files = super::rust_files(root);
// 同一父模块的所有子文件复用一次语法解析,计数仍保留重复声明检查
let mut declarations = BTreeMap::<PathBuf, BTreeMap<String, usize>>::new();
for relative in &files {
if crate_roots
.iter()
.any(|candidate| relative == Path::new(candidate))
{
continue;
}
let file_name = relative
.file_name()
.and_then(|value| value.to_str())
.expect("Rust source file name");
let parent = relative.parent().expect("Rust source parent");
let module_name = if file_name == "mod.rs" {
parent
.file_name()
.and_then(|value| value.to_str())
.expect("mod.rs module name")
} else {
relative
.file_stem()
.and_then(|value| value.to_str())
.expect("Rust source module name")
};
if file_name != "mod.rs" {
assert_ne!(
parent.file_name().and_then(|value| value.to_str()),
Some(module_name),
"{} repeats its parent module name",
root.join(relative).display()
);
let child_directory = relative.with_extension("");
assert!(
!files
.iter()
.any(|candidate| candidate.starts_with(&child_directory)),
"{} mixes a leaf module with a same-name module directory",
root.join(relative).display()
);
}
let declaration_parents = if parent.as_os_str().is_empty()
|| (file_name == "mod.rs"
&& parent
.parent()
.is_some_and(|ancestor| ancestor.as_os_str().is_empty()))
{
crate_roots
.iter()
.map(|candidate| root.join(candidate))
.collect::<Vec<_>>()
} else {
let declaration_directory = if file_name == "mod.rs" {
parent.parent().expect("nested mod.rs parent")
} else {
parent
};
vec![root.join(declaration_directory).join("mod.rs")]
};
let declaration_count = declaration_parents
.iter()
.map(|path| {
declarations
.entry(path.clone())
.or_insert_with(|| external_module_declarations(path))
.get(module_name)
.copied()
.unwrap_or_default()
})
.sum::<usize>();
assert_eq!(
declaration_count,
1,
"{} must be declared exactly once by its parent module",
root.join(relative).display()
);
}
}
fn external_module_declarations(path: &Path) -> BTreeMap<String, usize> {
if !path.is_file() {
return BTreeMap::new();
}
let source = fs::read_to_string(path).expect("read parent module source");
let syntax = syn::parse_file(&source).expect("parse parent module source");
let mut declarations = BTreeMap::new();
for item in syntax.items {
if let Item::Mod(module) = item
&& module.content.is_none()
{
*declarations.entry(module.ident.to_string()).or_default() += 1;
}
}
declarations
}
pub(super) fn backend_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.expect("backend workspace root")
}
/// 提取成员 `[dependencies]` 段内声明的依赖名;段落以下一个 `[` 表头结束。
fn cargo_metadata_json() -> &'static serde_json::Value {
static METADATA: OnceLock<serde_json::Value> = OnceLock::new();
METADATA.get_or_init(|| {
let output = std::process::Command::new("cargo")
.args(["metadata", "--format-version", "1", "--no-deps", "--locked"])
.current_dir(backend_root())
.output()
.expect("run cargo metadata");
assert!(
output.status.success(),
"cargo metadata failed: {}",
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).expect("parse cargo metadata")
})
}
/// 复用 Cargo 解析后的实际包名,覆盖重命名依赖与平台条件,排除测试和构建依赖
fn dependency_names(member: &str) -> Vec<String> {
let manifest = fs::read_to_string(backend_root().join(member).join("Cargo.toml"))
.expect("read member manifest");
let mut in_dependencies = false;
let mut names = Vec::new();
for line in manifest.lines() {
let line = line.trim();
if line.starts_with('[') {
in_dependencies = line == "[dependencies]";
continue;
}
if !in_dependencies || line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((name, _)) = line.split_once('=') {
names.push(name.trim().to_owned());
}
}
names
let manifest = backend_root().join(member).join("Cargo.toml");
let package = cargo_metadata_json()["packages"]
.as_array()
.expect("Cargo packages")
.iter()
.find(|package| {
Path::new(package["manifest_path"].as_str().expect("package manifest")) == manifest
})
.expect("workspace member");
package["dependencies"]
.as_array()
.expect("package dependencies")
.iter()
.filter(|dependency| dependency["kind"].is_null())
.map(|dependency| {
dependency["name"]
.as_str()
.expect("dependency name")
.to_owned()
})
.collect()
}
fn assert_no_dependency(member: &str, forbidden: &[&str]) {
-197
View File
@@ -1,197 +0,0 @@
use std::{fs, process::Command};
use codex_proxy_rs::bootstrap::GatewayConfig;
use gateway_host::LoadableConfig;
const CONFIG_EXAMPLE: &str = include_str!("../../../../deploy/config.example.yaml");
const POSTGRES_PASSWORD: &str = "111111111111111111111111111111111111111111111111";
const REDIS_PASSWORD: &str = "222222222222222222222222222222222222222222222222";
const ADMIN_PASSWORD: &str = "test-admin-password";
const TOPOLOGY_CHILD_ENV: &str = "CPR_TEST_TOPOLOGY_CHILD";
#[test]
fn config_loader_should_load_complete_terminal_example() {
parse_config(&valid_config()).expect("terminal config example");
}
#[test]
fn config_loader_should_resolve_paths_relative_to_config_file() {
let (config, _directory) = parse_config(&valid_config()).expect("resolved config");
let debug = format!("{config:?}");
assert!(debug.contains(".runtime/logs"));
assert!(debug.contains("frontend/dist"));
}
#[test]
fn config_loader_should_inject_connection_passwords_into_urls() {
parse_config(&valid_config()).expect("Store validates password injection into both URLs");
}
#[test]
fn config_loader_should_apply_only_explicit_topology_overrides() {
let invalid = valid_config()
.replace("host: '127.0.0.1'", "host: ''")
.replace("port: 8080", "port: 0")
.replace(
"url: 'postgres://codex_proxy@127.0.0.1:5432/codex_proxy'",
"url: 'invalid-postgres-url'",
)
.replace("url: 'redis://127.0.0.1:6379/'", "url: 'invalid-redis-url'")
.replace(POSTGRES_PASSWORD, "invalid-postgres-password")
.replace(REDIS_PASSWORD, "invalid-redis-password");
if std::env::var_os(TOPOLOGY_CHILD_ENV).is_some() {
parse_config(&invalid).expect("explicit package-owned environment overrides");
return;
}
assert!(parse_config(&invalid).is_err());
let status = Command::new(std::env::current_exe().expect("current test executable"))
.args([
"--exact",
"bootstrap::config_loader_should_apply_only_explicit_topology_overrides",
])
.env(TOPOLOGY_CHILD_ENV, "1")
.env("CPR_SERVER_HOST", "127.0.0.1")
.env("CPR_SERVER_PORT", "8080")
.env(
"CPR_DATABASE_URL",
"postgres://codex_proxy@127.0.0.1:5432/codex_proxy",
)
.env("CPR_REDIS_URL", "redis://127.0.0.1:6379/")
.env("CPR_DATABASE_PASSWORD", POSTGRES_PASSWORD)
.env("CPR_REDIS_PASSWORD", REDIS_PASSWORD)
.status()
.expect("run isolated environment override test");
assert!(status.success());
}
#[test]
fn bootstrap_config_debug_should_redact_all_passwords() {
let (config, _directory) = parse_config(&valid_config()).expect("config");
let debug = format!("{config:?}");
assert!(!debug.contains(POSTGRES_PASSWORD));
assert!(!debug.contains(REDIS_PASSWORD));
assert!(!debug.contains(ADMIN_PASSWORD));
assert!(debug.contains("[REDACTED]"));
}
#[test]
fn config_loader_should_reject_unknown_fields() {
assert_rejected(format!(
"{}\nunknown_terminal_field: true\n",
valid_config()
));
}
#[test]
fn config_loader_should_reject_removed_tls_section() {
assert_rejected(valid_config().replace("openai:\n", "openai:\n tls: {}\n"));
}
#[test]
fn config_loader_should_reject_missing_explicit_fields() {
assert_rejected(valid_config().replace(" request_id_header: 'x-request-id'\n", ""));
}
#[test]
fn config_loader_should_reject_unsupported_schema_version() {
assert_rejected(valid_config().replace("schema_version: 1", "schema_version: 2"));
}
#[test]
fn config_loader_should_reject_embedded_database_password() {
assert_rejected(valid_config().replace(
"postgres://codex_proxy@127.0.0.1:5432/codex_proxy",
"postgres://codex_proxy:embedded@127.0.0.1:5432/codex_proxy",
));
}
#[test]
fn config_loader_should_reject_non_hex_postgres_password() {
assert_rejected(valid_config().replace(POSTGRES_PASSWORD, &"g".repeat(48)));
}
#[test]
fn config_loader_should_reject_wrong_length_redis_password() {
assert_rejected(valid_config().replace(REDIS_PASSWORD, "1234"));
}
#[test]
fn config_loader_should_reject_weak_admin_password() {
assert_rejected(valid_config().replace(ADMIN_PASSWORD, "password"));
}
#[test]
fn config_loader_should_reject_admin_password_with_compose_interpolation() {
assert_rejected(valid_config().replace(ADMIN_PASSWORD, "unsafe$password"));
}
#[test]
fn config_loader_should_reject_removed_fingerprint_section() {
assert_rejected(valid_config().replace(
"openai:\n",
"openai:\n fingerprint:\n browser: removed\n",
));
}
#[test]
fn config_loader_should_reject_invalid_codex_core_version() {
assert_rejected(valid_config().replace("codex_version: '0.145.0'", "codex_version: 'latest'"));
}
#[test]
fn config_loader_should_reject_disabled_all_log_outputs() {
assert_rejected(
valid_config()
.replace("stdout: true", "stdout: false")
.replace("enabled: true", "enabled: false"),
);
}
#[test]
fn config_loader_should_reject_zero_server_port() {
assert_rejected(valid_config().replace("port: 8080", "port: 0"));
}
#[test]
fn config_loader_should_reject_invalid_desktop_profile_fields() {
assert_rejected(valid_config().replace("desktop_build: '5828'", "desktop_build: 'build'"));
}
fn assert_rejected(config: String) {
assert!(parse_config(&config).is_err());
}
fn valid_config() -> String {
CONFIG_EXAMPLE
.replacen(
"password: &postgres_password ''",
&format!("password: &postgres_password '{POSTGRES_PASSWORD}'"),
1,
)
.replacen(
"password: &redis_password ''",
&format!("password: &redis_password '{REDIS_PASSWORD}'"),
1,
)
.replace(
"default_password: ''",
&format!("default_password: '{ADMIN_PASSWORD}'"),
)
}
fn parse_config(config: &str) -> Result<(GatewayConfig, tempfile::TempDir), String> {
let directory = tempfile::tempdir().map_err(|error| error.to_string())?;
let deploy = directory.path().join("deploy");
fs::create_dir(&deploy).map_err(|error| error.to_string())?;
let path = deploy.join("config.yaml");
fs::write(&path, config).map_err(|error| error.to_string())?;
let mut config = config::Config::builder()
.add_source(config::File::from(path).required(true))
.build()
.and_then(config::Config::try_deserialize::<GatewayConfig>)
.map_err(|error| error.to_string())?;
config
.resolve_and_validate(&deploy)
.map_err(|error| error.to_string())?;
Ok((config, directory))
}
@@ -0,0 +1,51 @@
//! 验证网关基础命令无需加载配置,并拒绝非法或多余参数
use std::{fs, process::Command};
#[test]
fn basic_cli_options_do_not_load_configuration_and_reject_unexpected_arguments() {
let directory = tempfile::tempdir().unwrap();
fs::create_dir(directory.path().join("deploy")).unwrap();
fs::write(directory.path().join("deploy/config.yaml"), "invalid: [").unwrap();
for flag in ["--help", "-h", "help", "--version", "-V"] {
let output = Command::new(env!("CARGO_BIN_EXE_codex-proxy-rs"))
.arg(flag)
.current_dir(directory.path())
.env_clear()
.output()
.unwrap();
assert!(output.status.success(), "{flag}");
assert!(!output.stdout.is_empty());
assert!(output.stderr.is_empty());
}
for args in [
vec!["unknown"],
vec!["--help", "extra"],
vec!["serve", "extra"],
] {
let output = Command::new(env!("CARGO_BIN_EXE_codex-proxy-rs"))
.args(args)
.current_dir(directory.path())
.env_clear()
.output()
.unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(!String::from_utf8_lossy(&output.stderr).contains("configuration"));
}
}
#[cfg(unix)]
#[test]
fn non_utf8_command_is_not_interpreted_as_serve() {
use std::os::unix::ffi::OsStringExt as _;
let output = Command::new(env!("CARGO_BIN_EXE_codex-proxy-rs"))
.arg(std::ffi::OsString::from_vec(vec![0xff]))
.env_clear()
.output()
.unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("command must be UTF-8"));
}
@@ -0,0 +1,461 @@
//! 验证顶层配置加载、路径解析、显式覆盖与敏感字段脱敏
use std::{fs, process::Command};
use codex_proxy_rs::bootstrap::GatewayConfig;
use gateway_host::LoadableConfig;
const CONFIG_EXAMPLE: &str = include_str!("../../../../../deploy/config.example.yaml");
const POSTGRES_PASSWORD: &str = "111111111111111111111111111111111111111111111111";
const REDIS_PASSWORD: &str = "222222222222222222222222222222222222222222222222";
const ADMIN_PASSWORD: &str = "test-admin-password";
const TOPOLOGY_CHILD_ENV: &str = "CPR_TEST_TOPOLOGY_CHILD";
#[test]
fn config_loader_should_load_complete_terminal_example() {
parse_config(&valid_config()).expect("terminal config example");
}
#[test]
fn config_loader_should_resolve_paths_relative_to_config_file() {
let (config, _directory) = parse_config(&valid_config()).expect("resolved config");
let debug = format!("{config:?}");
assert!(debug.contains(".runtime/data"));
assert!(debug.contains(".runtime/logs"));
assert!(debug.contains("frontend/dist"));
}
#[test]
fn config_loader_should_share_resolved_assets_with_system_update() {
const CHILD_ENV: &str = "CPR_TEST_UPDATE_ASSETS_CHILD";
let Ok(case) = std::env::var(CHILD_ENV) else {
// 使用子进程覆盖无环境变量、Docker 路径和相对路径,避免污染并行测试
for (case, web_dist) in [
("binary", None),
("docker", Some("/app/web/dist")),
("relative", Some("../custom/dist")),
] {
let mut child = Command::new(std::env::current_exe().expect("test executable"));
child
.args([
"--exact",
"bootstrap::config::config_loader_should_share_resolved_assets_with_system_update",
])
.env(CHILD_ENV, case)
.env_remove("CPR_WEB_DIST_DIR");
if let Some(web_dist) = web_dist {
child.env("CPR_WEB_DIST_DIR", web_dist);
}
let output = child.output().expect("isolated configuration test");
assert!(
output.status.success(),
"{case}: {}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
return;
};
let mut document = valid_config_document();
document["api"]["asset_directory"] = serde_json::json!("../web/dist");
let (config, directory) = parse_config(&document.to_string()).expect("binary configuration");
let assets = match case.as_str() {
"docker" => std::path::PathBuf::from("/app/web/dist"),
"relative" => directory.path().join("deploy/../custom/dist"),
_ => directory.path().join("deploy/../web/dist"),
};
let debug = format!("{config:?}");
assert!(debug.contains(&format!("asset_directory: {assets:?}")));
assert!(debug.contains(&format!("web_dist_dir: Some({assets:?})")));
}
#[test]
fn config_loader_should_reject_missing_runtime_data_dir() {
let config = valid_config().replace(" runtime_data_dir: '../.runtime/data'\n", "");
assert!(parse_config(&config).is_err());
}
#[test]
fn config_loader_should_apply_only_explicit_topology_overrides() {
let invalid = valid_config()
.replace("host: '127.0.0.1'", "host: ''")
.replace("port: 8080", "port: 0")
.replace(
"url: 'postgres://codex_proxy@127.0.0.1:5432/codex_proxy'",
"url: 'invalid-postgres-url'",
)
.replace("url: 'redis://127.0.0.1:6379/'", "url: 'invalid-redis-url'")
.replace(POSTGRES_PASSWORD, "invalid-postgres-password")
.replace(REDIS_PASSWORD, "invalid-redis-password");
if std::env::var_os(TOPOLOGY_CHILD_ENV).is_some() {
parse_config(&invalid).expect("explicit package-owned environment overrides");
return;
}
assert!(parse_config(&invalid).is_err());
let status = Command::new(std::env::current_exe().expect("current test executable"))
.args([
"--exact",
"bootstrap::config::config_loader_should_apply_only_explicit_topology_overrides",
])
.env(TOPOLOGY_CHILD_ENV, "1")
.env("CPR_SERVER_HOST", "127.0.0.1")
.env("CPR_SERVER_PORT", "8080")
.env(
"CPR_DATABASE_URL",
"postgres://codex_proxy@127.0.0.1:5432/codex_proxy",
)
.env("CPR_REDIS_URL", "redis://127.0.0.1:6379/")
.env(
"CPR_DATABASE_PASSWORD",
"RdsTest9!abcdefGhRdsTest9!abcdefGh",
)
.env("CPR_REDIS_PASSWORD", "ManagedRedis9!abcdefGh")
.status()
.expect("run isolated environment override test");
assert!(status.success());
}
#[test]
fn bootstrap_config_debug_should_redact_all_passwords() {
let (config, _directory) = parse_config(&valid_config()).expect("config");
let debug = format!("{config:?}");
assert!(!debug.contains(POSTGRES_PASSWORD));
assert!(!debug.contains(REDIS_PASSWORD));
assert!(!debug.contains(ADMIN_PASSWORD));
assert!(debug.contains("[REDACTED]"));
}
#[test]
fn config_loader_should_ignore_unknown_fields_in_configuration_sections() {
let mut document = valid_config_document();
document["host"]["system_update"] = serde_json::json!({});
document["store"]["pool"] = serde_json::json!({});
for path in [
"",
"/host",
"/host/listen",
"/host/logging",
"/host/logging/file",
"/host/system_update",
"/store",
"/store/database",
"/store/redis",
"/store/pool",
"/admin",
"/client",
"/api",
"/openai",
] {
let mut extended = document.clone();
extended.pointer_mut(path).expect("configuration section")["unused_setting"] =
serde_json::json!({"nested": [true, null, "ignored"]});
parse_config(&extended.to_string())
.unwrap_or_else(|error| panic!("unknown field in {path}: {error}"));
}
}
#[test]
fn config_loader_should_reject_invalid_known_fields_alongside_unknown_fields() {
for port in [serde_json::json!(0), serde_json::json!("not-a-port")] {
let mut document = valid_config_document();
document["host"]["listen"]["unused_setting"] = serde_json::json!(true);
document["host"]["listen"]["port"] = port;
assert_rejected(document.to_string());
}
}
#[test]
fn config_loader_should_report_startup_configuration_diagnostics() {
const CHILD_ENV: &str = "CPR_TEST_CONFIG_DIAGNOSTICS_CHILD";
const UNUSED_SECRET: &str = "unused-value-must-not-appear-in-diagnostics";
if std::env::var_os(CHILD_ENV).is_some() {
gateway_host::load_config::<GatewayConfig>().expect("startup configuration");
return;
}
for case in [
"normal",
"unused",
"missing",
"timezoneEmpty",
"timezoneInvalid",
"timezoneNull",
"timezoneType",
] {
let mut document = valid_config_document();
if case == "unused" {
document["openai"]["unused_setting"] = serde_json::json!(UNUSED_SECRET);
} else if case == "missing" {
document["host"]["listen"]
.as_object_mut()
.unwrap()
.remove("port");
}
if case.starts_with("timezone") {
document["host"]["timezone"] = match case {
"timezoneEmpty" => serde_json::json!(""),
"timezoneInvalid" => serde_json::json!(UNUSED_SECRET),
"timezoneNull" => serde_json::json!(null),
_ => serde_json::json!(8),
};
}
let directory = tempfile::tempdir().unwrap();
fs::create_dir(directory.path().join("deploy")).unwrap();
fs::write(
directory.path().join("deploy/config.yaml"),
document.to_string(),
)
.unwrap();
let output = Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"bootstrap::config::config_loader_should_report_startup_configuration_diagnostics",
"--nocapture",
])
.env(CHILD_ENV, "1")
.current_dir(directory.path())
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.success(),
case != "missing" && !case.starts_with("timezone"),
"{case}: {stderr}"
);
match case {
"unused" => assert!(
stderr.contains("openai.unused_setting") && stderr.contains("已忽略"),
"{stderr}"
),
"missing" => assert!(stderr.contains("host.listen.port"), "{stderr}"),
case if case.starts_with("timezone") => {
assert!(stderr.contains("host.timezone"), "{stderr}")
}
_ => assert!(!stderr.contains("警告"), "{stderr}"),
}
assert!(!stderr.contains(UNUSED_SECRET), "{stderr}");
assert!(!stderr.contains("services"), "{stderr}");
}
}
#[test]
fn config_loader_should_reject_missing_explicit_fields() {
assert_rejected(valid_config().replace(" request_id_header: 'x-request-id'\n", ""));
}
#[test]
fn config_loader_should_allow_missing_provider_configuration() {
let mut document = valid_config_document();
document.as_object_mut().unwrap().remove("openai");
document.as_object_mut().unwrap().remove("xai");
parse_config(&document.to_string()).expect("providers use defaults when omitted");
}
#[test]
fn config_loader_should_reject_unsupported_schema_version() {
assert_rejected(valid_config().replace("schema_version: 1", "schema_version: 2"));
}
#[test]
fn config_loader_should_reject_embedded_database_password() {
assert_rejected(valid_config().replace(
"postgres://codex_proxy@127.0.0.1:5432/codex_proxy",
"postgres://codex_proxy:embedded@127.0.0.1:5432/codex_proxy",
));
}
#[test]
fn config_loader_should_accept_managed_store_passwords() {
for password in [
"RdsTest9!abcdefGh".to_owned(),
"RdsTest9!abcdefGhRdsTest9!abcdefGh".to_owned(),
POSTGRES_PASSWORD.to_owned(),
"x".repeat(128),
"Managed@:/?#%&+Password9!".to_owned(),
"数据库密码".repeat(4),
] {
for field in ["database", "redis"] {
let mut document = valid_config_document();
document["store"][field]["password"] = serde_json::json!(password);
let (config, _directory) =
parse_config(&document.to_string()).expect("managed store password");
assert!(!format!("{config:?}").contains(&password));
}
}
}
#[test]
fn config_loader_should_reject_invalid_store_passwords() {
for password in [
String::new(),
"x".repeat(15),
"x".repeat(129),
" ".repeat(32),
"\u{3000}".repeat(6),
"数据库密码".repeat(9),
"RdsTest9!abcdefGh\u{0000}".to_owned(),
"RdsTest9!abcdefGh\n".to_owned(),
"RdsTest9!abcdefGh\t".to_owned(),
"RdsTest9!abcdefGh\u{007f}".to_owned(),
] {
for field in ["database", "redis"] {
let mut document = valid_config_document();
document["store"][field]["password"] = serde_json::json!(password);
assert_rejected(document.to_string());
}
}
}
#[test]
fn config_loader_should_reject_embedded_redis_password() {
assert_rejected(valid_config().replace(
"redis://127.0.0.1:6379/",
"redis://default:embedded@127.0.0.1:6379/",
));
}
#[test]
fn config_loader_should_reject_weak_admin_password() {
assert_rejected(valid_config().replace(ADMIN_PASSWORD, "password"));
}
#[test]
fn config_loader_should_reject_admin_password_with_compose_interpolation() {
assert_rejected(valid_config().replace(ADMIN_PASSWORD, "unsafe$password"));
}
#[test]
fn config_loader_should_reject_zero_client_session_ttl() {
let mut config = valid_config_document();
*config
.pointer_mut("/client/session_ttl_minutes")
.expect("example client session TTL") = serde_json::json!(0);
assert_rejected(config.to_string());
}
#[test]
fn config_loader_should_default_missing_client_section() {
let mut config = valid_config_document();
config
.as_object_mut()
.expect("example config mapping")
.remove("client")
.expect("example client section");
parse_config(&config.to_string()).expect("client defaults when the section is omitted");
}
#[test]
fn config_loader_should_reject_missing_client_session_ttl() {
let mut config = valid_config_document();
config["client"]
.as_object_mut()
.expect("example client mapping")
.remove("session_ttl_minutes")
.expect("example client session TTL");
assert_rejected(config.to_string());
}
#[test]
fn config_loader_should_reject_disabled_all_log_outputs() {
assert_rejected(
valid_config()
.replace("stdout: true", "stdout: false")
.replace("enabled: true", "enabled: false"),
);
}
#[test]
fn config_loader_should_reject_zero_server_port() {
assert_rejected(valid_config().replace("port: 8080", "port: 0"));
}
#[test]
fn config_loader_should_accept_optional_openai_routing_strings() {
for value in [
serde_json::Value::Null,
serde_json::json!("us"),
serde_json::json!("custom-value"),
serde_json::json!(""),
] {
let mut document = valid_config_document();
document["openai"]["residency"] = value.clone();
document["openai"]["account_routing_override"] = value;
parse_config(&document.to_string()).expect("optional routing strings");
}
let mut document = valid_config_document();
document["openai"]
.as_object_mut()
.unwrap()
.remove("residency");
document["openai"]
.as_object_mut()
.unwrap()
.remove("account_routing_override");
parse_config(&document.to_string()).expect("omitted routing strings");
}
fn assert_rejected(config: String) {
assert!(parse_config(&config).is_err());
}
fn valid_config() -> String {
CONFIG_EXAMPLE
.replacen(
"password: &postgres_password ''",
&format!("password: &postgres_password '{POSTGRES_PASSWORD}'"),
1,
)
.replacen(
"password: &redis_password ''",
&format!("password: &redis_password '{REDIS_PASSWORD}'"),
1,
)
.replace(
"default_password: ''",
&format!("default_password: '{ADMIN_PASSWORD}'"),
)
}
fn valid_config_document() -> serde_json::Value {
// 按字段修改样例,避免注释或排版变化让测试输入悄悄失效;JSON 仍可由 YAML 文件入口加载
config::Config::builder()
.add_source(config::File::from_str(
&valid_config(),
config::FileFormat::Yaml,
))
.build()
.and_then(config::Config::try_deserialize)
.expect("example config document")
}
fn parse_config(config: &str) -> Result<(GatewayConfig, tempfile::TempDir), String> {
let directory = tempfile::tempdir().map_err(|error| error.to_string())?;
let deploy = directory.path().join("deploy");
fs::create_dir(&deploy).map_err(|error| error.to_string())?;
let path = deploy.join("config.yaml");
fs::write(&path, config).map_err(|error| error.to_string())?;
let mut config = config::Config::builder()
.add_source(config::File::from(path).required(true))
.build()
.and_then(config::Config::try_deserialize::<GatewayConfig>)
.map_err(|error| error.to_string())?;
config
.resolve_and_validate(&deploy)
.map_err(|error| error.to_string())?;
Ok((config, directory))
}
#[test]
fn deployment_timezone_defaults_and_valid_override_are_loaded() {
let mut document = valid_config_document();
document["host"].as_object_mut().unwrap().remove("timezone");
let (_config, _directory) = parse_config(&document.to_string()).unwrap();
let host: gateway_host::config::HostConfig =
serde_json::from_value(document["host"].clone()).unwrap();
assert_eq!(host.timezone.name(), "Asia/Shanghai");
document["host"]["timezone"] = serde_json::json!("Asia/Kathmandu");
let (_config, _directory) = parse_config(&document.to_string()).unwrap();
let host: gateway_host::config::HostConfig =
serde_json::from_value(document["host"].clone()).unwrap();
assert_eq!(host.timezone.name(), "Asia/Kathmandu");
}
@@ -0,0 +1,5 @@
//! 网关启动配置、命令行与服务组装的测试入口
mod command;
mod config;
mod server;
@@ -0,0 +1,148 @@
//! 验证服务组装为 HTTPS 代理探测传递自定义 CA
use std::{fs, process::Command};
#[tokio::test]
async fn proxy_probe_should_use_provider_custom_ca_for_https_proxies() {
use gateway_admin::ports::proxy::ProxyProbe;
use gateway_core::account::OutboundProxy;
use gateway_host::proxy_probe::HttpProxyProbe;
use std::{sync::Arc, time::Duration};
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio_rustls::{
TlsAcceptor,
rustls::{
ServerConfig,
pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject as _},
},
};
const CHILD_ENV: &str = "CPR_TEST_PROXY_TLS_DIRECTORY";
let Ok(directory) = std::env::var(CHILD_ENV) else {
let directory = tempfile::tempdir().unwrap();
generate_proxy_test_certificates(directory.path());
// 使用子进程隔离环境变量,避免并行测试读取到临时 CA 配置
for ca_env in ["CODEX_CA_CERTIFICATE", "SSL_CERT_FILE"] {
let mut child = Command::new(std::env::current_exe().unwrap());
child
.args([
"--exact",
"bootstrap::server::proxy_probe_should_use_provider_custom_ca_for_https_proxies",
"--nocapture",
])
.env(CHILD_ENV, directory.path())
.env_remove("CODEX_CA_CERTIFICATE")
.env_remove("SSL_CERT_FILE")
.env(ca_env, directory.path().join("ca.pem"));
if ca_env == "CODEX_CA_CERTIFICATE" {
child.env(
"SSL_CERT_FILE",
directory.path().join("missing-fallback.pem"),
);
}
let output = child.output().unwrap();
assert!(
output.status.success(),
"{ca_env}: {}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
return;
};
provider_openai::ensure_rustls_provider();
let directory = std::path::Path::new(&directory);
let certificate = CertificateDer::from_pem_file(directory.join("server.pem")).unwrap();
let key = PrivateKeyDer::from_pem_file(directory.join("server.key")).unwrap();
let acceptor = TlsAcceptor::from(Arc::new(
ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(vec![certificate], key)
.unwrap(),
));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let proxy =
OutboundProxy::parse(&format!("https://{}", listener.local_addr().unwrap())).unwrap();
let server = tokio::spawn(async move {
let (socket, _) = listener.accept().await.unwrap();
let mut stream = acceptor.accept(socket).await.unwrap();
let mut request = Vec::new();
while !request.ends_with(b"\r\n\r\n") {
assert!(request.len() < 8192);
request.push(stream.read_u8().await.unwrap());
}
assert!(request.starts_with(b"GET http://unresolvable.invalid/ip "));
let body = "{\"ip\":\"203.0.113.8\"}";
stream.write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap();
stream.shutdown().await.unwrap();
});
let probe = HttpProxyProbe::new("http://unresolvable.invalid/ip")
.with_client_builder(provider_openai::build_reqwest_client_with_custom_ca);
let result = probe.test(Some(&proxy), false).await;
assert!(result.success, "{}", result.message);
assert_eq!(result.exit_ip.unwrap().to_string(), "203.0.113.8");
tokio::time::timeout(Duration::from_secs(5), server)
.await
.unwrap()
.unwrap();
}
fn generate_proxy_test_certificates(directory: &std::path::Path) {
let openssl = |args: &[&str]| {
let output = Command::new("openssl")
.args(args)
.current_dir(directory)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
};
openssl(&[
"req",
"-x509",
"-newkey",
"rsa:2048",
"-nodes",
"-keyout",
"ca.key",
"-out",
"ca.pem",
"-days",
"1",
"-subj",
"/CN=Proxy Test CA",
]);
openssl(&[
"req",
"-newkey",
"rsa:2048",
"-nodes",
"-keyout",
"server.key",
"-out",
"server.csr",
"-subj",
"/CN=localhost",
]);
fs::write(directory.join("extensions"), "subjectAltName=IP:127.0.0.1\nbasicConstraints=critical,CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n").unwrap();
openssl(&[
"x509",
"-req",
"-in",
"server.csr",
"-CA",
"ca.pem",
"-CAkey",
"ca.key",
"-CAcreateserial",
"-out",
"server.pem",
"-days",
"1",
"-extfile",
"extensions",
]);
}
+49 -49
View File
@@ -1,6 +1,11 @@
//! 网关组合根测试入口,检查源码模块纪律与冻结的文件边界
mod architecture;
mod bootstrap;
// 组合根是否混入业务策略由代码审查判断,行数和标识符/注释关键词不能证明职责越界
// 依赖 DAG、公开模块面和模块镜像继续由 architecture 检查,源码纪律由下方测试维护
use std::{
collections::BTreeSet,
fs,
@@ -15,7 +20,12 @@ fn app_tree_matches_frozen_terminal_manifest() {
assert_eq!(
rust_files(&root.join("src")),
BTreeSet::from([
PathBuf::from("bootstrap.rs"),
PathBuf::from("bootstrap/command.rs"),
PathBuf::from("bootstrap/config.rs"),
PathBuf::from("bootstrap/mod.rs"),
PathBuf::from("bootstrap/plugins.rs"),
PathBuf::from("bootstrap/server.rs"),
PathBuf::from("bootstrap/startup.rs"),
PathBuf::from("lib.rs"),
PathBuf::from("main.rs"),
]),
@@ -24,7 +34,10 @@ fn app_tree_matches_frozen_terminal_manifest() {
rust_files(&root.join("tests")),
BTreeSet::from([
PathBuf::from("architecture.rs"),
PathBuf::from("bootstrap.rs"),
PathBuf::from("bootstrap/command.rs"),
PathBuf::from("bootstrap/config.rs"),
PathBuf::from("bootstrap/mod.rs"),
PathBuf::from("bootstrap/server.rs"),
PathBuf::from("main.rs"),
]),
);
@@ -43,58 +56,45 @@ fn cargo_library_root_is_conventional_lib() {
}
#[test]
fn production_files_have_no_hidden_modules_or_test_hooks() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
for relative in ["bootstrap.rs", "lib.rs", "main.rs"] {
let path = root.join(relative);
let source = fs::read_to_string(&path).expect("read production source");
assert!(
!source.contains("include!("),
"{} uses include!",
path.display()
);
let syntax = syn::parse_file(&source).expect("parse production source");
for item in &syntax.items {
if let Item::Mod(module) = item {
assert!(
module.content.is_none(),
"{} has an inline module",
path.display()
);
}
for attribute in item_attrs(item) {
assert!(
!is_path_or_test_cfg(attribute),
"{} has a production test/path hook",
path.display(),
);
fn workspace_production_files_have_no_hidden_modules_or_test_hooks() {
for member in architecture::WORKSPACE_MEMBERS {
let root = architecture::backend_root().join(member).join("src");
let sources = rust_files(&root);
assert!(!sources.is_empty(), "{member} has no production sources");
for relative in sources {
let path = root.join(relative);
let source = fs::read_to_string(&path).expect("read production source");
assert!(
!source.contains("#[cfg(test)]"),
"{} hosts tests in production src",
path.display()
);
assert!(
!source.contains("include!("),
"{} uses include!",
path.display()
);
let syntax = syn::parse_file(&source).expect("parse production source");
for item in &syntax.items {
if let Item::Mod(module) = item {
assert!(
module.content.is_none(),
"{} has an inline module",
path.display()
);
}
for attribute in item_attrs(item) {
assert!(
!is_path_or_test_cfg(attribute),
"{} has a production test/path hook",
path.display(),
);
}
}
}
}
}
#[test]
fn bootstrap_owns_only_bundle_wiring() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let bootstrap = fs::read_to_string(root.join("bootstrap.rs")).expect("read bootstrap");
for forbidden in [
"sqlx::",
"redis::",
"Repository",
"impl Provider",
"impl ExecutionService",
"tokio::spawn",
"access_token",
"refresh_token",
] {
assert!(
!bootstrap.contains(forbidden),
"bootstrap owns `{forbidden}`"
);
}
assert!(bootstrap.lines().count() <= 300);
}
fn rust_files(root: &Path) -> BTreeSet<PathBuf> {
let mut result = BTreeSet::new();
collect_rust_files(root, root, &mut result);
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "codex-proxy-plugin-cli"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish.workspace = true
rust-version.workspace = true
[[bin]]
name = "cpr-plugin"
path = "src/main.rs"
[dependencies]
clap = { version = "4.6.7", features = ["derive"] }
flate2 = "1.1.9"
gateway-plugin-sdk = { path = "../../crates/gateway-plugin/sdk" }
hex = "0.4.3"
serde_json = "1.0.150"
sha2 = "0.11.0"
tar = "0.4.44"
thiserror = "2.0.18"
[dev-dependencies]
tempfile = "3.27.0"
[lints]
workspace = true
+55
View File
@@ -0,0 +1,55 @@
# Codex Proxy Plugin CLI
`cpr-plugin` 校验并规范化作者清单、收集声明的资源,生成可安装归档及 SHA-256 校验文件。
输出合同与 [SDK 版本](../../crates/gateway-plugin/sdk/README.md)一致
## 安装
从仓库根目录安装到本地工具目录:
```bash
cargo +1.99.1 install --locked --path backend/apps/plugin-cli --root /path/to/local-tools
```
将 `/path/to/local-tools/bin` 加入 `PATH`,或使用可执行文件的完整路径;工具链与宿主 [验证命令](../../../docs/architecture.md#验证命令)一致
## 打包
先构建目标平台的二进制;有管理页面时,也构建静态资源。全部参数见 `cpr-plugin package --help`
```bash
cpr-plugin package \
--manifest /path/to/plugin/plugin.json \
--binary /path/to/plugin/target/x86_64-unknown-linux-gnu/release/plugin \
--target x86_64-unknown-linux-gnu \
--resource-map web=web/dist \
--output-dir /path/to/output
```
`--resource-map web=web/dist` 将包内 `web/` 前缀映射到作者清单所在目录下的 `web/dist/`。
可重复传入不同前缀,源目录必须位于项目内;没有对应资源时省略该参数,未使用的映射会被拒绝。
未映射的资源按清单中的相对路径读取,只收集 `resources` 声明的文件
输出为 `<插件 ID>-<版本>-<目标 triple>.tar.gz` 及同名 `.tar.gz.sha256`;归档根目录直接包含 `plugin.json`、可执行文件和声明资源。
输入清单及生成后的清单均不得超过 64 KiB,压缩包上限为 32 MiB,展开归档上限为 128 MiB
作者清单不填写 `package`;字段省略规则、能力版本和挂载阶段见 [SDK 扩展项声明](../../crates/gateway-plugin/sdk/docs/manifest.md#扩展项简写)。
CLI 生成规范化后的 `plugin.json`,补入协议版本、目标平台和文件摘要。打包成功只证明输入符合包合同,实际启动与能力仍需在目标宿主验证
支持 `x86_64-unknown-linux-gnu`、`aarch64-unknown-linux-gnu` 和 `aarch64-apple-darwin`。
完整开发示例在独立仓库 `codex-proxy-plugins` 的 `examples/workbench`,可通过[子模块](../../../docs/development.md#源码联调)检出;本工具不负责源码构建、上传或启用插件
### 图标资源
在作者清单的 `icon` 中指定图标,并在 `resources` 中登记对应路径与 MIME 类型,配置方式见
[SDK 插件图标](../../crates/gateway-plugin/sdk/docs/manifest.md#插件图标)。图标与其他声明资源一样参与打包和摘要计算,
无需额外命令参数;工具保留文件原始内容,宿主安装时验证图标内容与资源限制
## 开发
Rust 包名为 `codex-proxy-plugin-cli`,位于宿主的 `backend` workspace,共享工具链、锁文件和检查规则;内部仅依赖 `gateway-plugin-sdk`
```bash
cargo +1.99.1 run --manifest-path backend/Cargo.toml -p codex-proxy-plugin-cli --locked -- package --help
RUST_MIN_STACK=16777216 cargo +1.99.1 test --manifest-path backend/Cargo.toml -p codex-proxy-plugin-cli --locked
```
+564
View File
@@ -0,0 +1,564 @@
//! 插件打包命令行,校验输入资源并生成带摘要的平台归档
use std::{
collections::{BTreeMap, BTreeSet},
fs::{self, File, OpenOptions},
io::{self, Read as _, Write as _},
path::{Component, Path, PathBuf},
};
use clap::{Args, Parser, Subcommand};
use flate2::{Compression, GzBuilder};
use gateway_plugin_sdk::{
Manifest, ManifestError, PROTOCOL_VERSION, Package, PackageTarget, valid_package_path,
};
use sha2::{Digest as _, Sha256};
use tar::{Builder, EntryType, Header, HeaderMode};
use thiserror::Error;
const MAXIMUM_COMPRESSED_BYTES: u64 = 32 * 1024 * 1024;
const MAXIMUM_EXPANDED_BYTES: u64 = 128 * 1024 * 1024;
const MAXIMUM_MANIFEST_BYTES: u64 = 64 * 1024;
#[derive(Parser)]
#[command(name = "cpr-plugin", version, about = "Codex Proxy 插件开发工具")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
#[command(about = "校验清单并打包已编译的插件")]
Package(Options),
}
#[derive(Debug, Error)]
enum PackagerError {
#[error("{0}")]
Invalid(String),
#[error("failed to {action} {path}: {source}")]
Io {
action: &'static str,
path: PathBuf,
#[source]
source: io::Error,
},
#[error("source or generated manifest is invalid: {0}")]
Manifest(#[from] ManifestError),
#[error("failed to encode generated manifest: {0}")]
EncodeManifest(#[source] serde_json::Error),
}
#[derive(Debug, Args)]
struct Options {
#[arg(long, value_name = "FILE", help = "作者清单 plugin.json")]
manifest: PathBuf,
#[arg(long, value_name = "FILE", help = "已编译的目标平台插件二进制")]
binary: PathBuf,
#[arg(long, value_name = "TRIPLE", help = "目标平台 triple")]
target: String,
#[arg(long, value_name = "DIRECTORY", help = "归档与 SHA-256 文件的输出目录")]
output_dir: PathBuf,
#[arg(
long = "resource-map",
value_name = "PREFIX=DIRECTORY",
help = "包内资源前缀到项目内目录的映射,可重复传入"
)]
resource_maps: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
struct RuntimeTarget {
os: &'static str,
architecture: &'static str,
}
impl RuntimeTarget {
fn parse(triple: &str) -> Result<Self, PackagerError> {
match triple {
"x86_64-unknown-linux-gnu" => Ok(Self {
os: "linux",
architecture: "x86_64",
}),
"aarch64-unknown-linux-gnu" => Ok(Self {
os: "linux",
architecture: "aarch64",
}),
"aarch64-apple-darwin" => Ok(Self {
os: "macos",
architecture: "aarch64",
}),
_ => Err(PackagerError::Invalid(format!(
"unsupported plugin target {triple}"
))),
}
}
}
#[derive(Debug)]
struct PackageOutput {
archive: PathBuf,
checksum: PathBuf,
}
fn main() -> Result<(), PackagerError> {
match Cli::parse().command {
Commands::Package(options) => {
let output = package(&options)?;
println!("{}", output.archive.display());
println!("{}", output.checksum.display());
Ok(())
}
}
}
fn parse_resource_maps(values: &[String]) -> Result<BTreeMap<String, PathBuf>, PackagerError> {
let mut resource_maps = BTreeMap::new();
for value in values {
let (prefix, directory) = value.split_once('=').ok_or_else(|| {
PackagerError::Invalid(
"--resource-map must use package-prefix=source-directory".to_owned(),
)
})?;
if !valid_resource_prefix(prefix) {
return Err(PackagerError::Invalid(format!(
"invalid resource-map package prefix {prefix}"
)));
}
let directory = PathBuf::from(directory);
if !safe_relative_directory(&directory) {
return Err(PackagerError::Invalid(format!(
"resource-map source directory must be a non-empty project-relative path: {}",
directory.display()
)));
}
if resource_maps.insert(prefix.to_owned(), directory).is_some() {
return Err(PackagerError::Invalid(format!(
"duplicate resource-map package prefix {prefix}"
)));
}
}
Ok(resource_maps)
}
fn valid_resource_prefix(prefix: &str) -> bool {
valid_package_path(prefix) && !prefix.contains('/')
}
fn safe_relative_directory(path: &Path) -> bool {
!path.as_os_str().is_empty()
&& !path.is_absolute()
&& path
.components()
.all(|component| matches!(component, Component::Normal(_) | Component::CurDir))
}
fn package(options: &Options) -> Result<PackageOutput, PackagerError> {
let target = RuntimeTarget::parse(&options.target)?;
let resource_maps = parse_resource_maps(&options.resource_maps)?;
let manifest_bytes = read_regular_file(&options.manifest, MAXIMUM_MANIFEST_BYTES)?;
let mut manifest = Manifest::from_author_slice(&manifest_bytes)?;
let project_directory = options
.manifest
.parent()
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let canonical_project = canonicalize(project_directory)?;
let mut sources = BTreeMap::from([(manifest.main.clone(), options.binary.clone())]);
let mut used_maps = BTreeSet::new();
for package_path in manifest.resources.keys() {
let source = resource_source(
project_directory,
package_path,
&resource_maps,
&mut used_maps,
)?;
let canonical_source = canonicalize(&source)?;
if !canonical_source.starts_with(&canonical_project) {
return Err(PackagerError::Invalid(format!(
"resource source escapes the plugin project: {}",
source.display()
)));
}
if sources.insert(package_path.clone(), source).is_some() {
return Err(PackagerError::Invalid(format!(
"plugin main and resource share package path {package_path}"
)));
}
}
if used_maps.len() != resource_maps.len() {
let unused = resource_maps
.keys()
.filter(|prefix| !used_maps.contains(*prefix))
.cloned()
.collect::<Vec<_>>();
return Err(PackagerError::Invalid(format!(
"unused resource-map package prefix: {}",
unused.join(", ")
)));
}
reject_case_folded_paths(sources.keys())?;
let mut files = BTreeMap::new();
let mut source_bytes = 0u64;
for (package_path, source) in sources {
let remaining = MAXIMUM_EXPANDED_BYTES
.checked_sub(source_bytes)
.ok_or_else(package_limit)?;
let bytes = read_regular_file(&source, remaining)?;
source_bytes = source_bytes
.checked_add(u64::try_from(bytes.len()).map_err(|_| package_limit())?)
.ok_or_else(package_limit)?;
files.insert(package_path, bytes);
}
let digests = files
.iter()
.map(|(path, bytes)| (path.clone(), hex::encode(Sha256::digest(bytes))))
.collect();
manifest.package = Some(Package {
protocol_version: PROTOCOL_VERSION,
target: PackageTarget {
os: target.os.to_owned(),
architecture: target.architecture.to_owned(),
},
files: digests,
});
manifest.validate()?;
let mut generated_manifest =
serde_json::to_vec_pretty(&manifest).map_err(PackagerError::EncodeManifest)?;
generated_manifest.push(b'\n');
if generated_manifest.len() as u64 > MAXIMUM_MANIFEST_BYTES {
return Err(package_limit());
}
create_directory(&options.output_dir)?;
let plugin_id = manifest.plugin_id()?;
let archive_name = format!("{plugin_id}-{}-{}.tar.gz", manifest.version, options.target);
let archive_path = options.output_dir.join(&archive_name);
let archive_temporary = options
.output_dir
.join(format!(".{archive_name}.{}.tmp", std::process::id()));
let mut temporary = TemporaryFile::create(archive_temporary)?;
write_archive(
temporary.file_mut(),
&generated_manifest,
&manifest.main,
&files,
)?;
temporary.sync_all()?;
let compressed_bytes = temporary.metadata_len()?;
if compressed_bytes > MAXIMUM_COMPRESSED_BYTES {
return Err(package_limit());
}
temporary.persist(&archive_path)?;
let archive_digest = sha256_file(&archive_path)?;
let checksum_path = options.output_dir.join(format!("{archive_name}.sha256"));
let checksum_temporary = options
.output_dir
.join(format!(".{archive_name}.sha256.{}.tmp", std::process::id()));
let mut checksum = TemporaryFile::create(checksum_temporary)?;
let checksum_temporary_path = checksum.path().to_owned();
writeln!(checksum.file_mut(), "{archive_digest} {archive_name}").map_err(|source| {
PackagerError::Io {
action: "write",
path: checksum_temporary_path,
source,
}
})?;
checksum.sync_all()?;
checksum.persist(&checksum_path)?;
Ok(PackageOutput {
archive: archive_path,
checksum: checksum_path,
})
}
fn resource_source(
project_directory: &Path,
package_path: &str,
mappings: &BTreeMap<String, PathBuf>,
used_mappings: &mut BTreeSet<String>,
) -> Result<PathBuf, PackagerError> {
let (prefix, remainder) = package_path
.split_once('/')
.map_or((package_path, None), |(prefix, remainder)| {
(prefix, Some(remainder))
});
if let Some(directory) = mappings.get(prefix) {
let remainder = remainder.ok_or_else(|| {
PackagerError::Invalid(format!(
"mapped resource {package_path} must have a path below prefix {prefix}"
))
})?;
used_mappings.insert(prefix.to_owned());
Ok(project_directory.join(directory).join(remainder))
} else {
Ok(project_directory.join(package_path))
}
}
fn reject_case_folded_paths<'a>(
paths: impl IntoIterator<Item = &'a String>,
) -> Result<(), PackagerError> {
let mut folded = BTreeSet::new();
for path in paths {
if !folded.insert(path.to_ascii_lowercase()) {
return Err(PackagerError::Invalid(format!(
"package path collides case-insensitively: {path}"
)));
}
}
Ok(())
}
fn write_archive(
output: &mut File,
manifest: &[u8],
main_path: &str,
files: &BTreeMap<String, Vec<u8>>,
) -> Result<(), PackagerError> {
let encoder = GzBuilder::new().mtime(0).write(output, Compression::best());
let limited = LimitedWriter::new(encoder, MAXIMUM_EXPANDED_BYTES);
let mut archive = Builder::new(limited);
archive.mode(HeaderMode::Deterministic);
append_file(&mut archive, "plugin.json", manifest, 0o644)?;
for (path, bytes) in files {
let mode = if path == main_path { 0o755 } else { 0o644 };
append_file(&mut archive, path, bytes, mode)?;
}
archive.finish().map_err(|source| PackagerError::Io {
action: "finish archive",
path: PathBuf::from("<temporary archive>"),
source,
})?;
let limited = archive.into_inner().map_err(|source| PackagerError::Io {
action: "finish archive",
path: PathBuf::from("<temporary archive>"),
source,
})?;
limited
.into_inner()
.finish()
.map_err(|source| PackagerError::Io {
action: "finish compression",
path: PathBuf::from("<temporary archive>"),
source,
})?;
Ok(())
}
fn append_file<W: io::Write>(
archive: &mut Builder<W>,
path: &str,
bytes: &[u8],
mode: u32,
) -> Result<(), PackagerError> {
let mut header = Header::new_gnu();
header.set_entry_type(EntryType::Regular);
header.set_mode(mode);
header.set_uid(0);
header.set_gid(0);
header.set_mtime(0);
header.set_size(bytes.len() as u64);
header.set_cksum();
archive
.append_data(&mut header, path, bytes)
.map_err(|source| PackagerError::Io {
action: "append archive entry",
path: PathBuf::from(path),
source,
})
}
struct LimitedWriter<W> {
inner: W,
written: u64,
maximum: u64,
}
impl<W> LimitedWriter<W> {
const fn new(inner: W, maximum: u64) -> Self {
Self {
inner,
written: 0,
maximum,
}
}
fn into_inner(self) -> W {
self.inner
}
}
impl<W> io::Write for LimitedWriter<W>
where
W: io::Write,
{
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
let length = u64::try_from(bytes.len())
.map_err(|_| io::Error::other("plugin package exceeds expanded size limit"))?;
if length > self.maximum.saturating_sub(self.written) {
return Err(io::Error::other(
"plugin package exceeds expanded size limit",
));
}
let written = self.inner.write(bytes)?;
self.written = self
.written
.checked_add(written as u64)
.ok_or_else(|| io::Error::other("plugin package size overflow"))?;
Ok(written)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
struct TemporaryFile {
path: PathBuf,
file: File,
}
impl TemporaryFile {
fn create(path: PathBuf) -> Result<Self, PackagerError> {
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.map_err(|source| PackagerError::Io {
action: "create",
path: path.clone(),
source,
})?;
Ok(Self { path, file })
}
fn path(&self) -> &Path {
&self.path
}
fn file_mut(&mut self) -> &mut File {
&mut self.file
}
fn sync_all(&mut self) -> Result<(), PackagerError> {
let path = self.path.clone();
self.file_mut()
.sync_all()
.map_err(|source| PackagerError::Io {
action: "sync",
path,
source,
})
}
fn metadata_len(&self) -> Result<u64, PackagerError> {
self.file
.metadata()
.map(|metadata| metadata.len())
.map_err(|source| PackagerError::Io {
action: "inspect",
path: self.path.clone(),
source,
})
}
fn persist(mut self, destination: &Path) -> Result<(), PackagerError> {
fs::rename(&self.path, destination).map_err(|source| PackagerError::Io {
action: "publish",
path: destination.to_owned(),
source,
})?;
self.path.clear();
Ok(())
}
}
impl Drop for TemporaryFile {
fn drop(&mut self) {
if !self.path.as_os_str().is_empty() {
let _ = fs::remove_file(&self.path);
}
}
}
fn read_regular_file(path: &Path, maximum_bytes: u64) -> Result<Vec<u8>, PackagerError> {
let metadata = fs::symlink_metadata(path).map_err(|source| PackagerError::Io {
action: "inspect",
path: path.to_owned(),
source,
})?;
if !metadata.file_type().is_file() || metadata.file_type().is_symlink() {
return Err(PackagerError::Invalid(format!(
"package input must be a regular non-symlink file: {}",
path.display()
)));
}
if metadata.len() > maximum_bytes {
return Err(package_limit());
}
let file = File::open(path).map_err(|source| PackagerError::Io {
action: "open",
path: path.to_owned(),
source,
})?;
let mut bytes = Vec::with_capacity(usize::try_from(metadata.len()).unwrap_or_default());
file.take(maximum_bytes.saturating_add(1))
.read_to_end(&mut bytes)
.map_err(|source| PackagerError::Io {
action: "read",
path: path.to_owned(),
source,
})?;
if bytes.len() as u64 > maximum_bytes {
return Err(package_limit());
}
Ok(bytes)
}
fn canonicalize(path: &Path) -> Result<PathBuf, PackagerError> {
fs::canonicalize(path).map_err(|source| PackagerError::Io {
action: "canonicalize",
path: path.to_owned(),
source,
})
}
fn create_directory(path: &Path) -> Result<(), PackagerError> {
fs::create_dir_all(path).map_err(|source| PackagerError::Io {
action: "create directory",
path: path.to_owned(),
source,
})
}
fn sha256_file(path: &Path) -> Result<String, PackagerError> {
let mut file = File::open(path).map_err(|source| PackagerError::Io {
action: "open",
path: path.to_owned(),
source,
})?;
let mut digest = Sha256::new();
let mut buffer = [0u8; 64 * 1024];
loop {
let read = file.read(&mut buffer).map_err(|source| PackagerError::Io {
action: "hash",
path: path.to_owned(),
source,
})?;
if read == 0 {
break;
}
digest.update(&buffer[..read]);
}
Ok(hex::encode(digest.finalize()))
}
fn package_limit() -> PackagerError {
PackagerError::Invalid("plugin package exceeds installation size limits".to_owned())
}
+260
View File
@@ -0,0 +1,260 @@
//! 插件打包命令行的参数、归档校验与输出行为测试
use std::{
collections::BTreeMap,
fs::{self, File},
io::Read as _,
process::{Command, Output},
};
use flate2::read::MultiGzDecoder;
use gateway_plugin_sdk::{Capability, MANIFEST_VERSION, Manifest, PROTOCOL_VERSION, Stage};
use sha2::{Digest as _, Sha256};
fn cli(arguments: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_cpr-plugin"))
.args(arguments)
.output()
.unwrap()
}
fn assert_rejected(arguments: &[&str], message: &str) {
let output = cli(arguments);
assert!(
!output.status.success(),
"{arguments:?} unexpectedly succeeded"
);
assert!(
String::from_utf8_lossy(&output.stderr).contains(message),
"{arguments:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn package_arguments() -> Vec<&'static str> {
vec![
"package",
"--manifest",
"plugin.json",
"--binary",
"plugin-bin",
"--target",
"x86_64-unknown-linux-gnu",
"--output-dir",
"dist",
]
}
#[test]
fn root_help_lists_package_command() {
let output = cli(&["--help"]);
assert!(output.status.success());
let help = String::from_utf8(output.stdout).unwrap();
assert!(help.contains("cpr-plugin <COMMAND>"), "{help}");
assert!(help.contains("package"), "{help}");
}
#[test]
fn version_uses_the_user_command_name() {
let output = cli(&["--version"]);
assert!(output.status.success());
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
format!("cpr-plugin {}\n", env!("CARGO_PKG_VERSION"))
);
}
#[test]
fn package_help_does_not_require_inputs() {
let output = cli(&["package", "--help"]);
assert!(output.status.success());
let help = String::from_utf8(output.stdout).unwrap();
assert!(help.contains("cpr-plugin package"), "{help}");
assert!(help.contains("--resource-map"), "{help}");
}
#[test]
fn rejects_missing_subcommand() {
assert_rejected(&[], "Usage: cpr-plugin");
}
#[test]
fn rejects_unknown_subcommand() {
assert_rejected(&["publish"], "unrecognized subcommand");
}
#[test]
fn rejects_missing_package_arguments() {
assert_rejected(&["package"], "--manifest");
}
#[test]
fn rejects_duplicate_single_value_options() {
let mut arguments = package_arguments();
arguments.extend(["--manifest", "other.json"]);
assert_rejected(&arguments, "cannot be used multiple times");
}
#[test]
fn rejects_unknown_targets() {
let mut arguments = package_arguments();
arguments[6] = "x86_64-pc-windows-msvc";
assert_rejected(&arguments, "unsupported plugin target");
}
#[test]
fn rejects_unsafe_resource_mappings_before_reading_files() {
for (mapping, message) in [
("web=../web/dist", "project-relative path"),
("web=/web/dist", "project-relative path"),
("web=", "project-relative path"),
("web/assets=web/dist", "invalid resource-map package prefix"),
("web", "must use package-prefix=source-directory"),
] {
let mut arguments = package_arguments();
arguments.extend(["--resource-map", mapping]);
assert_rejected(&arguments, message);
}
}
#[test]
fn rejects_duplicate_resource_prefixes() {
let mut arguments = package_arguments();
arguments.extend([
"--resource-map",
"web=web/dist",
"--resource-map",
"web=other",
]);
assert_rejected(&arguments, "duplicate resource-map package prefix");
}
#[test]
fn packages_only_generated_resources_with_current_metadata() {
assert_packaged_resources("assets/icon.png", "image/png", b"png");
}
#[test]
fn packages_svg_icons_with_their_declared_mime_and_digest() {
assert_packaged_resources(
"assets/icon.svg",
"image/svg+xml",
br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 2h20v20H2z"/></svg>"#,
);
}
fn assert_packaged_resources(icon_path: &str, icon_mime: &str, icon: &[u8]) {
let directory = tempfile::tempdir().unwrap();
let project = directory.path().join("example");
fs::create_dir_all(project.join("web/dist")).unwrap();
fs::create_dir_all(project.join("assets")).unwrap();
let mut source: serde_json::Value = serde_json::from_slice(source_manifest()).unwrap();
source["icon"] = icon_path.into();
let resources = source["resources"].as_object_mut().unwrap();
resources.remove("assets/icon.png");
resources.insert(icon_path.into(), icon_mime.into());
fs::write(
project.join("plugin.json"),
serde_json::to_vec(&source).unwrap(),
)
.unwrap();
fs::write(project.join("LICENSE"), b"license").unwrap();
fs::write(project.join(icon_path), icon).unwrap();
fs::write(project.join("plugin-bin"), b"binary").unwrap();
fs::write(project.join("web/index.html"), b"source html").unwrap();
fs::write(project.join("web/dist/index.html"), b"built html").unwrap();
fs::write(project.join("web/dist/app.js"), b"built js").unwrap();
fs::write(project.join("web/dist/app.css"), b"built css").unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_cpr-plugin"))
.arg("package")
.arg("--manifest")
.arg(project.join("plugin.json"))
.arg("--binary")
.arg(project.join("plugin-bin"))
.args(["--target", "x86_64-unknown-linux-gnu"])
.arg("--output-dir")
.arg(project.join("dist"))
.args(["--resource-map", "web=web/dist"])
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let archive_name = "codex-proxy.request-workbench-0.1.0-x86_64-unknown-linux-gnu.tar.gz";
let archive_path = project.join("dist").join(archive_name);
let checksum_path = project.join("dist").join(format!("{archive_name}.sha256"));
assert_eq!(
String::from_utf8(output.stdout).unwrap(),
format!("{}\n{}\n", archive_path.display(), checksum_path.display())
);
let archive = File::open(&archive_path).unwrap();
let mut package = tar::Archive::new(MultiGzDecoder::new(archive));
let mut entries = BTreeMap::new();
for entry in package.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap().to_string_lossy().into_owned();
let mut bytes = Vec::new();
entry.read_to_end(&mut bytes).unwrap();
entries.insert(path, bytes);
}
assert_eq!(
entries.keys().cloned().collect::<Vec<_>>(),
vec![
"LICENSE".to_owned(),
icon_path.to_owned(),
"bin/plugin".to_owned(),
"plugin.json".to_owned(),
"web/app.css".to_owned(),
"web/app.js".to_owned(),
"web/index.html".to_owned(),
]
);
assert_eq!(entries["web/index.html"], b"built html");
let manifest: Manifest = serde_json::from_slice(&entries["plugin.json"]).unwrap();
assert_eq!(manifest.manifest_version, MANIFEST_VERSION);
assert_eq!(
manifest.contributes[&Capability::Middleware].id,
"codex-proxy.request-workbench.middleware"
);
assert_eq!(
manifest.contributes[&Capability::Management].stages,
[Stage::Management]
);
assert_eq!(manifest.resources[icon_path], icon_mime);
assert_eq!(entries[icon_path], icon);
let package = manifest.package.unwrap();
assert_eq!(package.files[icon_path], hex::encode(Sha256::digest(icon)));
assert_eq!(package.protocol_version, PROTOCOL_VERSION);
assert_eq!(package.target.os, "linux");
assert_eq!(package.target.architecture, "x86_64");
assert_eq!(package.files.len(), 6);
let checksum = fs::read_to_string(checksum_path).unwrap();
let digest = hex::encode(Sha256::digest(fs::read(archive_path).unwrap()));
assert_eq!(checksum, format!("{digest} {archive_name}\n"));
}
fn source_manifest() -> &'static [u8] {
br#"{
"manifestVersion":2,
"name":"request-workbench",
"displayName":"Request Workbench",
"publisher":"codex-proxy",
"version":"0.1.0",
"description":"test package",
"license":"Apache-2.0",
"engines":{"codex-proxy-rs":">=3.12.0, <4.0.0"},
"main":"bin/plugin",
"runtime":"trustedProcess",
"contributes":{
"middleware":{"version":6,"stages":["request"],"inputFormats":["openai"],"outputFormats":["openai"]},
"management":{"version":2}
},
"icon":"assets/icon.png",
"resources":{"LICENSE":"text/plain","assets/icon.png":"image/png","web/index.html":"text/html","web/app.js":"text/javascript","web/app.css":"text/css"}
}"#
}
+18 -5
View File
@@ -12,24 +12,37 @@ name = "main"
path = "tests/main.rs"
[dependencies]
argon2 = "0.5.3"
argon2 = "0.6.0"
async-trait = "0.1.89"
base64 = "0.23.0"
chrono = "0.4.45"
bytes = "1.11.1"
chrono = { version = "0.4.45", features = ["serde"] }
futures = "0.3.32"
gateway-core = { path = "../gateway-core" }
gateway-protocol = { path = "../gateway-protocol" }
heck = "0.5.0"
hex = "0.4.3"
hkdf = "0.13.0"
rand_core = { version = "0.6.4", features = ["getrandom"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.150", features = ["arbitrary_precision", "preserve_order"] }
secrecy = { version = "0.10.3", features = ["serde"] }
semver = "1.0.27"
sha2 = "0.11"
subtle = "2.6.1"
thiserror = "2.0.18"
tracing = "0.1.44"
uuid = { version = "1.23.3", features = ["v7"] }
uuid = { version = "1.23.3", features = ["v4", "v7"] }
cron = "0.17"
chrono-tz = "0.10"
tokio = { version = "1.52", features = ["rt", "time", "macros", "sync"] }
[dev-dependencies]
serde_json = { version = "1.0.150", features = ["arbitrary_precision", "preserve_order"] }
tokio = { version = "1.52.3", default-features = false, features = ["macros", "rt"] }
prettyplease = "0.3"
quote = "1"
syn = { version = "3", features = ["full", "visit-mut"] }
tempfile = "3"
tokio = { version = "1.52.3", default-features = false, features = ["macros", "rt", "test-util"] }
[lints]
workspace = true
@@ -0,0 +1,4 @@
//! 备份纯策略与 Worker 任务
pub mod policy;
pub mod task;
@@ -0,0 +1,167 @@
//! 备份纯策略:Cron 推进与保留候选
//!
//! 本模块不持有任何端口或对象,是 `BackupService` 与 `BackupTask` 共用的纯函数集
use std::str::FromStr;
use chrono::{DateTime, Duration, TimeZone as _, Utc};
use chrono_tz::Tz;
use cron::Schedule;
use crate::model::backup::{BackupError, BackupRecord, BackupStatus, code};
/// 向历史回溯的最大范围(分钟)
/// 覆盖 Feb 29 + 特定星期等罕见组合的最长间隔
const MAX_BACKDATE_MINUTES: i64 = 40 * 366 * 24 * 60;
/// 已解析并校验的备份计划
#[derive(Debug, Clone)]
pub struct BackupSchedule {
schedule: Schedule,
timezone: Tz,
}
impl BackupSchedule {
/// 按部署时区解析并校验 5 段 Cron
///
/// # Errors
///
/// Cron 非法时返回 [`BackupError`]
pub fn parse(
cron_expression: &str,
timezone: gateway_core::time::DeploymentTimeZone,
) -> Result<Self, BackupError> {
let expression = format!("0 {cron_expression}");
let schedule = Schedule::from_str(&expression).map_err(|_| {
BackupError::new(code::INVALID_CRON, "Cron 表达式必须是 5 段格式".to_owned())
})?;
Ok(Self {
schedule,
timezone: timezone.iana(),
})
}
/// 返回最接近 `at`(含 `at` 所在分钟)的最近一次触发时间
///
/// 若过去 `MAX_BACKDATE_MINUTES` 内没有任何触发(极罕见的稀疏计划或超长停机),
/// 返回 `None`,调用方不应为历史点补任务
#[must_use]
pub fn last_firing_at_or_before(&self, at: DateTime<Utc>) -> Option<DateTime<Utc>> {
let high_min = at.timestamp().div_euclid(60);
let low_min = high_min.saturating_sub(MAX_BACKDATE_MINUTES);
// 检查窗口内是否存在触发;不存在则直接返回 None
let low_instant = instant(low_min, self.timezone)?;
if self
.next_after(low_instant.to_utc())
.is_none_or(|firing| firing.timestamp() > high_min * 60 + 59)
{
return None;
}
// 二分查找最小的 u(分钟)使 after(u) > high;prev = after(u - 1 分钟)
let mut lo = low_min;
let mut hi = high_min;
while lo < hi {
let mid = lo + (hi - lo) / 2;
if self
.firing_after(mid)
.is_some_and(|firing| firing > high_min)
{
hi = mid;
} else {
lo = mid + 1;
}
}
self.firing_after(lo - 1)
.filter(|&firing| firing <= high_min)
.and_then(|firing| Utc.timestamp_opt(firing * 60, 0).single())
}
/// 返回第一个严格晚于 `at` 的触发时间
#[must_use]
pub fn next_after(&self, at: DateTime<Utc>) -> Option<DateTime<Utc>> {
let local = at.with_timezone(&self.timezone).naive_local().and_utc();
self.schedule.after(&local).find_map(|firing| {
let instant = self
.timezone
.from_local_datetime(&firing.naive_utc())
.earliest()?
.to_utc();
(instant > at).then_some(instant)
})
}
/// `after(u 分钟)` 返回的第一个触发时间(分钟索引),用于二分搜索
fn firing_after(&self, minute_index: i64) -> Option<i64> {
let instant = instant(minute_index, self.timezone)?.to_utc();
self.next_after(instant)
.map(|firing| firing.timestamp().div_euclid(60))
}
}
/// 构造指定分钟索引(UTC epoch 分钟)在目标时区下的 `DateTime`
fn instant(minute_index: i64, timezone: Tz) -> Option<DateTime<Tz>> {
timezone.timestamp_opt(minute_index * 60, 0).single()
}
/// 一条记录被纳入删除流程的原因
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetentionReason {
/// 超过 `retention_days`
Days,
/// 超过 `retention_count`
Count,
}
/// 保留决策
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetentionDecision {
pub record_id: String,
pub reason: RetentionReason,
}
/// 从按完成时间倒序的 completed 计划备份中决定纳入删除流程的记录
///
/// 自动保留规则:
/// - `retentionDays = 0` 时不按天数清理;`retentionCount = 0` 时不按份数清理
/// - 自动计划备份满足任一启用阈值即可进入删除
/// - 无论阈值如何,至少保留最近一个成功的计划备份(全局倒序下标 0)
/// - 手动备份不进入本规则;手工/计划记录各自的 `expires_at` 由到期扫描处理
/// - `offset` 是当前页在全部 completed 计划备份中的起始位置
#[must_use]
pub fn decide_retention(
retention_days: u32,
retention_count: u32,
now: DateTime<Utc>,
offset: u64,
scheduled_completed_desc: &[BackupRecord],
) -> Vec<RetentionDecision> {
let mut decisions = Vec::new();
for (index, record) in scheduled_completed_desc.iter().enumerate() {
let position = offset.saturating_add(index as u64);
if record.status != BackupStatus::Completed {
continue;
}
// 最近一个成功的计划备份永远保留
if position == 0 {
continue;
}
let expired_by_days = retention_days > 0
&& record.completed_at.is_some_and(|completed_at| {
completed_at <= now - Duration::days(i64::from(retention_days))
});
let exceeded_count = retention_count > 0 && position >= u64::from(retention_count);
if expired_by_days || exceeded_count {
decisions.push(RetentionDecision {
record_id: record.id.clone(),
reason: if expired_by_days {
RetentionReason::Days
} else {
RetentionReason::Count
},
});
}
}
decisions
}
@@ -0,0 +1,783 @@
//! Backup Worker 贡献:单个可取消 `DaemonTask` 承担调度、执行、删除收敛与保留清理
//!
//! 当前部署边界是单副本,因此不需要 Redis lease、fencing token 或 heartbeat
//! 长时间
//! `pg_dump` 与上传由本 Daemon 自身持有;Host 只负责 panic 后重启、健康与关闭
use std::sync::Arc;
use std::time::Duration;
use chrono::{DateTime, Utc};
use futures::future::BoxFuture;
use gateway_core::{
lifecycle::CancellationToken,
task::{
DaemonRestartPolicy, DaemonTask, WorkerContribution, WorkerDaemonContext, WorkerId,
WorkerKind, WorkerRegistration, WorkerRunnable, WorkerTaskError,
},
};
use tracing::{info, warn};
use crate::{
model::AdminError,
model::backup::{
BackupError, BackupObjectMetadata, BackupRecord, BackupStatus, BackupStatusTransition,
BackupStorageConfig, BackupTriggerKind, build_backup_seed, code,
},
ports::backup::{
BackupObjectStorePort, BackupRepository, DatabaseDumpPort, DumpRequest,
StatusTransitionUpdate, UploadObjectRequest,
},
};
impl From<BackupError> for WorkerTaskError {
fn from(error: BackupError) -> Self {
WorkerTaskError::safe(error.message()).with_source(error)
}
}
use super::policy::{BackupSchedule, decide_retention};
const BACKUP_WORKER_OWNER: &str = "backup";
/// 循环间隔:管理员修改 Cron 后该时长内生效;无工作时可取消等待
const BACKUP_LOOP_INTERVAL: Duration = Duration::from_secs(30);
/// 每个循环最多执行的保留删除数量
const RETENTION_BATCH_SIZE: usize = 10;
/// 保留扫描每页读取的 completed 计划备份数量
const RETENTION_PAGE_SIZE: u32 = 1000;
/// 每个循环最多完成的待删除记录数量
const PENDING_DELETION_BATCH: u32 = 20;
/// 备份 Daemon 任务
pub struct BackupTask {
timezone: gateway_core::time::DeploymentTimeZone,
repository: Arc<dyn BackupRepository>,
dump: Arc<dyn DatabaseDumpPort>,
object_store: Arc<dyn BackupObjectStorePort>,
}
impl BackupTask {
/// 组合仓储、导出器与对象存储适配器
#[must_use]
pub fn new(
repository: Arc<dyn BackupRepository>,
dump: Arc<dyn DatabaseDumpPort>,
object_store: Arc<dyn BackupObjectStorePort>,
) -> Self {
Self {
timezone: Default::default(),
repository,
dump,
object_store,
}
}
#[must_use]
pub fn with_timezone(mut self, timezone: gateway_core::time::DeploymentTimeZone) -> Self {
self.timezone = timezone;
self
}
pub(crate) fn into_worker(self) -> Result<WorkerContribution, AdminError> {
let id = WorkerId::try_new(WorkerKind::Backup, BACKUP_WORKER_OWNER)
.map_err(|_| AdminError::internal("备份 Worker ID 不合法"))?;
let restart = DaemonRestartPolicy::try_new(Duration::from_secs(1), Duration::from_secs(60))
.map_err(|_| AdminError::internal("备份 Worker 重启策略不合法"))?;
let registration = WorkerRegistration::try_new(
id,
WorkerRunnable::Daemon {
restart,
task: Box::new(self),
},
)
.map_err(|_| AdminError::internal("备份 Worker 注册信息不合法"))?;
Ok(WorkerContribution::Registration(registration))
}
}
impl DaemonTask for BackupTask {
fn run(&self, context: WorkerDaemonContext) -> BoxFuture<'_, Result<(), WorkerTaskError>> {
Box::pin(async move { self.run_daemon(context).await })
}
}
impl BackupTask {
async fn run_daemon(&self, context: WorkerDaemonContext) -> Result<(), WorkerTaskError> {
let cancellation = context.cancellation();
loop {
if cancellation.is_cancelled() {
return Ok(());
}
self.run_cycle(cancellation).await?;
context.succeeded();
tokio::select! {
_ = cancellation.cancelled() => return Ok(()),
_ = tokio::time::sleep(BACKUP_LOOP_INTERVAL) => {}
}
}
}
/// 执行一个周期:推进计划、恢复中间态、删除收敛、领取执行一个任务、保留清理
///
/// 对外暴露以便集成测试逐周期驱动;daemon 循环内部复用
///
/// # Errors
///
/// 仓储或基础设施不可用、任务执行失败时返回 [`WorkerTaskError`]
pub async fn run_cycle(&self, cancellation: &CancellationToken) -> Result<(), WorkerTaskError> {
if cancellation.is_cancelled() {
return Ok(());
}
self.advance_schedule(Utc::now()).await?;
self.recover_intermediate(cancellation).await?;
self.finish_pending_deletions().await?;
self.process_next_task(cancellation).await?;
self.run_retention_batch(Utc::now(), cancellation).await?;
Ok(())
}
/// 推进到期计划并幂等插入 scheduled 任务
async fn advance_schedule(&self, now: DateTime<Utc>) -> Result<(), WorkerTaskError> {
let settings = self.repository.load_settings().await.map_err(repo_error)?;
if !settings.schedule_enabled {
return Ok(());
}
let cron = settings.cron_expression.as_deref().unwrap_or_default();
let timezone = self.timezone.name();
if cron.is_empty() {
return Ok(());
}
let schedule = BackupSchedule::parse(cron, self.timezone).map_err(|source| {
WorkerTaskError::safe("backup schedule is invalid").with_source(source)
})?;
if settings.schedule_timezone.as_deref() != Some(timezone) {
// 时区切换只初始化未来游标,不把旧时区计划当作应补跑任务
if let Some(next_run_at) = schedule.next_after(now) {
self.repository
.advance_schedule_cursor(
next_run_at,
cron,
settings.schedule_timezone.as_deref(),
settings.next_run_at,
timezone,
)
.await
.map_err(repo_error)?;
}
return Ok(());
}
let due = match settings.next_run_at {
Some(next_run_at) if next_run_at <= now => true,
Some(_) => false,
None => {
// 刚启用计划但尚无游标:直接初始化
if let Some(next_run_at) = schedule.next_after(now) {
let _ = self
.repository
.advance_schedule_cursor(
next_run_at,
cron,
Some(timezone),
settings.next_run_at,
timezone,
)
.await
.map_err(repo_error)?;
}
return Ok(());
}
};
if !due {
return Ok(());
}
let scheduled_at = match schedule.last_firing_at_or_before(now) {
Some(scheduled_at) => scheduled_at,
None => {
// 无法从计划推导最近到期时间:推进游标避免反复触发
if let Some(next_run_at) = schedule.next_after(now) {
let _ = self
.repository
.advance_schedule_cursor(
next_run_at,
cron,
Some(timezone),
settings.next_run_at,
timezone,
)
.await
.map_err(repo_error)?;
}
return Ok(());
}
};
let prefix = settings.prefix.as_deref().unwrap_or_default();
let expires_at = if settings.retention_days > 0 {
Some(now + chrono::Duration::days(i64::from(settings.retention_days)))
} else {
None
};
let seed = build_backup_seed(
BackupTriggerKind::Scheduled,
Some(scheduled_at),
prefix,
now,
expires_at,
)
.map_err(infra_error)?;
let inserted = self
.repository
.insert_scheduled_record(
seed,
schedule.next_after(now),
cron,
timezone,
settings.next_run_at,
)
.await
.map_err(repo_error)?;
if inserted {
info!(scheduled_at = %scheduled_at, "计划备份任务已创建");
} else {
warn!(scheduled_at = %scheduled_at, "计划已变化或存在冲突,本轮未创建任务");
}
Ok(())
}
/// 恢复中间状态(dumping/uploading/deleting)
async fn recover_intermediate(
&self,
cancellation: &CancellationToken,
) -> Result<(), WorkerTaskError> {
let records = self
.repository
.list_intermediate_records()
.await
.map_err(repo_error)?;
for record in records {
match record.status {
BackupStatus::Dumping => self.recover_dumping(&record, cancellation).await?,
BackupStatus::Uploading => self.recover_uploading(&record, cancellation).await?,
_ => {}
}
}
Ok(())
}
/// dumping:有完整暂存则继续上传,否则清理暂存并标记失败
async fn recover_dumping(
&self,
record: &BackupRecord,
cancellation: &CancellationToken,
) -> Result<(), WorkerTaskError> {
match self.dump.inspect_staging(&record.id).await? {
Some(artifact) => {
let now = Utc::now();
let update = StatusTransitionUpdate {
size_bytes: Some(artifact.size_bytes),
sha256: Some(artifact.sha256.clone()),
..Default::default()
};
let Some(uploading_record) = self
.repository
.transition_status(
&record.id,
status_transition(BackupStatus::Dumping, BackupStatus::Uploading)?,
update,
now,
)
.await
.map_err(repo_error)?
else {
return Ok(());
};
self.upload_and_verify(
&uploading_record,
&artifact.path,
artifact.size_bytes,
&artifact.sha256,
now,
cancellation,
)
.await?;
}
None => {
self.fail_task(record, code::PG_DUMP_FAILED, "导出进程中断,暂存归档不完整")
.await?;
}
}
Ok(())
}
/// uploading:远端匹配则补记完成,有完整暂存则重试上传,否则失败
async fn recover_uploading(
&self,
record: &BackupRecord,
cancellation: &CancellationToken,
) -> Result<(), WorkerTaskError> {
let settings = self.repository.load_settings().await.map_err(repo_error)?;
let Some(config) = BackupStorageConfig::from_settings(&settings) else {
return Ok(());
};
if let Some(remote) = self
.object_store
.head_object(&config, &record.object_key)
.await?
&& record.size_bytes == Some(remote.size_bytes)
&& record.sha256.as_deref() == Some(remote.sha256.as_str())
{
let now = Utc::now();
if self
.repository
.transition_status(
&record.id,
status_transition(BackupStatus::Uploading, BackupStatus::Completed)?,
StatusTransitionUpdate::default(),
now,
)
.await
.map_err(repo_error)?
.is_some()
{
self.cleanup_staging(&record.id).await;
}
return Ok(());
}
match self.dump.inspect_staging(&record.id).await? {
Some(artifact) => {
self.upload_and_verify(
record,
&artifact.path,
artifact.size_bytes,
&artifact.sha256,
Utc::now(),
cancellation,
)
.await
}
None => {
self.fail_task(
record,
code::S3_UPLOAD_FAILED,
"远端对象缺失且本地无完整暂存",
)
.await
}
}
}
/// 完成待删除记录(deleting → DeleteObject → 硬删除)
async fn finish_pending_deletions(&self) -> Result<(), WorkerTaskError> {
let settings = self.repository.load_settings().await.map_err(repo_error)?;
let Some(config) = BackupStorageConfig::from_settings(&settings) else {
return Ok(());
};
let records = self
.repository
.list_pending_deletions(PENDING_DELETION_BATCH)
.await
.map_err(repo_error)?;
for record in records {
if self
.object_store
.delete_object(&config, &record.object_key)
.await
.is_ok()
{
self.repository
.delete_record(&record.id)
.await
.map_err(repo_error)?;
}
}
Ok(())
}
/// 领取一个 queued 任务并执行导出、上传与远端校验
async fn process_next_task(
&self,
cancellation: &CancellationToken,
) -> Result<(), WorkerTaskError> {
let now = Utc::now();
let Some(record) = self
.repository
.claim_next_queued(now)
.await
.map_err(repo_error)?
else {
return Ok(());
};
info!(
backup_id = %record.id,
attempt = record.attempt_count,
"领取备份任务"
);
if let Err(error) = self.execute_task(&record, cancellation).await {
warn!(backup_id = %record.id, error = %error, "备份任务执行失败");
}
Ok(())
}
/// 执行单个任务;内部处理全部状态迁移
async fn execute_task(
&self,
record: &BackupRecord,
cancellation: &CancellationToken,
) -> Result<(), WorkerTaskError> {
let settings = self.repository.load_settings().await.map_err(repo_error)?;
if BackupStorageConfig::from_settings(&settings).is_none() {
return self
.fail_task(record, code::INVALID_CONFIG, "备份存储配置不完整")
.await;
}
if !settings.storage_verified() {
return self
.fail_task(record, code::INVALID_CONFIG, "备份存储配置尚未通过连接测试")
.await;
}
let now = Utc::now();
let artifact = match self
.dump
.dump(DumpRequest {
backup_id: record.id.clone(),
cancellation: cancellation.clone(),
})
.await
{
Ok(artifact) => artifact,
Err(error) => {
let (error_code, message) = classify_dump_error(error);
return self.fail_task(record, error_code, message).await;
}
};
// dumping → uploading,持久化 size/sha256
let update = StatusTransitionUpdate {
size_bytes: Some(artifact.size_bytes),
sha256: Some(artifact.sha256.clone()),
..Default::default()
};
let Some(uploading_record) = self
.repository
.transition_status(
&record.id,
status_transition(BackupStatus::Dumping, BackupStatus::Uploading)?,
update,
now,
)
.await
.map_err(repo_error)?
else {
return Ok(());
};
self.upload_and_verify(
&uploading_record,
&artifact.path,
artifact.size_bytes,
&artifact.sha256,
now,
cancellation,
)
.await
}
/// 上传 + 远端校验,内部完成终态迁移
async fn upload_and_verify(
&self,
record: &BackupRecord,
source: &std::path::Path,
size_bytes: u64,
sha256: &str,
now: DateTime<Utc>,
cancellation: &CancellationToken,
) -> Result<(), WorkerTaskError> {
let settings = self.repository.load_settings().await.map_err(repo_error)?;
let Some(config) = BackupStorageConfig::from_settings(&settings) else {
return Ok(());
};
let store = self.object_store.clone();
let metadata =
BackupObjectMetadata::new(record.id.clone(), sha256.to_owned(), now, size_bytes);
if let Err(error) = store
.upload_file(
&config,
UploadObjectRequest {
object_key: record.object_key.clone(),
source: source.to_path_buf(),
metadata,
cancellation: cancellation.clone(),
},
)
.await
{
self.fail_uploaded_task(
record,
&config,
BackupError::new(
error.code(),
format!("上传到对象存储失败:{}", error.message()),
),
)
.await?;
return Ok(());
}
match store.head_object(&config, &record.object_key).await {
Ok(Some(remote)) if remote.size_bytes == size_bytes && remote.sha256 == sha256 => {
if self
.repository
.transition_status(
&record.id,
status_transition(BackupStatus::Uploading, BackupStatus::Completed)?,
StatusTransitionUpdate::default(),
Utc::now(),
)
.await
.map_err(repo_error)?
.is_some()
{
info!(
backup_id = %record.id,
size_bytes,
"备份完成"
);
self.cleanup_staging(&record.id).await;
}
}
Ok(Some(remote)) => {
let size_matches = remote.size_bytes == size_bytes;
let sha256_matches = remote.sha256 == sha256;
warn!(
backup_id = %record.id,
expected_size_bytes = size_bytes,
remote_size_bytes = remote.size_bytes,
sha256_matches,
"备份远端对象校验不一致"
);
self.fail_uploaded_task(
record,
&config,
BackupError::new(
code::REMOTE_VERIFICATION_FAILED,
match (size_matches, sha256_matches) {
(false, true) => "远端对象大小不一致",
(true, false) => "远端对象 sha256 校验值不一致",
(false, false) => "远端对象大小和 sha256 校验值均不一致",
(true, true) => "远端对象校验状态异常",
}
.to_owned(),
),
)
.await?;
}
Ok(None) => {
warn!(backup_id = %record.id, "上传完成后未找到远端对象");
self.fail_task(
record,
code::REMOTE_VERIFICATION_FAILED,
"上传完成后未找到远端对象",
)
.await?;
}
Err(error) => {
self.fail_uploaded_task(
record,
&config,
BackupError::new(
error.code(),
format!("远端对象校验失败:{}", error.message()),
),
)
.await?;
}
}
Ok(())
}
/// 清理失败并入原终态,后续清理沿用备份记录的到期策略
async fn fail_uploaded_task(
&self,
record: &BackupRecord,
config: &BackupStorageConfig,
mut error: BackupError,
) -> Result<(), WorkerTaskError> {
if let Err(cleanup) = self
.object_store
.delete_object(config, &record.object_key)
.await
{
error =
error.with_cleanup_failure("DeleteObject", &cleanup, record.expires_at.is_some());
}
self.fail_task(record, error.code(), error.message()).await
}
/// 把活跃任务写入失败终态并清理暂存
async fn fail_task(
&self,
record: &BackupRecord,
error_code: &'static str,
message: impl Into<String>,
) -> Result<(), WorkerTaskError> {
let message = message.into();
warn!(
backup_id = %record.id,
error_code,
error_message = %message,
"备份任务失败"
);
let now = Utc::now();
let update = StatusTransitionUpdate {
error_code: Some(error_code.to_string()),
error_message: Some(message),
completed_at: Some(now),
..Default::default()
};
let _ = self
.repository
.transition_status(
&record.id,
status_transition(record.status, BackupStatus::Failed)?,
update,
now,
)
.await
.map_err(repo_error)?;
self.cleanup_staging(&record.id).await;
Ok(())
}
async fn cleanup_staging(&self, backup_id: &str) {
if let Err(error) = self.dump.cleanup_staging(backup_id).await {
warn!(backup_id, error = %error, "清理备份暂存失败");
}
}
/// 执行一小批到期保留清理:先处理 `expires_at` 已到期的记录,再按天数/份数清理计划备份
async fn run_retention_batch(
&self,
now: DateTime<Utc>,
cancellation: &CancellationToken,
) -> Result<(), WorkerTaskError> {
let settings = self.repository.load_settings().await.map_err(repo_error)?;
// 1. expires_at 已到期的记录(手动或计划)无条件进入删除
let expired = self
.repository
.list_expired_records(RETENTION_BATCH_SIZE as u32)
.await
.map_err(repo_error)?;
for record in expired {
let Some(record) = self
.repository
.transition_to_deleting(&record.id, now, None)
.await
.map_err(repo_error)?
else {
continue;
};
self.finalize_deletion(&record).await?;
}
// 2. 计划备份按 retentionDays / retentionCount 清理
if settings.retention_days == 0 && settings.retention_count == 0 {
return Ok(());
}
let mut offset = 0;
let mut decisions = Vec::new();
loop {
if cancellation.is_cancelled() {
return Ok(());
}
let records = self
.repository
.list_scheduled_completed_desc(offset, RETENTION_PAGE_SIZE)
.await
.map_err(repo_error)?;
let remaining = RETENTION_BATCH_SIZE - decisions.len();
decisions.extend(
decide_retention(
settings.retention_days,
settings.retention_count,
now,
offset,
&records,
)
.into_iter()
.take(remaining),
);
if decisions.len() == RETENTION_BATCH_SIZE
|| records.len() < RETENTION_PAGE_SIZE as usize
{
break;
}
offset += records.len() as u64;
}
// 收齐本批候选后再删除,避免删除前页记录导致后续分页跳过数据
for decision in decisions {
let Some(record) = self
.repository
.transition_to_deleting(&decision.record_id, now, None)
.await
.map_err(repo_error)?
else {
continue;
};
self.finalize_deletion(&record).await?;
}
Ok(())
}
/// 对已进入 deleting 的记录执行对象删除并硬删除记录行
async fn finalize_deletion(&self, record: &BackupRecord) -> Result<(), WorkerTaskError> {
let settings = self.repository.load_settings().await.map_err(repo_error)?;
let Some(config) = BackupStorageConfig::from_settings(&settings) else {
return Ok(());
};
if self
.object_store
.delete_object(&config, &record.object_key)
.await
.is_ok()
{
self.repository
.delete_record(&record.id)
.await
.map_err(repo_error)?;
}
Ok(())
}
}
fn classify_dump_error(error: BackupError) -> (&'static str, &'static str) {
match error.code() {
code::CANCELLED => (code::CANCELLED, "备份已取消"),
code::ANALYTICS_NOT_READY => (
code::ANALYTICS_NOT_READY,
"用量统计正在重建,请在统计就绪后重试备份",
),
code::STAGING_SPACE_EXHAUSTED => (code::STAGING_SPACE_EXHAUSTED, "暂存磁盘空间不足"),
_ => (code::PG_DUMP_FAILED, "数据库导出失败"),
}
}
fn status_transition(
from: BackupStatus,
to: BackupStatus,
) -> Result<BackupStatusTransition, WorkerTaskError> {
BackupStatusTransition::try_new(from, to)
.ok_or_else(|| WorkerTaskError::safe("backup status transition is invalid"))
}
fn repo_error(error: crate::ports::store::AdminStoreError) -> WorkerTaskError {
WorkerTaskError::safe("backup store failed").with_source(error)
}
fn infra_error(error: BackupError) -> WorkerTaskError {
WorkerTaskError::safe(error.message()).with_source(error)
}
@@ -0,0 +1,341 @@
//! 管理服务与后台任务的启动组装,窄端口供服务器和 CLI 复用
use std::{path::Path, sync::Arc};
use gateway_core::{engine::probe::AccountProbe, runtime::SnapshotControl};
use crate::{
AccountsService, AdminBundle, AdminConfig, AdminRuntimePorts, AdminServices, ClientConfig,
ClientKeyService, CredentialsService, PluginDistributionPorts, PluginManagementService,
PluginsService, SettingsService, UsageDataService, backup, freeze_recovery,
model::AdminError,
ports::{
self, plugin_accounts::PluginAccountAccess, plugin_client_keys::PluginClientKeyAccess,
provider::ProviderAdminRegistry, store::AdminStorePorts,
},
public_service, reset_credit_worker, use_case,
use_case::{
account_groups::DefaultAccountGroupService,
accounts::DefaultAccountsService,
auth::{AuthService, DefaultAuthService},
backup::DefaultBackupService,
client_distribution::DefaultClientDistributionService,
client_keys::DefaultClientKeyService,
observability::DefaultObservabilityService,
settings::DefaultSettingsService,
system::DefaultSystemService,
},
};
/// 校验配置、接入已组装的 Provider 注册表并完成默认管理员幂等初始化
///
/// # Errors
///
/// 配置非法或默认管理员初始化失败时返回错误
pub async fn initialize(
config: AdminConfig,
client_config: ClientConfig,
store: AdminStorePorts,
runtime: AdminRuntimePorts,
) -> Result<AdminBundle, AdminError> {
initialize_inner(config, client_config, store, runtime, None).await
}
/// 使用组合根已绑定给 Runtime 的同一账号窄端口,避免为完整 Admin 重建第二实例
pub async fn initialize_with_plugin_accounts(
config: AdminConfig,
client_config: ClientConfig,
store: AdminStorePorts,
runtime: AdminRuntimePorts,
plugin_accounts: Arc<dyn PluginAccountAccess>,
) -> Result<AdminBundle, AdminError> {
initialize_inner(config, client_config, store, runtime, Some(plugin_accounts)).await
}
async fn initialize_inner(
mut config: AdminConfig,
mut client_config: ClientConfig,
store: AdminStorePorts,
runtime: AdminRuntimePorts,
plugin_accounts: Option<Arc<dyn PluginAccountAccess>>,
) -> Result<AdminBundle, AdminError> {
let AdminRuntimePorts {
upstream_info,
timezone,
service_middleware,
plugin_preparation,
plugin_management,
published_snapshot,
plugin_distribution,
plugin_inspector,
pricing_source,
providers,
snapshot,
account_probe: probe,
proxy_probe,
client_distribution,
system,
client_key_verifier,
} = runtime;
config
.resolve_and_validate(Path::new("."))
.map_err(|error| AdminError::invalid(error.to_string()))?;
client_config
.resolve_and_validate(Path::new("."))
.map_err(|error| AdminError::invalid(error.to_string()))?;
let auth = Arc::new(DefaultAuthService::new(
&config,
client_config.session_ttl_minutes,
store.auth(),
client_key_verifier.clone(),
));
auth.ensure_default_admin(config.default_password.expose())
.await?;
let accounts = initialize_accounts(
store.accounts(),
store.account_runtime(),
store.settings(),
timezone,
providers.clone(),
snapshot.clone(),
probe.clone(),
upstream_info.clone(),
);
let backup_ports = store.backup();
let backups = Arc::new(DefaultBackupService::new(
backup_ports.repository(),
backup_ports.object_store(),
store.auth(),
snapshot.clone(),
timezone,
));
let backup_task = backup::task::BackupTask::new(
backup_ports.repository(),
backup_ports.dump(),
backup_ports.object_store(),
)
.with_timezone(timezone);
let system_preflight = Arc::new(
use_case::system_preflight::PluginSystemUpdatePreflight::new(
store.plugins(),
plugin_inspector.clone(),
),
);
let system = Arc::new(DefaultSystemService::new(system, system_preflight));
let observations: Arc<dyn ports::store::ObservabilityStore> = Arc::new(
use_case::observability::CachedObservabilityStore::new(store.observability()),
);
let key_usage = Arc::new(use_case::key_usage::DefaultKeyUsageService::new(
auth.clone(),
client_key_verifier,
store.client_keys(),
observations.clone(),
system.clone(),
timezone,
));
let credentials = Arc::new(CredentialsService::new(
providers.clone(),
store.accounts(),
store.proxies(),
snapshot.clone(),
upstream_info.clone(),
));
let plugin_accounts = plugin_accounts.unwrap_or_else(|| {
initialize_plugin_accounts(
providers.clone(),
store.accounts(),
snapshot.clone(),
upstream_info.clone(),
)
});
let import_tasks = use_case::import_tasks::DefaultImportTasksService::new(credentials.clone());
let import_task = use_case::import_tasks::ImportTaskWorker(import_tasks.clone());
let settings = initialize_settings(
store.settings(),
snapshot.clone(),
providers.clone(),
pricing_source,
);
let usage_data = initialize_usage_data(store.usage_data(), store.settings(), providers.clone());
let mut public_services = public_service::Registry::new(service_middleware);
public_services.register_settings(&settings)?;
public_services.register_accounts(&accounts)?;
public_services.register_usage_data(&usage_data)?;
let services = AdminServices {
peer_link: Arc::new(use_case::peer_link::DefaultPeerLinkService::new(
store.client_keys(),
store.accounts(),
store.account_runtime(),
providers.clone(),
)),
timezone,
public_services: Arc::new(public_services),
plugin_management: Arc::new(PluginManagementService::new(
plugin_management,
store.plugins(),
published_snapshot.clone(),
)),
plugins: Arc::new(PluginsService::new(
store.plugins(),
plugin_inspector,
PluginDistributionPorts::new(plugin_distribution, store.proxies()),
snapshot.clone(),
plugin_preparation,
published_snapshot,
store.plugin_state(),
)),
key_usage,
proxies: Arc::new(use_case::proxies::DefaultProxiesService::new(
store.proxies(),
proxy_probe,
snapshot.clone(),
providers.clone(),
upstream_info.clone(),
)),
auth,
accounts: accounts.clone(),
account_groups: Arc::new(DefaultAccountGroupService::new(
upstream_info.clone(),
store.account_groups(),
store.account_runtime(),
snapshot.clone(),
)),
client_keys: Arc::new(DefaultClientKeyService::new(
store.client_keys(),
snapshot.clone(),
providers.clone(),
)),
client_distribution: Arc::new(DefaultClientDistributionService::new(client_distribution)),
observability: Arc::new(DefaultObservabilityService::new(
observations.clone(),
store.accounts(),
store.settings(),
providers.clone(),
timezone,
system.clone(),
upstream_info,
)),
settings,
usage_data,
system,
credentials,
_plugin_accounts: plugin_accounts,
import_tasks,
backups,
};
let freeze_recovery =
freeze_recovery::FreezeRecoveryTask::new(freeze_recovery::FreezeRecoveryDeps {
accounts: Arc::clone(&accounts) as Arc<dyn AccountsService>,
store: store.accounts(),
runtime: store.account_runtime(),
settings: store.settings(),
});
let worker_contributions = vec![
backup_task.into_worker()?,
import_task.into_worker()?,
freeze_recovery.into_worker()?,
reset_credit_worker::ResetCreditAutomationTask {
accounts,
store: store.accounts(),
settings: store.settings(),
}
.into_worker()?,
];
Ok(AdminBundle {
services,
worker_contributions,
})
}
/// CLI 只组合账号用例,不初始化管理员、管理服务或后台任务;写入仍复用同一事务与审计
pub fn initialize_plugin_accounts(
providers: ports::provider::ProviderAdminRegistry,
accounts: Arc<dyn ports::store::AccountStore>,
snapshot: Arc<dyn gateway_core::runtime::SnapshotControl>,
upstream_info: Option<Arc<dyn ports::peer::PeerUpstreamService>>,
) -> Arc<dyn PluginAccountAccess> {
Arc::new(use_case::plugin_accounts::DefaultPluginAccountAccess::new(
providers,
accounts,
snapshot,
upstream_info,
))
}
/// 为 Runtime 创建非秘密 Client Key 目录与预算重置的窄端口
#[must_use]
pub fn initialize_plugin_client_keys(
providers: ports::provider::ProviderAdminRegistry,
store: Arc<dyn ports::store::ClientKeyStore>,
snapshot: Arc<dyn SnapshotControl>,
) -> Arc<dyn PluginClientKeyAccess> {
let service: Arc<dyn ClientKeyService> =
Arc::new(DefaultClientKeyService::new(store, snapshot, providers));
Arc::new(use_case::plugin_client_keys::DefaultPluginClientKeyAccess::new(service))
}
/// 为 Runtime 组合实例自有资源写入;权限和归属在同一存储事务复核
#[must_use]
pub fn initialize_plugin_resources(
store: Arc<dyn ports::plugin_resources::PluginResourceStore>,
snapshot: Arc<dyn SnapshotControl>,
) -> Arc<dyn ports::plugin_resources::PluginResourceAccess> {
Arc::new(use_case::plugin_resources::DefaultPluginResourceAccess { store, snapshot })
}
/// 服务端与 CLI 复用账号用例,不随命令启动后台调度
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "账号存储、发布、探测与互联分别通过独立端口注入"
)]
pub fn initialize_accounts(
accounts: Arc<dyn ports::store::AccountStore>,
account_runtime: Arc<dyn ports::store::AccountRuntimeStore>,
settings: Arc<dyn ports::store::SettingsStore>,
timezone: gateway_core::time::DeploymentTimeZone,
providers: ProviderAdminRegistry,
snapshot: Arc<dyn SnapshotControl>,
probe: Arc<dyn AccountProbe>,
upstream_info: Option<Arc<dyn ports::peer::PeerUpstreamService>>,
) -> Arc<dyn AccountsService> {
Arc::new(DefaultAccountsService::new(
accounts,
account_runtime,
settings,
timezone,
providers,
snapshot,
probe,
upstream_info,
))
}
/// 设置服务不依赖 Web 管理会话,CLI 与服务器通过同一用例执行事务和发布
pub fn initialize_settings(
store: Arc<dyn ports::store::SettingsStore>,
snapshot: Arc<dyn SnapshotControl>,
providers: ProviderAdminRegistry,
pricing_source: Arc<dyn ports::pricing::PricingSource>,
) -> Arc<dyn SettingsService> {
Arc::new(DefaultSettingsService::new(
store,
snapshot,
providers,
pricing_source,
))
}
/// HTTP、插件及 CLI 共用的用量领域用例,宿主提供已验证的调用主体
#[must_use]
pub fn initialize_usage_data(
store: Arc<dyn ports::usage_data::UsageDataStore>,
settings: Arc<dyn ports::store::SettingsStore>,
providers: ProviderAdminRegistry,
) -> Arc<dyn UsageDataService> {
Arc::new(use_case::usage_data::DefaultUsageDataService::new(
store, settings, providers,
))
}
@@ -0,0 +1,286 @@
//! 账号错误保护的到期探测与按冻结快照恢复
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use chrono::{DateTime, Utc};
use futures::StreamExt as _;
use gateway_core::account::{AccountErrorProtection, ProviderAccountId};
use gateway_core::engine::probe::AccountProbeTarget;
use gateway_core::routing::UpstreamModelId;
use gateway_core::task::{
ScheduledTask, WorkerContribution, WorkerCycleContext, WorkerId, WorkerKind,
WorkerLeaseRequest, WorkerRegistration, WorkerRunnable, WorkerSchedule, WorkerTaskError,
};
use tracing::warn;
use crate::model::AdminError;
use crate::model::accounts::{
AccountConnectionTestEvent, AccountFreeze, AccountPageItem, AccountRuntimeSnapshot,
};
use crate::ports::store::{AccountRuntimeStore, AccountStore, SettingsStore};
use crate::use_case::accounts::AccountsService;
/// worker 周期;冷却观测与探测都依赖该节奏
pub const FREEZE_RECOVERY_INTERVAL: Duration = Duration::from_secs(30);
pub const FREEZE_RECOVERY_WORKER_OWNER: &str = "account-freeze-recovery";
pub const WORKER_INITIAL_BACKOFF: Duration = Duration::from_secs(1);
pub const WORKER_MAXIMUM_BACKOFF: Duration = Duration::from_secs(60);
pub const WORKER_LEASE_TTL: Duration = Duration::from_secs(15 * 60);
pub const WORKER_LEASE_RENEWAL: Duration = Duration::from_secs(5 * 60);
/// 冻结恢复所需的管理端口组合
#[derive(Clone)]
pub struct FreezeRecoveryDeps {
pub accounts: Arc<dyn AccountsService>,
pub store: Arc<dyn AccountStore>,
pub runtime: Arc<dyn AccountRuntimeStore>,
pub settings: Arc<dyn SettingsStore>,
}
pub struct FreezeRecoveryTask {
deps: FreezeRecoveryDeps,
}
impl FreezeRecoveryTask {
pub fn new(deps: FreezeRecoveryDeps) -> Self {
Self { deps }
}
pub(crate) fn into_worker(self) -> Result<WorkerContribution, AdminError> {
let id = WorkerId::try_new(
WorkerKind::AccountFreezeRecovery,
FREEZE_RECOVERY_WORKER_OWNER,
)
.map_err(|_| AdminError::internal("冻结恢复 Worker ID 不合法"))?;
let schedule = WorkerSchedule::try_new(
FREEZE_RECOVERY_INTERVAL,
WORKER_INITIAL_BACKOFF,
WORKER_MAXIMUM_BACKOFF,
WORKER_LEASE_TTL,
WORKER_LEASE_RENEWAL,
)
.map_err(|_| AdminError::internal("冻结恢复 Worker 调度配置不合法"))?;
let lease = WorkerLeaseRequest::try_new(id.clone(), WORKER_LEASE_TTL)
.map_err(|_| AdminError::internal("冻结恢复 Worker 租约配置不合法"))?;
let registration = WorkerRegistration::try_new(
id,
WorkerRunnable::Scheduled {
schedule,
lease: Some(lease),
task: Box::new(self),
},
)
.map_err(|_| AdminError::internal("冻结恢复 Worker 注册信息不合法"))?;
Ok(WorkerContribution::Registration(registration))
}
/// 无法读取或校验配置时跳过本轮,不能把未知状态当作允许解冻
async fn freeze_policy(&self) -> Option<AccountErrorProtection> {
let settings = self.deps.settings.load_runtime_settings().await.ok()?;
let policy = settings.values.account_error_protection;
policy.validate().ok()?;
Some(policy)
}
async fn active_freezes(&self) -> BTreeMap<String, AccountFreeze> {
self.deps.runtime.active_freezes().await.unwrap_or_default()
}
async fn run_cycle_inner(&self) {
let Some(policy) = self.freeze_policy().await else {
return;
};
let freezes = self.active_freezes().await;
if freezes.is_empty() {
return;
}
self.recover_due_freezes(&freezes, &policy).await;
}
async fn load_account(&self, account_id: &str) -> Option<Option<AccountPageItem>> {
let account_id = ProviderAccountId::new(account_id.to_owned()).ok()?;
self.deps
.store
.load_account(account_id.as_str(), AccountRuntimeSnapshot::default())
.await
.ok()
}
/// 到达探测时间后才恢复;关闭自动冻结或探测时,已有冻结仍等待原冷却结束
async fn recover_due_freezes(
&self,
freezes: &BTreeMap<String, AccountFreeze>,
policy: &AccountErrorProtection,
) {
for (account_id, freeze) in freezes {
if freeze.until > Utc::now() {
continue;
}
if policy.enabled && policy.probe_enabled && freeze.requires_probe {
self.probe_and_recover(account_id, freeze, policy).await;
} else if let Err(error) = self
.deps
.runtime
.finish_freeze(account_id, freeze, None)
.await
{
warn!(account_id, error = %error, "解除到期冻结失败");
}
}
}
async fn probe_and_recover(
&self,
account_id: &str,
freeze: &AccountFreeze,
policy: &AccountErrorProtection,
) {
let Some(Some(item)) = self.load_account(account_id).await else {
return;
};
if item.account.credential_revision != freeze.credential_revision {
let _ = self
.deps
.runtime
.finish_freeze(account_id, freeze, None)
.await;
return;
}
if !item.account.enabled {
// 停用账号没有自动恢复意义;解冻交给管理员手动恢复
return;
}
let Ok(account) = ProviderAccountId::new(account_id.to_owned()) else {
return;
};
let Some(model) = self.resolve_probe_model(&account, freeze, policy).await else {
// 没有可用探测模型时按失败处理,顺延冻结等待下一轮
self.postpone(&account, freeze, policy).await;
return;
};
let probe_succeeded = match self
.deps
.accounts
.test_connection(AccountProbeTarget::FreezeRecovery(account.clone()), model)
.await
{
Ok(events) => drain_probe(events).await,
Err(error) => {
tracing::debug!(account_id, error = %error, "冻结恢复探测发起失败");
false
}
};
if probe_succeeded {
match self
.deps
.runtime
.finish_freeze(account_id, freeze, None)
.await
{
Ok(true) => tracing::info!(account_id, "冻结恢复探测成功:账号已解冻"),
Ok(false) => {}
Err(error) => warn!(account_id, error = %error, "解除冻结失败"),
}
} else {
self.postpone(&account, freeze, policy).await;
}
}
/// 探测失败只顺延本次仍存在的冻结,旧探测不能重新冻结已手动恢复的账号
async fn postpone(
&self,
account: &ProviderAccountId,
freeze: &AccountFreeze,
policy: &AccountErrorProtection,
) {
// 探测期间关闭保护或探测时,不再把已经到期的冻结向后延长
let Some(current) = self.freeze_policy().await else {
return;
};
if !current.enabled || !current.probe_enabled {
let _ = self
.deps
.runtime
.finish_freeze(account.as_str(), freeze, None)
.await;
return;
}
let Some(until) = SystemTime::now().checked_add(Duration::from_secs(
freeze.duration_seconds.unwrap_or(policy.duration_seconds),
)) else {
return;
};
let until = DateTime::<Utc>::from(until);
match self
.deps
.runtime
.finish_freeze(account.as_str(), freeze, Some(until))
.await
{
Ok(true) => {
tracing::info!(
account_id = account.as_str(),
postpone_seconds = Duration::from_secs(
freeze.duration_seconds.unwrap_or(policy.duration_seconds)
)
.as_secs(),
"冻结恢复探测失败:账号冻结顺延",
);
}
Ok(false) => {}
Err(error) => {
warn!(
account_id = account.as_str(),
error = %error,
"冻结顺延失败"
);
}
}
}
async fn resolve_probe_model(
&self,
account: &ProviderAccountId,
freeze: &AccountFreeze,
policy: &AccountErrorProtection,
) -> Option<UpstreamModelId> {
if let Some(model) = if freeze.duration_seconds.is_some() {
freeze.probe_model.as_deref()
} else {
policy.probe_model.as_deref()
} {
return UpstreamModelId::new(model.to_owned()).ok();
}
let models = self.deps.accounts.models(account, false).await.ok()?;
models.models.into_iter().next().map(|model| model.id)
}
}
impl ScheduledTask for FreezeRecoveryTask {
fn run_cycle(
&self,
context: WorkerCycleContext,
) -> futures::future::BoxFuture<'_, Result<(), WorkerTaskError>> {
Box::pin(async move {
if context.cancellation().is_cancelled() {
return Ok(());
}
self.run_cycle_inner().await;
Ok(())
})
}
}
/// 消耗连接测试事件流并返回是否以 `Completed` 终止
async fn drain_probe(mut events: crate::model::accounts::AccountConnectionTestEventStream) -> bool {
let mut completed = false;
while let Some(event) = events.next().await {
match event {
AccountConnectionTestEvent::Completed => completed = true,
AccountConnectionTestEvent::Failed { .. } => return false,
_ => {}
}
}
completed
}
+194 -124
View File
@@ -1,43 +1,58 @@
//! 管理控制面的语义模型、用例与外部能力端口。
//! 管理控制面的语义模型、用例与外部能力端口
//!
//! 本 crate 不包含 HTTP wire、数据库实现或具体 Provider 实现。
//! 本 crate 不包含 HTTP wire、数据库实现或具体 Provider 实现
use std::{fmt, path::Path, sync::Arc};
use gateway_core::{
engine::probe::AccountProbe,
routing::{ProviderKind, snapshot::SnapshotControl},
engine::execution::ClientKeyVerifier, engine::probe::AccountProbe, runtime::SnapshotControl,
task::WorkerContribution,
};
use secrecy::{ExposeSecret as _, SecretString};
use serde::Deserialize;
pub mod backup;
mod bootstrap;
pub mod freeze_recovery;
pub mod model;
pub mod ports;
pub mod public_service;
mod reset_credit_worker;
mod use_case;
pub use use_case::plugins::{PluginDistributionPorts, PluginManagementService, PluginsService};
pub use use_case::key_usage::KeyUsageService;
pub use use_case::peer_link::{PeerLinkError, PeerLinkService};
pub use use_case::{
accounts::AccountsService, auth::AuthService, client_keys::ClientKeyService,
observability::ObservabilityService, openai::OpenAiService, settings::SettingsService,
system::SystemService, xai::XaiService,
account_groups::AccountGroupService,
accounts::AccountsService,
auth::AuthService,
backup::BackupService,
client_distribution::ClientDistributionService,
client_keys::ClientKeyService,
credentials::{CredentialsService, ProviderCredentials},
import_tasks::ImportTasksService,
observability::{ObservabilityService, ProviderUsageBillingProjector},
proxies::ProxiesService,
settings::SettingsService,
system::SystemService,
usage_data::UsageDataService,
};
pub use bootstrap::{
initialize, initialize_accounts, initialize_plugin_accounts, initialize_plugin_client_keys,
initialize_plugin_resources, initialize_settings, initialize_usage_data,
initialize_with_plugin_accounts,
};
use model::{AdminError, AdminErrorKind};
use ports::{
provider::{ProviderAdmin, ProviderAdminError, ProviderAdminErrorKind, ProviderAdminRegistry},
store::AdminStorePorts,
system::SystemOperations,
};
use use_case::{
accounts::DefaultAccountsService, auth::DefaultAuthService,
client_keys::DefaultClientKeyService, observability::DefaultObservabilityService,
openai::DefaultOpenAiService, settings::DefaultSettingsService, system::DefaultSystemService,
xai::DefaultXaiService,
client_distribution::ClientDistributionResolver, plugin_accounts::PluginAccountAccess,
provider::ProviderAdminRegistry, system::SystemOperations,
};
const OPENAI_PROVIDER_KIND: &str = "openai";
const XAI_PROVIDER_KIND: &str = "xai";
const MINIMUM_INITIAL_PASSWORD_BYTES: usize = 12;
const WEAK_INITIAL_PASSWORDS: &[&str] = &[
const WEAK_ADMIN_PASSWORDS: &[&str] = &[
"",
"admin",
"123456",
@@ -48,7 +63,9 @@ const WEAK_INITIAL_PASSWORDS: &[&str] = &[
"codex-proxy-rs",
];
/// 只用于首次幂等创建默认管理员的启动密码。
const DEFAULT_CLIENT_SESSION_TTL_MINUTES: u64 = 24 * 60;
/// 只用于首次幂等创建默认管理员的启动密码
#[derive(Clone, Deserialize)]
#[serde(transparent)]
pub struct InitialAdminPassword(SecretString);
@@ -78,21 +95,54 @@ impl fmt::Debug for InitialAdminPassword {
}
}
/// 管理控制面的启动配置。
/// 管理控制面的启动配置
#[derive(Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct AdminConfig {
pub session_ttl_minutes: u64,
#[serde(default = "default_absolute_session_ttl_minutes")]
pub session_absolute_ttl_minutes: u64,
pub default_username: String,
pub default_password: InitialAdminPassword,
}
impl AdminConfig {
/// 校验 Admin-owned 字段;当前配置不含相对路径。
fn default_absolute_session_ttl_minutes() -> u64 {
30 * 24 * 60
}
/// Client 登录域的通用启动配置
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct ClientConfig {
pub session_ttl_minutes: u64,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
session_ttl_minutes: DEFAULT_CLIENT_SESSION_TTL_MINUTES,
}
}
}
impl ClientConfig {
/// 校验 Client session TTL;当前配置不含相对路径
///
/// # Errors
///
/// 用户名、会话有效期或初始密码不满足安全约束时返回错误。
/// 会话有效期为零或无法安全换算时返回错误
pub fn resolve_and_validate(&mut self, _source_dir: &Path) -> Result<(), AdminConfigError> {
if self.session_ttl_minutes == 0 || i64::try_from(self.session_ttl_minutes).is_err() {
return Err(AdminConfigError::InvalidField("client.session_ttl_minutes"));
}
Ok(())
}
}
impl AdminConfig {
/// 校验 Admin-owned 字段;当前配置不含相对路径
///
/// # Errors
///
/// 用户名、会话有效期或初始密码不满足安全约束时返回错误
pub fn resolve_and_validate(&mut self, _source_dir: &Path) -> Result<(), AdminConfigError> {
if self.default_username.trim().is_empty()
|| self.default_username.chars().any(char::is_control)
@@ -102,10 +152,17 @@ impl AdminConfig {
if self.session_ttl_minutes == 0 || i64::try_from(self.session_ttl_minutes).is_err() {
return Err(AdminConfigError::InvalidField("admin.session_ttl_minutes"));
}
if self.session_absolute_ttl_minutes == 0
|| i64::try_from(self.session_absolute_ttl_minutes).is_err()
{
return Err(AdminConfigError::InvalidField(
"admin.session_absolute_ttl_minutes",
));
}
let password = self.default_password.expose().trim();
if password.len() < MINIMUM_INITIAL_PASSWORD_BYTES
|| password.contains('$')
|| WEAK_INITIAL_PASSWORDS.contains(&password.to_ascii_lowercase().as_str())
|| WEAK_ADMIN_PASSWORDS.contains(&password.to_ascii_lowercase().as_str())
{
return Err(AdminConfigError::WeakInitialPassword);
}
@@ -118,37 +175,93 @@ impl fmt::Debug for AdminConfig {
formatter
.debug_struct("AdminConfig")
.field("session_ttl_minutes", &self.session_ttl_minutes)
.field(
"session_absolute_ttl_minutes",
&self.session_absolute_ttl_minutes,
)
.field("default_username", &self.default_username)
.field("default_password", &"[REDACTED]")
.finish()
}
}
/// Admin-owned 启动配置错误;不回显任何配置值。
/// Admin-owned 启动配置错误;不回显任何配置值
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum AdminConfigError {
#[error("configuration field `{0}` is invalid")]
#[error("配置字段 `{0}` 不合法")]
InvalidField(&'static str),
#[error("admin.default_password does not meet the initial password policy")]
#[error("admin.default_password 不符合初始密码策略")]
WeakInitialPassword,
}
/// API 持有的管理资源能力集合。
/// API 持有的管理资源能力集合
///
/// 字段全部私有;调用方经 accessor 直接调用能力,不需要命名内部 `use_case` 模块。
/// 字段全部私有;调用方经 accessor 直接调用能力,不需要命名内部 `use_case` 模块
#[derive(Clone)]
pub struct AdminServices {
timezone: gateway_core::time::DeploymentTimeZone,
public_services: Arc<public_service::Registry>,
plugins: Arc<PluginsService>,
plugin_management: Arc<PluginManagementService>,
proxies: Arc<dyn ProxiesService>,
auth: Arc<dyn AuthService>,
key_usage: Arc<dyn KeyUsageService>,
peer_link: Arc<dyn PeerLinkService>,
accounts: Arc<dyn AccountsService>,
account_groups: Arc<dyn AccountGroupService>,
client_keys: Arc<dyn ClientKeyService>,
client_distribution: Arc<dyn ClientDistributionService>,
observability: Arc<dyn ObservabilityService>,
settings: Arc<dyn SettingsService>,
usage_data: Arc<dyn UsageDataService>,
system: Arc<dyn SystemService>,
openai: Arc<dyn OpenAiService>,
xai: Arc<dyn XaiService>,
credentials: Arc<CredentialsService>,
// Runtime 只持有 Weak,这个强引用保障账号端口与 Admin 服务同寿命
_plugin_accounts: Arc<dyn PluginAccountAccess>,
backups: Arc<dyn BackupService>,
import_tasks: Arc<dyn ImportTasksService>,
}
impl AdminServices {
#[must_use]
pub const fn timezone(&self) -> gateway_core::time::DeploymentTimeZone {
self.timezone
}
pub fn public_services(&self) -> Arc<public_service::Registry> {
self.public_services.clone()
}
#[must_use]
pub fn plugin_management(&self) -> &PluginManagementService {
&self.plugin_management
}
#[must_use]
pub fn plugins(&self) -> &PluginsService {
&self.plugins
}
#[must_use]
pub fn import_tasks(&self) -> &dyn ImportTasksService {
self.import_tasks.as_ref()
}
#[must_use]
pub fn key_usage(&self) -> &dyn KeyUsageService {
self.key_usage.as_ref()
}
#[must_use]
pub fn peer_link(&self) -> &dyn PeerLinkService {
self.peer_link.as_ref()
}
#[must_use]
pub fn proxies(&self) -> &dyn ProxiesService {
self.proxies.as_ref()
}
#[must_use]
pub fn auth(&self) -> &dyn AuthService {
self.auth.as_ref()
@@ -159,17 +272,31 @@ impl AdminServices {
self.accounts.as_ref()
}
#[must_use]
pub fn account_groups(&self) -> &dyn AccountGroupService {
self.account_groups.as_ref()
}
#[must_use]
pub fn client_keys(&self) -> &dyn ClientKeyService {
self.client_keys.as_ref()
}
#[must_use]
pub fn client_distribution(&self) -> &dyn ClientDistributionService {
self.client_distribution.as_ref()
}
#[must_use]
pub fn usage_data(&self) -> Arc<dyn UsageDataService> {
self.usage_data.clone()
}
#[must_use]
pub fn observability(&self) -> &dyn ObservabilityService {
self.observability.as_ref()
}
#[must_use]
pub fn settings(&self) -> &dyn SettingsService {
self.settings.as_ref()
}
@@ -180,19 +307,20 @@ impl AdminServices {
}
#[must_use]
pub fn openai(&self) -> &dyn OpenAiService {
self.openai.as_ref()
pub fn credentials(&self) -> &CredentialsService {
self.credentials.as_ref()
}
#[must_use]
pub fn xai(&self) -> &dyn XaiService {
self.xai.as_ref()
pub fn backups(&self) -> &dyn BackupService {
self.backups.as_ref()
}
}
/// Admin 初始化完成后的封闭能力包。
/// Admin 初始化完成后的封闭能力包
pub struct AdminBundle {
services: AdminServices,
worker_contributions: Vec<WorkerContribution>,
}
impl AdminBundle {
@@ -200,91 +328,33 @@ impl AdminBundle {
pub fn services(&self) -> AdminServices {
self.services.clone()
}
/// 取出 Admin Worker 贡献;只能调用一次,与其它 Bundle 的贡献一并交给 Host
pub fn take_worker_contributions(&mut self) -> Vec<WorkerContribution> {
std::mem::take(&mut self.worker_contributions)
}
}
/// 校验配置、建立动态 Provider 注册表并完成默认管理员幂等初始化。
///
/// # Errors
///
/// 配置非法、Provider 注册冲突/缺失或默认管理员初始化失败时返回错误。
pub async fn initialize(
mut config: AdminConfig,
store: AdminStorePorts,
providers: Vec<Arc<dyn ProviderAdmin>>,
snapshot: Arc<dyn SnapshotControl>,
probe: Arc<dyn AccountProbe>,
system: Arc<dyn SystemOperations>,
) -> Result<AdminBundle, AdminError> {
config
.resolve_and_validate(Path::new("."))
.map_err(|error| AdminError::invalid(error.to_string()))?;
let registry = ProviderAdminRegistry::new(providers).map_err(map_provider_registry_error)?;
let openai = registry
.require(&provider_kind(OPENAI_PROVIDER_KIND)?)
.map_err(map_provider_registry_error)?;
let xai = registry
.require(&provider_kind(XAI_PROVIDER_KIND)?)
.map_err(map_provider_registry_error)?;
pub use use_case::peer_link::upstream::{
PeerUpstreamBundle, initialize as initialize_peer_upstream,
};
let auth = Arc::new(DefaultAuthService::new(
config.default_username,
config.session_ttl_minutes,
store.auth(),
));
auth.ensure_default_admin(config.default_password.expose())
.await?;
let accounts = Arc::new(DefaultAccountsService::new(
store.accounts(),
store.settings(),
registry.clone(),
snapshot.clone(),
probe.clone(),
));
let services = AdminServices {
auth,
accounts,
client_keys: Arc::new(DefaultClientKeyService::new(
store.client_keys(),
snapshot.clone(),
)),
observability: Arc::new(DefaultObservabilityService::new(
store.observability(),
store.settings(),
registry,
)),
settings: Arc::new(DefaultSettingsService::new(
store.settings(),
snapshot.clone(),
)),
system: Arc::new(DefaultSystemService::new(system)),
openai: Arc::new(DefaultOpenAiService::new(
openai,
store.accounts(),
snapshot.clone(),
)),
xai: Arc::new(DefaultXaiService::new(
xai,
store.accounts(),
snapshot.clone(),
)),
};
Ok(AdminBundle { services })
}
fn provider_kind(value: &'static str) -> Result<ProviderKind, AdminError> {
ProviderKind::new(value).map_err(|_| AdminError::internal("Built-in Provider kind is invalid"))
}
fn map_provider_registry_error(error: ProviderAdminError) -> AdminError {
let kind = match error.kind() {
ProviderAdminErrorKind::Invalid | ProviderAdminErrorKind::Unsupported => {
AdminErrorKind::Invalid
}
ProviderAdminErrorKind::NotFound => AdminErrorKind::NotFound,
ProviderAdminErrorKind::Conflict => AdminErrorKind::Conflict,
ProviderAdminErrorKind::Unavailable => AdminErrorKind::Unavailable,
ProviderAdminErrorKind::Internal => AdminErrorKind::Internal,
};
AdminError::new(kind, "Provider registry initialization failed")
/// 组合根提供给控制面的运行能力;与配置和存储端口分别传入
pub struct AdminRuntimePorts {
pub upstream_info: Option<Arc<dyn ports::peer::PeerUpstreamService>>,
pub timezone: gateway_core::time::DeploymentTimeZone,
pub service_middleware: public_service::PlanSource,
pub plugin_preparation: Arc<dyn ports::plugins::PluginPreparation>,
pub plugin_management: Arc<dyn ports::plugin_management::PluginManagement>,
pub published_snapshot: gateway_core::runtime::RuntimeSnapshotHandle,
pub plugin_distribution: Arc<dyn ports::plugins::PluginDistribution>,
pub plugin_inspector: Arc<dyn ports::plugins::PluginPackageInspector>,
pub pricing_source: Arc<dyn ports::pricing::PricingSource>,
pub providers: ProviderAdminRegistry,
pub snapshot: Arc<dyn SnapshotControl>,
pub account_probe: Arc<dyn AccountProbe>,
pub proxy_probe: Arc<dyn ports::proxy::ProxyProbe>,
pub client_distribution: Arc<dyn ClientDistributionResolver>,
pub system: Arc<dyn SystemOperations>,
pub client_key_verifier: Arc<dyn ClientKeyVerifier>,
}
@@ -0,0 +1,192 @@
//! 跨 Provider 账号分组的管理命令与查询投影
use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use gateway_core::{
account::{AccountStatusFacts, FastMode},
routing::AccountGroupId,
};
use super::{PageSize, Revision, observability::DecimalAmount};
/// 名称与描述的共同约束,HTTP、用例和存储边界只映射错误
pub fn validate_group_fields(
name: &str,
description: Option<&str>,
) -> Result<(), super::AdminModelError> {
if name.trim() != name
|| name.is_empty()
|| name.chars().count() > 100
|| name.chars().any(char::is_control)
|| description
.is_some_and(|value| value.len() > 4096 || value.chars().any(char::is_control))
{
return Err(super::AdminModelError::InvalidAccountGroupFields);
}
Ok(())
}
/// 账号分组持久化使用的标准 `#RRGGBBAA` 颜色
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGroupColor(String);
impl AccountGroupColor {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
(value.len() == 9
&& value.starts_with('#')
&& value[1..].bytes().all(|byte| byte.is_ascii_hexdigit()))
.then(|| Self(value.to_ascii_uppercase()))
}
}
/// 当前观测时刻的分组成员可用性
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccountGroupAccountSummary {
pub available: u64,
pub limited: u64,
pub total: u64,
}
/// 根据可用账号与运行时租约计算的分组调度槽位
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccountGroupCapacity {
pub used_slots: Option<u64>,
/// `None` 表示可用账号中存在不限制并发的账号
pub total_slots: Option<u64>,
}
/// 分组账号成功且已向下游交付的请求美元费用
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGroupUsage {
pub today_usd: DecimalAmount,
/// 当前 usage retention 窗口内、当前组成员实际完成请求的累计成本
pub retained_total_usd: DecimalAmount,
}
/// 当前页账号组 membership 对应的持久账号事实;运行态在 Admin query service 合并
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGroupMemberFact {
pub inherited_version: Option<gateway_core::account::QuotaObservationVersion>,
pub group_id: AccountGroupId,
pub account_id: String,
pub status: AccountStatusFacts,
pub total_slots: Option<u64>,
}
/// 账号与 Client Key 视图内嵌的轻量分组引用
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGroupRef {
pub id: AccountGroupId,
pub name: String,
pub color: AccountGroupColor,
pub enabled: bool,
}
/// 账号分组列表查询
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGroupListQuery {
pub page: u32,
pub page_size: PageSize,
pub search: Option<String>,
pub enabled: Option<bool>,
}
/// 完整账号分组摘要
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGroupRecord {
pub allow_image_generation: bool,
pub fast_mode: FastMode,
pub id: AccountGroupId,
pub name: String,
pub description: Option<String>,
pub color: AccountGroupColor,
pub enabled: bool,
pub member_count: u64,
pub provider_counts: BTreeMap<String, u64>,
pub client_key_count: u64,
pub account_summary: AccountGroupAccountSummary,
pub capacity: AccountGroupCapacity,
pub usage: AccountGroupUsage,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// 账号分组分页结果
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGroupPage {
pub config_revision: Revision,
pub items: Vec<AccountGroupRecord>,
pub total: u64,
pub page: u32,
pub page_size: u16,
}
/// 分组选择器使用的轻量分页结果
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGroupOptionsPage {
pub config_revision: Revision,
pub items: Vec<AccountGroupRef>,
pub total: u64,
pub page: u32,
pub page_size: u16,
}
/// 创建账号分组
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateAccountGroup {
pub allow_image_generation: bool,
pub fast_mode: FastMode,
pub name: String,
pub description: Option<String>,
pub color: AccountGroupColor,
}
/// 已生成稳定标识、可直接提交存储的创建命令
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewAccountGroup {
pub allow_image_generation: bool,
pub fast_mode: FastMode,
pub id: AccountGroupId,
pub name: String,
pub description: Option<String>,
pub color: AccountGroupColor,
}
/// 更新账号分组的描述字段
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateAccountGroup {
pub allow_image_generation: Option<bool>,
pub fast_mode: Option<FastMode>,
pub id: AccountGroupId,
pub name: String,
pub description: Option<String>,
pub color: AccountGroupColor,
}
/// 启用或停用账号分组
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetAccountGroupEnabled {
pub id: AccountGroupId,
pub enabled: bool,
}
/// 删除未被 Client Key 引用的账号分组
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteAccountGroup {
pub id: AccountGroupId,
}
/// 账号分组变更结果
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountGroupMutation {
pub config_revision: Revision,
pub id: AccountGroupId,
pub record: Option<AccountGroupRecord>,
}
@@ -1,28 +1,49 @@
//! 多 Provider 账号目录与连接测试的公共事实。
//! 多 Provider 账号目录与连接测试的公共事实
use std::pin::Pin;
use std::{collections::BTreeMap, pin::Pin};
use chrono::{DateTime, Utc};
use futures::Stream;
use gateway_core::routing::ProviderKind;
use gateway_core::{
engine::probe::AccountProbeErrorSource, error::GatewayErrorKind, routing::ProviderKind,
upstream::UpstreamSendState,
};
use super::{PageSize, Revision, observability::TimeRange};
use super::{PageSize, Revision, account_groups::AccountGroupRef, observability::TimeRange};
/// Provider 账号当前可用性;权威定义与字符串编解码在 gateway-core,此处仅复用。
pub use gateway_core::engine::credential::AccountAvailability;
pub use gateway_core::account::{
AccountConcurrencyLimit, AccountErrorReason, AccountStatus, AccountStatusFacts,
AccountStatusProjection, AccountWeight, CredentialState, QuotaAccessState, QuotaEvidence,
QuotaState, resolve_account_status,
};
/// 管理页使用的归一化账号状态。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountStatus {
Active,
Expired,
QuotaExhausted,
Disabled,
Banned,
/// 账号可用的管理操作
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ProviderAccountCapabilities {
pub quota_credits: bool,
pub quota: bool,
pub quota_refresh: bool,
pub profile: bool,
pub subscription: bool,
pub avatar: bool,
pub reset_credits: bool,
pub consume_reset_credit: bool,
}
/// 账号列表排序字段。
/// 导入时应用的账号设置;名称只覆盖单账号导入,其余设置统一应用
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountImportSettings {
pub name: Option<String>,
pub notes: Option<String>,
pub enabled: bool,
pub concurrency_limit: Option<AccountConcurrencyLimit>,
pub weight: AccountWeight,
pub model_access: Option<gateway_core::account::AccountModelAccess>,
pub group_ids: Vec<gateway_core::routing::AccountGroupId>,
}
/// 账号列表排序字段
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountSortField {
Email,
@@ -33,39 +54,71 @@ pub enum AccountSortField {
ExpiresAt,
}
/// 账号列表排序方向。
/// 账号列表排序方向
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortDirection {
Asc,
Desc,
}
/// 一组完整的账号排序规则。
/// 一组完整的账号排序规则
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccountSort {
pub field: AccountSortField,
pub direction: SortDirection,
}
/// 账号列表的存储查询条件。
/// 账号列表的存储查询条件
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountListQuery {
pub page: u32,
pub page_size: PageSize,
pub provider_kind: Option<ProviderKind>,
pub group_filter: Option<AccountGroupFilter>,
pub search: Option<String>,
pub status: Option<AccountStatus>,
pub sort: Option<AccountSort>,
}
/// 账号公共存储投影;Provider 专属字段不进入此结构。
/// Admin query service 从运行态存储取得的当前账号冷却快照
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AccountRuntimeSnapshot {
pub protection_rules: BTreeMap<String, Vec<gateway_core::account::AccountProtectionMatch>>,
pub cooldown: BTreeMap<String, gateway_core::account::AccountCooldown>,
/// `None` 表示实时 lease 存储不可用;`Some` 中未出现的账号当前使用量为零
pub in_flight: Option<BTreeMap<String, u64>>,
}
/// 恢复任务读取的冻结快照;generation 将异步结果绑定到本次冻结
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountFreeze {
pub credential_revision: Revision,
pub until: DateTime<Utc>,
pub generation: String,
pub requires_probe: bool,
pub duration_seconds: Option<u64>,
pub probe_model: Option<String>,
pub protection_rules: Vec<gateway_core::account::AccountProtectionMatch>,
}
/// 可选的账号分组成员筛选条件
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AccountGroupFilter {
Group(gateway_core::routing::AccountGroupId),
Ungrouped,
}
/// 账号公共存储投影;Provider 专属字段不进入此结构
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountRecord {
pub auto_use_quota_credits: bool,
pub id: String,
pub provider_kind: ProviderKind,
pub groups: Vec<AccountGroupRef>,
pub name: String,
pub notes: Option<String>,
pub email: Option<String>,
pub upstream_user_id: String,
pub upstream_user_id: Option<String>,
pub upstream_account_id: Option<String>,
pub plan_type: Option<String>,
pub authentication_kind: String,
@@ -74,51 +127,37 @@ pub struct AccountRecord {
pub access_token_expires_at: Option<DateTime<Utc>>,
pub next_refresh_at: Option<DateTime<Utc>>,
pub enabled: bool,
pub availability: AccountAvailability,
pub availability_reason: Option<String>,
pub cooldown_until: Option<DateTime<Utc>>,
pub availability_observed_at: DateTime<Utc>,
pub quota_observed_at: Option<DateTime<Utc>>,
pub concurrency_limit: Option<AccountConcurrencyLimit>,
pub weight: AccountWeight,
pub model_access: gateway_core::account::AccountModelAccess,
pub outbound_proxy_id: Option<String>,
pub outbound_proxy: Option<gateway_core::account::OutboundProxy>,
pub outbound_proxy_enabled: bool,
pub credential_state: CredentialState,
pub credential_observed_at: DateTime<Utc>,
pub quota: QuotaState,
pub last_error_reason: Option<AccountErrorReason>,
pub last_error_message: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// 单一货币的账号成本聚合。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountCost {
pub currency: String,
pub amount: super::observability::DecimalAmount,
impl AccountRecord {
/// 配置保留绑定,执行请求时禁用代理按直连处理
#[must_use]
pub fn effective_outbound_proxy(&self) -> Option<&gateway_core::account::OutboundProxy> {
self.outbound_proxy
.as_ref()
.filter(|_| self.outbound_proxy_enabled)
}
}
/// 账号在一个模型上的历史用量。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountModelUsage {
pub model: String,
pub request_count: u64,
pub success_count: u64,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
pub cached_tokens: Option<u64>,
pub cache_write_tokens: Option<u64>,
pub reasoning_tokens: Option<u64>,
pub image_input_tokens: Option<u64>,
pub image_output_tokens: Option<u64>,
pub image_request_count: u64,
pub image_request_failed_count: u64,
pub total_tokens: Option<u64>,
pub cost_coverage: super::observability::CostCoverage,
pub costs: Vec<AccountCost>,
pub last_used_at: DateTime<Utc>,
}
pub use super::observability::{
AccountModelUsage, AccountPoolMetrics as AccountSummary, AccountRequestBucket,
CurrencyCost as AccountCost,
};
/// 账号在一个小时窗口内的请求数。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountRequestBucket {
pub bucket_start: DateTime<Utc>,
pub request_count: u64,
}
/// 账号历史用量聚合。
/// 账号历史用量聚合
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountUsage {
pub account_id: String,
@@ -126,9 +165,9 @@ pub struct AccountUsage {
pub success_count: u64,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
pub cached_tokens: Option<u64>,
pub cache_write_tokens: Option<u64>,
pub reasoning_tokens: Option<u64>,
pub cached_input_tokens: Option<u64>,
pub cache_write_input_tokens: Option<u64>,
pub reasoning_output_tokens: Option<u64>,
pub image_input_tokens: Option<u64>,
pub image_output_tokens: Option<u64>,
pub image_request_count: u64,
@@ -141,9 +180,9 @@ pub struct AccountUsage {
pub models: Vec<AccountModelUsage>,
}
/// 某个账号在调用方指定时间窗口内的本地用量查询。
/// 某个账号在调用方指定时间窗口内的本地用量查询
///
/// `key` 只用于把聚合结果关联回调用方的窗口,不承载 Provider 私有语义。
/// `key` 只用于把聚合结果关联回调用方的窗口,不承载 Provider 私有语义
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountUsageWindowQuery {
pub account_id: String,
@@ -151,7 +190,7 @@ pub struct AccountUsageWindowQuery {
pub range: TimeRange,
}
/// 一个账号时间窗口用量查询的结果。
/// 一个账号时间窗口用量查询的结果
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountUsageWindowResult {
pub account_id: String,
@@ -159,38 +198,83 @@ pub struct AccountUsageWindowResult {
pub usage: AccountUsage,
}
/// 账号列表页所需的完整存储事实。
/// 账号列表页所需的完整存储事实
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountPage {
pub config_revision: Revision,
pub items: Vec<AccountRecord>,
pub items: Vec<AccountPageItem>,
pub total: u64,
pub summary: AccountSummary,
}
/// 统一账号目录的全局状态计数,不受当前筛选和分页影响。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccountSummary {
pub total: u64,
pub active: u64,
pub quota_exhausted: u64,
pub unavailable: u64,
}
/// 账号启停写入命令。
/// 同一状态快照下的账号事实与唯一状态投影
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetAccountEnabled {
pub account_id: String,
pub enabled: bool,
pub struct AccountPageItem {
pub protection_rules: Vec<gateway_core::account::AccountProtectionMatch>,
pub account: AccountRecord,
pub projection: AccountStatusProjection,
pub capacity: AccountCapacity,
}
/// 账号批量删除命令。
/// 网关配置的账号并发上限与查询时的占用,不包含排队请求或上游隐藏限制
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccountCapacity {
/// 实时租约读取失败时为 `None`,不能当作空闲
pub used_slots: Option<u64>,
/// 应用账号覆盖或继承默认值后的普通并发上限;`None` 表示不限
pub total_slots: Option<u64>,
}
/// 账号可编辑事实的一次性替换命令
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateAccount {
pub account_id: String,
/// 缺省保留名称,空字符串清除名称并使用邮箱展示
pub name: Option<String>,
/// 缺省保留备注;空字符串清空备注
pub notes: Option<String>,
pub enabled: bool,
pub concurrency_limit: Option<AccountConcurrencyLimit>,
pub weight: AccountWeight,
pub model_access: Option<gateway_core::account::AccountModelAccess>,
pub group_ids: Vec<gateway_core::routing::AccountGroupId>,
pub outbound_proxy: Option<super::proxies::AccountProxySelection>,
}
/// 账号更新结果
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountUpdateResult {
pub config_revision: Revision,
pub account_id: gateway_core::account::ProviderAccountId,
}
/// 仅修改显式字段的批量账号设置命令
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BatchUpdateAccounts {
pub auto_use_quota_credits: Option<bool>,
pub account_ids: Vec<String>,
pub enabled: Option<bool>,
pub concurrency_limit: Option<Option<AccountConcurrencyLimit>>,
pub weight: Option<AccountWeight>,
pub model_access: Option<gateway_core::account::AccountModelAccess>,
pub group_ids: Option<Vec<gateway_core::routing::AccountGroupId>>,
pub outbound_proxy: Option<super::proxies::AccountProxySelection>,
}
/// 批量账号更新结果
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountsUpdateResult {
pub config_revision: Revision,
pub account_ids: Vec<gateway_core::account::ProviderAccountId>,
}
/// 账号批量删除命令
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteAccounts {
pub account_ids: Vec<String>,
}
/// 账号连接测试的语义事件。
/// 账号连接测试的语义事件
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AccountConnectionTestEvent {
Started {
@@ -205,20 +289,41 @@ pub enum AccountConnectionTestEvent {
Content {
text: String,
},
Completed {
account_status: AccountStatus,
},
Completed,
Failed {
source: AccountProbeErrorSource,
gateway_error_code: GatewayErrorKind,
send_state: Option<UpstreamSendState>,
message: String,
provider_error_code: Option<String>,
provider_error_type: Option<String>,
upstream_status: Option<u16>,
upstream_content_type: Option<String>,
upstream_body: Option<String>,
account_status: AccountStatus,
},
}
/// 每次连接测试独占的有限事件流。
/// 每次连接测试独占的有限事件流
pub type AccountConnectionTestEventStream =
Pin<Box<dyn Stream<Item = AccountConnectionTestEvent> + Send + 'static>>;
/// 单个账号普通并发池的实时租约明细,不关联请求历史
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountConcurrencyEntry {
pub lease_id: String,
pub request_id: Option<String>,
pub client_key_id: Option<String>,
pub client_key_name: Option<String>,
pub model: Option<String>,
pub client_ip: Option<String>,
pub started_at: Option<DateTime<Utc>>,
pub acquired_at: Option<DateTime<Utc>>,
}
/// 同一次 Redis 读取中的当前占用数和分页明细
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountConcurrencyPage {
pub entries: Vec<AccountConcurrencyEntry>,
pub total: u64,
pub observed_at: DateTime<Utc>,
}
@@ -0,0 +1,68 @@
//! 统计投影的新鲜度、覆盖范围与预计算费用合同
use chrono::{DateTime, Utc};
use super::observability::{
DecimalAmount, RequestMetricPoint, UsageConcurrencyPoint, UsageOverview,
};
/// 数据可见性状态,不以缺失投影冒充零统计
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AnalyticsState {
Ready,
#[default]
Building,
Lagging,
Stale,
}
impl AnalyticsState {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ready => "ready",
Self::Building => "building",
Self::Lagging => "lagging",
Self::Stale => "stale",
}
}
}
/// 与统计结果在同一读取快照内取得的覆盖和版本事实
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AnalyticsMetadata {
pub coverage_start: Option<DateTime<Utc>>,
pub coverage_end: Option<DateTime<Utc>>,
pub generated_at: DateTime<Utc>,
pub lag_seconds: u64,
pub state: AnalyticsState,
pub percentiles_approximate: bool,
pub schema_version: u32,
pub histogram_version: u32,
/// 派生数据版本供缓存判断失效,不作为源事务提交顺序
pub generation: u64,
}
/// 单次请求经 Provider 费用合同确认后可累加的 USD 情景金额
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UsageBillingProjection {
pub standard_amount: Option<DecimalAmount>,
pub no_cache_amount: Option<DecimalAmount>,
}
/// 时间桶内的情景费用,未知金额不改写为零
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsageProjectedCostPoint {
pub bucket_start: DateTime<Utc>,
pub standard_amount: Option<DecimalAmount>,
pub no_cache_amount: Option<DecimalAmount>,
}
/// 同一数据库快照内的完整洞察输入
#[derive(Debug, Clone, PartialEq)]
pub struct UsageInsightsObservation {
pub overview: UsageOverview,
pub trend: Vec<RequestMetricPoint>,
pub costs: Vec<UsageProjectedCostPoint>,
pub concurrency: Vec<UsageConcurrencyPoint>,
}
@@ -0,0 +1,159 @@
//! 管理写入的审计分类;适配器只能补充事务实际写入的字段与实体标识
use super::{
MutationActor, MutationContext,
auth::{AdminAuditEvent, AuditActorKind, admin_session_actor_ref},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MutationAuditOperation {
ModelPricingSync,
ModelPricingUpdate,
RuntimeSettingsReplace,
AdminApiKeyChanged { exists: bool },
AccountGroupCreate,
AccountGroupUpdate,
AccountGroupEnabled { enabled: bool },
AccountGroupDelete,
ClientApiKeyResetBudget,
ProviderAccountUpdate,
BackupStorageUpdate,
BackupScheduleUpdate,
BackupCreate,
BackupDelete,
BackupConnectionTest,
BackupDownloadUrl,
ProviderAccountRecover,
ProviderAccountBatchUpdate,
ProviderAccountDelete,
ProviderAccountExportCredentials,
PluginArtifactInstall,
PluginArtifactAccept,
PluginArtifactDelete,
PluginInstanceConfigure,
PluginInstanceDelete,
PluginSourceCredentialCreate,
PluginSourceCredentialDelete,
ClientApiKeyUpdateBudgetLimits,
ClientApiKeyCreate,
ClientApiKeyUpdate,
ClientApiKeyEnabled { enabled: bool },
ClientApiKeyDelete,
PluginSourceChangeSource,
PluginSourceDelete,
OutboundProxyCreate,
OutboundProxyUpdate,
OutboundProxyDelete,
OutboundProxyTest,
AccountGroupPluginReconcile,
ClientApiKeyPluginReconcile,
ProviderAccountImportDocument,
ProviderAccountRotateCredential,
ProviderAccountRefreshCredential,
ProviderAccountAuthorize,
ProviderAccountReauthorize,
}
impl MutationAuditOperation {
#[must_use]
pub const fn classification(self) -> (&'static str, &'static str) {
match self {
Self::ModelPricingSync => ("pricing.sync", "model_pricing"),
Self::ModelPricingUpdate => ("pricing.update", "model_pricing"),
Self::RuntimeSettingsReplace => ("settings.replace", "runtime_settings"),
Self::AdminApiKeyChanged { exists } => (
if exists {
"admin_api_key.replace"
} else {
"admin_api_key.delete"
},
"runtime_settings",
),
Self::AccountGroupCreate => ("create", "account_group"),
Self::AccountGroupUpdate => ("update", "account_group"),
Self::AccountGroupEnabled { enabled } => {
(if enabled { "enable" } else { "disable" }, "account_group")
}
Self::AccountGroupDelete => ("delete", "account_group"),
Self::ClientApiKeyResetBudget => ("reset_budget", "client_api_key"),
Self::ProviderAccountUpdate => ("update", "provider_account"),
Self::BackupStorageUpdate => ("backup.s3_config_updated", "backup_settings"),
Self::BackupScheduleUpdate => ("backup.schedule_updated", "backup_settings"),
Self::BackupCreate => ("backup.created", "backup"),
Self::BackupDelete => ("backup.delete_requested", "backup"),
Self::BackupConnectionTest => ("backup.s3_connection_tested", "backup_settings"),
Self::BackupDownloadUrl => ("backup.download_url_created", "backup"),
Self::ProviderAccountRecover => ("recover", "provider_account"),
Self::ProviderAccountBatchUpdate => ("batch_update", "provider_account"),
Self::ProviderAccountDelete => ("delete", "provider_account"),
Self::ProviderAccountExportCredentials => ("export_credentials", "provider_account"),
Self::PluginArtifactInstall => ("install", "plugin_artifact"),
Self::PluginArtifactAccept => ("accept", "plugin_artifact"),
Self::PluginArtifactDelete => ("delete", "plugin_artifact"),
Self::PluginInstanceConfigure => ("configure", "plugin_instance"),
Self::PluginInstanceDelete => ("delete", "plugin_instance"),
Self::PluginSourceCredentialCreate => ("create", "plugin_source_credential"),
Self::PluginSourceCredentialDelete => ("delete", "plugin_source_credential"),
Self::ClientApiKeyUpdateBudgetLimits => ("update_budget_limits", "client_api_key"),
Self::ClientApiKeyCreate => ("create", "client_api_key"),
Self::ClientApiKeyUpdate => ("update", "client_api_key"),
Self::ClientApiKeyEnabled { enabled } => {
(if enabled { "enable" } else { "disable" }, "client_api_key")
}
Self::ClientApiKeyDelete => ("delete", "client_api_key"),
Self::PluginSourceChangeSource => ("change_source", "plugin_source"),
Self::PluginSourceDelete => ("delete", "plugin_source"),
Self::OutboundProxyCreate => ("create", "outbound_proxy"),
Self::OutboundProxyUpdate => ("update", "outbound_proxy"),
Self::OutboundProxyDelete => ("delete", "outbound_proxy"),
Self::OutboundProxyTest => ("test", "outbound_proxy"),
Self::AccountGroupPluginReconcile => ("plugin_reconcile", "account_group"),
Self::ClientApiKeyPluginReconcile => ("plugin_reconcile", "client_api_key"),
Self::ProviderAccountImportDocument => ("import_document", "provider_account"),
Self::ProviderAccountRotateCredential => ("rotate_credential", "provider_account"),
Self::ProviderAccountRefreshCredential => ("refresh_credential", "provider_account"),
Self::ProviderAccountAuthorize => ("authorize", "provider_account"),
Self::ProviderAccountReauthorize => ("reauthorize", "provider_account"),
}
}
}
/// 分类与稳定实体标识共同组成审计意图,字段差异由同一提交事务补齐
#[derive(Debug, Clone, Copy)]
pub struct MutationAuditIntent<'a> {
pub operation: MutationAuditOperation,
pub entity_ref: &'a str,
}
impl MutationAuditIntent<'_> {
#[must_use]
pub fn event(self, context: &MutationContext, changed_fields: Vec<String>) -> AdminAuditEvent {
let (actor_kind, actor_admin_user_id, actor_ref) = match &context.actor {
MutationActor::AdminSession { admin_user_id } => (
AuditActorKind::AdminSession,
Some(admin_user_id.clone()),
admin_session_actor_ref(admin_user_id),
),
MutationActor::AdminApiKey => (
AuditActorKind::AdminApiKey,
None,
"admin_api_key".to_owned(),
),
MutationActor::System => (AuditActorKind::System, None, "system".to_owned()),
};
let (action, entity_kind) = self.operation.classification();
AdminAuditEvent {
id: format!("audit_{}", uuid::Uuid::now_v7().simple()),
actor_kind,
actor_admin_user_id,
actor_ref,
request_id: Some(context.request_id.clone()),
action: action.to_owned(),
entity_kind: entity_kind.to_owned(),
entity_ref: self.entity_ref.to_owned(),
config_revision: None,
changed_fields,
occurred_at: chrono::Utc::now(),
}
}
}
+135 -29
View File
@@ -1,17 +1,30 @@
//! 管理员认证、会话和安全审计事实。
//! 控制面认证、统一会话和管理员安全审计事实
use chrono::{DateTime, Utc};
use gateway_core::{engine::execution::ClientAuthenticationError, policy::ClientApiKeyId};
use super::{MutationActor, MutationContext, Revision};
/// 已认证的管理主体。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdminPrincipal {
Session { admin_user_id: String },
ApiKey,
/// 会话审计使用的稳定管理员标识
#[must_use]
pub fn admin_session_actor_ref(admin_user_id: &str) -> String {
format!("admin:{admin_user_id}")
}
/// 传给管理用例的安全请求上下文。
/// 已认证的管理主体
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdminPrincipal {
Session {
admin_user_id: String,
},
ApiKey,
/// 安装者完整信任的插件;身份仅由内部 HTTP 分派端口签发
Plugin {
instance_id: String,
},
}
/// 传给管理用例的安全请求上下文
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdminRequestContext {
pub principal: AdminPrincipal,
@@ -26,6 +39,7 @@ impl AdminRequestContext {
admin_user_id: admin_user_id.clone(),
},
AdminPrincipal::ApiKey => MutationActor::AdminApiKey,
AdminPrincipal::Plugin { .. } => MutationActor::System,
};
MutationContext {
actor,
@@ -34,47 +48,139 @@ impl AdminRequestContext {
}
}
/// 管理员登录命令。
/// 登录方式只决定凭据验证流程,不能直接授予会话权限
#[derive(Clone, PartialEq, Eq)]
pub struct LoginCommand {
pub username: Option<String>,
pub password: String,
pub enum LoginCommand {
Admin {
username: Option<String>,
password: String,
},
Key {
api_key: String,
},
}
impl std::fmt::Debug for LoginCommand {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Admin { username, .. } => formatter
.debug_struct("AdminLogin")
.field("username", username)
.field("password", &"[REDACTED]")
.finish(),
Self::Key { .. } => formatter
.debug_struct("KeyLogin")
.field("api_key", &"[REDACTED]")
.finish(),
}
}
}
/// 登录或刷新后仅向当前浏览器交付的会话凭据
#[derive(Clone, PartialEq, Eq)]
pub struct SessionGrant {
pub session_id: String,
pub refresh_token: Option<String>,
pub session: AuthSession,
}
impl std::fmt::Debug for SessionGrant {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LoginCommand")
.field("username", &self.username)
.field("password", &"[REDACTED]")
.debug_struct("SessionGrant")
.field("session_id", &"[REDACTED]")
.field(
"refresh_token",
&self.refresh_token.as_ref().map(|_| "[REDACTED]"),
)
.field("session", &self.session)
.finish()
}
}
/// 登录成功后返回的会话事实。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoginResult {
pub session_id: String,
pub expires_at: DateTime<Utc>,
}
/// 登录状态机可被 API 精确映射的失败类型。
/// 登录状态机可被 API 精确映射的失败类型
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum LoginError {
#[error("invalid administrator credentials")]
#[error("invalid login credentials")]
InvalidCredentials,
#[error("administrator authentication is unavailable")]
#[error("too many login attempts")]
TooManyAttempts { retry_after_seconds: u64 },
#[error("authentication is unavailable")]
Unavailable,
}
/// Redis 中可恢复的管理员会话。
impl From<ClientAuthenticationError> for LoginError {
fn from(error: ClientAuthenticationError) -> Self {
match error {
ClientAuthenticationError::InvalidKey => Self::InvalidCredentials,
ClientAuthenticationError::SnapshotUnavailable
| ClientAuthenticationError::ProviderUnavailable => Self::Unavailable,
}
}
}
/// 服务端已验证的身份绑定,不保存密码或原始 API Key
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdminSession {
pub admin_user_id: String,
pub enum SessionSubject {
Admin {
admin_user_id: String,
credential_fingerprint: String,
},
Key {
client_key_id: ClientApiKeyId,
},
}
/// 仅已登录管理员可以修改自己的密码
pub struct ChangePassword {
pub current_password: String,
pub new_password: String,
}
impl std::fmt::Debug for ChangePassword {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("ChangePassword([REDACTED])")
}
}
/// 访问有效期与逻辑会话;没有续期凭据的会话保持固定期限
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthSession {
pub subject: SessionSubject,
pub expires_at: DateTime<Utc>,
pub absolute_expires_at: Option<DateTime<Utc>>,
pub renewal: Option<SessionRenewal>,
}
/// 长效登录的凭据摘要与续期窗口;访问到期不删除整段登录
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionRenewal {
pub expires_at: DateTime<Utc>,
pub access_digest: [u8; 32],
pub refresh_digest: [u8; 32],
pub previous: Option<SessionRefreshReplay>,
}
/// 仅保存上一代摘要及派生盐;响应丢失时须持有上一代凭据才能重建同一响应
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionRefreshReplay {
pub digest: [u8; 32],
pub salt: [u8; 32],
pub expires_at: DateTime<Utc>,
}
/// 安全审计事件类型。
impl AuthSession {
#[must_use]
pub fn retained_until(&self) -> DateTime<Utc> {
self.renewal
.as_ref()
.map_or(self.expires_at, |renewal| renewal.expires_at)
}
}
/// 安全审计事件类型
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuditActorKind {
AdminSession,
@@ -83,7 +189,7 @@ pub enum AuditActorKind {
Anonymous,
}
/// 管理写操作留下的最小审计事实。
/// 管理写操作留下的最小审计事实
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdminAuditEvent {
pub id: String,
@@ -0,0 +1,35 @@
//! 备份领域的稳定错误码
/// 配置或参数非法
pub const INVALID_CONFIG: &str = "backup.invalid_config";
/// Cron 表达式非法
pub const INVALID_CRON: &str = "backup.invalid_cron";
/// IANA 时区非法
/// 记录不存在
pub const RECORD_NOT_FOUND: &str = "backup.record_not_found";
/// 已存在活跃任务,拒绝创建
pub const ACTIVE_TASK_CONFLICT: &str = "backup.active_task_conflict";
/// 记录状态与期望不符,无法迁移
pub const STATE_CONFLICT: &str = "backup.state_conflict";
/// 已有记录时禁止切换存储目标
pub const STORAGE_IDENTITY_LOCKED: &str = "backup.storage_identity_locked";
/// `pg_dump` 非零退出或失败
pub const PG_DUMP_FAILED: &str = "backup.pg_dump_failed";
/// 用量汇总尚未完整发布
pub const ANALYTICS_NOT_READY: &str = "backup.analytics_not_ready";
/// 暂存磁盘空间不足
pub const STAGING_SPACE_EXHAUSTED: &str = "backup.staging_space_exhausted";
/// S3 认证失败
pub const S3_AUTH_FAILED: &str = "backup.s3_auth_failed";
/// S3 权限拒绝
pub const S3_PERMISSION_DENIED: &str = "backup.s3_permission_denied";
/// S3 上传失败
pub const S3_UPLOAD_FAILED: &str = "backup.s3_upload_failed";
/// 远端对象校验失败
pub const REMOTE_VERIFICATION_FAILED: &str = "backup.remote_verification_failed";
/// 任务被取消
pub const CANCELLED: &str = "backup.cancelled";
/// 仓储或对象存储暂不可用
pub const STORE_UNAVAILABLE: &str = "backup.store_unavailable";
/// 上游 S3 服务返回无效或失败响应
pub const UPSTREAM_FAILURE: &str = "backup.upstream_failure";
@@ -0,0 +1,583 @@
//! S3 备份领域模型、状态机与稳定错误
//!
//! 领域类型只携带事实,不承载基础设施细节
//! Secret 使用 [`secrecy::SecretString`],
//! 全流程不实现 `Serialize`,避免被错误地写入 wire 或日志
//! 管理员身份等审计事实
//! 只存在于 `admin_audit_events`,本模型不重复保存
use std::fmt;
use chrono::{DateTime, Utc};
use secrecy::SecretString;
use uuid::Uuid;
use super::PageSize;
/// 备份触发来源
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackupTriggerKind {
Manual,
Scheduled,
}
impl BackupTriggerKind {
/// 稳定持久化值
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Manual => "manual",
Self::Scheduled => "scheduled",
}
}
/// 解析持久化值
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value {
"manual" => Some(Self::Manual),
"scheduled" => Some(Self::Scheduled),
_ => None,
}
}
}
impl fmt::Display for BackupTriggerKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
/// 备份任务状态机(六状态)
///
/// 迁移 `backup_records_lifecycle_ck` 与字段空值语义共同定义合法持久化行;
/// [`Self::allows_transition_to`] 只约束领域内的显式状态迁移,两层都必须满足
/// 删除成功后记录行被硬删除,因此不存在 `deleted` 终态
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BackupStatus {
Queued,
Dumping,
Uploading,
Completed,
Failed,
Deleting,
}
impl BackupStatus {
/// 稳定持久化值
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Queued => "queued",
Self::Dumping => "dumping",
Self::Uploading => "uploading",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Deleting => "deleting",
}
}
/// 解析持久化值
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value {
"queued" => Some(Self::Queued),
"dumping" => Some(Self::Dumping),
"uploading" => Some(Self::Uploading),
"completed" => Some(Self::Completed),
"failed" => Some(Self::Failed),
"deleting" => Some(Self::Deleting),
_ => None,
}
}
/// 该状态是否消耗全局活跃名额
#[must_use]
pub const fn is_active(self) -> bool {
matches!(self, Self::Queued | Self::Dumping | Self::Uploading)
}
/// 该状态是否允许进入删除流程
#[must_use]
pub const fn can_be_deleted(self) -> bool {
matches!(self, Self::Completed | Self::Failed)
}
/// 领域内允许的显式迁移
#[must_use]
pub fn allows_transition_to(self, target: BackupStatus) -> bool {
matches!(
(self, target),
(Self::Queued, Self::Dumping)
| (Self::Queued, Self::Failed)
| (Self::Dumping, Self::Uploading)
| (Self::Dumping, Self::Failed)
| (Self::Uploading, Self::Completed)
| (Self::Uploading, Self::Failed)
| (Self::Completed, Self::Deleting)
| (Self::Failed, Self::Deleting)
)
}
}
impl fmt::Display for BackupStatus {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
/// 已通过领域状态机校验的显式迁移
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BackupStatusTransition {
from: BackupStatus,
to: BackupStatus,
}
impl BackupStatusTransition {
/// 校验并构造显式迁移;非法状态边返回 `None`
#[must_use]
pub fn try_new(from: BackupStatus, to: BackupStatus) -> Option<Self> {
from.allows_transition_to(to).then_some(Self { from, to })
}
/// 迁移要求的当前状态
#[must_use]
pub const fn from(self) -> BackupStatus {
self.from
}
/// 迁移完成后的目标状态
#[must_use]
pub const fn to(self) -> BackupStatus {
self.to
}
}
/// S3 存储配置更新命令;`secret_access_key` 为 `None` 表示保留旧值
#[derive(Clone)]
pub struct UpdateBackupStorageCommand {
pub endpoint: String,
pub region: String,
pub bucket: String,
pub access_key_id: String,
pub secret_access_key: Option<SecretString>,
pub prefix: String,
pub force_path_style: bool,
}
impl fmt::Debug for UpdateBackupStorageCommand {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("UpdateBackupStorageCommand")
.field("endpoint", &self.endpoint)
.field("region", &self.region)
.field("bucket", &self.bucket)
.field("access_key_id", &self.access_key_id)
.field("secret_access_key", &self.secret_access_key.is_some())
.field("prefix", &self.prefix)
.field("force_path_style", &self.force_path_style)
.finish()
}
}
/// 调度配置更新命令
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateBackupScheduleCommand {
pub schedule_enabled: bool,
pub cron_expression: String,
pub retention_days: u32,
pub retention_count: u32,
}
/// 完整保存的备份设置(含 Secret)
///
/// 只从仓储读取;不实现 `Serialize`,防止 Secret 进入 wire 或日志
#[derive(Clone)]
pub struct BackupSettings {
pub storage_revision: u64,
pub endpoint: Option<String>,
pub region: Option<String>,
pub bucket: Option<String>,
pub access_key_id: Option<String>,
pub secret_access_key: Option<SecretString>,
pub prefix: Option<String>,
pub force_path_style: bool,
pub schedule_enabled: bool,
pub cron_expression: Option<String>,
/// 记录持久化游标采用的时区,由服务端维护,不是独立部署设置
pub schedule_timezone: Option<String>,
pub retention_days: u32,
pub retention_count: u32,
pub next_run_at: Option<DateTime<Utc>>,
pub last_verified_at: Option<DateTime<Utc>>,
pub updated_at: DateTime<Utc>,
}
impl fmt::Debug for BackupSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BackupSettings")
.field("storage_revision", &self.storage_revision)
.field("endpoint", &self.endpoint)
.field("region", &self.region)
.field("bucket", &self.bucket)
.field("access_key_id", &self.access_key_id)
.field("secret_access_key", &self.secret_access_key.is_some())
.field("prefix", &self.prefix)
.field("force_path_style", &self.force_path_style)
.field("schedule_enabled", &self.schedule_enabled)
.field("cron_expression", &self.cron_expression)
.field("schedule_timezone", &self.schedule_timezone)
.field("retention_days", &self.retention_days)
.field("retention_count", &self.retention_count)
.field("next_run_at", &self.next_run_at)
.field("last_verified_at", &self.last_verified_at)
.field("updated_at", &self.updated_at)
.finish()
}
}
impl BackupSettings {
/// S3 存储配置是否完整(bucket/region/密钥/前缀齐全)
#[must_use]
pub fn storage_configured(&self) -> bool {
self.endpoint.is_some()
&& self.region.is_some()
&& self.bucket.is_some()
&& self.access_key_id.is_some()
&& self.secret_access_key.is_some()
&& self.prefix.is_some()
}
/// 探测是否已对当前 revision 成功;`last_verified_at` 非空即表示当前配置已通过探针
#[must_use]
pub fn storage_verified(&self) -> bool {
self.last_verified_at.is_some()
}
}
/// 已经校验并可用于对象存储操作的存储配置快照
#[derive(Clone)]
pub struct BackupStorageConfig {
pub storage_revision: u64,
pub endpoint: String,
pub region: String,
pub bucket: String,
pub access_key_id: String,
pub secret_access_key: SecretString,
pub prefix: String,
pub force_path_style: bool,
}
impl fmt::Debug for BackupStorageConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BackupStorageConfig")
.field("storage_revision", &self.storage_revision)
.field("endpoint", &self.endpoint)
.field("region", &self.region)
.field("bucket", &self.bucket)
.field("access_key_id", &self.access_key_id)
.field("secret_access_key", &"[REDACTED]")
.field("prefix", &self.prefix)
.field("force_path_style", &self.force_path_style)
.finish()
}
}
impl BackupStorageConfig {
/// 从完整设置推导对象存储配置;设置不完整时返回 `None`
#[must_use]
pub fn from_settings(settings: &BackupSettings) -> Option<Self> {
Some(Self {
storage_revision: settings.storage_revision,
endpoint: settings.endpoint.clone()?,
region: settings.region.clone()?,
bucket: settings.bucket.clone()?,
access_key_id: settings.access_key_id.clone()?,
secret_access_key: settings.secret_access_key.clone()?,
prefix: settings.prefix.clone()?,
force_path_style: settings.force_path_style,
})
}
}
/// 一条备份记录
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackupRecord {
pub id: String,
pub trigger_kind: BackupTriggerKind,
pub status: BackupStatus,
pub scheduled_at: Option<DateTime<Utc>>,
pub object_key: String,
pub size_bytes: Option<u64>,
pub sha256: Option<String>,
pub attempt_count: u32,
pub error_code: Option<String>,
pub error_message: Option<String>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
/// 创建时确定的过期时间;手工和计划备份均可设置,到期自动清理
pub expires_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// 备份记录分页查询
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackupRecordListQuery {
pub page: u32,
pub page_size: PageSize,
pub status: Option<BackupStatus>,
pub trigger: Option<BackupTriggerKind>,
}
/// 备份记录分页结果
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackupRecordPage {
pub items: Vec<BackupRecord>,
pub total: u64,
pub page: u32,
pub page_size: PageSize,
}
impl BackupRecordPage {
/// 计算总页数;空集合视为一页
#[must_use]
pub fn total_pages(&self) -> u32 {
let size = u64::from(self.page_size.get());
if self.total == 0 {
1
} else {
self.total.div_ceil(size).min(u64::from(u32::MAX)) as u32
}
}
}
/// 创建备份任务的持久化数据;id 与对象 key 由领域生成
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackupRecordSeed {
pub id: String,
pub trigger_kind: BackupTriggerKind,
pub scheduled_at: Option<DateTime<Utc>>,
pub object_key: String,
pub expires_at: Option<DateTime<Utc>>,
}
/// 连接测试结果;只携带稳定阶段与脱敏消息
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConnectionTestResult {
pub ok: bool,
pub stage: &'static str,
pub code: Option<&'static str>,
pub message: String,
}
/// 对象存储探测阶段
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionTestStage {
PutObject,
HeadObject,
GetObject,
DeleteObject,
}
impl ConnectionTestStage {
/// 稳定 wire 值
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::PutObject => "putObject",
Self::HeadObject => "headObject",
Self::GetObject => "getObject",
Self::DeleteObject => "deleteObject",
}
}
}
/// 上传到对象存储并期望远端校验的归档 metadata
///
/// `size_bytes` 在上传时不写入 metadata,但 `HeadObject` 会返回远端大小,
/// 用于与本地归档校验一致
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackupObjectMetadata {
pub backup_id: String,
pub sha256: String,
pub created_at: DateTime<Utc>,
pub size_bytes: u64,
}
impl BackupObjectMetadata {
/// 构造上传 metadata
#[must_use]
pub const fn new(
backup_id: String,
sha256: String,
created_at: DateTime<Utc>,
size_bytes: u64,
) -> Self {
Self {
backup_id,
sha256,
created_at,
size_bytes,
}
}
}
/// 短时下载地址结果
#[derive(Clone, PartialEq, Eq)]
pub struct DownloadUrlResult {
pub url: String,
pub file_name: String,
pub expires_in: std::time::Duration,
}
impl fmt::Debug for DownloadUrlResult {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DownloadUrlResult")
.field("url", &"[REDACTED]")
.field("file_name", &self.file_name)
.field("expires_in", &self.expires_in)
.finish()
}
}
/// 备份基础设施错误:只携带稳定错误码与脱敏消息
///
/// 该类型不构成第二套 kind 枚举;`gateway-admin` 边界统一映射为既有
/// `AdminErrorKind`
/// 稳定 `error_code` 同时用于任务记录、日志与 UI
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct BackupError {
code: &'static str,
message: String,
}
impl BackupError {
/// 构造基础设施错误
#[must_use]
pub const fn new(code: &'static str, message: String) -> Self {
Self { code, message }
}
/// 稳定错误码,例如 `backup.s3_auth_failed`
#[must_use]
pub const fn code(&self) -> &'static str {
self.code
}
/// 脱敏后的可操作消息
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
/// 清理失败附加到原始终态,不覆盖上传失败或主动取消的分类
#[must_use]
pub fn with_cleanup_failure(
mut self,
stage: &'static str,
error: &Self,
retry_on_expiration: bool,
) -> Self {
let recovery = if retry_on_expiration {
"等待备份到期清理"
} else {
"未安排自动重试"
};
self.message.push_str(&format!(
";清理阶段 {stage} 失败({}),{recovery}",
error.code()
));
self
}
}
/// 构造稳定错误码常量
pub mod code;
/// 构建对象 key:`{prefix}/YYYY/MM/DD/codex-proxy-rs_{UTC seconds}_{backup id}.sql.gz`
///
/// 规范化规则:prefix 不得为空、不得以前导 `/` 开头、不得包含 `..`、反斜杠或控制字符;
/// 输出统一使用 `/` 分隔且不含空段
pub fn build_object_key(
prefix: &str,
backup_id: &str,
at: DateTime<Utc>,
) -> Result<String, BackupError> {
let prefix = normalize_prefix(prefix)?;
Ok(format!(
"{prefix}/{}/{:02}/{:02}/codex-proxy-rs_{}_{}.sql.gz",
at.format("%Y"),
at.format("%m"),
at.format("%d"),
at.format("%Y%m%d_%H%M%S"),
short_backup_id(backup_id),
))
}
/// 下载文件名:`codex-proxy-rs_{短 id}.sql.gz`,与对象 key 的文件名一致
#[must_use]
pub fn build_download_file_name(backup_id: &str) -> String {
format!("codex-proxy-rs_{}.sql.gz", short_backup_id(backup_id))
}
/// 截取备份 id 的可读短标识:去掉 `backup_` 前缀,最多保留 8 位 hex
#[must_use]
fn short_backup_id(backup_id: &str) -> String {
let hex = backup_id.strip_prefix("backup_").unwrap_or(backup_id);
hex.get(..8).unwrap_or(hex).to_owned()
}
/// 规范化并校验对象 prefix
///
/// 返回去掉尾部 `/` 的规范 prefix;空串、前导 `/`、`..` 段、反斜杠和控制字符均拒绝
pub fn normalize_prefix(prefix: &str) -> Result<String, BackupError> {
if prefix.is_empty() || prefix.starts_with('/') {
return Err(BackupError::new(
code::INVALID_CONFIG,
"存储对象前缀必须非空且不能以前导 / 开头".to_owned(),
));
}
if prefix.bytes().any(|byte| byte.is_ascii_control()) || prefix.contains('\\') {
return Err(BackupError::new(
code::INVALID_CONFIG,
"存储对象前缀包含非法字符".to_owned(),
));
}
if prefix.split('/').any(|segment| segment == "..") {
return Err(BackupError::new(
code::INVALID_CONFIG,
"存储对象前缀不能包含 .. 路径段".to_owned(),
));
}
Ok(prefix.trim_end_matches('/').to_owned())
}
/// 生成新的备份记录 id
#[must_use]
pub fn new_backup_id() -> String {
format!("backup_{}", Uuid::now_v7().simple())
}
/// 从触发类型与计划时间构造可持久化 seed;id 与对象 key 一次生成保持一致
pub fn build_backup_seed(
trigger_kind: BackupTriggerKind,
scheduled_at: Option<DateTime<Utc>>,
prefix: &str,
at: DateTime<Utc>,
expires_at: Option<DateTime<Utc>>,
) -> Result<BackupRecordSeed, BackupError> {
let id = new_backup_id();
let object_key = build_object_key(prefix, &id, at)?;
Ok(BackupRecordSeed {
id,
trigger_kind,
scheduled_at,
object_key,
expires_at,
})
}
@@ -0,0 +1,58 @@
//! 管理设置页使用的 Codex 客户端下载信息
use chrono::{DateTime, Utc};
/// Windows 安装包架构
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ClientArchitecture {
X64,
Arm64,
}
impl ClientArchitecture {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::X64 => "x64",
Self::Arm64 => "arm64",
}
}
}
/// 下载地址的可信来源
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientDownloadSource {
MicrosoftStore,
OfficialOpenAi,
}
impl ClientDownloadSource {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::MicrosoftStore => "microsoft_store",
Self::OfficialOpenAi => "official_openai",
}
}
}
/// 一个已经过 Host 校验、可以直接交给浏览器下载的安装包
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientDownloadPackage {
pub architecture: ClientArchitecture,
pub source: ClientDownloadSource,
pub version: Option<String>,
pub file_name: String,
pub size_bytes: Option<u64>,
pub download_url: String,
pub expires_at: Option<DateTime<Utc>>,
}
/// Codex Desktop Windows 下载解析结果
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodexDesktopWindowsDownloads {
pub resolved_at: DateTime<Utc>,
pub cached: bool,
pub warning: Option<String>,
pub packages: Vec<ClientDownloadPackage>,
}
@@ -1,26 +1,49 @@
//! Client API Key 的 Command、Result 与安全秘密类型。
//! Client API Key 的 Command、Result 与安全秘密类型
use std::{fmt, num::NonZeroU16};
use std::{collections::BTreeMap, fmt, num::NonZeroU16};
use chrono::{DateTime, Utc};
use gateway_core::{
policy::{ClientApiKeyId, RateLimits},
routing::ProviderKind,
engine::budget::{ClientBudgetLimits, ClientBudgetStatus},
policy::{ClientApiKeyId, PlaintextClientApiKey, RateLimits},
routing::{AccountGroupId, ProviderKind},
};
use super::{AdminModelError, Revision};
use super::{
AdminModelError, Revision, account_groups::AccountGroupRef,
plugin_resources::PluginResourceOwner,
};
/// Client Key 列表保持旧 HTTP 合同允许的完整非零 `u16` 页大小。
pub type ProviderRequestProfileOverrides =
BTreeMap<ProviderKind, gateway_core::account::OpaqueProviderData>;
pub type ProviderRequestProfileOverrideUpdates =
BTreeMap<ProviderKind, Option<gateway_core::account::OpaqueProviderData>>;
/// Client Key 关联分组的数量与唯一性约束
pub fn validate_group_ids(group_ids: &[AccountGroupId]) -> Result<(), AdminModelError> {
if group_ids.len() > 1000
|| group_ids
.iter()
.collect::<std::collections::BTreeSet<_>>()
.len()
!= group_ids.len()
{
return Err(AdminModelError::InvalidClientKeyGroups);
}
Ok(())
}
/// Client Key 列表保持旧 HTTP 合同允许的完整非零 `u16` 页大小
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ClientKeyPageSize(NonZeroU16);
impl ClientKeyPageSize {
/// 创建 1 至 65535 的 Client Key 页大小。
/// 创建 1 至 65535 的 Client Key 页大小
///
/// # Errors
///
/// `value` 为零时返回 [`AdminModelError::InvalidClientKeyPageSize`]。
/// `value` 为零时返回 [`AdminModelError::InvalidClientKeyPageSize`]
pub fn new(value: u16) -> Result<Self, AdminModelError> {
NonZeroU16::new(value)
.map(Self)
@@ -33,7 +56,7 @@ impl ClientKeyPageSize {
}
}
/// Client Key 列表排序字段。
/// Client Key 列表排序字段
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientKeySortField {
Name,
@@ -42,21 +65,21 @@ pub enum ClientKeySortField {
LastUsedAt,
}
/// Client Key 列表排序方向。
/// Client Key 列表排序方向
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortDirection {
Asc,
Desc,
}
/// Client Key 列表排序规则。
/// Client Key 列表排序规则
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClientKeySort {
pub field: ClientKeySortField,
pub direction: SortDirection,
}
/// 与排序字段绑定的游标值。
/// 与排序字段绑定的游标值
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClientKeyCursorValue {
Name(String),
@@ -65,7 +88,7 @@ pub enum ClientKeyCursorValue {
LastUsedAt(Option<DateTime<Utc>>),
}
/// Client Key 的稳定键集游标。
/// Client Key 的稳定键集游标
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientKeyCursor {
pub sort: ClientKeySort,
@@ -73,7 +96,7 @@ pub struct ClientKeyCursor {
pub id: ClientApiKeyId,
}
/// Client Key 列表查询。
/// Client Key 列表查询
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientKeyListQuery {
pub cursor: Option<ClientKeyCursor>,
@@ -82,22 +105,26 @@ pub struct ClientKeyListQuery {
pub sort: ClientKeySort,
}
/// 不含完整明文 Key 的管理投影。
/// 不含完整明文 Key 的管理投影
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientKeyRecord {
pub peer_sharing: gateway_core::policy::ClientPeerSharing,
pub request_profile_overrides: ProviderRequestProfileOverrides,
pub id: ClientApiKeyId,
pub name: String,
pub label: Option<String>,
pub provider_kind: ProviderKind,
pub groups: Vec<AccountGroupRef>,
pub provider_kinds: Vec<ProviderKind>,
pub prefix: String,
pub enabled: bool,
pub limits: RateLimits,
pub budget: ClientBudgetStatus,
pub last_used_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Client Key 列表页。
/// Client Key 列表页
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientKeyPage {
pub config_revision: Revision,
@@ -106,7 +133,7 @@ pub struct ClientKeyPage {
pub next_cursor: Option<ClientKeyCursor>,
}
/// 仅在创建或显式 reveal 时跨越管理边界的明文 Key。
/// 仅在创建或显式读取密钥配置时返回的明文 Key;Debug 隐去明文
#[derive(Clone, PartialEq, Eq)]
pub struct ClientKeySecret {
pub record: ClientKeyRecord,
@@ -138,23 +165,30 @@ impl fmt::Debug for ClientKeySecret {
}
}
/// API 提交的 Client Key 创建命令。
/// API 提交的 Client Key 创建命令
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateClientKey {
pub peer_sharing: gateway_core::policy::ClientPeerSharing,
pub request_profile_overrides: ProviderRequestProfileOverrides,
pub custom_key: Option<PlaintextClientApiKey>,
pub name: String,
pub label: Option<String>,
pub provider_kind: ProviderKind,
pub group_ids: Vec<AccountGroupId>,
pub limits: RateLimits,
pub budget: ClientBudgetLimits,
}
/// 管理用例生成 ID 与明文后的持久化命令。
/// 管理用例生成 ID 与明文后的持久化命令
#[derive(Clone, PartialEq, Eq)]
pub struct NewClientKey {
pub peer_sharing: gateway_core::policy::ClientPeerSharing,
pub request_profile_overrides: ProviderRequestProfileOverrides,
pub id: ClientApiKeyId,
pub name: String,
pub label: Option<String>,
pub provider_kind: ProviderKind,
pub group_ids: Vec<AccountGroupId>,
pub limits: RateLimits,
pub budget: ClientBudgetLimits,
pub plaintext: String,
}
@@ -164,36 +198,70 @@ impl fmt::Debug for NewClientKey {
.debug_struct("NewClientKey")
.field("id", &self.id)
.field("name", &self.name)
.field("provider_kind", &self.provider_kind)
.field("group_ids", &self.group_ids)
.field("plaintext", &"[REDACTED]")
.finish_non_exhaustive()
}
}
/// 修改 Client Key 的公开策略字段。
/// 修改 Client Key 的公开策略字段
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateClientKey {
pub peer_sharing: Option<gateway_core::policy::ClientPeerSharing>,
pub request_profile_override_updates: ProviderRequestProfileOverrideUpdates,
pub id: ClientApiKeyId,
pub name: String,
pub label: Option<String>,
pub provider_kind: ProviderKind,
pub group_ids: Vec<AccountGroupId>,
pub limits: RateLimits,
pub daily_limit_usd: Option<gateway_core::metering::Decimal>,
pub weekly_limit_usd: Option<gateway_core::metering::Decimal>,
}
/// 修改 Client Key 启用状态。
/// 修改 Client Key 启用状态
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetClientKeyEnabled {
pub id: ClientApiKeyId,
pub enabled: bool,
}
/// 删除 Client Key。
/// 管理员选择的金额重置范围
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientKeyBudgetPeriod {
Daily,
Weekly,
All,
}
/// 清零所选窗口已用金额,保留限额、到期时间与历史费用
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResetClientKeyBudget {
pub id: ClientApiKeyId,
pub period: ClientKeyBudgetPeriod,
}
/// 仅更新指定金额上限;省略的周期保持不变,不修改已用金额或窗口
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateClientKeyBudgetLimits {
pub id: ClientApiKeyId,
pub daily_limit_usd: Option<gateway_core::metering::Decimal>,
pub weekly_limit_usd: Option<gateway_core::metering::Decimal>,
}
/// 预算变更的调用来源;插件身份由 Runtime 给出,不能从插件请求反序列化
#[derive(Debug, Clone)]
pub enum ClientKeyBudgetMutationOrigin {
Admin,
Plugin(PluginResourceOwner),
}
/// 删除 Client Key
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteClientKey {
pub id: ClientApiKeyId,
}
/// Client Key 创建结果;完整明文仅存在于该一次性结果中。
/// Client Key 创建结果;完整明文仅存在于该一次性结果中
pub struct CreatedClientKey {
pub config_revision: Revision,
pub secret: ClientKeySecret,
@@ -209,7 +277,7 @@ impl fmt::Debug for CreatedClientKey {
}
}
/// Client Key 普通写操作结果。
/// Client Key 普通写操作结果
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientKeyMutation {
pub config_revision: Revision,
@@ -0,0 +1,44 @@
//! 故障诊断导出的目标、已采集证据与缺口
use chrono::{DateTime, Utc};
use super::{
observability::{OpsError, UsageDetail},
system::SystemVersion,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticTarget {
Request(String),
Operation(String),
}
impl DiagnosticTarget {
#[must_use]
pub fn id(&self) -> &str {
match self {
Self::Request(id) | Self::Operation(id) => id,
}
}
}
pub struct DiagnosticGap {
pub section: String,
pub reason: &'static str,
}
pub struct DiagnosticRequest {
pub request_id: String,
pub detail: Option<UsageDetail>,
pub errors: Vec<OpsError>,
}
/// 原始受控详情只能经导出投影脱敏后交付,不参与普通日志或 Debug
pub struct DiagnosticBundle {
pub target: DiagnosticTarget,
pub collected_at: DateTime<Utc>,
pub environment: Option<SystemVersion>,
pub error: Option<OpsError>,
pub requests: Vec<DiagnosticRequest>,
pub gaps: Vec<DiagnosticGap>,
}
@@ -0,0 +1,84 @@
//! 进程内账号导入任务;只保存执行所需输入与可公开的结果
use chrono::{DateTime, Utc};
use gateway_core::{account::ProviderAccountId, routing::ProviderKind};
use uuid::Uuid;
use super::{MutationContext, provider_credentials::ImportCredentials};
pub const MAX_IMPORT_TASK_ITEMS: usize = 200;
#[derive(Debug)]
pub struct ImportTaskInput {
pub provider: ProviderKind,
pub command: ImportCredentials,
}
#[derive(Debug)]
pub struct SubmitImportTask {
pub submission_id: Uuid,
/// API 对完整输入计算摘要;用于区分丢失响应后的重试与修改后的新提交
pub fingerprint: [u8; 32],
pub context: MutationContext,
pub items: Vec<ImportTaskInput>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportItemStatus {
Pending,
Running,
Succeeded,
Failed,
Unknown,
Skipped,
}
impl ImportItemStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Running => "running",
Self::Succeeded => "succeeded",
Self::Failed => "failed",
Self::Unknown => "unknown",
Self::Skipped => "skipped",
}
}
}
#[derive(Debug, Clone)]
pub struct ImportTaskItem {
pub index: usize,
pub provider: ProviderKind,
pub status: ImportItemStatus,
pub account_ids: Vec<ProviderAccountId>,
pub message: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ImportTaskCounts {
pub pending: usize,
pub running: usize,
pub succeeded: usize,
pub failed: usize,
pub unknown: usize,
pub skipped: usize,
pub imported_accounts: usize,
}
#[derive(Debug, Clone)]
pub struct ImportTaskSummary {
pub task_id: Uuid,
pub created_at: DateTime<Utc>,
pub finished_at: Option<DateTime<Utc>>,
pub stop_requested: bool,
pub total: usize,
pub counts: ImportTaskCounts,
}
#[derive(Debug, Clone)]
pub struct ImportTaskDetail {
pub summary: ImportTaskSummary,
pub items: Vec<ImportTaskItem>,
}
@@ -0,0 +1,39 @@
//! 单页 Key 用量的查询合同;身份范围不由调用方提供
use super::{
client_keys::ClientKeyRecord,
observability::{
HealthTimeline, ObservabilityPageSize, RequestMetricPoint, TimeRange, UsageOverview,
UsagePage,
},
};
#[derive(Debug, Clone)]
pub struct KeyUsageQuery {
pub range: TimeRange,
pub model: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub enum KeyUsageRecordKind {
Success,
Error,
All,
}
#[derive(Debug, Clone)]
pub struct KeyUsageRecordsQuery {
pub usage: KeyUsageQuery,
pub kind: KeyUsageRecordKind,
pub current_page: u32,
pub page_size: ObservabilityPageSize,
}
pub struct KeyUsageOverview {
pub key: ClientKeyRecord,
pub overview: UsageOverview,
pub trend: Vec<RequestMetricPoint>,
pub health_timeline: HealthTimeline,
}
pub type KeyUsageRecords = UsagePage;
+113 -16
View File
@@ -1,34 +1,76 @@
//! 管理控制面使用的 Command、Result 与稳定值对象。
//! 管理控制面使用的 Command、Result 与稳定值对象
use std::num::{NonZeroU16, NonZeroU64};
pub mod account_groups;
pub mod accounts;
pub mod analytics;
pub mod audit;
pub mod auth;
pub mod backup;
pub mod client_distribution;
pub mod client_keys;
pub mod diagnostics;
pub mod import_tasks;
pub mod key_usage;
pub mod observability;
pub mod peer_link;
pub mod plugin_client_keys;
pub mod plugin_resources;
pub mod plugins;
pub mod pricing;
pub mod provider_credentials;
pub mod proxies;
pub mod quota_forecast;
pub mod quota_forecast_sampling;
pub mod reset_credit_automation;
pub mod retention;
pub mod settings;
pub mod system;
pub mod upstream_info;
pub mod usage_data;
/// 管理用例对外返回的稳定错误分类。
/// 管理用例对外返回的稳定错误分类
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdminErrorKind {
Invalid,
Unauthorized,
Forbidden,
NotFound,
Conflict,
RateLimited,
BadGateway,
UpstreamResultUnknown,
Unavailable,
Internal,
}
/// 不携带基础设施细节的管理用例错误。
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
impl AdminErrorKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Invalid => "invalid",
Self::Forbidden => "forbidden",
Self::Unauthorized => "unauthorized",
Self::NotFound => "not_found",
Self::Conflict => "conflict",
Self::RateLimited => "rate_limited",
Self::BadGateway => "bad_gateway",
Self::UpstreamResultUnknown => "upstream_result_unknown",
Self::Unavailable => "unavailable",
Self::Internal => "internal",
}
}
}
/// 管理用例的安全消息与内部原因分离,原始来源不进入响应
#[derive(Debug, Clone, thiserror::Error)]
#[error("{message}")]
pub struct AdminError {
kind: AdminErrorKind,
message: String,
source: Option<gateway_core::error::ErrorSource>,
sensitive_context_redacted: bool,
}
impl AdminError {
@@ -37,19 +79,53 @@ impl AdminError {
Self {
kind,
message: message.into(),
source: None,
sensitive_context_redacted: false,
}
}
#[must_use]
pub fn with_source(mut self, source: impl Into<gateway_core::error::ErrorSource>) -> Self {
self.source = Some(source.into());
self
}
#[must_use]
pub fn redact_sensitive_context(mut self) -> Self {
self.sensitive_context_redacted = true;
self
}
#[must_use]
pub fn error_details(&self) -> Option<gateway_core::error::ErrorDetails> {
gateway_core::error::ErrorDetails::capture(
self.source.as_ref(),
None,
self.sensitive_context_redacted,
)
}
#[must_use]
pub const fn kind(&self) -> AdminErrorKind {
self.kind
}
/// 返回已脱敏、可公开给管理员的文案
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub fn invalid(message: impl Into<String>) -> Self {
Self::new(AdminErrorKind::Invalid, message)
}
#[must_use]
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(AdminErrorKind::Invalid, message)
}
#[must_use]
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(AdminErrorKind::NotFound, message)
@@ -60,6 +136,16 @@ impl AdminError {
Self::new(AdminErrorKind::Conflict, message)
}
#[must_use]
pub fn bad_gateway(message: impl Into<String>) -> Self {
Self::new(AdminErrorKind::BadGateway, message)
}
#[must_use]
pub fn upstream_result_unknown(message: impl Into<String>) -> Self {
Self::new(AdminErrorKind::UpstreamResultUnknown, message)
}
#[must_use]
pub fn unavailable(message: impl Into<String>) -> Self {
Self::new(AdminErrorKind::Unavailable, message)
@@ -71,16 +157,19 @@ impl AdminError {
}
}
/// PostgreSQL 中所有正整数 revision 的管理层表示。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// PostgreSQL 中所有正整数 revision 的管理层表示
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
#[serde(transparent)]
pub struct Revision(NonZeroU64);
impl Revision {
/// 创建正整数 revision。
/// 创建正整数 revision
///
/// # Errors
///
/// `value` 为零时返回 [`AdminModelError::ZeroRevision`]。
/// `value` 为零时返回 [`AdminModelError::ZeroRevision`]
pub fn new(value: u64) -> Result<Self, AdminModelError> {
NonZeroU64::new(value)
.map(Self)
@@ -93,18 +182,18 @@ impl Revision {
}
}
/// 管理列表统一使用的受限页大小。
/// 管理列表统一使用的受限页大小
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PageSize(NonZeroU16);
impl PageSize {
pub const MAX: u16 = 200;
/// 创建 1 至 200 的页大小。
/// 创建 1 至 200 的页大小
///
/// # Errors
///
/// `value` 不在有效范围时返回 [`AdminModelError::InvalidPageSize`]。
/// `value` 不在有效范围时返回 [`AdminModelError::InvalidPageSize`]
pub fn new(value: u16) -> Result<Self, AdminModelError> {
if value > Self::MAX {
return Err(AdminModelError::InvalidPageSize(value));
@@ -120,28 +209,36 @@ impl PageSize {
}
}
/// 可审计管理写操作的发起者。
#[derive(Debug, Clone, PartialEq, Eq)]
/// 可审计管理写操作的发起者
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum MutationActor {
AdminSession { admin_user_id: String },
AdminApiKey,
System,
}
/// 管理写操作必须携带的审计上下文。
#[derive(Debug, Clone, PartialEq, Eq)]
/// 管理写操作必须携带的审计上下文
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct MutationContext {
pub actor: MutationActor,
pub request_id: String,
}
/// 领域值对象构造失败。
/// 领域值对象构造失败
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AdminModelError {
#[error("revision must be greater than zero")]
ZeroRevision,
#[error("page size {0} is outside 1..=200")]
InvalidPageSize(u16),
#[error("observability page size {0} is outside 1..=100")]
InvalidObservabilityPageSize(u16),
#[error("account group name or description is invalid")]
InvalidAccountGroupFields,
#[error("client key group IDs are duplicated or exceed 1000 entries")]
InvalidClientKeyGroups,
#[error("client key page size must be inside 1..=65535")]
InvalidClientKeyPageSize,
#[error("request outcome must be 1..=256 bytes without control characters")]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
//! 当前认证 Key 可公开的本地互联观测,不携带 HTTP 或 Provider 私有字段
use super::provider_credentials::ProviderQuotaWindowRole;
use chrono::{DateTime, Utc};
use gateway_core::engine::budget::{ClientBudgetAccess, ClientBudgetStatus};
/// 调用边界提供的响应资源预算,超限时整体拒绝而不截断授权范围
#[derive(Debug, Clone, Copy)]
pub struct PeerLinkLimits {
pub max_accounts: usize,
pub max_quota_windows: usize,
pub max_models: usize,
pub max_response_bytes: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PeerLinkExport {
pub observed_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub key_limits: gateway_core::policy::RateLimits,
pub budget: ClientBudgetStatus,
pub budget_access: ClientBudgetAccess,
pub budget_observed_at: DateTime<Utc>,
pub pool: Option<PeerPoolSummary>,
pub quota: Option<Vec<PeerAccountQuota>>,
pub models: Vec<PeerModelSummary>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PeerPoolSummary {
pub ready: u32,
pub cooldown: u32,
pub exhausted: u32,
pub disabled: u32,
pub error: u32,
pub unknown: u32,
pub plans: Option<std::collections::BTreeMap<String, u32>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PeerAccountQuota {
pub id: String,
pub models: Vec<String>,
pub observed_at: Option<DateTime<Utc>>,
pub expires_at: Option<DateTime<Utc>>,
pub windows: Vec<PeerQuotaWindow>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PeerQuotaWindow {
pub key: String,
pub group: String,
pub label: String,
pub limit_id: Option<String>,
pub limit_name: Option<String>,
pub role: Option<ProviderQuotaWindowRole>,
pub window_seconds: Option<u64>,
pub used_percent: Option<f64>,
pub reset_at: Option<DateTime<Utc>>,
pub limit_reached: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerModelState {
Available,
Exhausted,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerModelSummary {
pub model: String,
pub complete: bool,
pub state: PeerModelState,
pub observed_at: Option<DateTime<Utc>>,
pub expires_at: Option<DateTime<Utc>>,
}
@@ -0,0 +1,40 @@
//! 插件可见的 Client Key 非秘密资源投影
use gateway_core::policy::ClientApiKeyId;
use gateway_core::routing::AccountGroupId;
use super::PageSize;
/// 插件可读取的当前 Key 范围,不含密钥或配置秘密
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginClientKeyFacts {
pub id: ClientApiKeyId,
pub enabled: bool,
pub group_ids: Vec<AccountGroupId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginClientKeyCursor {
pub name: String,
pub id: ClientApiKeyId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginClientKeyListQuery {
pub cursor: Option<PluginClientKeyCursor>,
pub limit: PageSize,
}
/// 插件只能发现调用所需的公开身份,不取得 Key 前缀、策略细节或明文
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginClientKey {
pub id: ClientApiKeyId,
pub name: String,
pub enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginClientKeyPage {
pub items: Vec<PluginClientKey>,
pub next_cursor: Option<PluginClientKeyCursor>,
}

Some files were not shown because too many files have changed in this diff Show More