Skip to content

feat(channel): OpenAI balance refresh via /v1/organization/costs (admin key)#4995

Open
a-lapkov wants to merge 23 commits into
QuantumNous:mainfrom
a-lapkov:feat/openai-admin-key-balance
Open

feat(channel): OpenAI balance refresh via /v1/organization/costs (admin key)#4995
a-lapkov wants to merge 23 commits into
QuantumNous:mainfrom
a-lapkov:feat/openai-admin-key-balance

Conversation

@a-lapkov
Copy link
Copy Markdown

@a-lapkov a-lapkov commented May 20, 2026

📝 变更描述 / Description

OpenAI 频道的 "刷新余额" 一直 403 — /v1/dashboard/billing/subscription 已被 OpenAI 弃用。本 PR 改用最新的 /v1/organization/costs 端点,并新增 3 个字段保存到 ChannelOtherSettings

  • openai_admin_key — admin-scoped key (sk-admin-...), 与推理用密钥分离
  • openai_prepaid_amount (optional) — 预付总额(USD)
  • openai_prepaid_since (optional) — 预付期开始时间 (Unix timestamp)

Balance 语义:

  • prepaid_amount > 0prepaid_since > 0 时:balance = prepaid_amount - sum(costs since prepaid_since)(剩余余额)
  • 否则:fallback 到当月累计 usage(spent MTD)
  • 结果 < 0 时 clamp 到 0

安全: openai_admin_key 在 API 响应中被 clearChannelInfo() 清零,与现有 inference key 的 masking 处理一致;前端编辑时显示 "Leave empty to keep existing admin key" 占位符;后端 UpdateChannel 中保留 DB 中已存的值。

OpenAI channel "Refresh Balance" was returning 403 because /v1/dashboard/billing/subscription was deprecated. This PR switches to /v1/organization/costs and adds 3 new optional fields under ChannelOtherSettings:

  • openai_admin_key — admin-scoped key (sk-admin-...), separate from the inference key
  • openai_prepaid_amount — total prepaid USD on the OpenAI account
  • openai_prepaid_since — Unix timestamp when the prepaid period started

Balance semantics:

  • When both prepaid_amount and prepaid_since are set: balance = prepaid_amount - sum(costs since prepaid_since) (remaining)
  • Otherwise: falls back to month-to-date cumulative cost (spent MTD)
  • Negative result is clamped to 0

Security: openai_admin_key is stripped from API responses by clearChannelInfo() (mirroring the existing inference-key masking); the frontend shows "Leave empty to keep existing admin key" placeholder when editing; backend UpdateChannel preserves the DB value when the payload field is empty.

🚀 变更类型 / Type

  • 🐛 Bug 修复 / Bug Fix
  • ✨ 新功能 / New Feature

🔗 关联任务 / Related Issue

Closes #4994

📦 改动 / Changes

  • dto/channel_settings.go: 3 new fields (OpenAIAdminKey, OpenAIPrepaidAmount, OpenAIPrepaidSince)
  • controller/channel-billing.go: 新增 updateChannelOpenAIBalance + 4 JSON DTO;OpenAI 分支改为调用新函数;分页 cap = 50;prepaid 模式 vs MTD 模式分流
  • controller/channel.go: clearChannelInfo() 清除 admin key;UpdateChannel 保留 DB 中的 admin key
  • model/channel.go: Clean() 方法清除 admin key
  • controller/channel_billing_test.go: 10 个测试 (happy path / pagination / no key / 403 / bad JSON / start_time / Clean / prepaid remaining / prepaid exhausted / prepaid partial fallback)
  • web/default/src/features/channels/...: 3 个新表单字段,仅 type=OpenAI 时显示;admin key 编辑时占位
  • web/default/src/i18n/locales/{en,ru,zh,fr,ja,vi}.json: 6 个新翻译键

🤝 兼容性 / Backward Compatibility

  • 现有 OpenAI 频道无 admin_key → 推理照常工作,刷新余额给出明确错误信息
  • 现有 OpenAI 频道有 admin_key 但无 prepaid 字段 → 显示 MTD 累计支出(旧行为)
  • Azure / Custom 分支未改动
  • 多 key 模式频道在调度层被跳过(既有行为)

⚠️ 语义变化 / Semantics Note

channel.Balance 的语义现在取决于是否配置了 prepaid:

  • 配置了 prepaid: "剩余可用" = prepaid_amount - sum(costs since prepaid_since)
  • 未配置: "当月已花费" (MTD spend, fallback)

OpenAI Costs API 不直接返回剩余额度,所以预付剩余需要管理员手动输入 prepaid_amount + prepaid_since

