Skip to content

feat(billing): add per-resolution image model billing#5035

Open
Micah-Zheng wants to merge 4 commits into
QuantumNous:mainfrom
Micah-Zheng:feat/image-resolution-billing
Open

feat(billing): add per-resolution image model billing#5035
Micah-Zheng wants to merge 4 commits into
QuantumNous:mainfrom
Micah-Zheng:feat/image-resolution-billing

Conversation

@Micah-Zheng
Copy link
Copy Markdown
Contributor

@Micah-Zheng Micah-Zheng commented May 22, 2026

问题描述

部分图片模型或上游聚合渠道更适合按输出分辨率计费,而不是只按 token 倍率或固定单次价格计费。当前模型定价页只能配置按 token、固定单次价格或表达式计费,无法为图片生成模型设置 1K / 2K / 4K 这类按分辨率档位的固定图片价格。

修复方案

  1. 新增 image_model_setting.models 配置,用于为图片模型选择 tokenper_size 计费模式,并保存 1K / 2K / 4K 的每张图片价格。
  2. Image API 请求结算时读取输出 size,将尺寸归类为 1K、2K、4K,并在 per_size 模式下按「单价 × 图片数量 × 分组倍率 × QuotaPerUnit」结算。
  3. 模型定价可视化编辑器在「固定价格」模式下增加「按分辨率」子模式,并同步维护 ModelPrice=0 哨兵值与 image_model_setting.models
  4. 用量日志展示按分辨率计费的档位、图片数量和单价,详情弹窗同步展示该计费明细。
  5. 补齐 default 主题六种语言的新增文案。

行为说明

  • 默认仍为 token 模式,现有模型计费行为不变。
  • 启用 per_size 后,实际结算使用按分辨率价格,不再叠加普通 token / tiered expression 计费,避免双重扣费。
  • 如果请求 size 为 auto 或无法识别,默认按 2K 档位计费。
  • 从「按分辨率」切回固定价、按 token 或表达式计费时,会移除对应模型的 image_model_setting 配置,避免旧配置继续影响后端扣费。

影响范围

  • 后端:Image API 路径的图片生成结算、系统配置热更新、用量日志 other 字段。
  • 前端:系统设置 → 模型定价编辑器、用量日志列表与详情弹窗。
  • 不修改数据库 schema;配置仍走现有 options 表。

相比上一版本的改动(CodeRabbit review 修复)

  • model-pricing-sheet: 切换到「新增模型」时补充重置 perRequestSubMode/price1k/price2k/price4k,避免旧值泄漏进新模型
  • model-pricing-sheet: 将 Sub-mode / 1K price / 2K price / 4K price 及分辨率档位 label 统一走 t()
  • model-ratio-visual-editor: React.memo comparator 补充 imageModelSetting 比较,避免 prop 更新被忽略
  • model-ratio-visual-editor: 将 safeJsonParse(imageModelSetting) 提到 .map() 循环外,避免重复解析
  • ratio-settings-card: ImageModelSetting zod schema 增加 JSON 格式验证
  • ratio-settings-card: modelNormalizedDefaultssaveModelRatios 中对 ImageModelSetting 统一走 normalizeJsonString,避免格式差异导致误判 dirty
  • image_model_setting.go: parsePositiveInt 增加整数溢出保护(上限 65536)
  • zh.json: auth.resetPasswordConfirm.retry 单位从英文 s 改为

验证

  • git diff --check
  • cd web/default && bun run i18n:sync
  • cd web/default && bun run typecheck

备注

本地环境没有 go / gofmt 命令,因此未在本机运行 Go 测试或 gofmt。

Summary by CodeRabbit

  • New Features

    • Per-resolution image pricing: charge per image by tier (1K, 2K, 4K) as an alternative to token billing.
    • Models can be configured for per-image billing with per-tier prices; runtime classifies image size and applies per-size billing.
  • Improvements

    • Admin billing UI: controls to edit per-resolution pricing, preview, and persistence.
    • Quota/surcharge, consumption flow, and usage logs now include per-size image billing details for clearer billing and reporting.
  • Localization

    • Added/updated translation keys for new image pricing UI and labels.

Review Change Stack

Signed-off-by: Micah-Zheng <102610064+Micah-Zheng@users.noreply.github.com>
- model-pricing-sheet: reset perRequestSubMode/price1k/price2k/price4k
  in else branch when switching to add-new-model mode
- model-pricing-sheet: wrap Sub-mode/1K price/2K price/4K price and
  resolution tier labels with t() for i18n consistency
- model-ratio-visual-editor: add imageModelSetting to React.memo
  comparator so prop updates are not silently ignored
- model-ratio-visual-editor: hoist safeJsonParse(imageModelSetting) out
  of per-model .map() to avoid redundant JSON parsing
- ratio-settings-card: validate ImageModelSetting as JSON in zod schema
- ratio-settings-card: normalize ImageModelSetting in modelNormalizedDefaults
  and saveModelRatios to prevent false-positive dirty diffs
