feat: billing correction, credit quota, scheduler modes, image-to-image, 5h/7d cost, and codex-auto-review#144
Conversation
…_url When adding accounts via OAuth flow, if no proxy_url is provided in either the generate-auth-url step or the exchange-code step, the request to auth.openai.com goes direct — which fails from geo-blocked regions (HK, etc.) with 403 or Cloudflare 502. Add a fallback to `h.store.GetProxyURL()` (the system default proxy) in both `ExchangeOAuthCode` and `OAuthCallback` handlers, so OAuth token exchange always goes through a working proxy chain. Fixes the issue where the admin UI's "Add Account via OAuth" button returns a Cloudflare 502 HTML page instead of a proper error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Credit quota (james-6-23#141): - Add credit_enabled / credit_skip_usage_window to accounts - DB migrations, Account struct, admin API endpoint - AccountUsageModal toggle switches with i18n - Credit accounts skip usage window checks in scheduler Scheduler mode (james-6-23#133): - scheduler_mode: round_robin (default) / remaining_quota - remaining_quota sorts by usage percentage ascending - FastScheduler, Store, Settings API, frontend dropdown - All test calls updated Image studio (james-6-23#135, james-6-23#136): - admin/image_studio.go: add image-to-image generation mode - proxy/images.go: improve error handling and retry logic - proxy/admin_images.go: add image-to-image endpoint - Frontend: add image-to-image tab in ImageStudio Docker security (james-6-23#134): - SQLite compose files bind to 127.0.0.1 by default Co-Authored-By: Claude
- Add mode toggle: text-to-image / image-to-image - File upload with drag-and-drop, base64 conversion - Thumbnail preview grid with remove buttons - Wire createImageEditJob API call - Add 'upload' i18n key (zh/en) Co-Authored-By: Claude
- Register missing PATCH /accounts/:id/credit route - Add updateAccountCredit function to frontend api.ts - Add credit_enabled/credit_skip_usage_window to accountResponse and populate from database row in ListAccounts Co-Authored-By: Claude
Previous change removed ${BIND_HOST:-0.0.0.0} in favor of hardcoded
127.0.0.1. This breaks users who set BIND_HOST=0.0.0.0 in .env.
Now defaults to 127.0.0.1 (secure) but allows override via env var.
Co-Authored-By: Claude
CreditEnabled and CreditSkipUsageWindow were inserted into the SELECT before ScoreBiasOverride, but the Scan() targets were in the wrong order (ScoreBiasOverride first). This caused all three values to be silently corrupted at runtime. In GetAccountByID, there was also a duplicate &a.CreditEnabled and missing &a.ScoreBiasOverride - same root cause from the credit agent. Co-Authored-By: Claude
Image/Proxy fixes (11 bugs): - CRITICAL: fix double-write in non-stream retry path - HIGH: add client disconnect check to prevent account drain - Add 20MB upload limit, 100MB base64 limit, 10-image cap - Add PNG→JPEG fallback for image edit jobs - Fix saveImageJobAssets timeout (use ctx, not Background) - Fix rerunFromJob dropping input_images on edit jobs - Use Promise.allSettled, optimize base64 decode - Set HTTP client Timeout to 10min safety net Auth/Scheduler fixes (7 bugs): - SetSchedulerMode now re-sorts all buckets - Rebuild respects schedulerMode for initial sort - GetSchedulerMode fallback for nil settings - usageExhaustedLocked respects CreditSkipUsageWindow - Reset zeroCursor on each scan pass - Add remaining_quota test coverage (3 new tests) - Wire SetBaseLimit into Store.SetMaxConcurrency Docs: README, zh-CN README, CONFIGURATION, API docs updated Co-Authored-By: Claude
- Fix broken api.ts where updateAccountCredit was injected into getAccountUsage body (sed corruption), breaking frontend build - Update billing tests for corrected gpt-5.5 pricing ($5/$30) - Rewrite TestGPT55PricingMatchesGPT54Fallback to verify 2x pricing Co-Authored-By: Claude
When streaming image generation and a mid-stream error is retryable, do not retry if SSE data has already been committed to the client. Retrying would interleave SSE events from two different upstream accounts on the same connection. Co-Authored-By: Claude
Read errors (content policy, safety refusals, upstream failures) were not logged to usage_logs or reported to account health metrics. Add logUsageForRequest calls before non-retryable returns in both non-stream and stream error paths. Co-Authored-By: Claude
Add "Cost (USD)" column showing account_billed from usage stats. Column is toggleable via column settings. Data already returned by backend (account_billed field in accountResponse). Co-Authored-By: Claude
Replace single total cost column with windowed billing display: - Add GetAccountBilledSince() to sum account_billed per time window - Add billed_5h / billed_7d to accountResponse - Display "5h: $X.XX / 7d: $Y.YY" in accounts table - Windows based on reset_5h_at / reset_7d_at from account state Co-Authored-By: Claude
Co-Authored-By: Claude
…ode change Commit fd7b5a2 accidentally deleted s.modelMapping.Store() and gated SetPromptFilterConfig behind the ModelMapping != "" condition. This caused model mapping to never load from database on cold start, and prompt filtering to not initialize unless model mapping was also set. Co-Authored-By: Claude
… guard streamStarted was set before streamImagesResponse was called, making the retry guard always dead (streamStarted always true). Replace with c.Writer.Written() which checks if headers have actually been committed, allowing retry when no data has been sent to the client yet. Co-Authored-By: Claude
…editEnabled gating - PATCH /accounts/:id/credit now uses tri-state (*bool): nil fields are left unchanged, preventing silent zeroing of unprovided fields - SetSchedulerMode validates mode string, coerces unknowns to round_robin - CreditSkipUsageWindow now gated behind CreditEnabled (both must be true) Co-Authored-By: Claude
- Verify empty mode coerces to round_robin - Verify SetSchedulerMode re-sorts buckets for remaining_quota - Verify lowest-usage account picked after mode switch Co-Authored-By: Claude
…o data sent When streamImagesResponse returns an error before any SSE data was committed (e.g., streaming not supported), the client received a dropped connection with no error. Now writes a JSON error response when c.Writer.Written() is false on the non-retryable fallthrough. Co-Authored-By: Claude
- README: enriched feature table, OAuth PKCE section, credit docs - CONFIGURATION: added BIND_HOST, CODEX_PROXY_URL, scheduler_mode - API: added credit/billed fields, image edit-jobs endpoint, settings - New CHANGELOG.md summarizing all 17 iteration commits Co-Authored-By: Claude
…ilies - gpt-4o: add CacheReadPricePerMToken $1.25 (was missing) - gpt-4o-mini: add CacheReadPricePerMToken $0.075 - New families: gpt-4.1, gpt-4.1-mini, gpt-4.1-nano - New families: o3, o4-mini, o3-mini All prices verified against official OpenAI pricing (May 2026). Co-Authored-By: Claude
Models with long context pricing (gpt-5.5, gpt-5.5-pro, gpt-5.4, gpt-5.4-pro) now automatically use elevated rates when input_tokens exceeds 272,000. Pricing verified against official OpenAI docs. Also add gpt-4o cache read pricing ($1.25/M) and gpt-4.1 / o-series families with correct cache prices. Co-Authored-By: Claude
gpt-5.5-pro and gpt-5.4-pro were swallowed by generic gpt-5.5/gpt-5.4 cases, making their $30/$180 pricing rules dead code. Add explicit -pro cases before generic matches so pro variants route correctly. Add TestProModelsHaveCorrectPricing to prevent regression. Co-Authored-By: Claude
…ANGELOG Co-Authored-By: Claude
codex-auto-review is Codex's automatic approval review model: - 272K context, 128K max output, thinking low/medium/high/xhigh - Tools support for approval evaluation - Mapped to gpt-5.3-codex pricing tier ($1.75/$14/M) Source: CLIProxyAPI models.json registry. Co-Authored-By: Claude
…match) codex-auto-review has 272K context + 4 thinking levels, matching gpt-5.4 specs exactly. Previously mapped to gpt-5.3-codex (.75/$14) which has only 128K context and no thinking support.
Findings from CPA analysis and live testing: - Model only works via chatgpt.com/backend-api/codex, not public API - Official codex_client_models.json: visibility=hide, shell_type=shell_command - Available on Plus/Pro/Team/Business plans, explicitly excludes free - codex2api currently only proxies public API, so this model is disabled - Pricing remains mapped to gpt-5.4 ($2.50/$15.00) based on spec match Co-Authored-By: Claude
Tested on hk2 backend API (chatgpt.com/backend-api/codex): upstream response returns "model":"gpt-5.4" for codex-auto-review requests. Model is confirmed as a gpt-5.4 routing alias. Pricing at gpt-5.4 tier verified correct. Co-Authored-By: Claude
- TestLongContextPricingTriggersAbove272KTokens - TestLongContextPricingWithPriorityTier - TestLongContextPricingDoesNotApplyWhenNoLongPricingDefined - TestProModelsHaveCorrectPricing (existing, verified) Co-Authored-By: Claude
Previously zeroCursor.Store(0) was at the outer loop, so after scanning the Healthy tier (failing), the cursor advanced and Warm/Risky tiers started from a stale position — violating lowest-usage-first ordering. Now resets per-tier so each bucket independently scans from index 0. Co-Authored-By: Claude
Co-Authored-By: Claude
Allows downstream clients to opt into Codex beta features (e.g. responses_websockets) by passing the header through to the upstream backend API. Co-Authored-By: Claude
📝 WalkthroughWalkthroughAdds scheduler modes and per-account credit controls, expands billing pricing and exposes cost APIs, implements image-edit (image-to-image) jobs and hardened proxy/image handling, persists scheduler_mode and credit columns, updates admin endpoints and frontend UIs, and documents Docker localhost-binding and related config. ChangesScheduler Mode Configuration & Account Credit Controls
Billing Cost Calculation & Tests
Image Edit Job Creation & Resilience
Frontend, API client, and Localization
Documentation & Configuration
🎯 4 (Complex) | ⏱️ ~60 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/API.md (1)
290-292:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win修正模型列表示例中的重复 ID。
Line 291 与 Line 290 都是
gpt-5.5,示例列表出现重复条目,容易误导调用方。🧩 建议修正
- { "id": "gpt-5.5", "object": "model", "owned_by": "openai" }, - { "id": "gpt-5.5", "object": "model", "owned_by": "openai" }, + { "id": "gpt-5.5", "object": "model", "owned_by": "openai" }, + { "id": "gpt-5.4", "object": "model", "owned_by": "openai" },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/API.md` around lines 290 - 292, 示例模型列表中出现重复的 "gpt-5.5" 条目,导致误导调用方;请在 docs/API.md 中找到模型数组里重复的 { "id": "gpt-5.5", "object": "model", "owned_by": "openai" }(第二个出现的条目),将其删除或替换为正确且唯一的模型 ID(例如改为 "gpt-5.4" 或其他真实模型 ID),确保最终模型列表中的 id 值唯一且与实际可用模型一致。
🧹 Nitpick comments (6)
docs/API.md (1)
484-488: ⚡ Quick win补充 credit 字段的生效前提,避免误配置。
建议在参数说明中明确:
credit_skip_usage_window仅在credit_enabled=true时生效。📝 建议补充
-| credit_skip_usage_window | bool | 否 | 跳过 7 天/5 小时用量窗口惩罚,省略时保持原值 | +| credit_skip_usage_window | bool | 否 | 跳过 7 天/5 小时用量窗口惩罚(仅在 `credit_enabled=true` 时生效),省略时保持原值 |🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/API.md` around lines 484 - 488, Update the parameter table so the description for credit_skip_usage_window clearly states it only applies when credit_enabled is true; modify the row for credit_skip_usage_window to add a parenthetical or sentence like "仅在 credit_enabled=true 时生效" (or the English equivalent) and, if helpful, add a short note to the credit_enabled row indicating that related credit fields (e.g., credit_skip_usage_window) depend on it.docs/CONFIGURATION.md (1)
46-46: ⚡ Quick win区分 BIND_HOST 与应用监听地址,减少运维误解。
Line 46 建议将
BIND_HOST描述为“Docker 端口发布绑定地址”,避免与CODEX_BIND(应用监听地址)混淆。🔧 建议修正
-| `BIND_HOST` | 否 | `127.0.0.1`(SQLite)/ `0.0.0.0`(PostgreSQL) | HTTP 绑定地址。SQLite compose 默认 `127.0.0.1` 仅本机访问;标准 compose 默认 `0.0.0.0` 所有网络接口 | +| `BIND_HOST` | 否 | `127.0.0.1`(SQLite)/ `0.0.0.0`(PostgreSQL) | Docker 端口发布绑定地址(非进程监听地址)。SQLite compose 默认 `127.0.0.1` 仅本机访问;标准 compose 默认 `0.0.0.0` 所有网络接口 |🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/CONFIGURATION.md` at line 46, Update the BIND_HOST environment variable description to clarify it is the Docker/container port publish bind address (used by Docker/compose to bind host network interfaces) and not the application listen address; explicitly mention CODEX_BIND as the application listen address (socket/interface the app binds to inside the container) so readers won’t confuse the two (edit the table row referencing BIND_HOST and add a short note referencing CODEX_BIND for contrast).admin/image_studio.go (1)
337-340: ⚡ Quick winConsider validating input image count before creating the job.
The handler validates that
input_imagesis non-empty but does not check againstMaxImageEditInputCount. The count validation happens later inbuildAdminImageEditRequest, causing the job to be created and then fail asynchronously. For consistent UX, consider adding early validation:if len(req.InputImages) == 0 { writeError(c, http.StatusBadRequest, "图生图需要上传参考图片") return } +if len(req.InputImages) > proxy.MaxImageEditInputCount { + writeError(c, http.StatusBadRequest, fmt.Sprintf("参考图片数量超过限制 (%d, 最多 %d)", len(req.InputImages), proxy.MaxImageEditInputCount)) + return +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@admin/image_studio.go` around lines 337 - 340, The handler currently only checks req.InputImages non-empty but defers max-count validation to buildAdminImageEditRequest, causing jobs to be created then fail asynchronously; update the handler (the function handling the request in admin/image_studio.go) to also validate len(req.InputImages) <= MaxImageEditInputCount before creating the job and call writeError(c, http.StatusBadRequest, ...) when exceeded, mirroring the validation performed in buildAdminImageEditRequest so failures are returned synchronously; reference req.InputImages, MaxImageEditInputCount, buildAdminImageEditRequest, and writeError when locating where to add the early check.database/postgres.go (1)
3367-3395: ⚡ Quick winMissing rows-affected check may silently ignore updates to non-existent accounts.
UpdateAccountCreditdoesn't check if any rows were affected, unlike similar functions likeSetAccountEnabled(line 3346) which returnsql.ErrNoRowswhen no rows match. This could make it harder to detect when an update targets a non-existent or deleted account.Suggested fix
- _, err := db.conn.ExecContext(ctx, query, args...) - return err + res, err := db.conn.ExecContext(ctx, query, args...) + if err != nil { + return err + } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return sql.ErrNoRows + } + return nil🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@database/postgres.go` around lines 3367 - 3395, UpdateAccountCredit currently executes the UPDATE without checking whether any row was modified, so updates targeting non-existent accounts are silently ignored; modify UpdateAccountCredit to inspect the sql.Result from db.conn.ExecContext (via RowsAffected) and if RowsAffected == 0 return sql.ErrNoRows (matching behavior in SetAccountEnabled), otherwise return nil or the Exec error—use the existing variables (query, args) and handle the result before returning.database/billing.go (1)
358-375: 💤 Low valueRedundant struct copy in
calculateCostBreakdownwrapper.The wrapper function copies all fields from the returned
CostBreakdowninto a new struct with identical values. SinceCostBreakdownis now the same type (no longer a separate unexported type), simply returningbddirectly would suffice.♻️ Simplified wrapper
func calculateCostBreakdown(inputTokens, outputTokens, cachedTokens int, model string, serviceTier string) CostBreakdown { - bd := CalculateCostBreakdown(inputTokens, outputTokens, cachedTokens, model, serviceTier) - return CostBreakdown{ - InputCost: bd.InputCost, - OutputCost: bd.OutputCost, - CacheReadCost: bd.CacheReadCost, - TotalCost: bd.TotalCost, - InputPricePerMToken: bd.InputPricePerMToken, - OutputPricePerMToken: bd.OutputPricePerMToken, - CacheReadPricePerMToken: bd.CacheReadPricePerMToken, - ServiceTierCostMultiplier: bd.ServiceTierCostMultiplier, - } + return CalculateCostBreakdown(inputTokens, outputTokens, cachedTokens, model, serviceTier) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@database/billing.go` around lines 358 - 375, The calculateCostBreakdown wrapper unnecessarily copies every field from the CostBreakdown returned by CalculateCostBreakdown into a new identical CostBreakdown; change calculateCostBreakdown to simply return the bd value from CalculateCostBreakdown directly (i.e., return CalculateCostBreakdown(inputTokens, outputTokens, cachedTokens, model, serviceTier)) and remove the manual field-by-field construction, keeping the function name calculateCostBreakdown and the call to CalculateCostBreakdown unchanged.admin/handler.go (1)
636-655: 🏗️ Heavy liftN+1 query pattern may cause timeouts with many accounts.
This loop issues up to 2
GetAccountBilledSincequeries per account. With hundreds of accounts, this could exceed the 5-second context timeout and degrade admin API responsiveness.Consider batching: fetch all account IDs with valid reset timestamps, then query billed sums in a single aggregate query grouped by account_id.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@admin/handler.go` around lines 636 - 655, The current loop calls h.db.GetAccountBilledSince per account (via acc.GetReset5hAt / acc.GetReset7dAt) causing an N+1 query problem; instead gather all account IDs that have non-zero GetReset5hAt and GetReset7dAt into two slices/maps, call a new batched DB method (e.g. GetAccountsBilledSinceGrouped(ctx, []accountID, sinceTime) or a single aggregate that accepts multiple (account_id, since) rows) to return billed sums keyed by account_id, then iterate accounts and set accounts[i].Billed5h and accounts[i].Billed7d from the returned map(s); update handler.go to replace per-account GetAccountBilledSince calls with these batched calls and add the corresponding DB interface/implementation changes in h.db.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@admin/handler.go`:
- Around line 686-698: The handler fetches acc via h.store.FindByID before
calling h.store.UpdateAccountCredit, then returns acc.CreditEnabled and
acc.CreditSkipUsageWindow which are stale; after a successful
UpdateAccountCredit(id, req.CreditEnabled, req.CreditSkipUsageWindow) call,
re-fetch the account (e.g., call h.store.FindByID(id) or otherwise obtain the
updated account) and use that updated object's CreditEnabled and
CreditSkipUsageWindow for the JSON response so the returned fields reflect the
applied changes.
In `@auth/fast_scheduler.go`:
- Around line 102-112: The else branch inside SetSchedulerMode has inconsistent
indentation: move the else and its inner sort.SliceStable block to align with
the matching if/else level used in SetSchedulerMode so formatting matches
surrounding code; locate the SetSchedulerMode function and the
sort.SliceStable(entries, ...) lambda (which references
entries[i].dispatchScore, entries[i].proven, entries[i].dbID) and adjust the
braces/indentation so the closing brace and the else keyword are at the same
indentation level as the corresponding if block.
In `@frontend/src/components/AccountUsageModal.tsx`:
- Around line 140-149: The credit switch buttons in AccountUsageModal.tsx (the
button that calls handleCreditToggle and uses creditEnabled/savingCredit) lack
accessible names and remove default focus styles; add an accessible label via
aria-label or aria-labelledby (e.g., aria-label="Enable credits" or link to a
visible label element) and restore visible keyboard focus by replacing
focus:outline-none with a visible focus style (e.g., focus:ring or focus-visible
styles) so keyboard users see focus; apply the same changes to the second switch
instance that mirrors lines 157-166.
In `@frontend/src/pages/ImageStudio.tsx`:
- Around line 816-825: The submit logic is using the component state
imageToImageMode directly which can be mutated by rerunFromJob before submitJob
runs; update submitJob to accept an explicit mode parameter (e.g., isEditMode)
and decide between createImageEditJob/createImageJob based on that local
parameter instead of reading imageToImageMode from state, and update
rerunFromJob to compute the correct isEditMode for the job it wants to rerun and
pass it into submitJob; additionally ensure rerunFromJob resets UI-only state
(call setImageToImageMode and clear/set input images via setInputImageDataURLs)
appropriately for non-edit reruns so the UI and payload remain consistent.
- Around line 437-471: handleImageFileChange reads files asynchronously and
snapshots inputImageDataURLs earlier, allowing race conditions to exceed
MAX_INPUT_IMAGES; fix by clamping inside the state updater: when calling
setInputImageDataURLs, use the functional form (prev => { const merged =
[...prev, ...dataURLs]; const allowed = merged.slice(0, MAX_INPUT_IMAGES); if
(allowed.length < merged.length) showToast(t('images.maxInputImages', { max:
MAX_INPUT_IMAGES }), 'error'); return allowed; }) so the limit is enforced
atomically regardless of overlapping uploads; reference MAX_INPUT_IMAGES,
inputImageDataURLs, setInputImageDataURLs and handleImageFileChange.
In `@README.zh-CN.md`:
- Line 528: Update the README zh-CN description for the `remaining_quota`
scheduling policy to remove ambiguity: change the phrase “优先使用用量的账号” to
“优先使用用量较低的账号”, so the full line reads “`remaining_quota` | 优先使用用量较低的账号;用量相同时轮询”,
ensuring the `remaining_quota` label clearly matches its intended behavior.
---
Outside diff comments:
In `@docs/API.md`:
- Around line 290-292: 示例模型列表中出现重复的 "gpt-5.5" 条目,导致误导调用方;请在 docs/API.md
中找到模型数组里重复的 { "id": "gpt-5.5", "object": "model", "owned_by": "openai"
}(第二个出现的条目),将其删除或替换为正确且唯一的模型 ID(例如改为 "gpt-5.4" 或其他真实模型 ID),确保最终模型列表中的 id
值唯一且与实际可用模型一致。
---
Nitpick comments:
In `@admin/handler.go`:
- Around line 636-655: The current loop calls h.db.GetAccountBilledSince per
account (via acc.GetReset5hAt / acc.GetReset7dAt) causing an N+1 query problem;
instead gather all account IDs that have non-zero GetReset5hAt and GetReset7dAt
into two slices/maps, call a new batched DB method (e.g.
GetAccountsBilledSinceGrouped(ctx, []accountID, sinceTime) or a single aggregate
that accepts multiple (account_id, since) rows) to return billed sums keyed by
account_id, then iterate accounts and set accounts[i].Billed5h and
accounts[i].Billed7d from the returned map(s); update handler.go to replace
per-account GetAccountBilledSince calls with these batched calls and add the
corresponding DB interface/implementation changes in h.db.
In `@admin/image_studio.go`:
- Around line 337-340: The handler currently only checks req.InputImages
non-empty but defers max-count validation to buildAdminImageEditRequest, causing
jobs to be created then fail asynchronously; update the handler (the function
handling the request in admin/image_studio.go) to also validate
len(req.InputImages) <= MaxImageEditInputCount before creating the job and call
writeError(c, http.StatusBadRequest, ...) when exceeded, mirroring the
validation performed in buildAdminImageEditRequest so failures are returned
synchronously; reference req.InputImages, MaxImageEditInputCount,
buildAdminImageEditRequest, and writeError when locating where to add the early
check.
In `@database/billing.go`:
- Around line 358-375: The calculateCostBreakdown wrapper unnecessarily copies
every field from the CostBreakdown returned by CalculateCostBreakdown into a new
identical CostBreakdown; change calculateCostBreakdown to simply return the bd
value from CalculateCostBreakdown directly (i.e., return
CalculateCostBreakdown(inputTokens, outputTokens, cachedTokens, model,
serviceTier)) and remove the manual field-by-field construction, keeping the
function name calculateCostBreakdown and the call to CalculateCostBreakdown
unchanged.
In `@database/postgres.go`:
- Around line 3367-3395: UpdateAccountCredit currently executes the UPDATE
without checking whether any row was modified, so updates targeting non-existent
accounts are silently ignored; modify UpdateAccountCredit to inspect the
sql.Result from db.conn.ExecContext (via RowsAffected) and if RowsAffected == 0
return sql.ErrNoRows (matching behavior in SetAccountEnabled), otherwise return
nil or the Exec error—use the existing variables (query, args) and handle the
result before returning.
In `@docs/API.md`:
- Around line 484-488: Update the parameter table so the description for
credit_skip_usage_window clearly states it only applies when credit_enabled is
true; modify the row for credit_skip_usage_window to add a parenthetical or
sentence like "仅在 credit_enabled=true 时生效" (or the English equivalent) and, if
helpful, add a short note to the credit_enabled row indicating that related
credit fields (e.g., credit_skip_usage_window) depend on it.
In `@docs/CONFIGURATION.md`:
- Line 46: Update the BIND_HOST environment variable description to clarify it
is the Docker/container port publish bind address (used by Docker/compose to
bind host network interfaces) and not the application listen address; explicitly
mention CODEX_BIND as the application listen address (socket/interface the app
binds to inside the container) so readers won’t confuse the two (edit the table
row referencing BIND_HOST and add a short note referencing CODEX_BIND for
contrast).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 40383735-2e43-4e60-aa96-8c87e9b398cb
📒 Files selected for processing (31)
.env.example.env.sqlite.exampleREADME.mdREADME.zh-CN.mdadmin/handler.goadmin/image_studio.goadmin/oauth.goauth/fast_scheduler.goauth/fast_scheduler_test.goauth/store.godatabase/billing.godatabase/billing_test.godatabase/postgres.godatabase/sqlite.godocker-compose.sqlite.local.ymldocker-compose.sqlite.ymldocs/API.mddocs/CHANGELOG.mddocs/CONFIGURATION.mdfrontend/src/api.tsfrontend/src/components/AccountUsageModal.tsxfrontend/src/locales/en.jsonfrontend/src/locales/zh.jsonfrontend/src/pages/Accounts.tsxfrontend/src/pages/ImageStudio.tsxfrontend/src/pages/Settings.tsxfrontend/src/types.tsproxy/admin_images.goproxy/executor.goproxy/images.goproxy/model_registry.go
There was a problem hiding this comment.
Pull request overview
This PR delivers a v2 feature sweep across billing/pricing, scheduling behavior, admin image tooling (incl. image-to-image), and operational hardening (SQLite Docker port binding + OAuth proxy fallback). It extends the backend scheduler and billing engine, surfaces new per-account fields in the admin UI/API, and updates documentation accordingly.
Changes:
- Expand/adjust billing rules (GPT-5.5(+pro), long-context premiums, cache-read pricing) and expose cost breakdowns/tests.
- Add
scheduler_mode(round_robin / remaining_quota) + per-account credit flags, with admin API + UI support. - Add admin image-to-image jobs and improve image request retry/error handling; harden SQLite compose port binding defaults.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| README.zh-CN.md | Documents new scheduler modes, credit flags, billing tips, OAuth PKCE, image-to-image, SQLite bind defaults. |
| README.md | English README updates for scheduler modes, credit flags, pricing notes, OAuth PKCE, image-to-image, SQLite bind defaults. |
| proxy/model_registry.go | Registers codex-auto-review as a disabled builtin model with notes. |
| proxy/images.go | Adds image retry caps, edit input limits, base64 decode guards, and stream retry/error logging adjustments. |
| proxy/executor.go | Changes pooled HTTP client timeout behavior. |
| proxy/admin_images.go | Adds in-process Images Edits execution helper for admin image studio edit jobs. |
| frontend/src/types.ts | Adds billed window fields + credit flags + scheduler_mode + image edit payload input_images typing. |
| frontend/src/pages/Settings.tsx | Adds scheduler mode selector to settings UI. |
| frontend/src/pages/ImageStudio.tsx | Adds text-to-image vs image-to-image mode and reference image upload handling. |
| frontend/src/pages/Accounts.tsx | Adds per-account billed (5h/7d) column rendering. |
| frontend/src/locales/zh.json | Adds zh translations for credit settings, billed, image-to-image UI strings, scheduler mode. |
| frontend/src/locales/en.json | Adds en translations for credit settings, billed, image-to-image UI strings, scheduler mode. |
| frontend/src/components/AccountUsageModal.tsx | Adds per-account credit flag toggles in the usage modal. |
| frontend/src/api.ts | Adds PATCH account credit endpoint + image edit job creation endpoint. |
| docs/CONFIGURATION.md | Documents BIND_HOST, CODEX_ALLOW_ANONYMOUS, scheduler mode, and credit flags. |
| docs/CHANGELOG.md | Adds a May 2026 v2 changelog entry summarizing the release. |
| docs/API.md | Documents new fields (credit + billed windows) and new admin endpoints (credit + image edit jobs). |
| docker-compose.sqlite.yml | Binds SQLite compose ports to 127.0.0.1 by default (override via BIND_HOST). |
| docker-compose.sqlite.local.yml | Same localhost-only bind default for local SQLite compose. |
| database/sqlite.go | Adds scheduler_mode to system settings and credit columns to accounts (SQLite migrations). |
| database/postgres.go | Adds credit columns, system setting scheduler_mode, billed aggregation helper, and scan/select updates. |
| database/billing.go | Expands pricing rules, adds long-context pricing, exports pricing/cost breakdown APIs. |
| database/billing_test.go | Updates/extends billing tests for new pricing rules and exported APIs. |
| auth/store.go | Adds scheduler_mode plumbing, credit behavior in scheduling penalties, and store update methods. |
| auth/fast_scheduler.go | Adds remaining_quota mode logic and resorting on mode change. |
| auth/fast_scheduler_test.go | Adds/updates tests for scheduler mode behavior. |
| admin/oauth.go | Adds system proxy fallback when per-request/session proxy URL is absent. |
| admin/image_studio.go | Adds admin image edit job endpoint and execution path via proxy image edits. |
| admin/handler.go | Exposes credit + billed fields; adds PATCH credit route; exposes scheduler_mode in settings API. |
| .env.sqlite.example | Documents SQLite compose localhost binding and BIND_HOST override. |
| .env.example | Documents standard compose bind expectations and BIND_HOST override. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| client: &http.Client{ | ||
| Transport: transport, | ||
| Timeout: 0, | ||
| Timeout: 10 * time.Minute, |
| cursor := &s.cursors[tierIdx] | ||
| if s.schedulerMode == "remaining_quota" { | ||
| cursor = &zeroCursor | ||
| } | ||
| acc, stale := s.scanRangeLocked(tier, 0, len(bucket), cursor, baseLimit, now, apiKeyID, exclude, filter) |
| if len(req.InputImages) == 0 { | ||
| writeError(c, http.StatusBadRequest, "图生图需要上传参考图片") | ||
| return | ||
| } |
| if t := acc.GetReset5hAt(); !t.IsZero() { | ||
| billed, err := h.db.GetAccountBilledSince(ctx, accounts[i].ID, t.Add(-5*time.Hour)) | ||
| if err == nil { | ||
| accounts[i].Billed5h = &billed | ||
| } |
| if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil { | ||
| writeError(c, http.StatusBadRequest, "请求格式错误") | ||
| return | ||
| } |
| </Field> | ||
|
|
||
| <div className="flex items-center gap-2"> | ||
| <span className="text-xs font-semibold text-muted-foreground">{t('images.mode')}:</span> |
…tation, race conditions, and docs
- handler: re-fetch account after UpdateAccountCredit, add DisallowUnknownFields
- fast_scheduler: fix } else { indentation (was nested inside if body)
- AccountUsageModal: add aria-label, focus-visible styles, disabled cursor state
- ImageStudio: clamp input images in setter, decouple rerun from stale UI state
- billing: simplify calculateCostBreakdown wrapper
- postgres: return sql.ErrNoRows when UpdateAccountCredit affects zero rows
- image_studio: validate input image count before job creation
- docs: fix duplicate model ID, clarify BIND_HOST and credit_skip_usage_window
- README.zh-CN: fix remaining_quota description ambiguity
- locales: add missing images.mode key
Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
admin/image_studio.go (1)
337-344:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReject blank
input_imagesentries before queueing the job.The current guard only checks slice length, so
["", " "]passes validation and creates a job that can only fail later whenbuildAdminImageEditRequestsends emptyimage_urlvalues upstream. Trim/filter each entry first and return400if nothing valid remains.💡 Minimal fix
if len(req.InputImages) == 0 { writeError(c, http.StatusBadRequest, "图生图需要上传参考图片") return } + normalizedImages := make([]string, 0, len(req.InputImages)) + for _, raw := range req.InputImages { + raw = strings.TrimSpace(raw) + if raw != "" { + normalizedImages = append(normalizedImages, raw) + } + } + req.InputImages = normalizedImages + if len(req.InputImages) == 0 { + writeError(c, http.StatusBadRequest, "图生图需要上传参考图片") + return + } if len(req.InputImages) > proxy.MaxImageEditInputCount { writeError(c, http.StatusBadRequest, fmt.Sprintf("参考图片数量超过限制 (%d, 最多 %d)", len(req.InputImages), proxy.MaxImageEditInputCount)) return }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@admin/image_studio.go` around lines 337 - 344, The validation currently only checks len(req.InputImages) but allows blank/whitespace entries like ["", " "]; before the existing length checks and before queueing the job (in admin/image_studio.go), trim and filter req.InputImages to remove empty/whitespace-only strings (e.g., build a filtered slice), then use that filtered slice for subsequent checks against proxy.MaxImageEditInputCount and when calling buildAdminImageEditRequest; if the filtered slice is empty, return writeError(c, http.StatusBadRequest, "图生图需要上传参考图片") and if it exceeds proxy.MaxImageEditInputCount return the existing formatted error.database/billing.go (1)
125-139: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winReturn a copy from
GetModelPricinginstead of a shared global pointer.Now that this helper is exported, callers can accidentally mutate the package-level pricing table through the returned pointer and change billing for the whole process. Returning a copied struct preserves the read API without exposing mutable global state.
💡 Minimal fix
func GetModelPricing(model string) *ModelPricing { normalized := normalizeBillingModelName(model) if pricing := claudeFamilyPricing(normalized); pricing != nil { - return pricing + copy := *pricing + return © } if pricing := geminiFamilyPricing(normalized); pricing != nil { - return pricing + copy := *pricing + return © } if codexModel, ok := normalizeCodexBillingModel(normalized); ok { normalized = codexModel } if pricing := modelRulePricing(normalized); pricing != nil { - return pricing + copy := *pricing + return © } - return defaultModelPricing + copy := *defaultModelPricing + return © }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@database/billing.go` around lines 125 - 139, GetModelPricing currently returns pointers to package-level ModelPricing values which allows callers to mutate shared state; update it to return a copy instead. Modify GetModelPricing so that whenever it intends to return a non-nil pricing (results from claudeFamilyPricing, geminiFamilyPricing, modelRulePricing or defaultModelPricing), it returns a distinct copy (e.g., copy the struct value and return a pointer to that copy) or change the function signature to return a ModelPricing value and return copies directly; keep the same lookup flow (normalizeBillingModelName, normalizeCodexBillingModel, claudeFamilyPricing, geminiFamilyPricing, modelRulePricing) but ensure the returned object is not the original package-level pointer.proxy/executor.go (1)
201-205:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRemove the 10-minute timeout from pooled streaming clients.
http.Client.Timeoutapplies to the entire request-response cycle, including body reading. For pooled Codex/OpenAI clients that stream SSE responses, this means any stream exceeding 10 minutes gets aborted mid-response, causing the same stream-cutoff failures this change aims to reduce. Remove the timeout here and rely on request contexts (which callers already provide vianewDrainableUpstreamContext) or set deadlines at non-streaming call sites instead.Minimal fix
entry := &poolEntry{ client: &http.Client{ Transport: transport, - Timeout: 10 * time.Minute, }, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@proxy/executor.go` around lines 201 - 205, The pooled streaming client creation sets http.Client.Timeout to 10*time.Minute which aborts long SSE streams; remove the Timeout field from the http.Client in the poolEntry initialization so pooled clients rely on caller-provided contexts (e.g., newDrainableUpstreamContext) or set per-call deadlines at non-streaming sites; update the client creation where poolEntry and its client with Transport: transport are constructed to omit the Timeout assignment.admin/handler.go (1)
688-705:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon’t gate credit updates on runtime-store presence.
ListAccountsreturns rows fromh.db.ListActive(...)even when there is no matchingaccountMapentry, so an admin can see a valid account thath.store.FindByID(id)still returnsnilfor. This handler will 404 that account before attempting the update, and the post-update response has the same blind spot. Use the DB as the existence/source-of-truth lookup here instead of the runtime pool.🐛 Proposed fix
- acc := h.store.FindByID(id) - if acc == nil { - writeError(c, http.StatusNotFound, "账号不存在") - return - } + row, err := h.db.GetAccountByID(c.Request.Context(), id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + writeError(c, http.StatusNotFound, "账号不存在") + return + } + writeInternalError(c, err) + return + } // 传入 *bool:nil = 不修改该字段 if err := h.store.UpdateAccountCredit(id, req.CreditEnabled, req.CreditSkipUsageWindow); err != nil { writeError(c, http.StatusInternalServerError, "更新信用设置失败: "+err.Error()) return } - acc = h.store.FindByID(id) - if acc != nil { - c.JSON(http.StatusOK, gin.H{"message": "信用设置已更新", "credit_enabled": acc.CreditEnabled, "credit_skip_usage_window": acc.CreditSkipUsageWindow}) - } else { - c.JSON(http.StatusOK, gin.H{"message": "信用设置已更新"}) - } + row, err = h.db.GetAccountByID(c.Request.Context(), id) + if err == nil { + c.JSON(http.StatusOK, gin.H{ + "message": "信用设置已更新", + "credit_enabled": row.CreditEnabled, + "credit_skip_usage_window": row.CreditSkipUsageWindow, + }) + return + } + c.JSON(http.StatusOK, gin.H{"message": "信用设置已更新"})🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@admin/handler.go` around lines 688 - 705, The handler currently gates existence on the runtime store via h.store.FindByID which causes valid DB accounts to 404; change the existence and post-update response to use the persistent DB lookup instead of the runtime pool (use your DB lookup method — e.g., h.db.FindByID / h.db.GetAccount or whichever exists) to check the account and to fetch credit fields after calling h.store.UpdateAccountCredit(id, ...); remove the initial h.store.FindByID nil-return 404 check and instead 1) verify existence with the DB, 2) call h.store.UpdateAccountCredit with req.CreditEnabled/req.CreditSkipUsageWindow, and 3) reload the account from the DB to populate credit_enabled and credit_skip_usage_window in the JSON response.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@auth/fast_scheduler.go`:
- Around line 279-281: The code resets zeroCursor to 0 on every acquire when
s.schedulerMode == "remaining_quota", which causes scans to always start at
bucket 0 and hotspot the first account; change this to maintain a persistent
per-tier cursor instead of resetting zeroCursor each call—replace the
zeroCursor.Store(0) / cursor = &zeroCursor pattern with a persistent atomic
cursor per tier (e.g., a map from tier -> atomic.Int64 or sync/atomic value) and
rotate it (atomic increment with wrap modulo bucket count) so each acquire picks
up the next starting index within the lowest-usage cohort, ensuring even
distribution.
In `@database/billing_test.go`:
- Around line 297-301: The test currently repeats the lowercase entry and
therefore doesn't exercise case-insensitive normalization; update the third
element in the test data (the duplicate {"codex-auto-review", "gpt-5.4"}) to a
differently-cased variant such as {"Codex-Auto-Review", "gpt-5.4"} (or
"CODEX-AUTO-REVIEW") so the slice of inputs (the entries containing
{"codex-auto-review", ...}, {"codex-auto-review-v2", ...}, {"codex-auto-review",
...}, {"codex_auto_review", ...}) properly verifies case-insensitive
normalization in the test.
In `@frontend/src/pages/ImageStudio.tsx`:
- Around line 811-826: The submitJob function can mis-detect edit mode when
createJobPayload omits input_images; change submitJob to require forceMode for
all calls (make forceMode non-optional: forceMode: 'text' | 'edit'), compute
isEditMode as forceMode === 'edit', run the input_images presence check and
prompt validation based on that, and route to api.createImageEditJob or
api.createImageJob accordingly; update all callers of submitJob/Generate button
to pass the explicit forceMode value so the required-image check cannot be
bypassed.
---
Outside diff comments:
In `@admin/handler.go`:
- Around line 688-705: The handler currently gates existence on the runtime
store via h.store.FindByID which causes valid DB accounts to 404; change the
existence and post-update response to use the persistent DB lookup instead of
the runtime pool (use your DB lookup method — e.g., h.db.FindByID /
h.db.GetAccount or whichever exists) to check the account and to fetch credit
fields after calling h.store.UpdateAccountCredit(id, ...); remove the initial
h.store.FindByID nil-return 404 check and instead 1) verify existence with the
DB, 2) call h.store.UpdateAccountCredit with
req.CreditEnabled/req.CreditSkipUsageWindow, and 3) reload the account from the
DB to populate credit_enabled and credit_skip_usage_window in the JSON response.
In `@admin/image_studio.go`:
- Around line 337-344: The validation currently only checks len(req.InputImages)
but allows blank/whitespace entries like ["", " "]; before the existing length
checks and before queueing the job (in admin/image_studio.go), trim and filter
req.InputImages to remove empty/whitespace-only strings (e.g., build a filtered
slice), then use that filtered slice for subsequent checks against
proxy.MaxImageEditInputCount and when calling buildAdminImageEditRequest; if the
filtered slice is empty, return writeError(c, http.StatusBadRequest,
"图生图需要上传参考图片") and if it exceeds proxy.MaxImageEditInputCount return the
existing formatted error.
In `@database/billing.go`:
- Around line 125-139: GetModelPricing currently returns pointers to
package-level ModelPricing values which allows callers to mutate shared state;
update it to return a copy instead. Modify GetModelPricing so that whenever it
intends to return a non-nil pricing (results from claudeFamilyPricing,
geminiFamilyPricing, modelRulePricing or defaultModelPricing), it returns a
distinct copy (e.g., copy the struct value and return a pointer to that copy) or
change the function signature to return a ModelPricing value and return copies
directly; keep the same lookup flow (normalizeBillingModelName,
normalizeCodexBillingModel, claudeFamilyPricing, geminiFamilyPricing,
modelRulePricing) but ensure the returned object is not the original
package-level pointer.
In `@proxy/executor.go`:
- Around line 201-205: The pooled streaming client creation sets
http.Client.Timeout to 10*time.Minute which aborts long SSE streams; remove the
Timeout field from the http.Client in the poolEntry initialization so pooled
clients rely on caller-provided contexts (e.g., newDrainableUpstreamContext) or
set per-call deadlines at non-streaming sites; update the client creation where
poolEntry and its client with Transport: transport are constructed to omit the
Timeout assignment.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5dd9034b-2b79-414b-91ed-826d1cc30c70
📒 Files selected for processing (16)
README.zh-CN.mdadmin/handler.goadmin/image_studio.goauth/fast_scheduler.godatabase/billing.godatabase/billing_test.godatabase/postgres.godocs/API.mddocs/CHANGELOG.mddocs/CONFIGURATION.mdfrontend/src/components/AccountUsageModal.tsxfrontend/src/locales/en.jsonfrontend/src/locales/zh.jsonfrontend/src/pages/ImageStudio.tsxproxy/executor.goproxy/model_registry.go
✅ Files skipped from review due to trivial changes (4)
- docs/CHANGELOG.md
- docs/CONFIGURATION.md
- README.zh-CN.md
- frontend/src/locales/en.json
| if s.schedulerMode == "remaining_quota" { | ||
| zeroCursor.Store(0) | ||
| cursor = &zeroCursor |
There was a problem hiding this comment.
remaining_quota now hot-spots the first account in each tier.
Because zeroCursor is reset to 0 on every acquire, scans always start from bucket index 0. With usage snapshots only changing on updates/rebuilds, equal-usage accounts never rotate and the head entry absorbs traffic until it fills up. Keep a persistent cursor per tier, or at least rotate within the lowest-usage cohort, so this mode still spreads load instead of pinning to the first row.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@auth/fast_scheduler.go` around lines 279 - 281, The code resets zeroCursor to
0 on every acquire when s.schedulerMode == "remaining_quota", which causes scans
to always start at bucket 0 and hotspot the first account; change this to
maintain a persistent per-tier cursor instead of resetting zeroCursor each
call—replace the zeroCursor.Store(0) / cursor = &zeroCursor pattern with a
persistent atomic cursor per tier (e.g., a map from tier -> atomic.Int64 or
sync/atomic value) and rotate it (atomic increment with wrap modulo bucket
count) so each acquire picks up the next starting index within the lowest-usage
cohort, ensuring even distribution.
- normalizeCodexBillingModel: lowercase input for case-insensitive matching - billing_test: use uppercase CODEX-AUTO-REVIEW to actually test case insensitivity - ImageStudio: pass explicit forceMode from Generate button to prevent image-to-image mode silently falling through to text endpoint Co-Authored-By: Claude <noreply@anthropic.com>
|
https://linux.do/u/kkkyyx/summary 佬私信我留个联系方式😉 |
概要
基于
fix/oauth-proxy-url-fallback,合计 37 commits,30 个文件,+1780/-151。覆盖 11 个 issue/PR:定价修正、credit 额度策略、调度模式、图生图、5h/7d 成本展示、codex-auto-review 模型注册、生图稳定性、Docker 安全、OAuth 代理回退等。10+ 轮多模型交叉审查,全量测试通过。
Closes #129, Closes #132, Closes #133, Closes #134, Closes #135, Closes #136, Closes #138, Closes #139, Closes #140, Closes #141, Closes #143
定价
gpt-5.5 修正
之前跟 gpt-5.4 同价($2.50/$15.00 per 1M tokens),实际 OpenAI 官方定价是 2 倍:
gpt-5.5-pro、gpt-5.4-pro 新增独立定价($30/$180),通过
normalizeCodexBillingModel前缀匹配优先于通用规则。长上下文溢价
输入 token 超过 272,000 时自动切换到长上下文定价(input 2x, output 1.5x),覆盖 gpt-5.5、gpt-5.5-pro、gpt-5.4、gpt-5.4-pro。
ModelPricing新增Long*系列字段,CalculateCostBreakdown自动判断。codex-auto-review
Codex 内部自动审批模型,规格 272K context + 四档 thinking。实测上游返回
"model": "gpt-5.4",定价映射到 gpt-5.4 档。大小写均匹配。已注册到 builtin model list。credit 额度策略 (#141)
accounts表新增两列:credit_enabled— 标记账号存在额外 creditcredit_skip_usage_window— 跳过 5h/7d 用量窗口限制后者需要前者为 true 才生效。管理后台通过
PATCH /api/admin/accounts/:id/credit支持部分更新。含DisallowUnknownFields校验和 rows-affected 检查。调度模式 (#133)
system_settings新增scheduler_mode:round_robin(默认)——按 dispatch score 降序,cursor 轮转remaining_quota——按 7d usage 升序,优先低用量账号切换时实时重排全部 tier bucket。Settings 页面提供下拉选择器。
5h/7d 窗口成本 (#143)
账号列表新增成本列,按 5h 和 7d 额度窗口分别统计美元消耗。
GetAccountBilledSince按reset_5h_at/reset_7d_at聚合usage_logs.account_billed。图生图 (#135, #136)
Image Studio 新增 image-to-image 模式。支持上传参考图、输入 prompt 生成。后端通过
POST /api/admin/images/edit-jobs。.slice(0, MAX_INPUT_IMAGES)防竞态、显式forceMode参数防路由错误OAuth 代理回退 (#138)
当 OAuth session 和请求均未指定 proxy_url 时,回退到系统默认代理(
h.store.GetProxyURL())。修复地理受限地区 exchange-code 直接调用auth.openai.com导致 403 → 502 的问题。Docker (#134)
SQLite compose 端口绑定改为
127.0.0.1(通过BIND_HOST环境变量可覆盖)。CONFIGURATION.md区分 BIND_HOST(Docker 端口发布地址)与 CODEX_BIND(进程监听地址)。Bug 修复
scheduler_mode改动误删了s.modelMapping.Store()调用,重启后 prompt filter 不加载。已恢复。credit_enabled/credit_skip_usage_window/score_bias_override三个字段的 SELECT 列序与 Scan 参数顺序不一致,运行时数据损坏。已修正。codexAllowedForwardHeaders。submitJob不再依赖 stale UI state,显式forceMode参数。remaining_quotaREADME 中文歧义修正。兼容性
IF NOT EXISTS+ 安全默认值git pull && docker compose restart