✅ 提交前检查项 / Checklist

  • 代码遵守 AGENTS.md Rule 1(使用 common.Marshal/Unmarshal 而非 encoding/json
  • 无需数据库迁移(所有字段存入既有 settings TEXT JSON 列)
  • go test ./controller/... 全部通过(10 OpenAI tests + existing)
  • go vet ./controller/... ./dto/... ./model/... 干净
  • gofmt -l 干净
  • 前端 npm run build 通过
  • npm run typecheck 通过 (tsc -b exit 0)
  • 6 个语言文件全部同步(npm run i18n:sync:0 missing keys)

📸 运行证明 / Proof of Work

Test output

=== RUN   TestUpdateChannelOpenAIBalance_SingleBucket           --- PASS
=== RUN   TestUpdateChannelOpenAIBalance_Pagination              --- PASS
=== RUN   TestUpdateChannelOpenAIBalance_NoAdminKey              --- PASS
=== RUN   TestUpdateChannelOpenAIBalance_Upstream403             --- PASS
=== RUN   TestUpdateChannelOpenAIBalance_BadJSON                 --- PASS
=== RUN   TestUpdateChannelOpenAIBalance_StartTimeIsFirstOfMonthUTC --- PASS
=== RUN   TestChannelClean_RemovesOpenAIAdminKey                 --- PASS
=== RUN   TestUpdateChannelOpenAIBalance_PrepaidRemaining        --- PASS
=== RUN   TestUpdateChannelOpenAIBalance_PrepaidExhausted        --- PASS
=== RUN   TestUpdateChannelOpenAIBalance_PrepaidPartialUnsetFallsBackToMTD --- PASS
PASS
ok  	github.com/QuantumNous/new-api/controller

Verification of Costs API parameter

Pagination cursor verified against openai-python SDK src/openai/resources/admin/organization/usage.py: def costs(start_time, end_time, group_by, limit, page, ...) — query param is page.

Production deployment

Running on production fork: alapkov/new-api:0.13.2-openaiadmin.3 since 2026-05-20.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Walkthrough

Adds OpenAI admin-key-backed balance refresh using paginated /v1/organization/costs aggregation; introduces DTO fields and controller/model handling to hide/preserve admin keys; adds frontend form fields and schema handling; expands tests and i18n strings.

Changes

OpenAI Admin Key Integration

Layer / File(s) Summary
OpenAI Costs API backend and tests
controller/channel-billing.go, controller/channel_billing_test.go, controller/channel.go, model/channel.go, dto/channel_settings.go
Implements updateChannelOpenAIBalance (paginated /v1/organization/costs aggregation using admin key), updates OpenAI branch in updateChannelBalance, adds tests for pagination, errors, and prepaid flows, and ensures OpenAIAdminKey is cleaned/preserved by controller/model.
Form schema and UI
web/default/src/features/channels/lib/channel-form.ts, web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
Adds openai_admin_key, openai_prepaid_amount, and openai_prepaid_since to form schema/defaults, prevents echoing admin key on edit, conditionally persists settings for OpenAI channels, and adds UI inputs with help text.
Internationalization and report updates
web/default/src/i18n/locales/{en,fr,ja,ru,vi,zh}.json, web/default/src/i18n/locales/_reports/*
Adds translation entries for admin-key and prepaid-related UI strings across locales and updates untranslated counts/reports.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • seefs001

🐰 A sk-admin key tucked safe away,
I count the costs so balances stay;
Paginate, clamp, then write with care,
Translations sing and UI fields are there.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Prepaid balance logic (OpenAIPrepaidAmount, OpenAIPrepaidSince) extends beyond the #4994 scope of fixing deprecated endpoints, introducing new accounting features not requested in the linked issue. Clarify whether prepaid-balance features are in-scope for this PR or should be deferred to a follow-up; if out-of-scope, remove prepaid-related code changes and tests.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: switching OpenAI balance refresh to use the /v1/organization/costs endpoint with an admin key.
Linked Issues check ✅ Passed All core requirements from #4994 are met: deprecated endpoints replaced, OpenAIAdminKey added to ChannelOtherSettings, pagination implemented, error handling for missing keys/403 responses, tests added, and UI support provided.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 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 `@controller/channel-billing.go`:
- Around line 458-459: The call site in controller/channel-billing.go treats the
result of updateChannelOpenAIBalance as channel.Balance (remaining) but
updateChannelOpenAIBalance now returns month-to-date spent, causing healthy
channels to be auto-disabled; fix by making the behavior consistent: either
change updateChannelOpenAIBalance to return remaining balance (remaining = limit
- spent) and set channel.Balance to that, or keep it returning spent but update
the caller to compute remaining before using/disabling (use channel.Limit or
billing limit to compute remaining and only disable when remaining <= 0); ensure
references to updateChannelOpenAIBalance, channel.Balance, and the
disable/threshold logic are updated together so the semantic (remaining vs
spent) is unambiguous.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Line 2017: Replace the hardcoded placeholder 'sk-admin-...' in the
ChannelMutateDrawer input with a localized string: change
placeholder='sk-admin-...' to placeholder={t('channels.adminKeyPlaceholder')}
(or another descriptive i18n key) using the existing useTranslation() t()
function in channel-mutate-drawer.tsx, and add the corresponding key/value to
the translation JSON files; ensure the input prop (placeholder) and the new i18n
key name are used consistently.

In `@web/default/src/i18n/locales/fr.json`:
- Line 210: Replace the untranslated English message entries with proper French
translations: find the JSON key "Admin key (sk-admin-...) used for fetching
usage statistics. Not used for inference." (and the other identical entry around
line 2732) and replace the English value with a French string that preserves any
tokens/formatting (for example: "Clé admin (sk-admin-...) utilisée pour
récupérer les statistiques d'utilisation. Non utilisée pour l'inférence.").
Ensure the JSON syntax and punctuation remain unchanged and run a quick
validation to confirm the locale file stays valid.

In `@web/default/src/i18n/locales/ja.json`:
- Line 210: Replace the English fallback values in the Japanese locale for the
newly added keys by providing proper Japanese translations: find the entry with
the English value "Admin key (sk-admin-...) used for fetching usage statistics.
Not used for inference." (and the other newly added English entry reported
around the same change) in ja.json and update their strings to natural Japanese
equivalents that convey the same meaning; ensure the translations preserve the
parenthetical "sk-admin-..." and the distinction that the key is for fetching
usage statistics and not for inference.

In `@web/default/src/i18n/locales/ru.json`:
- Line 2732: The Russian locale entry "OpenAI Admin Key" is left untranslated;
update the ru.json value for the key "OpenAI Admin Key" to an appropriate
Russian translation (e.g., "Административный ключ OpenAI") so the UI is fully
localized; locate the string "OpenAI Admin Key" in ru.json and replace the
English text with the Russian translation while preserving the JSON key and
formatting.

In `@web/default/src/i18n/locales/vi.json`:
- Line 210: Replace the two English values for the locale keys that read "Admin
key (sk-admin-...) used for fetching usage statistics. Not used for inference."
and its duplicate at the other occurrence with their Vietnamese translations;
update both occurrences in vi.json (the string key/value entries) to a correct
Vietnamese phrase that conveys the same meaning (e.g., "Khóa quản trị
(sk-admin-...) dùng để lấy số liệu sử dụng. Không dùng cho suy luận."). Ensure
both identical keys/values are updated so Vietnamese UI shows the translated
text.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 117ca298-f961-4a3f-bce1-45994db263a5

📥 Commits

Reviewing files that changed from the base of the PR and between 20d3e73 and 06b6212.

📒 Files selected for processing (14)
  • controller/channel-billing.go
  • controller/channel_billing_test.go
  • dto/channel_settings.go
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/lib/channel-form.ts
  • web/default/src/i18n/locales/_reports/_sync-report.json
  • web/default/src/i18n/locales/_reports/ja.untranslated.json
  • web/default/src/i18n/locales/_reports/ru.untranslated.json
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json

Comment on lines 458 to +459
case constant.ChannelTypeOpenAI:
if channel.GetBaseURL() != "" {
baseURL = channel.GetBaseURL()
}
return updateChannelOpenAIBalance(channel)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

OpenAI balance semantic change can incorrectly auto-disable healthy channels.

updateChannelOpenAIBalance now returns month-to-date spent amount, but downstream logic still treats channel.Balance as remaining and disables on balance <= 0. This can disable OpenAI channels at month start (spent = 0).

🔧 Minimal mitigation diff
 func updateAllChannelsBalance() error {
@@
-        } else {
-            // err is nil & balance <= 0 means quota is used up
-            if balance <= 0 {
+        } else {
+            // OpenAI balance is month-to-date spend (not remaining quota).
+            // Keep auto-disable logic only for channel types where balance semantics are "remaining".
+            if channel.Type != constant.ChannelTypeOpenAI && balance <= 0 {
                 service.DisableChannel(*types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, "", channel.GetAutoBan()), "余额不足")
             }
         }
🤖 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 `@controller/channel-billing.go` around lines 458 - 459, The call site in
controller/channel-billing.go treats the result of updateChannelOpenAIBalance as
channel.Balance (remaining) but updateChannelOpenAIBalance now returns
month-to-date spent, causing healthy channels to be auto-disabled; fix by making
the behavior consistent: either change updateChannelOpenAIBalance to return
remaining balance (remaining = limit - spent) and set channel.Balance to that,
or keep it returning spent but update the caller to compute remaining before
using/disabling (use channel.Limit or billing limit to compute remaining and
only disable when remaining <= 0); ensure references to
updateChannelOpenAIBalance, channel.Balance, and the disable/threshold logic are
updated together so the semantic (remaining vs spent) is unambiguous.

<FormLabel>{t('OpenAI Admin Key')}</FormLabel>
<FormControl>
<Textarea
placeholder='sk-admin-...'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the admin key placeholder text.

placeholder='sk-admin-...' is user-facing and should go through t() like the rest of this form.

🌐 Suggested fix
-                            placeholder='sk-admin-...'
+                            placeholder={t('sk-admin-...')}

As per coding guidelines, “All user-facing text content must support i18n using the t() function from useTranslation() in React components.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
placeholder='sk-admin-...'
placeholder={t('sk-admin-...')}
🤖 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
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`
at line 2017, Replace the hardcoded placeholder 'sk-admin-...' in the
ChannelMutateDrawer input with a localized string: change
placeholder='sk-admin-...' to placeholder={t('channels.adminKeyPlaceholder')}
(or another descriptive i18n key) using the existing useTranslation() t()
function in channel-mutate-drawer.tsx, and add the corresponding key/value to
the translation JSON files; ensure the input prop (placeholder) and the new i18n
key name are used consistently.

"Admin": "Administrateur",
"Admin access required": "Accès administrateur requis",
"Admin area": "Espace administrateur",
"Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.": "Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize new French entries instead of copying English strings.

Both new values are still English, so French users will see mixed-language UI.

💡 Suggested fix
-    "Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.": "Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.",
+    "Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.": "Clé admin (sk-admin-...) utilisée pour récupérer les statistiques d’utilisation. Non utilisée pour l’inférence.",
...
-    "OpenAI Admin Key": "OpenAI Admin Key",
+    "OpenAI Admin Key": "Clé admin OpenAI",

Also applies to: 2732-2732

🤖 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 `@web/default/src/i18n/locales/fr.json` at line 210, Replace the untranslated
English message entries with proper French translations: find the JSON key
"Admin key (sk-admin-...) used for fetching usage statistics. Not used for
inference." (and the other identical entry around line 2732) and replace the
English value with a French string that preserves any tokens/formatting (for
example: "Clé admin (sk-admin-...) utilisée pour récupérer les statistiques
d'utilisation. Non utilisée pour l'inférence."). Ensure the JSON syntax and
punctuation remain unchanged and run a quick validation to confirm the locale
file stays valid.

"Admin": "管理者",
"Admin access required": "管理者アクセスが必要です",
"Admin area": "管理者エリア",
"Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.": "Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate newly added JA locale strings instead of leaving English fallbacks.

Both new entries are still English in ja.json, so Japanese users will see mixed-language UI. Please provide Japanese translations for these two values.

💡 Suggested update
-    "Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.": "Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.",
+    "Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.": "使用量統計の取得に使用する管理者キー(sk-admin-...)。推論には使用されません。",
...
-    "OpenAI Admin Key": "OpenAI Admin Key",
+    "OpenAI Admin Key": "OpenAI 管理者キー",

Also applies to: 2732-2732

🤖 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 `@web/default/src/i18n/locales/ja.json` at line 210, Replace the English
fallback values in the Japanese locale for the newly added keys by providing
proper Japanese translations: find the entry with the English value "Admin key
(sk-admin-...) used for fetching usage statistics. Not used for inference." (and
the other newly added English entry reported around the same change) in ja.json
and update their strings to natural Japanese equivalents that convey the same
meaning; ensure the translations preserve the parenthetical "sk-admin-..." and
the distinction that the key is for fetching usage statistics and not for
inference.

"Open theme settings": "Открыть настройки темы",
"Open weights": "Открытые веса",
"OpenAI": "OpenAI",
"OpenAI Admin Key": "OpenAI Admin Key",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate the Russian label instead of keeping it in English.

"OpenAI Admin Key" is currently untranslated in ru.json, so Russian users will see mixed-language UI.

💡 Suggested fix
-    "OpenAI Admin Key": "OpenAI Admin Key",
+    "OpenAI Admin Key": "Ключ администратора 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 `@web/default/src/i18n/locales/ru.json` at line 2732, The Russian locale entry
"OpenAI Admin Key" is left untranslated; update the ru.json value for the key
"OpenAI Admin Key" to an appropriate Russian translation (e.g.,
"Административный ключ OpenAI") so the UI is fully localized; locate the string
"OpenAI Admin Key" in ru.json and replace the English text with the Russian
translation while preserving the JSON key and formatting.

"Admin": "Quản trị viên",
"Admin access required": "Yêu cầu quyền truy cập Admin",
"Admin area": "Khu vực quản trị",
"Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.": "Admin key (sk-admin-...) used for fetching usage statistics. Not used for inference.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Vietnamese locale values are still in English

Line 210 and Line 2732 currently keep English values, so Vietnamese users will see untranslated UI text. Please provide Vietnamese translations for both entries.

Also applies to: 2732-2732

🤖 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 `@web/default/src/i18n/locales/vi.json` at line 210, Replace the two English
values for the locale keys that read "Admin key (sk-admin-...) used for fetching
usage statistics. Not used for inference." and its duplicate at the other
occurrence with their Vietnamese translations; update both occurrences in
vi.json (the string key/value entries) to a correct Vietnamese phrase that
conveys the same meaning (e.g., "Khóa quản trị (sk-admin-...) dùng để lấy số
liệu sử dụng. Không dùng cho suy luận."). Ensure both identical keys/values are
updated so Vietnamese UI shows the translated text.

a-lapkov added 4 commits May 20, 2026 21:04
Add Channel.Clean() that zeroes both Key and the OpenAIAdminKey nested in
OtherSettings, and extend the controller's clearChannelInfo helper with the
same OtherSettings stripping so every GET/Search/List/Update response path
already calling it benefits automatically. Previously editing channel id=1
returned the raw admin key in the JSON body, populating the form textarea
in plain text.

Adds a test asserting Channel.Clean() zeros OpenAIAdminKey.
Mirror the inference-key behavior for OpenAIAdminKey nested in OtherSettings:
when a PUT /api/channel payload sends an empty admin_key inside settings,
copy the existing value from the DB row before invoking GORM's struct-based
Updates(channel). Without this merge, editing a channel with a masked form
field would clobber the stored admin key with an empty string, breaking
the OpenAI balance refresh on subsequent runs.
Stop loading openai_admin_key from the channel response into the edit form
(the backend now masks it anyway), and stop deleting it from settings JSON
on save when the field is blank. Empty input now means 'preserve' rather
than 'clear', matching how the main inference key field already behaves.

Also adds an isEditing-aware placeholder ('Leave empty to keep existing
admin key') on the OpenAI Admin Key textarea so users understand the
masked-state UX.
Add the new placeholder string for the OpenAI Admin Key textarea in edit
mode across all six frontend locales (en/zh/ru translated, fr/ja/vi fall
back to English source).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
web/default/src/i18n/locales/fr.json (1)

2164-2164: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate this new French locale value instead of leaving English text.

The fr.json value is still English, so French users will see mixed-language UI. Please localize it (e.g., “Laissez vide pour conserver la clé admin existante”).

🤖 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 `@web/default/src/i18n/locales/fr.json` at line 2164, The French locale file
contains an untranslated entry for the key "Leave empty to keep existing admin
key" in fr.json; replace the English string value with a proper French
translation (for example "Laissez vide pour conserver la clé admin existante")
so the JSON value for that exact key is localized and remains valid JSON in the
fr.json file.
web/default/src/i18n/locales/vi.json (1)

2164-2164: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Vietnamese locale value is still untranslated (Line 2164).

Please translate the value so Vietnamese users don’t see English fallback text.

🌐 Suggested fix
-    "Leave empty to keep existing admin key": "Leave empty to keep existing admin key",
+    "Leave empty to keep existing admin key": "Để trống để giữ khóa quản trị hiện có",
🤖 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 `@web/default/src/i18n/locales/vi.json` at line 2164, The Vietnamese locale
file contains an untranslated entry for the key "Leave empty to keep existing
admin key"; replace the English value in vi.json with an accurate Vietnamese
translation (e.g., "Để trống để giữ khóa quản trị hiện có" or another
appropriate phrasing) so the key "Leave empty to keep existing admin key" is
localized for Vietnamese users.
🧹 Nitpick comments (2)
controller/channel.go (1)

70-76: ⚡ Quick win

Use channel.Clean() to avoid duplicated secret-scrubbing logic.

Line 70-76 duplicates the same admin-key masking now implemented in model.Channel.Clean(). Reusing Clean() here reduces drift risk.

Suggested refactor
 func clearChannelInfo(channel *model.Channel) {
 	if channel.ChannelInfo.IsMultiKey {
 		channel.ChannelInfo.MultiKeyDisabledReason = nil
 		channel.ChannelInfo.MultiKeyDisabledTime = nil
 	}
-	// Strip secrets embedded in OtherSettings JSON (e.g. OpenAIAdminKey) before serializing
-	// the channel back to API clients. The inference Key column is already cleared either via
-	// gorm Omit("key") at the query layer or by explicit channel.Key = "" in the caller.
-	if other := channel.GetOtherSettings(); other.OpenAIAdminKey != "" {
-		other.OpenAIAdminKey = ""
-		channel.SetOtherSettings(other)
-	}
+	channel.Clean()
 }
🤖 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 `@controller/channel.go` around lines 70 - 76, The manual OpenAIAdminKey
scrubbing block duplicates existing logic; call channel.Clean() instead of
manually clearing OtherSettings.OpenAIAdminKey so the model.Channel.Clean()
implementation is reused; remove the GetOtherSettings/SetOtherSettings
conditional and replace it with a single channel.Clean() call prior to
serializing/returning the channel to API clients.
web/default/src/i18n/locales/en.json (1)

2164-2164: ⚡ Quick win

Use a hierarchical i18n key for the new admin-key placeholder string.

This new key is a raw sentence key; please switch it to a semantic namespaced key (for example, channel.openai.adminKey.keepExistingPlaceholder) to comply with project i18n conventions and keep cross-locale keys consistent.

As per coding guidelines, Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency.

🤖 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 `@web/default/src/i18n/locales/en.json` at line 2164, The JSON contains a
raw-sentence i18n key "Leave empty to keep existing admin key"; replace it with
a hierarchical namespaced key (e.g.
channel.openai.adminKey.keepExistingPlaceholder) and move the sentence into that
key's value, updating the key name wherever it's consumed (search for the
literal sentence or the current key) so all lookups use
channel.openai.adminKey.keepExistingPlaceholder; also add the same namespaced
key to other locale files to keep cross-locale consistency.
🤖 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 `@controller/channel.go`:
- Around line 903-910: The merge logic unconditionally preserves OpenAIAdminKey;
restrict it so the key is only carried over when the channel is (still) an
OpenAI channel: check the channel type before copying (e.g. ensure
channel.GetType() or channel.Type == "openai") and only set
incomingOther.OpenAIAdminKey from originOther.OpenAIAdminKey in that case;
additionally, if the incoming channel type is not OpenAI, ensure
incomingOther.OpenAIAdminKey is cleared (set to empty) and call
channel.SetOtherSettings(incomingOther) to avoid retaining a stale key.

In `@web/default/src/features/channels/lib/channel-form.ts`:
- Around line 326-331: Trim the incoming openai_admin_key from formData before
making the persistence decision: read const trimmed =
formData.openai_admin_key?.trim(); if trimmed is a non-empty string set
settingsObj.openai_admin_key = trimmed, otherwise delete
settingsObj.openai_admin_key so whitespace-only values are not sent; update the
branch around formData.openai_admin_key in the channel-form logic to use this
trimmed variable.

---

Duplicate comments:
In `@web/default/src/i18n/locales/fr.json`:
- Line 2164: The French locale file contains an untranslated entry for the key
"Leave empty to keep existing admin key" in fr.json; replace the English string
value with a proper French translation (for example "Laissez vide pour conserver
la clé admin existante") so the JSON value for that exact key is localized and
remains valid JSON in the fr.json file.

In `@web/default/src/i18n/locales/vi.json`:
- Line 2164: The Vietnamese locale file contains an untranslated entry for the
key "Leave empty to keep existing admin key"; replace the English value in
vi.json with an accurate Vietnamese translation (e.g., "Để trống để giữ khóa
quản trị hiện có" or another appropriate phrasing) so the key "Leave empty to
keep existing admin key" is localized for Vietnamese users.

---

Nitpick comments:
In `@controller/channel.go`:
- Around line 70-76: The manual OpenAIAdminKey scrubbing block duplicates
existing logic; call channel.Clean() instead of manually clearing
OtherSettings.OpenAIAdminKey so the model.Channel.Clean() implementation is
reused; remove the GetOtherSettings/SetOtherSettings conditional and replace it
with a single channel.Clean() call prior to serializing/returning the channel to
API clients.

In `@web/default/src/i18n/locales/en.json`:
- Line 2164: The JSON contains a raw-sentence i18n key "Leave empty to keep
existing admin key"; replace it with a hierarchical namespaced key (e.g.
channel.openai.adminKey.keepExistingPlaceholder) and move the sentence into that
key's value, updating the key name wherever it's consumed (search for the
literal sentence or the current key) so all lookups use
channel.openai.adminKey.keepExistingPlaceholder; also add the same namespaced
key to other locale files to keep cross-locale consistency.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 50471656-ed2d-4acb-94d1-33126cb3e261

📥 Commits

Reviewing files that changed from the base of the PR and between 06b6212 and baa9ee5.

📒 Files selected for processing (11)
  • controller/channel.go
  • controller/channel_billing_test.go
  • model/channel.go
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/lib/channel-form.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (2)
  • web/default/src/i18n/locales/zh.json
  • web/default/src/i18n/locales/ru.json

Comment thread controller/channel.go
Comment on lines +903 to +910
incomingOther := channel.GetOtherSettings()
if incomingOther.OpenAIAdminKey == "" {
originOther := originChannel.GetOtherSettings()
if originOther.OpenAIAdminKey != "" {
incomingOther.OpenAIAdminKey = originOther.OpenAIAdminKey
channel.SetOtherSettings(incomingOther)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent stale admin-key retention when channel type is not OpenAI.

At Line 904, the preservation merge runs regardless of channel type. If a channel is edited from OpenAI to another type, this can keep openai_admin_key in settings unintentionally.

Suggested fix
-	incomingOther := channel.GetOtherSettings()
-	if incomingOther.OpenAIAdminKey == "" {
+	incomingOther := channel.GetOtherSettings()
+	if channel.Type == constant.ChannelTypeOpenAI && incomingOther.OpenAIAdminKey == "" {
 		originOther := originChannel.GetOtherSettings()
 		if originOther.OpenAIAdminKey != "" {
 			incomingOther.OpenAIAdminKey = originOther.OpenAIAdminKey
 			channel.SetOtherSettings(incomingOther)
 		}
 	}
🤖 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 `@controller/channel.go` around lines 903 - 910, The merge logic
unconditionally preserves OpenAIAdminKey; restrict it so the key is only carried
over when the channel is (still) an OpenAI channel: check the channel type
before copying (e.g. ensure channel.GetType() or channel.Type == "openai") and
only set incomingOther.OpenAIAdminKey from originOther.OpenAIAdminKey in that
case; additionally, if the incoming channel type is not OpenAI, ensure
incomingOther.OpenAIAdminKey is cleared (set to empty) and call
channel.SetOtherSettings(incomingOther) to avoid retaining a stale key.

Comment on lines +326 to +331
if (formData.openai_admin_key) {
settingsObj.openai_admin_key = formData.openai_admin_key
} else {
// Strip any masked/empty value carried over from the parsed `settings` field so we
// don't send a stray empty string; backend will copy the existing value from DB.
delete settingsObj.openai_admin_key
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim openai_admin_key before deciding whether to persist it.

At Line 326, whitespace-only input is treated as a value and gets sent. Trim first, then conditionally set/delete to avoid persisting invalid keys.

Suggested fix
   if (formData.type === 1) {
-    if (formData.openai_admin_key) {
-      settingsObj.openai_admin_key = formData.openai_admin_key
+    const adminKey = formData.openai_admin_key?.trim()
+    if (adminKey) {
+      settingsObj.openai_admin_key = adminKey
     } else {
       // Strip any masked/empty value carried over from the parsed `settings` field so we
       // don't send a stray empty string; backend will copy the existing value from DB.
       delete settingsObj.openai_admin_key
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (formData.openai_admin_key) {
settingsObj.openai_admin_key = formData.openai_admin_key
} else {
// Strip any masked/empty value carried over from the parsed `settings` field so we
// don't send a stray empty string; backend will copy the existing value from DB.
delete settingsObj.openai_admin_key
if (formData.type === 1) {
const adminKey = formData.openai_admin_key?.trim()
if (adminKey) {
settingsObj.openai_admin_key = adminKey
} else {
// Strip any masked/empty value carried over from the parsed `settings` field so we
// don't send a stray empty string; backend will copy the existing value from DB.
delete settingsObj.openai_admin_key
}
🤖 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 `@web/default/src/features/channels/lib/channel-form.ts` around lines 326 -
331, Trim the incoming openai_admin_key from formData before making the
persistence decision: read const trimmed = formData.openai_admin_key?.trim(); if
trimmed is a non-empty string set settingsObj.openai_admin_key = trimmed,
otherwise delete settingsObj.openai_admin_key so whitespace-only values are not
sent; update the branch around formData.openai_admin_key in the channel-form
logic to use this trimmed variable.

a-lapkov and others added 6 commits May 20, 2026 21:20
Introduce OpenAIPrepaidAmount and OpenAIPrepaidSince on ChannelOtherSettings
so admins can declare a prepaid USD top-up plus its start timestamp. These
will be consumed by the balance refresh path to compute remaining prepaid
balance instead of month-to-date spend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When both OpenAIPrepaidAmount and OpenAIPrepaidSince are set on the channel,
sum Costs API buckets from the prepaid-since timestamp through now and
return prepaid_amount - total_costs (clamped to 0) as the balance. When
either field is unset, fall back to the existing month-to-date spend
behavior to preserve backward compatibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add three tests for updateChannelOpenAIBalance covering the new prepaid
semantics: remaining balance computation when both fields are set, clamping
to zero when costs exceed the prepaid amount, and falling back to the
month-to-date spend path when only one of the two prepaid fields is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two numeric inputs to the channel mutate drawer, gated to OpenAI
(type 1), that bind to openai_prepaid_amount and openai_prepaid_since on
the channel form. The schema, defaults, transformer and settings JSON
builder are updated to round-trip the values through the API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add label and help-text strings for the new OpenAI prepaid amount and
prepaid-since timestamp inputs across all 6 locales. Russian and Chinese
get full translations; French, Japanese and Vietnamese fall back to the
English source, in line with prior conventions for channel-specific UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
web/default/src/i18n/locales/fr.json (2)

2735-2736: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate newly added prepaid OpenAI labels to French.

Line 2735 and Line 2736 are still English, so the FR locale will render mixed-language UI.

💡 Suggested fix
-    "OpenAI Prepaid Amount (USD)": "OpenAI Prepaid Amount (USD)",
-    "OpenAI Prepaid Since (Unix timestamp)": "OpenAI Prepaid Since (Unix timestamp)",
+    "OpenAI Prepaid Amount (USD)": "Montant prépayé OpenAI (USD)",
+    "OpenAI Prepaid Since (Unix timestamp)": "OpenAI prépayé depuis (horodatage Unix)",
🤖 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 `@web/default/src/i18n/locales/fr.json` around lines 2735 - 2736, The two
locale keys "OpenAI Prepaid Amount (USD)" and "OpenAI Prepaid Since (Unix
timestamp)" are still in English; update their French translations in fr.json by
replacing the English values with proper French phrases (e.g., "Montant prépayé
OpenAI (USD)" and "Prépayé OpenAI depuis (horodatage Unix)") so the UI renders
consistently in French for the keys "OpenAI Prepaid Amount (USD)" and "OpenAI
Prepaid Since (Unix timestamp)".

4069-4069: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the new OpenAI prepaid helper texts.

Line 4069 and Line 4145 remain untranslated and should be French in fr.json.

💡 Suggested fix
-    "Total USD prepaid into the OpenAI account. Used together with \"Prepaid since\" to compute remaining balance.": "Total USD prepaid into the OpenAI account. Used together with \"Prepaid since\" to compute remaining balance.",
+    "Total USD prepaid into the OpenAI account. Used together with \"Prepaid since\" to compute remaining balance.": "Total USD prépayé sur le compte OpenAI. Utilisé avec « Prépayé depuis » pour calculer le solde restant.",
...
-    "Unix timestamp when the prepaid period started. If 0, balance shows month-to-date spend instead.": "Unix timestamp when the prepaid period started. If 0, balance shows month-to-date spend instead.",
+    "Unix timestamp when the prepaid period started. If 0, balance shows month-to-date spend instead.": "Horodatage Unix du début de la période prépayée. Si 0, le solde affiche plutôt les dépenses cumulées du mois.",

Also applies to: 4145-4145

🤖 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 `@web/default/src/i18n/locales/fr.json` at line 4069, Translate the two new
English helper text entries in fr.json into French: locate the exact English
string "Total USD prepaid into the OpenAI account. Used together with \"Prepaid
since\" to compute remaining balance." and its counterpart (the other
untranslated helper text added for the OpenAI prepaid UI) and replace their
values with correct French translations, preserving quotation escaping and JSON
formatting so the keys remain identical and only the French-localized values
change.
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx (1)

2013-2016: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the admin-key placeholder string.

Create mode still uses a hardcoded user-facing placeholder ('sk-admin-...') instead of t(...).

As per coding guidelines, “All user-facing text content must support i18n using the t() function from useTranslation() in React components.”

🤖 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
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`
around lines 2013 - 2016, The adminKeyPlaceholder variable in
channel-mutate-drawer.tsx currently uses a hardcoded string ('sk-admin-...') in
create mode; change that to use the t() i18n function instead (e.g. replace the
literal with t('<translation_key>', { defaultValue: 'sk-admin-...' })) so both
create and edit branches use localized text; update the adminKeyPlaceholder
assignment that references isEditing and t() to supply a translation key for the
create-mode placeholder.
controller/channel-billing.go (1)

461-470: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

OpenAI spend-as-balance still conflicts with auto-disable logic.

In fallback mode this writes month-to-date spend into channel.Balance, but updateAllChannelsBalance still disables channels on balance <= 0. That can disable valid OpenAI channels at month start (spend is 0).

Suggested minimal fix
diff --git a/controller/channel-billing.go b/controller/channel-billing.go
@@
-            // err is nil & balance <= 0 means quota is used up
-            if balance <= 0 {
+            // OpenAI balance may represent MTD spend, not remaining quota.
+            if channel.Type != constant.ChannelTypeOpenAI && balance <= 0 {
                 service.DisableChannel(*types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, "", channel.GetAutoBan()), "余额不足")
             }
🤖 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 `@controller/channel-billing.go` around lines 461 - 470, The current code
writes month-to-date spend (total) into channel.Balance in fallback mode which
conflicts with updateAllChannelsBalance's disable logic; instead, when using the
OpenAI "spend-as-balance" fallback (i.e., the path that currently ends up with
prepaidMode false but using OpenAIPrepaidAmount), compute and store the
remaining balance as settings.OpenAIPrepaidAmount - total (clamped to >=0) and
pass that to channel.UpdateBalance; keep the existing behavior of writing total
only if you truly intend to store cumulative spend, but do not write total when
the system expects a remaining balance—adjust the branch around
prepaidMode/total to set balance = settings.OpenAIPrepaidAmount - total (min 0)
before calling channel.UpdateBalance so updateAllChannelsBalance won’t wrongly
disable valid OpenAI channels; reference UpdateBalance,
updateAllChannelsBalance, prepaidMode, total, and settings.OpenAIPrepaidAmount.
🤖 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
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 2047-2052: The number inputs are bound directly to the Input
component via {...field}, causing the form to receive strings and breaking Zod
number validation; update the bindings for the numeric fields in
channel-mutate-drawer.tsx (the Input that currently uses type='number'
step='0.01' placeholder='20.00' and the other similar Input) to coerce values to
numbers—either by replacing {...field} with explicit onChange and value handlers
that parseFloat(value) (or Number(value) / handle empty -> undefined) before
calling field.onChange, or by using the native event property valueAsNumber in
onChange to pass numeric values to field.onChange; ensure value stays a number
(or undefined) so Zod number validation passes.

In `@web/default/src/features/channels/lib/channel-form.ts`:
- Around line 357-362: The cleanup for prepaid fields is currently only executed
when openai_admin_key is present, causing stale openai_prepaid_amount and
openai_prepaid_since to persist for non-OpenAI types; update the logic in
channel-form.ts so that deletion of settingsObj.openai_prepaid_amount and
settingsObj.openai_prepaid_since happens unconditionally (i.e., remove any
gating on settingsObj.openai_admin_key) — locate the block referencing
settingsObj and the keys openai_prepaid_amount/openai_prepaid_since and simply
delete those keys if they exist regardless of openai_admin_key.

In `@web/default/src/i18n/locales/ru.json`:
- Around line 2735-2736: The Russian translations for the keys "OpenAI Prepaid
Amount (USD)" and "OpenAI Prepaid Since (Unix timestamp)" (and the similar
entries referenced at lines 4069 and 4145) mix English and Russian; update these
localization values to fully Russian text (e.g., replace "prepaid" with
"предоплата" and "Unix timestamp" with a Russian equivalent like "метка времени
Unix" or "Unix-временная метка") so all related keys (including "OpenAI Prepaid
Amount (USD)" and "OpenAI Prepaid Since (Unix timestamp)") use consistent
Russian phrasing across the file.

In `@web/default/src/i18n/locales/zh.json`:
- Line 2736: The Chinese translation for the same concept is inconsistent:
change the value for the "OpenAI Prepaid Since (Unix timestamp)" entry
(currently "OpenAI 预付起始 (Unix 时间戳)") to match the other occurrence ("预付起始时间") so
both labels use identical wording — update the JSON value for the "OpenAI
Prepaid Since (Unix timestamp)" key to "OpenAI 预付起始时间 (Unix 时间戳)" and verify the
other corresponding key/value that uses "预付起始时间" remains the same to ensure copy
consistency across the locale.

---

Duplicate comments:
In `@controller/channel-billing.go`:
- Around line 461-470: The current code writes month-to-date spend (total) into
channel.Balance in fallback mode which conflicts with updateAllChannelsBalance's
disable logic; instead, when using the OpenAI "spend-as-balance" fallback (i.e.,
the path that currently ends up with prepaidMode false but using
OpenAIPrepaidAmount), compute and store the remaining balance as
settings.OpenAIPrepaidAmount - total (clamped to >=0) and pass that to
channel.UpdateBalance; keep the existing behavior of writing total only if you
truly intend to store cumulative spend, but do not write total when the system
expects a remaining balance—adjust the branch around prepaidMode/total to set
balance = settings.OpenAIPrepaidAmount - total (min 0) before calling
channel.UpdateBalance so updateAllChannelsBalance won’t wrongly disable valid
OpenAI channels; reference UpdateBalance, updateAllChannelsBalance, prepaidMode,
total, and settings.OpenAIPrepaidAmount.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 2013-2016: The adminKeyPlaceholder variable in
channel-mutate-drawer.tsx currently uses a hardcoded string ('sk-admin-...') in
create mode; change that to use the t() i18n function instead (e.g. replace the
literal with t('<translation_key>', { defaultValue: 'sk-admin-...' })) so both
create and edit branches use localized text; update the adminKeyPlaceholder
assignment that references isEditing and t() to supply a translation key for the
create-mode placeholder.

In `@web/default/src/i18n/locales/fr.json`:
- Around line 2735-2736: The two locale keys "OpenAI Prepaid Amount (USD)" and
"OpenAI Prepaid Since (Unix timestamp)" are still in English; update their
French translations in fr.json by replacing the English values with proper
French phrases (e.g., "Montant prépayé OpenAI (USD)" and "Prépayé OpenAI depuis
(horodatage Unix)") so the UI renders consistently in French for the keys
"OpenAI Prepaid Amount (USD)" and "OpenAI Prepaid Since (Unix timestamp)".
- Line 4069: Translate the two new English helper text entries in fr.json into
French: locate the exact English string "Total USD prepaid into the OpenAI
account. Used together with \"Prepaid since\" to compute remaining balance." and
its counterpart (the other untranslated helper text added for the OpenAI prepaid
UI) and replace their values with correct French translations, preserving
quotation escaping and JSON formatting so the keys remain identical and only the
French-localized values change.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04815a1e-b26d-4301-ac5c-6bc7d5a03b4a

📥 Commits

Reviewing files that changed from the base of the PR and between baa9ee5 and 0702b5e.

📒 Files selected for processing (15)
  • controller/channel-billing.go
  • controller/channel_billing_test.go
  • dto/channel_settings.go
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/lib/channel-form.ts
  • web/default/src/i18n/locales/_reports/_sync-report.json
  • web/default/src/i18n/locales/_reports/fr.untranslated.json
  • web/default/src/i18n/locales/_reports/ja.untranslated.json
  • web/default/src/i18n/locales/_reports/vi.untranslated.json
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (3)
  • web/default/src/i18n/locales/_reports/fr.untranslated.json
  • web/default/src/i18n/locales/_reports/_sync-report.json
  • web/default/src/i18n/locales/en.json

Comment on lines +2047 to +2052
<Input
type='number'
step='0.01'
placeholder='20.00'
{...field}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Number inputs are stored as strings, breaking Zod number validation.

These fields are bound directly to Input, so edits produce string values while schema requires numbers; submit can fail with type errors.

Suggested fix
@@
                           <Input
                             type='number'
                             step='0.01'
                             placeholder='20.00'
-                            {...field}
+                            {...field}
+                            onChange={(e) =>
+                              field.onChange(
+                                e.target.value === '' ? 0 : Number(e.target.value)
+                              )
+                            }
                           />
@@
                           <Input
                             type='number'
                             placeholder='1700000000'
-                            {...field}
+                            {...field}
+                            onChange={(e) =>
+                              field.onChange(
+                                e.target.value === ''
+                                  ? 0
+                                  : Math.trunc(Number(e.target.value))
+                              )
+                            }
                           />

Also applies to: 2074-2078

🤖 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
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`
around lines 2047 - 2052, The number inputs are bound directly to the Input
component via {...field}, causing the form to receive strings and breaking Zod
number validation; update the bindings for the numeric fields in
channel-mutate-drawer.tsx (the Input that currently uses type='number'
step='0.01' placeholder='20.00' and the other similar Input) to coerce values to
numbers—either by replacing {...field} with explicit onChange and value handlers
that parseFloat(value) (or Number(value) / handle empty -> undefined) before
calling field.onChange, or by using the native event property valueAsNumber in
onChange to pass numeric values to field.onChange; ensure value stays a number
(or undefined) so Zod number validation passes.

Comment on lines +357 to +362
if ('openai_prepaid_amount' in settingsObj) {
delete settingsObj.openai_prepaid_amount
}
if ('openai_prepaid_since' in settingsObj) {
delete settingsObj.openai_prepaid_since
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prepaid cleanup is incorrectly gated by presence of openai_admin_key.

For non-OpenAI types, openai_prepaid_amount/since are only deleted when openai_admin_key exists. Stale prepaid fields can remain if admin key is absent.

Suggested fix
-  } else if ('openai_admin_key' in settingsObj) {
-    delete settingsObj.openai_admin_key
-    if ('openai_prepaid_amount' in settingsObj) {
-      delete settingsObj.openai_prepaid_amount
-    }
-    if ('openai_prepaid_since' in settingsObj) {
-      delete settingsObj.openai_prepaid_since
-    }
+  } else {
+    if ('openai_admin_key' in settingsObj) delete settingsObj.openai_admin_key
+    if ('openai_prepaid_amount' in settingsObj) delete settingsObj.openai_prepaid_amount
+    if ('openai_prepaid_since' in settingsObj) delete settingsObj.openai_prepaid_since
   }
🤖 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 `@web/default/src/features/channels/lib/channel-form.ts` around lines 357 -
362, The cleanup for prepaid fields is currently only executed when
openai_admin_key is present, causing stale openai_prepaid_amount and
openai_prepaid_since to persist for non-OpenAI types; update the logic in
channel-form.ts so that deletion of settingsObj.openai_prepaid_amount and
settingsObj.openai_prepaid_since happens unconditionally (i.e., remove any
gating on settingsObj.openai_admin_key) — locate the block referencing
settingsObj and the keys openai_prepaid_amount/openai_prepaid_since and simply
delete those keys if they exist regardless of openai_admin_key.

Comment on lines +2735 to +2736
"OpenAI Prepaid Amount (USD)": "Сумма prepaid OpenAI (USD)",
"OpenAI Prepaid Since (Unix timestamp)": "OpenAI prepaid с (Unix timestamp)",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize mixed-language Russian translations for prepaid labels/descriptions.

Line 2735, Line 2736, Line 4069, and Line 4145 still mix English and Russian (prepaid, Unix timestamp, Prepaid since), which makes RU UI inconsistent.

💡 Proposed localization cleanup
-    "OpenAI Prepaid Amount (USD)": "Сумма prepaid OpenAI (USD)",
-    "OpenAI Prepaid Since (Unix timestamp)": "OpenAI prepaid с (Unix timestamp)",
+    "OpenAI Prepaid Amount (USD)": "Сумма предоплаты OpenAI (USD)",
+    "OpenAI Prepaid Since (Unix timestamp)": "Предоплата OpenAI с (Unix-временная метка)",

-    "Total USD prepaid into the OpenAI account. Used together with \"Prepaid since\" to compute remaining balance.": "Общая сумма pre-paid на счёте OpenAI в USD. Вместе с \"Prepaid since\" используется для расчёта остатка.",
+    "Total USD prepaid into the OpenAI account. Used together with \"Prepaid since\" to compute remaining balance.": "Общая сумма предоплаты в USD на счёте OpenAI. Используется вместе с «Предоплата с» для расчёта остатка.",

-    "Unix timestamp when the prepaid period started. If 0, balance shows month-to-date spend instead.": "Unix timestamp начала prepaid-периода. Если 0 — показывается расход с начала месяца.",
+    "Unix timestamp when the prepaid period started. If 0, balance shows month-to-date spend instead.": "Unix-временная метка начала периода предоплаты. Если 0 — показывается расход с начала месяца.",

Also applies to: 4069-4069, 4145-4145

🤖 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 `@web/default/src/i18n/locales/ru.json` around lines 2735 - 2736, The Russian
translations for the keys "OpenAI Prepaid Amount (USD)" and "OpenAI Prepaid
Since (Unix timestamp)" (and the similar entries referenced at lines 4069 and
4145) mix English and Russian; update these localization values to fully Russian
text (e.g., replace "prepaid" with "предоплата" and "Unix timestamp" with a
Russian equivalent like "метка времени Unix" or "Unix-временная метка") so all
related keys (including "OpenAI Prepaid Amount (USD)" and "OpenAI Prepaid Since
(Unix timestamp)") use consistent Russian phrasing across the file.

"OpenAI Admin Key": "OpenAI 管理员密钥",
"OpenAI Compatible": "兼容 OpenAI",
"OpenAI Prepaid Amount (USD)": "OpenAI 预付金额 (USD)",
"OpenAI Prepaid Since (Unix timestamp)": "OpenAI 预付起始 (Unix 时间戳)",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align “Prepaid since” terminology across label and helper text.

Line 2736 uses 预付起始, while Line 4069 uses 预付起始时间 for the same concept. Please keep them identical to avoid UI copy inconsistency.

💡 Suggested wording
-    "OpenAI Prepaid Since (Unix timestamp)": "OpenAI 预付起始 (Unix 时间戳)",
+    "OpenAI Prepaid Since (Unix timestamp)": "OpenAI 预付起始时间 (Unix 时间戳)",

As per coding guidelines, "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

Also applies to: 4069-4069

🤖 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 `@web/default/src/i18n/locales/zh.json` at line 2736, The Chinese translation
for the same concept is inconsistent: change the value for the "OpenAI Prepaid
Since (Unix timestamp)" entry (currently "OpenAI 预付起始 (Unix 时间戳)") to match the
other occurrence ("预付起始时间") so both labels use identical wording — update the
JSON value for the "OpenAI Prepaid Since (Unix timestamp)" key to "OpenAI 预付起始时间
(Unix 时间戳)" and verify the other corresponding key/value that uses "预付起始时间"
remains the same to ensure copy consistency across the locale.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAI channel balance refresh returns 403 (dashboard/billing endpoints deprecated)

1 participant