- image_model_setting.go: add overflow guard to parsePositiveInt
- zh.json: fix auth.resetPasswordConfirm.retry unit from 's' to '秒'
- i18n: add Sub-mode/1K price/2K price/4K price/tier label keys to all locales

Signed-off-by: Micah-Zheng <102610064+Micah-Zheng@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aad89468-42ac-4307-ae6b-e9e53b3eea91

📥 Commits

Reviewing files that changed from the base of the PR and between 41163d3 and 2c0070f.

📒 Files selected for processing (1)
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx

Walkthrough

This PR implements per-resolution image billing for image-generation models: adds image-model config and index rebuild, runtime size-tier classification and per-size surcharge in quota calculation, admin UI for per-resolution tier pricing, usage-log display, and i18n updates.

Changes

Per-Size Image Billing Feature

Layer / File(s) Summary
Image model configuration and billing lookup
setting/operation_setting/image_model_setting.go, model/option.go
Defines billing modes and size tiers, ImageModelConfig and ImageModelSetting types, default model map, atomic in-memory snapshot, RebuildImageModelIndex, and lookup helpers (GetImageModelConfig, IsImagePerSizeBilling, GetImagePerSizePrice, ClassifyImageSizeTier). Config updates trigger index rebuild.
Image size classification and per-size quota calculation
relay/image_handler.go, service/text_quota.go
At request time, detects per-size billing, classifies image size into 1K/2K/4K (fallback), sets gin context flags (billing, tier, count). Quota calculation applies per-size surcharge using GetImagePerSizePrice, scales by group ratio and quota-per-unit, and derives final quota from rounded surcharge with special-case handling. PostTextConsumeQuota skips tiered settlement when per-size billing is active and logs per-size metadata.
Admin model pricing UI with per-resolution support
web/default/src/features/system-settings/models/model-pricing-sheet.tsx, web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx, web/default/src/features/system-settings/models/model-ratio-form.tsx, web/default/src/features/system-settings/models/ratio-settings-card.tsx
Adds per-request sub-mode (fixed vs per-resolution) and per-resolution tier price inputs (price1k/price2k/price4k). Visual editor shows per-resolution rows, editor state initializes/persists per-resolution prices, and persistence writes image_model_setting.models updates (sentinel price handling for per-resolution per-request).
Billing settings defaults and types
web/default/src/features/system-settings/billing/index.tsx, web/default/src/features/system-settings/billing/section-registry.tsx, web/default/src/features/system-settings/types.ts
Adds image_model_setting.models to default billing settings and BillingSettings type; initializes model defaults from settings and normalizes ImageModelSetting in model defaults flow.
Usage log fields and per-size charge display
web/default/src/features/usage-logs/types.ts, web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx, web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
Extends LogOtherData with per-size billing fields and renders per-size image generation row in billing breakdown and detail segments in usage logs (including optional group-ratio segment).
Internationalization
web/default/src/i18n/locales/*, web/default/src/i18n/locales/_reports/*
Adds translation keys for per-resolution pricing UI (sub-mode, 1K/2K/4K price labels, image unit labels) across locales and updates untranslated reports and sync report counts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • seefs001
  • Calcium-Ion

🐰 A rabbit hops through tiers of pricing,
Per K, per K, each image slicing,
1K, 2K, 4K in view,
Small hops, big charges — all true!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(billing): add per-resolution image model billing' directly and accurately summarizes the main change: introducing per-resolution (1K/2K/4K tier) image model billing configuration and logic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

Signed-off-by: Micah-Zheng <102610064+Micah-Zheng@users.noreply.github.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: 8

🤖 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/usage-logs/components/dialogs/details-dialog.tsx`:
- Around line 296-303: The image billing row uses image_per_size_count and
image_size_tier directly which lets 0 and empty string appear; before pushing
the row (in the block guarded by other.image_per_size_billing &&
other.image_per_size_price != null) clamp the count to a positive integer (e.g.,
const count = Math.max(1, other.image_per_size_count ?? 1)) and default the tier
to a concrete fallback like '2K' (e.g., const tier = other.image_size_tier ??
'2K'), then use those sanitized values in the rows.push call that constructs the
label/value (rows.push with label 'Image Generation' and value using fmtPrice
and t).

In `@web/default/src/i18n/locales/_reports/_sync-report.json`:
- Around line 12-38: _sync-report.json shows missingCount: 7 for all non-en
locales but actual key comparison between en.json and
fr.json/ja.json/ru.json/vi.json/zh.json shows no missing leaf keys; regenerate
the i18n sync report (or re-run the sync tool) so missingCount reflects current
key coverage, or update the report generation logic to compute missingCount the
same way the key-comparison uses (handle normalization/empty values/leaf-key
resolution consistently); specifically re-run the reporter that produces
_reports/_sync-report.json and verify the fields "missingCount",
"untranslatedCount", and listed untranslated keys match the outputs from compare
of en.json against fr.json/ja.json/ru.json/vi.json/zh.json, then commit the
regenerated _sync-report.json and adjust PR text if the metric semantics
changed.

In `@web/default/src/i18n/locales/_reports/ja.untranslated.json`:
- Around line 87-90: Replace the flat English translation keys ("Sub-mode", "1K
price", "2K price", "4K price") with hierarchical, semantically clear keys to
match project conventions; for example change those keys to modelPricing.subMode
(or modelPricing.requestMode.subMode), modelPricing.tier1KPrice,
modelPricing.tier2KPrice and modelPricing.tier4KPrice in ja.untranslated.json
and apply the same renames in ru.untranslated.json and zh.untranslated.json,
updating any code references that consume these keys to use the new dot-notated
keys so lookups remain correct.

In `@web/default/src/i18n/locales/fr.json`:
- Around line 2901-2904: The French locale file contains untranslated keys
"Sub-mode", "1K price", "2K price", and "4K price" which should be translated to
French; update the JSON entries for these keys in
web/default/src/i18n/locales/fr.json (the string keys "Sub-mode", "1K price",
"2K price", "4K price") with their proper French equivalents (e.g., translate to
"Sous-mode" and appropriate French phrases for price labels such as "Prix pour
1K", "Prix pour 2K", "Prix pour 4K" or your project's established wording)
ensuring you keep the same keys and valid JSON formatting.

In `@web/default/src/i18n/locales/ja.json`:
- Around line 2901-2907: The listed localization entries ("Sub-mode", "1K
price", "2K price", "4K price", "1K (≤ 1024px)", "2K (≤ 2048px)", "4K (>
2048px)") are still English in the ja.json locale; update each value to the
appropriate Japanese translation (e.g., translate "Sub-mode" and the size/price
labels and their parentheses/units) so the keys remain the same but their values
are Japanese text, preserving punctuation and units like "≤" and ">" exactly as
in the keys.

In `@web/default/src/i18n/locales/ru.json`:
- Around line 2901-2904: The Russian locale file ru.json contains untranslated
labels for billing sub-mode keys ("Sub-mode", "1K price", "2K price", "4K
price"); update their values to proper Russian translations (e.g., "Режим",
"Цена за 1К", "Цена за 2К", "Цена за 4К" or other approved wording) so RU users
see consistent Russian UI, leaving the keys unchanged and only modifying the
right-hand side strings in ru.json.

In `@web/default/src/i18n/locales/vi.json`:
- Around line 2901-2904: The four English labels "Sub-mode", "1K price", "2K
price", and "4K price" in the vi.json locale should be replaced with their
Vietnamese translations so the per-resolution pricing form is fully localized;
update the values for the keys "Sub-mode" (e.g., "Chế độ phụ"), "1K price"
(e.g., "Giá 1K"), "2K price" ("Giá 2K") and "4K price" ("Giá 4K") in the vi.json
file to the Vietnamese strings.

In `@web/default/src/i18n/locales/zh.json`:
- Around line 2901-2907: The new zh.json entries ("Sub-mode", "1K price", "2K
price", "4K price", "1K (≤ 1024px)", "2K (≤ 2048px)", "4K (> 2048px)") are still
in English; replace each value with an appropriate Chinese translation (e.g.,
translate "Sub-mode" to "子模式", "1K price" to "1K 价格", "2K price" to "2K 价格", "4K
price" to "4K 价格", and localize the resolution labels like "1K (≤ 1024px)" ->
"1K(≤1024px)", etc.) so the zh locale has no English fallbacks.
🪄 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: 8edcf33e-b26f-49a0-b445-499156ed20fb

📥 Commits

Reviewing files that changed from the base of the PR and between e13d673 and 491e622.

📒 Files selected for processing (24)
  • model/option.go
  • relay/image_handler.go
  • service/text_quota.go
  • setting/operation_setting/image_model_setting.go
  • web/default/src/features/system-settings/billing/index.tsx
  • web/default/src/features/system-settings/billing/section-registry.tsx
  • web/default/src/features/system-settings/models/model-pricing-sheet.tsx
  • web/default/src/features/system-settings/models/model-ratio-form.tsx
  • web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • web/default/src/features/system-settings/models/ratio-settings-card.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/default/src/features/usage-logs/types.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/_reports/zh.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 +12 to +38
"missingCount": 7,
"extrasCount": 0,
"untranslatedCount": 21
},
"ja": {
"file": "ja.json",
"missingCount": 0,
"missingCount": 7,
"extrasCount": 0,
"untranslatedCount": 120
"untranslatedCount": 124
},
"ru": {
"file": "ru.json",
"missingCount": 0,
"missingCount": 7,
"extrasCount": 0,
"untranslatedCount": 135
"untranslatedCount": 139
},
"vi": {
"file": "vi.json",
"missingCount": 0,
"missingCount": 7,
"extrasCount": 0,
"untranslatedCount": 23
},
"zh": {
"file": "zh.json",
"missingCount": 0,
"missingCount": 7,
"extrasCount": 0,
"untranslatedCount": 99
"untranslatedCount": 103
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all new translation keys in en.json that are missing from other locales

# Get the list of keys added to en.json (assuming this is the base locale)
echo "=== Checking for new keys in en.json ==="
rg -n '"(Sub-mode|1K price|2K price|4K price)":' web/default/src/i18n/locales/en.json

echo -e "\n=== Searching for any other per-resolution billing related keys ==="
# Look for related keys that might be the missing 3
rg -n '".*([Pp]er.?(size|resolution)|image.*price|tier.*price).*":' web/default/src/i18n/locales/en.json

echo -e "\n=== Checking coverage in all locale files ==="
for locale in fr ja ru vi zh; do
  echo "--- $locale.json ---"
  rg -c '"(Sub-mode|1K price|2K price|4K price)":' "web/default/src/i18n/locales/$locale.json" || echo "0 matches"
done

Repository: QuantumNous/new-api

Length of output: 1007


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== List reports dir =="
ls -la web/default/src/i18n/locales/_reports || true
echo

echo "== Show sync report file header (first ~120 lines) =="
wc -l web/default/src/i18n/locales/_reports/_sync-report.json
sed -n '1,160p' web/default/src/i18n/locales/_reports/_sync-report.json
echo

echo "== Extract locale sections from sync report (missing/untranslated keys if present) =="
# Try to extract the ja/ru/zh/fr/vi blocks around counts
rg -n '"(fr|ja|ru|vi|zh)"\s*:\s*\{' -n web/default/src/i18n/locales/_reports/_sync-report.json
echo
python3 - <<'PY'
import json
p="web/default/src/i18n/locales/_reports/_sync-report.json"
with open(p,"r",encoding="utf-8") as f:
    data=json.load(f)
# Print available top-level structure
print("Top-level keys:", list(data.keys())[:30])
for loc in ["fr","ja","ru","vi","zh"]:
    if loc in data:
        print("\n== ",loc,"==")
        d=data[loc]
        # show keys present in that locale object
        print("fields:", list(d.keys()))
        for k in ["file","missingCount","untranslatedCount","missingKeys","untranslatedKeys"]:
            if k in d:
                v=d[k]
                if isinstance(v,list):
                    print(k, "len=",len(v))
                    print("sample:", v[:20])
                else:
                    print(k, v)
PY

echo "== Show untranslated report files if present =="
for f in web/default/src/i18n/locales/_reports/*untranslated*.json; do
  [ -f "$f" ] || continue
  echo
  echo "---- $f ----"
  wc -l "$f"
  # print first 120 lines (or whole if shorter)
  if [ "$(wc -l <"$f")" -le 160 ]; then
    cat -n "$f"
  else
    sed -n '1,160p' "$f"
  fi
done

echo
echo "== If untranslated files include key arrays, list the keys for ja/ru/zh =="
python3 - <<'PY'
import glob, json
targets=["ja","ru","zh"]
paths=[]
for loc in targets:
    # find best matching file
    cand=glob.glob(f"web/default/src/i18n/locales/_reports/{loc}*untranslated*.json")
    paths.append((loc,cand[0] if cand else None))
print(paths)
for loc,p in paths:
    if not p: 
        continue
    with open(p,"r",encoding="utf-8") as f:
        data=json.load(f)
    # Try common shapes
    keys=[]
    if isinstance(data,dict):
        # maybe { "keys":[...] } or direct mapping
        if "keys" in data and isinstance(data["keys"],list):
            keys=data["keys"]
        else:
            # direct mapping: { "Some.key": "en string", ... }
            keys=list(data.keys())
    elif isinstance(data,list):
        keys=data
    print("\n==",loc,"== file:",p)
    print("key count:",len(keys))
    # show keys
    for k in keys[:200]:
        print(k)
PY

echo
echo "== Compare which keys exist in en.json but are absent in each locale json =="
python3 - <<'PY'
import json
from pathlib import Path

base=Path("web/default/src/i18n/locales/en.json")
en=json.loads(base.read_text(encoding="utf-8"))
locales=["fr","ja","ru","vi","zh"]

# flatten dot-keys for top-level keys only (reports likely use flat JSON keys)
# We'll restrict to keys that look like the newly mentioned ones:
candidates={"Sub-mode","1K price","2K price","4K price"}

# Determine missing keys overall by exact top-level keys.
en_keys=set(en.keys())
print("Total en top-level keys:",len(en_keys))
for loc in locales:
    loc_path=Path(f"web/default/src/i18n/locales/{loc}.json")
    d=json.loads(loc_path.read_text(encoding="utf-8"))
    missing=sorted(en_keys - set(d.keys()))
    print(f"\n== {loc} ==")
    print("missing top-level keys count:",len(missing))
    # show any that match candidates or include billing strings
    match=[k for k in missing if any(c in k for c in candidates) or "K price" in k or "Sub-mode" in k or "price" in k or "resolution" in k]
    print("missing keys matching billing-ish candidates:",len(match))
    for k in match:
        print("-",k)
PY

Repository: QuantumNous/new-api

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
from pathlib import Path

base=Path("web/default/src/i18n/locales")
en=json.loads((base/"en.json").read_text(encoding="utf-8"))
en_keys=set(en.keys())

locales=["fr","ja","ru","vi","zh"]
for loc in locales:
    loc_data=json.loads((base/f"{loc}.json").read_text(encoding="utf-8"))
    loc_keys=set(loc_data.keys())
    missing=sorted(en_keys-loc_keys)
    print(f"\n== Missing in {loc}.json: {len(missing)} ==")
    for k in missing:
        print("-",k)

    # Load untranslated report keys for this locale (if present)
    u_path=base/"_reports"/f"{loc}.untranslated.json"
    if u_path.exists():
        untranslated=json.loads(u_path.read_text(encoding="utf-8"))
        u_keys=set(untranslated.keys())
        inter=[k for k in missing if k in u_keys]
        print(f"Intersection(missing, untranslated report keys) for {loc}: {len(inter)}")
        if inter:
            for k in inter:
                print("  -",k)

    # Show which missing keys are in the 4 mentioned in review
    watch=["Sub-mode","1K price","2K price","4K price"]
    watch_present=[k for k in missing if k in watch]
    print(f"Missing among watch keys {watch}: {len(watch_present)}/{len(watch_present)} -> {watch_present}")
PY

Repository: QuantumNous/new-api

Length of output: 923


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
from pathlib import Path

base=Path("web/default/src/i18n/locales")

def load(p):
    return json.loads(p.read_text(encoding="utf-8"))

en=load(base/"en.json")
print("Top-level en keys:", len(en))
# show top-level value types
types={}
for k,v in list(en.items())[:50]:
    types[type(v).__name__]=types.get(type(v).__name__,0)+1
print("Sample top-level value types (first 50 keys):", types)

def find_paths(obj, target, path=()):
    paths=[]
    if isinstance(obj, dict):
        for k,v in obj.items():
            if k==target:
                paths.append(path+(k,))
            paths += find_paths(v, target, path+(k,))
    elif isinstance(obj, list):
        for i,v in enumerate(obj):
            paths += find_paths(v, target, path+(str(i),))
    return paths

for key in ["Sub-mode","1K price","2K price","4K price"]:
    paths=find_paths(en,key)
    print(f"\nPaths to key '{key}' in en.json (up to 10):", paths[:10])

ja=load(base/"ja.json")
print("\nTop-level ja keys:", len(ja))

for key in ["Sub-mode","1K price","2K price","4K price"]:
    paths=find_paths(ja,key)
    print(f"Paths to key '{key}' in ja.json (up to 10):", paths[:10])
PY

python3 - <<'PY'
import json
from pathlib import Path

base=Path("web/default/src/i18n/locales")

def load(p):
    return json.loads(p.read_text(encoding="utf-8"))

def flatten_keys(obj, prefix=()):
    # Returns set of "paths" to leaf values where leaf is non-dict/list
    out=set()
    if isinstance(obj, dict):
        for k,v in obj.items():
            out |= flatten_keys(v, prefix+(str(k),))
    elif isinstance(obj, list):
        for i,v in enumerate(obj):
            out |= flatten_keys(v, prefix+(str(i),))
    else:
        out.add(".".join(prefix))
    return out

en=load(base/"en.json")
for loc in ["fr","ja","ru","vi","zh"]:
    d=load(base/f"{loc}.json")
    en_keys=flatten_keys(en)
    loc_keys=flatten_keys(d)
    missing=en_keys-loc_keys
    print(f"\n== Leaf-path missing in {loc}.json: {len(missing)} ==")
    # show a few missing paths that look relevant
    cand=[m for m in missing if any(s in m for s in ["Sub-mode","1K price","2K price","4K price","price"])]
    print("Candidate missing (price/Sub-mode):", len(cand))
    for m in sorted(cand)[:30]:
        print("-",m)
    # show first 20 missing paths overall
    for m in sorted(missing)[:20]:
        print("  -",m)
PY

python3 - <<'PY'
import json
from pathlib import Path
base=Path("web/default/src/i18n/locales")
def load(p): return json.loads(p.read_text(encoding="utf-8"))

en=load(base/"en.json")
loc=load(base/"fr.json")

# If missingCount could be "empty" values: check leaf paths where en has non-empty but locale has "" or None
def iter_leaf(obj, prefix=()):
    if isinstance(obj, dict):
        for k,v in obj.items():
            yield from iter_leaf(v,prefix+(str(k),))
    elif isinstance(obj, list):
        for i,v in enumerate(obj):
            yield from iter_leaf(v,prefix+(str(i),))
    else:
        yield (".".join(prefix), obj)

en_leaf=dict(iter_leaf(en))
loc_leaf=dict(iter_leaf(loc))

empties=[k for k,v in en_leaf.items() if v not in ("",None) and (k not in loc_leaf or loc_leaf[k] in ("",None))]
print("\nLeaf paths where en has non-empty but locale has empty/missing:", len(empties))
for k in sorted(empties)[:40]:
    print("-",k)
PY

Repository: QuantumNous/new-api

Length of output: 1236


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
from pathlib import Path

base=Path("web/default/src/i18n/locales")
def load(loc):
    return json.loads((base/f"{loc}.json").read_text(encoding="utf-8"))

def all_path_keys(obj, prefix=()):
    # collect every dict key path (not just leaves)
    out=set()
    if isinstance(obj, dict):
        for k,v in obj.items():
            out.add(".".join(prefix+(str(k),)))
            out |= all_path_keys(v, prefix+(str(k),))
    elif isinstance(obj, list):
        for i,v in enumerate(obj):
            out |= all_path_keys(v, prefix+(str(i),))
    return out

def leaf_paths(obj, prefix=()):
    out=set()
    if isinstance(obj, dict):
        for k,v in obj.items():
            out |= leaf_paths(v, prefix+(str(k),))
    elif isinstance(obj, list):
        for i,v in enumerate(obj):
            out |= leaf_paths(v, prefix+(str(i),))
    else:
        out.add(".".join(prefix))
    return out

en=load("en")
en_leaves=leaf_paths(en)
en_keys=all_path_keys(en)
print("en leaf count:",len(en_leaves))
print("en all-dict-key-path count:",len(en_keys))

watch=["translation.Sub-mode","translation.1K price","translation.2K price","translation.4K price","Sub-mode","1K price","2K price","4K price"]

for loc in ["fr","ja","ru","vi","zh"]:
    d=load(loc)
    leaves=leaf_paths(d)
    keys=all_path_keys(d)
    missing_leaves=en_leaves-leaves
    missing_keys=en_keys-keys
    print(f"\n== {loc} ==")
    print("missing leaf paths:",len(missing_leaves))
    print("missing dict-key-paths:",len(missing_keys))
    # show if any watch paths are missing in either representation
    for w in watch:
        if w in missing_leaves or w in missing_keys:
            print("MISSING watch path:",w)
PY

Repository: QuantumNous/new-api

Length of output: 406


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la web/default/src/i18n/locales/_reports
echo

echo "== Any *missing* report files? =="
ls -1 web/default/src/i18n/locales/_reports | rg -i 'missing' || true
echo

# Grep sync tool outputs for "missing" keys arrays if any
rg -n '"missingCount"|missingKeys|missing.*untranslated|missing' web/default/src/i18n/locales/_reports/_sync-report.json || true
echo

# Search for reports that might include missing keys for locales
for loc in fr ja ru vi zh; do
  echo "== Potential missing report for $loc =="
  ls -1 web/default/src/i18n/locales/_reports | rg -i "^${loc}.*missing.*\.json$" || true
done

# Also inspect locale untranslated reports for any keys that may be part of 'missing' (heuristic)
for f in web/default/src/i18n/locales/_reports/*untranslated*.json; do
  echo
  echo "---- $f ----"
  # show if report contains keys that are not in locale.json (shouldn't happen), or just count lines
  wc -l "$f"
done

Repository: QuantumNous/new-api

Length of output: 1657


Clarify i18n sync report missingCount=7 vs actual locale key coverage

The “3 unaccounted missing keys” claim doesn’t hold: comparing web/default/src/i18n/locales/en.json with fr.json/ja.json/ru.json/vi.json/zh.json shows no missing leaf keys, and the untranslated reports for ja/ru/zh include the 4 keys (“Sub-mode”, “1K price”, “2K price”, “4K price”) that were called out.

However, _reports/_sync-report.json still reports missingCount: 7 for all non-English locales. Either the report’s “missing” metric is computed differently than key presence (e.g., normalization/empties/other rules), or the report is stale—update/regenerate the sync report and align the PR description to what missingCount actually represents.

🤖 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/_reports/_sync-report.json` around lines 12 -
38, _sync-report.json shows missingCount: 7 for all non-en locales but actual
key comparison between en.json and fr.json/ja.json/ru.json/vi.json/zh.json shows
no missing leaf keys; regenerate the i18n sync report (or re-run the sync tool)
so missingCount reflects current key coverage, or update the report generation
logic to compute missingCount the same way the key-comparison uses (handle
normalization/empty values/leaf-key resolution consistently); specifically
re-run the reporter that produces _reports/_sync-report.json and verify the
fields "missingCount", "untranslatedCount", and listed untranslated keys match
the outputs from compare of en.json against
fr.json/ja.json/ru.json/vi.json/zh.json, then commit the regenerated
_sync-report.json and adjust PR text if the metric semantics changed.

Comment on lines +87 to +90
"Sub-mode": "Sub-mode",
"1K price": "1K price",
"2K price": "2K price",
"4K price": "4K price",
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use hierarchical translation key names.

The new translation keys use plain English labels ("Sub-mode", "1K price", "2K price", "4K price") instead of hierarchical dot notation. This pattern also appears in ru.untranslated.json and zh.untranslated.json.

As per coding guidelines, translation keys should use hierarchical and semantically clear names such as dashboard.overview.title. For per-resolution pricing, consider keys like:

  • modelPricing.subMode or modelPricing.requestMode.subMode
  • modelPricing.tier1KPrice
  • modelPricing.tier2KPrice
  • modelPricing.tier4KPrice

This improves semantic clarity, organization, and reduces potential key collisions.

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/_reports/ja.untranslated.json` around lines 87 -
90, Replace the flat English translation keys ("Sub-mode", "1K price", "2K
price", "4K price") with hierarchical, semantically clear keys to match project
conventions; for example change those keys to modelPricing.subMode (or
modelPricing.requestMode.subMode), modelPricing.tier1KPrice,
modelPricing.tier2KPrice and modelPricing.tier4KPrice in ja.untranslated.json
and apply the same renames in ru.untranslated.json and zh.untranslated.json,
updating any code references that consume these keys to use the new dot-notated
keys so lookups remain correct.

Comment on lines +2901 to +2904
"Sub-mode": "Sub-mode",
"1K price": "1K price",
"2K price": "2K price",
"4K price": "4K price",
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 remaining English labels in the French locale.

Line 2901–2904 are still English (Sub-mode, 1K price, 2K price, 4K price), so FR users will see mixed-language UI in the new billing flow.

💡 Suggested fix
-    "Sub-mode": "Sub-mode",
-    "1K price": "1K price",
-    "2K price": "2K price",
-    "4K price": "4K price",
+    "Sub-mode": "Sous-mode",
+    "1K price": "Prix 1K",
+    "2K price": "Prix 2K",
+    "4K price": "Prix 4K",
🤖 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 2901 - 2904, The French
locale file contains untranslated keys "Sub-mode", "1K price", "2K price", and
"4K price" which should be translated to French; update the JSON entries for
these keys in web/default/src/i18n/locales/fr.json (the string keys "Sub-mode",
"1K price", "2K price", "4K price") with their proper French equivalents (e.g.,
translate to "Sous-mode" and appropriate French phrases for price labels such as
"Prix pour 1K", "Prix pour 2K", "Prix pour 4K" or your project's established
wording) ensuring you keep the same keys and valid JSON formatting.

Comment on lines +2901 to +2907
"Sub-mode": "Sub-mode",
"1K price": "1K price",
"2K price": "2K price",
"4K price": "4K price",
"1K (≤ 1024px)": "1K (≤ 1024px)",
"2K (≤ 2048px)": "2K (≤ 2048px)",
"4K (> 2048px)": "4K (> 2048px)",
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

Untranslated strings in Japanese locale

These values are still English in ja.json, so Japanese users will see mixed-language UI.

Proposed translation patch
-    "Sub-mode": "Sub-mode",
-    "1K price": "1K price",
-    "2K price": "2K price",
-    "4K price": "4K price",
-    "1K (≤ 1024px)": "1K (≤ 1024px)",
-    "2K (≤ 2048px)": "2K (≤ 2048px)",
-    "4K (> 2048px)": "4K (> 2048px)",
+    "Sub-mode": "サブモード",
+    "1K price": "1K 価格",
+    "2K price": "2K 価格",
+    "4K price": "4K 価格",
+    "1K (≤ 1024px)": "1K(≤ 1024px)",
+    "2K (≤ 2048px)": "2K(≤ 2048px)",
+    "4K (> 2048px)": "4K(> 2048px)",
🤖 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` around lines 2901 - 2907, The listed
localization entries ("Sub-mode", "1K price", "2K price", "4K price", "1K (≤
1024px)", "2K (≤ 2048px)", "4K (> 2048px)") are still English in the ja.json
locale; update each value to the appropriate Japanese translation (e.g.,
translate "Sub-mode" and the size/price labels and their parentheses/units) so
the keys remain the same but their values are Japanese text, preserving
punctuation and units like "≤" and ">" exactly as in the keys.

Comment on lines +2901 to +2904
"Sub-mode": "Sub-mode",
"1K price": "1K price",
"2K price": "2K price",
"4K price": "4K price",
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

Untranslated Russian locale values in billing sub-mode labels

Line 2901-2904 are still English, so RU users will see mixed-language UI.

💡 Suggested fix
-    "Sub-mode": "Sub-mode",
-    "1K price": "1K price",
-    "2K price": "2K price",
-    "4K price": "4K price",
+    "Sub-mode": "Подрежим",
+    "1K price": "Цена 1K",
+    "2K price": "Цена 2K",
+    "4K price": "Цена 4K",
📝 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
"Sub-mode": "Sub-mode",
"1K price": "1K price",
"2K price": "2K price",
"4K price": "4K price",
"Sub-mode": "Подрежим",
"1K price": "Цена 1K",
"2K price": "Цена 2K",
"4K price": "Цена 4K",
🤖 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 2901 - 2904, The Russian
locale file ru.json contains untranslated labels for billing sub-mode keys
("Sub-mode", "1K price", "2K price", "4K price"); update their values to proper
Russian translations (e.g., "Режим", "Цена за 1К", "Цена за 2К", "Цена за 4К" or
other approved wording) so RU users see consistent Russian UI, leaving the keys
unchanged and only modifying the right-hand side strings in ru.json.

Comment on lines +2901 to +2904
"Sub-mode": "Sub-mode",
"1K price": "1K price",
"2K price": "2K price",
"4K price": "4K price",
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 billing labels in Vietnamese locale

Line 2901–Line 2904 are still English in vi.json, so users will see mixed-language UI in the per-resolution pricing form.

💬 Suggested fix
-    "Sub-mode": "Sub-mode",
-    "1K price": "1K price",
-    "2K price": "2K price",
-    "4K price": "4K price",
+    "Sub-mode": "Chế độ phụ",
+    "1K price": "Giá 1K",
+    "2K price": "Giá 2K",
+    "4K price": "Giá 4K",
📝 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
"Sub-mode": "Sub-mode",
"1K price": "1K price",
"2K price": "2K price",
"4K price": "4K price",
"Sub-mode": "Chế độ phụ",
"1K price": "Giá 1K",
"2K price": "Giá 2K",
"4K price": "Giá 4K",
🤖 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` around lines 2901 - 2904, The four
English labels "Sub-mode", "1K price", "2K price", and "4K price" in the vi.json
locale should be replaced with their Vietnamese translations so the
per-resolution pricing form is fully localized; update the values for the keys
"Sub-mode" (e.g., "Chế độ phụ"), "1K price" (e.g., "Giá 1K"), "2K price" ("Giá
2K") and "4K price" ("Giá 4K") in the vi.json file to the Vietnamese strings.

Comment on lines +2901 to +2907
"Sub-mode": "Sub-mode",
"1K price": "1K price",
"2K price": "2K price",
"4K price": "4K price",
"1K (≤ 1024px)": "1K (≤ 1024px)",
"2K (≤ 2048px)": "2K (≤ 2048px)",
"4K (> 2048px)": "4K (> 2048px)",
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 zh locale values instead of leaving English fallbacks.

Lines 2901-2907 are still English in zh.json, so Chinese users will see mixed-language UI for the new per-resolution billing fields.

💡 Suggested fix
-    "Sub-mode": "Sub-mode",
-    "1K price": "1K price",
-    "2K price": "2K price",
-    "4K price": "4K price",
-    "1K (≤ 1024px)": "1K (≤ 1024px)",
-    "2K (≤ 2048px)": "2K (≤ 2048px)",
-    "4K (> 2048px)": "4K (> 2048px)",
+    "Sub-mode": "子模式",
+    "1K price": "1K 价格",
+    "2K price": "2K 价格",
+    "4K price": "4K 价格",
+    "1K (≤ 1024px)": "1K(≤ 1024px)",
+    "2K (≤ 2048px)": "2K(≤ 2048px)",
+    "4K (> 2048px)": "4K(> 2048px)",
📝 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
"Sub-mode": "Sub-mode",
"1K price": "1K price",
"2K price": "2K price",
"4K price": "4K price",
"1K (≤ 1024px)": "1K (≤ 1024px)",
"2K (≤ 2048px)": "2K (≤ 2048px)",
"4K (> 2048px)": "4K (> 2048px)",
"Sub-mode": "子模式",
"1K price": "1K 价格",
"2K price": "2K 价格",
"4K price": "4K 价格",
"1K (≤ 1024px)": "1K≤ 1024px",
"2K (≤ 2048px)": "2K≤ 2048px",
"4K (> 2048px)": "4K> 2048px",
🤖 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` around lines 2901 - 2907, The new
zh.json entries ("Sub-mode", "1K price", "2K price", "4K price", "1K (≤
1024px)", "2K (≤ 2048px)", "4K (> 2048px)") are still in English; replace each
value with an appropriate Chinese translation (e.g., translate "Sub-mode" to
"子模式", "1K price" to "1K 价格", "2K price" to "2K 价格", "4K price" to "4K 价格", and
localize the resolution labels like "1K (≤ 1024px)" -> "1K(≤1024px)", etc.) so
the zh locale has no English fallbacks.

…ils dialog

Signed-off-by: Micah-Zheng <102610064+Micah-Zheng@users.noreply.github.com>
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.

1 participant