perf(model-pricing): improve model pricing editor UX - #5275
Conversation
- expose a draft commit handle from the model pricing editor panel before saving. - commit the open visual editor into the parent form before page-level save runs. - support both desktop side editor and mobile sheet save paths.
|
Linter diff in the way? Review this PR in Change Stack to focus on meaningful changes and expand context only when needed. WalkthroughAdds a structured JsonCodeEditor, central pricing core and snapshot utilities, new price input UI, localized JSON validation, and converts pricing editor/visual editor/form to forwardRef with imperative commit APIs that coordinate validation and persistence during saves. ChangesPricing & JSON editor
🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers:
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
web/default/src/features/system-settings/models/model-ratio-form.tsx (1)
93-100: 💤 Low valueAsync handler without explicit loading state during commit phase.
handleSaveis async, but the button only disables viaisSavingwhich activates afterform.handleSubmitbegins. IfcommitOpenEditorinvolves async validation (e.g.,form.trigger()), there's a brief window where the button remains clickable.In practice, the risk is low since
commitDraftin the pricing sheet callsform.trigger()which is fast. However, for robustness, consider adding a local loading state or ensuring double-click prevention.🤖 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/system-settings/models/model-ratio-form.tsx` around lines 93 - 100, handleSave can leave a small race where the save button is still clickable during commitOpenEditor; add a short-lived local flag (e.g., isCommitting) to guard against double submits: set isCommitting = true at the top of handleSave, return early if already true, await visualEditorRef.current?.commitOpenEditor(), then clear isCommitting before/after calling await form.handleSubmit(onSave)(), and ensure the save button is disabled when isCommitting || isSaving; reference handleSave, visualEditorRef.current.commitOpenEditor, form.handleSubmit and the button's isSaving prop to implement this.
🤖 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.
Nitpick comments:
In `@web/default/src/features/system-settings/models/model-ratio-form.tsx`:
- Around line 93-100: handleSave can leave a small race where the save button is
still clickable during commitOpenEditor; add a short-lived local flag (e.g.,
isCommitting) to guard against double submits: set isCommitting = true at the
top of handleSave, return early if already true, await
visualEditorRef.current?.commitOpenEditor(), then clear isCommitting
before/after calling await form.handleSubmit(onSave)(), and ensure the save
button is disabled when isCommitting || isSaving; reference handleSave,
visualEditorRef.current.commitOpenEditor, form.handleSubmit and the button's
isSaving prop to implement this.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e0333037-1566-435d-acd1-79afea3fd587
📒 Files selected for processing (3)
web/default/src/features/system-settings/models/model-pricing-sheet.tsxweb/default/src/features/system-settings/models/model-ratio-form.tsxweb/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
- add consistent tab and field spacing so each pricing mode keeps the same visual rhythm. - wrap per-request and tiered sections in shared field groups to match the per-token form structure. - keep fixed-price descriptions and validation messages aligned with the updated field layout.
- add consistent tab and field spacing so each pricing mode keeps the same visual rhythm. - wrap per-request and tiered sections in shared field groups to match the per-token form structure. - keep fixed-price descriptions and validation messages aligned with the updated field layout.
- Commit the open visual editor draft before saving model pricing settings - Show unsaved draft differences against persisted model pricing values - Move model pricing actions into the editor toolbar and refine the visual editor layout
- extract pricing form primitives, snapshot helpers, and table column setup to keep the editor components smaller. - remove draft comparison UI now that switching models discards unsaved edits. - refine the model list with a fixed actions column and tighter mode and price summary display.
- keep the global reset action in the top toolbar while moving visual-mode saves into the model editor footer. - pin the actions header with the rest of the model table headers so horizontal scrolling keeps context visible. - add action icons to make save and reset controls easier to scan.
- render pricing JSON fields from shared configuration to reduce duplicated form markup. - use fixed-height JSON textareas so long model maps scroll internally instead of stretching the page. - arrange JSON editors in responsive columns to make wider settings pages easier to scan.
- introduce a shared themed JSON editor with line numbers, formatting, status feedback, and keyboard editing helpers. - use the shared editor in model pricing JSON mode so pricing maps get consistent editor behavior. - localize structured JSON validation messages so parse errors avoid browser-specific English text.
- place the model pricing tab switcher beside the page title instead of spanning the content area. - keep the switcher width tied to its labels while preserving spacing around title status content.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
web/default/src/features/system-settings/models/utils.ts (1)
149-151:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winParse the original input to keep error line/column coordinates accurate.
trim()is correct for empty checks, but parsing and error-position extraction againsttrimmedshifts coordinates when the input has leading whitespace/newlines, so reported locations can point to the wrong line.Suggested fix
export function validateJsonString( value: string, options: JsonValidationOptions = {} ) { const { allowEmpty = true, predicate, predicateMessage } = options const trimmed = value.trim() @@ try { - const parsed = JSON.parse(trimmed) + const parsed = JSON.parse(value) @@ } catch (error: unknown) { return { valid: false, - message: formatErrorMessage(error, trimmed), - error: buildSyntaxError(error, trimmed), + message: formatErrorMessage(error, value), + error: buildSyntaxError(error, value), } } }Also applies to: 165-166, 180-181
🤖 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/system-settings/models/utils.ts` around lines 149 - 151, The code currently trims the input into `trimmed` and uses it for parsing/error-position extraction which shifts reported line/column numbers; change the logic so parsing and any error-position calculations use the original `value` (preserving leading whitespace/newlines) while keeping `trimmed` only for the empty-check (and optional predicate checks if desired). Update the block that destructures `const { allowEmpty = true, predicate, predicateMessage } = options` and the uses of `trimmed` (the `trim()` assignment and subsequent parsing/error handling) to ensure parsing functions and error position reporters reference `value` instead of `trimmed`, and apply the same change for the other occurrences around the `trimmed` usages at the other two locations.web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx (2)
639-659:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winLocalize the mode filter labels.
These options are rendered directly in the toolbar, but the labels are still hardcoded English strings, so this filter stays untranslated when the locale changes.
Suggested fix
{ - label: 'Per-token', + label: t('Per-token'), value: 'per-token', count: modeCounts['per-token'], }, { - label: 'Per-request', + label: t('Per-request'), value: 'per-request', count: modeCounts['per-request'], }, { - label: 'Expression', + label: t('Expression'), value: 'tiered_expr', count: modeCounts.tiered_expr, },As per coding guidelines, all user-facing text in TSX should go through
t()viauseTranslation().🤖 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/system-settings/models/model-ratio-visual-editor.tsx` around lines 639 - 659, The filter option labels for billingMode are hardcoded English strings; update them to use the translator function t() (from useTranslation) so they are localized — e.g. replace 'Per-token', 'Per-request', 'Expression' with t('...') keys (like t('mode.perToken'), t('mode.perRequest'), t('mode.expression') or your project's existing keys) in the options array where filters is defined, keeping the existing value and count fields (modeCounts['per-token'], modeCounts['per-request'], modeCounts.tiered_expr) unchanged; ensure useTranslation() is imported/used in this file and that the chosen translation keys exist in the locale files.Source: Coding guidelines
799-816:⚠️ Potential issue | 🟠 Major | ⚡ Quick winInclude the
saved*snapshot props in the custommemocomparator.This component derives
modelsfrom both the saved and draft JSON blobs, but the equality function only compares the draft side. If only the saved snapshot changes, the editor can keep rendering staleisDraftChanged/isDraftNew/isDraftDeletedstate until some unrelated prop changes.Suggested fix
(prevProps, nextProps) => { return ( + prevProps.savedModelPrice === nextProps.savedModelPrice && + prevProps.savedModelRatio === nextProps.savedModelRatio && + prevProps.savedCacheRatio === nextProps.savedCacheRatio && + prevProps.savedCreateCacheRatio === nextProps.savedCreateCacheRatio && + prevProps.savedCompletionRatio === nextProps.savedCompletionRatio && + prevProps.savedImageRatio === nextProps.savedImageRatio && + prevProps.savedAudioRatio === nextProps.savedAudioRatio && + prevProps.savedAudioCompletionRatio === nextProps.savedAudioCompletionRatio && + prevProps.savedBillingMode === nextProps.savedBillingMode && + prevProps.savedBillingExpr === nextProps.savedBillingExpr && prevProps.modelPrice === nextProps.modelPrice && prevProps.modelRatio === nextProps.modelRatio && prevProps.cacheRatio === nextProps.cacheRatio && prevProps.createCacheRatio === nextProps.createCacheRatio && prevProps.completionRatio === nextProps.completionRatio &&🤖 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/system-settings/models/model-ratio-visual-editor.tsx` around lines 799 - 816, The memo comparator for ModelRatioVisualEditor currently only compares the draft props and misses the saved snapshot props, causing stale derived state; update the custom equality function (the second argument passed to memo when creating ModelRatioVisualEditor from ModelRatioVisualEditorComponent) to also compare the corresponding saved* props (e.g., savedModelPrice, savedModelRatio, savedCacheRatio, savedCreateCacheRatio, savedCompletionRatio, savedImageRatio, savedAudioRatio, savedAudioCompletionRatio, savedBillingMode, savedBillingExpr, and any other saved* snapshot props used by the component) alongside the existing draft props so the component re-renders when either draft or saved snapshots change.web/default/src/features/system-settings/models/model-pricing-sheet.tsx (1)
409-435:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject empty fixed-price drafts before
commitDraft()returns data.Selecting Per-request with an empty
pricestill passes this validator. Downstream,persistPricingData()inmodel-ratio-visual-editor.tsxonly serializes fixed-price mode whendata.priceis present, so this path can silently rewrite the model as token-priced or drop it entirely if no ratio fields are set.Suggested fix
const validatePricingValues = useCallback(() => { + if (pricingMode === 'per-request' && !hasValue(form.getValues('price'))) { + form.setError('price', { + message: t('Fixed price is required before saving.'), + }) + return false + } + if ( pricingMode === 'per-token' && toNumberOrNull(promptPrice) === null && laneConfigs.some( ({ key }) => laneEnabled[key] && hasValue(lanePrices[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/system-settings/models/model-pricing-sheet.tsx` around lines 409 - 435, The validator currently only checks token-pricing paths; add a check in validatePricingValues to reject empty fixed-price ("per-request") drafts before commitDraft() returns data by verifying the fixed price exists (e.g. toNumberOrNull(promptPrice) !== null or data.price present) when pricingMode === 'per-request'; if missing, call form.setError for the fixed-price field (e.g. 'price' or 'promptPrice') with an appropriate message so persistPricingData() in model-ratio-visual-editor.tsx won't silently drop or convert the model. Ensure the new check references pricingMode, promptPrice (or the fixed-price field), and uses the same form API as the existing errors.
🧹 Nitpick comments (3)
web/default/src/features/system-settings/models/model-pricing-snapshots.ts (1)
63-74: ⚡ Quick winCode duplication: extract shared numeric helpers.
The
toNumberOrNullfunction (lines 63-67) is identical to the one inmodel-pricing-core.ts(lines 151-155), andratioToPrice(lines 69-74) duplicates similar logic fromratioToBasePriceandderiveLanePricein the core module. Extract these shared utilities to a common location to maintain a single source of truth.♻️ Refactor to use shared helpers
Option 1: Import from model-pricing-core.ts:
+import { toNumberOrNull as coreToNumberOrNull } from './model-pricing-core' import { formatPricingNumber } from './pricing-format' // ... types ... -const toNumberOrNull = (value?: string) => { - if (!hasPricingValue(value)) return null - const num = Number(value) - return Number.isFinite(num) ? num : null -} +const toNumberOrNull = (value?: string) => coreToNumberOrNull(value) const ratioToPrice = (ratio?: string, denominator?: string) => { const ratioNumber = toNumberOrNull(ratio) const denominatorNumber = denominator ? toNumberOrNull(denominator) : 2 if (ratioNumber === null || denominatorNumber === null) return '' return formatPricingNumber(ratioNumber * denominatorNumber) }Option 2: Create a shared utility module (preferred):
Create
web/default/src/features/system-settings/models/pricing-utils.ts:export function toNumberOrNull(value: unknown): number | null { const hasValue = value !== '' && value !== null && value !== undefined && value !== false if (!hasValue && value !== 0) return null const num = Number(value) return Number.isFinite(num) ? num : null } export function computePrice(ratio: unknown, denominator: number): string { const ratioNumber = toNumberOrNull(ratio) if (ratioNumber === null) return '' return formatPricingNumber(ratioNumber * denominator) }Then import in both files.
🤖 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/system-settings/models/model-pricing-snapshots.ts` around lines 63 - 74, Extract the duplicated numeric helpers into a single shared module (e.g., pricing-utils) and import them where needed: move the existing toNumberOrNull and the ratio logic out of model-pricing-snapshots (functions toNumberOrNull and ratioToPrice) and into the new shared utilities (preserve function names or use clear names like toNumberOrNull and computePrice/computeRatioPrice), ensure the helper accepts the same input types (string/unknown) and returns number|null or '' as before, and have ratioToBasePrice and deriveLanePrice in the core module use the same exported helpers; update imports in model-pricing-snapshots and the core module and keep formatPricingNumber available (import it into the new utils if necessary).web/default/src/components/json-code-editor.tsx (2)
109-117: ⚡ Quick winReplace the nested ternary in indent removal logic.
This branch currently uses a 2-level nested ternary; switch to
if/else(or a tiny helper) to comply with the TS/TSX rule and simplify maintenance.As per coding guidelines: “Prohibit nested ternary expressions with 2 or more levels; use if-else, early returns, or extract functions instead.”Suggested refactor
- .map((line) => - line.startsWith(' ') - ? line.slice(2) - : line.startsWith('\t') - ? line.slice(1) - : line - ) + .map((line) => { + if (line.startsWith(' ')) return line.slice(2) + if (line.startsWith('\t')) return line.slice(1) + return line + })🤖 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/components/json-code-editor.tsx` around lines 109 - 117, The nested ternary inside the indent-removal branch of the nextBlock computation (the lines.map callback used when event.shiftKey is true) should be replaced with an explicit if/else or small helper function to avoid a 2-level nested ternary; locate the map callback around nextBlock and replace the expression that currently chooses between line.slice(2), line.slice(1) or line with a clear if/else (or extracted function like removeIndent(line): string) that checks startsWith(' ') then startsWith('\t') else returns line, preserving existing behavior.Source: Coding guidelines
39-49: ⚡ Quick winAvoid destructuring component props in TSX component signatures.
Please keep props as a single
propsobject and access fields viaprops.xxxto match the repo rule and keep prop flow explicit.As per coding guidelines: “Do not destructure component props; use `props.xxx` directly instead for clarity.”Suggested refactor
-export function JsonCodeEditor({ - value, - onChange, - disabled, - heightClassName = 'h-56 min-h-56 max-h-56', - className, - id, - 'aria-describedby': ariaDescribedBy, - 'aria-invalid': ariaInvalid, - ...rootProps -}: JsonCodeEditorProps) { +export function JsonCodeEditor(props: JsonCodeEditorProps) { + const heightClassName = props.heightClassName ?? 'h-56 min-h-56 max-h-56'🤖 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/components/json-code-editor.tsx` around lines 39 - 49, The component currently destructures props in the JsonCodeEditor function signature; change the signature to accept a single props object (e.g., function JsonCodeEditor(props: JsonCodeEditorProps)) and update all internal usages to reference props.value, props.onChange, props.disabled, props.heightClassName, props.className, props.id, props['aria-describedby'] (or props.ariaDescribedBy if normalized), props['aria-invalid'] (or props.ariaInvalid), and spread props as needed (e.g., ...props) instead of using the destructured rootProps variable so the implementation matches the repo rule against signature-level destructuring.Source: Coding guidelines
🤖 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/system-settings/models/model-pricing-core.ts`:
- Around line 208-296: In buildPreviewRows fix two hardcoded labels by replacing
'BillingMode' and 'ModelPrice' with translated strings using the t(...) helper:
in the mode === 'tiered_expr' branch change the label for the row with key
'mode' to t('BillingMode'), and in the mode === 'per-request' branch change the
label for the row with key 'price' to t('ModelPrice') so those preview rows are
localized like the others.
- Around line 23-34: The schema created by createModelPricingSchema currently
allows any string for numeric fields (price, ratio, cacheRatio,
createCacheRatio, completionRatio, imageRatio, audioRatio,
audioCompletionRatio); update the schema to validate these fields as numeric by
either applying z.string().regex(numericDraftRegex, t('Invalid number')) or
using z.preprocess to coerce to numbers and then z.number().optional(), ensuring
the same t(...) translation is used for error messages; locate
createModelPricingSchema and replace each .string().optional() for those numeric
fields with the chosen numeric validation/coercion approach so downstream
toNumberOrNull/parsing logic only receives valid numeric input.
In
`@web/default/src/features/system-settings/models/model-ratio-table-columns.tsx`:
- Around line 137-160: The action column (id: 'actions') renders icon-only
Buttons for edit and delete which lack accessible labels; update the two Button
components (the ones using Pencil and Trash2) to include descriptive aria-label
props (e.g., aria-label="Edit ratio" and aria-label="Delete ratio") and ensure
they still call onEdit(row.original) and onDelete(row.original.name)
respectively so keyboard and screen-reader users can identify and operate them.
In `@web/default/src/features/system-settings/models/ratio-settings-card.tsx`:
- Around line 119-124: The predicateMessage on the AutoGroups field is a
hardcoded English string; wrap it in the i18n function so it uses translations
by replacing predicateMessage: 'Expected a JSON array of group identifiers' with
predicateMessage: t('...') and ensure the t function from useTranslation() is in
scope (or passed into this module) — update the AutoGroups createJsonStringField
invocation to use predicateMessage: t('expectedJsonArrayOfGroupIdentifiers') (or
an appropriate key) and add/import/obtain const { t } = useTranslation() where
other translations are used in this file so the key resolves.
In `@web/default/src/features/system-settings/models/utils.ts`:
- Around line 111-114: The computed missingCommaLine currently always subtracts
one from position.line which misreports same-line comma omissions; update the
logic around missingCommaLine (in the block that uses isMissingCommaError and
position) to return position.line when the missing comma is on the same line
(detectable via position.column > 0 or other token/column info) and only use
position.line - 1 when the comma should be on the previous line; keep the
overall conditional on isMissingCommaError and preserve undefined otherwise so
references to missingCommaLine and isMissingCommaError in this module continue
to work.
---
Outside diff comments:
In `@web/default/src/features/system-settings/models/model-pricing-sheet.tsx`:
- Around line 409-435: The validator currently only checks token-pricing paths;
add a check in validatePricingValues to reject empty fixed-price ("per-request")
drafts before commitDraft() returns data by verifying the fixed price exists
(e.g. toNumberOrNull(promptPrice) !== null or data.price present) when
pricingMode === 'per-request'; if missing, call form.setError for the
fixed-price field (e.g. 'price' or 'promptPrice') with an appropriate message so
persistPricingData() in model-ratio-visual-editor.tsx won't silently drop or
convert the model. Ensure the new check references pricingMode, promptPrice (or
the fixed-price field), and uses the same form API as the existing errors.
In
`@web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx`:
- Around line 639-659: The filter option labels for billingMode are hardcoded
English strings; update them to use the translator function t() (from
useTranslation) so they are localized — e.g. replace 'Per-token', 'Per-request',
'Expression' with t('...') keys (like t('mode.perToken'), t('mode.perRequest'),
t('mode.expression') or your project's existing keys) in the options array where
filters is defined, keeping the existing value and count fields
(modeCounts['per-token'], modeCounts['per-request'], modeCounts.tiered_expr)
unchanged; ensure useTranslation() is imported/used in this file and that the
chosen translation keys exist in the locale files.
- Around line 799-816: The memo comparator for ModelRatioVisualEditor currently
only compares the draft props and misses the saved snapshot props, causing stale
derived state; update the custom equality function (the second argument passed
to memo when creating ModelRatioVisualEditor from
ModelRatioVisualEditorComponent) to also compare the corresponding saved* props
(e.g., savedModelPrice, savedModelRatio, savedCacheRatio, savedCreateCacheRatio,
savedCompletionRatio, savedImageRatio, savedAudioRatio,
savedAudioCompletionRatio, savedBillingMode, savedBillingExpr, and any other
saved* snapshot props used by the component) alongside the existing draft props
so the component re-renders when either draft or saved snapshots change.
In `@web/default/src/features/system-settings/models/utils.ts`:
- Around line 149-151: The code currently trims the input into `trimmed` and
uses it for parsing/error-position extraction which shifts reported line/column
numbers; change the logic so parsing and any error-position calculations use the
original `value` (preserving leading whitespace/newlines) while keeping
`trimmed` only for the empty-check (and optional predicate checks if desired).
Update the block that destructures `const { allowEmpty = true, predicate,
predicateMessage } = options` and the uses of `trimmed` (the `trim()` assignment
and subsequent parsing/error handling) to ensure parsing functions and error
position reporters reference `value` instead of `trimmed`, and apply the same
change for the other occurrences around the `trimmed` usages at the other two
locations.
---
Nitpick comments:
In `@web/default/src/components/json-code-editor.tsx`:
- Around line 109-117: The nested ternary inside the indent-removal branch of
the nextBlock computation (the lines.map callback used when event.shiftKey is
true) should be replaced with an explicit if/else or small helper function to
avoid a 2-level nested ternary; locate the map callback around nextBlock and
replace the expression that currently chooses between line.slice(2),
line.slice(1) or line with a clear if/else (or extracted function like
removeIndent(line): string) that checks startsWith(' ') then startsWith('\t')
else returns line, preserving existing behavior.
- Around line 39-49: The component currently destructures props in the
JsonCodeEditor function signature; change the signature to accept a single props
object (e.g., function JsonCodeEditor(props: JsonCodeEditorProps)) and update
all internal usages to reference props.value, props.onChange, props.disabled,
props.heightClassName, props.className, props.id, props['aria-describedby'] (or
props.ariaDescribedBy if normalized), props['aria-invalid'] (or
props.ariaInvalid), and spread props as needed (e.g., ...props) instead of using
the destructured rootProps variable so the implementation matches the repo rule
against signature-level destructuring.
In `@web/default/src/features/system-settings/models/model-pricing-snapshots.ts`:
- Around line 63-74: Extract the duplicated numeric helpers into a single shared
module (e.g., pricing-utils) and import them where needed: move the existing
toNumberOrNull and the ratio logic out of model-pricing-snapshots (functions
toNumberOrNull and ratioToPrice) and into the new shared utilities (preserve
function names or use clear names like toNumberOrNull and
computePrice/computeRatioPrice), ensure the helper accepts the same input types
(string/unknown) and returns number|null or '' as before, and have
ratioToBasePrice and deriveLanePrice in the core module use the same exported
helpers; update imports in model-pricing-snapshots and the core module and keep
formatPricingNumber available (import it into the new utils if necessary).
🪄 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: 23fdd61c-a5e1-4b42-9eaf-4f37233c1db0
📒 Files selected for processing (19)
web/default/src/components/json-code-editor.tsxweb/default/src/features/system-settings/components/settings-page.tsxweb/default/src/features/system-settings/models/model-pricing-core.tsweb/default/src/features/system-settings/models/model-pricing-inputs.tsxweb/default/src/features/system-settings/models/model-pricing-sheet.tsxweb/default/src/features/system-settings/models/model-pricing-snapshots.tsweb/default/src/features/system-settings/models/model-ratio-form.tsxweb/default/src/features/system-settings/models/model-ratio-table-columns.tsxweb/default/src/features/system-settings/models/model-ratio-visual-editor.tsxweb/default/src/features/system-settings/models/ratio-settings-card.tsxweb/default/src/features/system-settings/models/tiered-pricing-editor.tsxweb/default/src/features/system-settings/models/utils.tsweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/fr.jsonweb/default/src/i18n/locales/ja.jsonweb/default/src/i18n/locales/ru.jsonweb/default/src/i18n/locales/vi.jsonweb/default/src/i18n/locales/zh.jsonweb/default/src/i18n/static-keys.ts
✅ Files skipped from review due to trivial changes (4)
- web/default/src/i18n/static-keys.ts
- web/default/src/i18n/locales/ja.json
- web/default/src/i18n/locales/zh.json
- web/default/src/i18n/locales/fr.json
| export const createModelPricingSchema = (t: (key: string) => string) => | ||
| z.object({ | ||
| name: z.string().min(1, t('Model name is required')), | ||
| price: z.string().optional(), | ||
| ratio: z.string().optional(), | ||
| cacheRatio: z.string().optional(), | ||
| createCacheRatio: z.string().optional(), | ||
| completionRatio: z.string().optional(), | ||
| imageRatio: z.string().optional(), | ||
| audioRatio: z.string().optional(), | ||
| audioCompletionRatio: z.string().optional(), | ||
| }) |
There was a problem hiding this comment.
Schema validation gap: numeric price/ratio fields accept any string.
The Zod schema defines all price/ratio fields as .string().optional() without format validation. While numericDraftRegex on line 72 suggests UI-level input filtering, invalid numeric strings can still pass schema validation and cause downstream parsing errors in toNumberOrNull or computation logic.
🛡️ Add numeric format validation to the schema
export const createModelPricingSchema = (t: (key: string) => string) =>
z.object({
name: z.string().min(1, t('Model name is required')),
- price: z.string().optional(),
- ratio: z.string().optional(),
- cacheRatio: z.string().optional(),
- createCacheRatio: z.string().optional(),
- completionRatio: z.string().optional(),
- imageRatio: z.string().optional(),
- audioRatio: z.string().optional(),
- audioCompletionRatio: z.string().optional(),
+ price: z.string().regex(/^\d*\.?\d*$/, { error: () => t('Invalid number') }).optional(),
+ ratio: z.string().regex(/^\d*\.?\d*$/, { error: () => t('Invalid number') }).optional(),
+ cacheRatio: z.string().regex(/^\d*\.?\d*$/, { error: () => t('Invalid number') }).optional(),
+ createCacheRatio: z.string().regex(/^\d*\.?\d*$/, { error: () => t('Invalid number') }).optional(),
+ completionRatio: z.string().regex(/^\d*\.?\d*$/, { error: () => t('Invalid number') }).optional(),
+ imageRatio: z.string().regex(/^\d*\.?\d*$/, { error: () => t('Invalid number') }).optional(),
+ audioRatio: z.string().regex(/^\d*\.?\d*$/, { error: () => t('Invalid number') }).optional(),
+ audioCompletionRatio: z.string().regex(/^\d*\.?\d*$/, { error: () => t('Invalid number') }).optional(),
})Alternatively, use Zod 4's new unified error callback pattern for cleaner validation:
+const numericStringSchema = z.string().optional().refine(
+ (val) => !val || /^\d*\.?\d*$/.test(val),
+ { error: (issue) => t('Invalid number') }
+)
+
export const createModelPricingSchema = (t: (key: string) => string) =>
z.object({
name: z.string().min(1, t('Model name is required')),
- price: z.string().optional(),
- ratio: z.string().optional(),
- cacheRatio: z.string().optional(),
- createCacheRatio: z.string().optional(),
- completionRatio: z.string().optional(),
- imageRatio: z.string().optional(),
- audioRatio: z.string().optional(),
- audioCompletionRatio: z.string().optional(),
+ price: numericStringSchema,
+ ratio: numericStringSchema,
+ cacheRatio: numericStringSchema,
+ createCacheRatio: numericStringSchema,
+ completionRatio: numericStringSchema,
+ imageRatio: numericStringSchema,
+ audioRatio: numericStringSchema,
+ audioCompletionRatio: numericStringSchema,
})🤖 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/system-settings/models/model-pricing-core.ts` around
lines 23 - 34, The schema created by createModelPricingSchema currently allows
any string for numeric fields (price, ratio, cacheRatio, createCacheRatio,
completionRatio, imageRatio, audioRatio, audioCompletionRatio); update the
schema to validate these fields as numeric by either applying
z.string().regex(numericDraftRegex, t('Invalid number')) or using z.preprocess
to coerce to numbers and then z.number().optional(), ensuring the same t(...)
translation is used for error messages; locate createModelPricingSchema and
replace each .string().optional() for those numeric fields with the chosen
numeric validation/coercion approach so downstream toNumberOrNull/parsing logic
only receives valid numeric input.
| export function buildPreviewRows( | ||
| values: ModelPricingFormValues, | ||
| mode: PricingMode, | ||
| billingExpr: string, | ||
| requestRuleExpr: string, | ||
| promptPrice: string, | ||
| lanePrices: Record<LaneKey, string>, | ||
| laneEnabled: Record<LaneKey, boolean>, | ||
| t: (key: string) => string | ||
| ): PreviewRow[] { | ||
| if (mode === 'tiered_expr') { | ||
| const effectiveExpr = combineBillingExpr(billingExpr, requestRuleExpr) | ||
| return [ | ||
| { key: 'mode', label: 'BillingMode', value: 'tiered_expr' }, | ||
| { | ||
| key: 'expr', | ||
| label: t('Expression'), | ||
| value: effectiveExpr || t('Empty'), | ||
| multiline: true, | ||
| }, | ||
| ] | ||
| } | ||
|
|
||
| if (mode === 'per-request') { | ||
| return [ | ||
| { | ||
| key: 'price', | ||
| label: 'ModelPrice', | ||
| value: values.price || t('Empty'), | ||
| }, | ||
| ] | ||
| } | ||
|
|
||
| return [ | ||
| { | ||
| key: 'inputPrice', | ||
| label: t('Input price'), | ||
| value: promptPrice ? `$${promptPrice}` : t('Empty'), | ||
| }, | ||
| { | ||
| key: 'completion', | ||
| label: t('Completion price'), | ||
| value: | ||
| laneEnabled.completion && lanePrices.completion | ||
| ? `$${lanePrices.completion}` | ||
| : t('Empty'), | ||
| }, | ||
| { | ||
| key: 'cache', | ||
| label: t('Cache read price'), | ||
| value: | ||
| laneEnabled.cache && lanePrices.cache | ||
| ? `$${lanePrices.cache}` | ||
| : t('Empty'), | ||
| }, | ||
| { | ||
| key: 'createCache', | ||
| label: t('Cache write price'), | ||
| value: | ||
| laneEnabled.createCache && lanePrices.createCache | ||
| ? `$${lanePrices.createCache}` | ||
| : t('Empty'), | ||
| }, | ||
| { | ||
| key: 'image', | ||
| label: t('Image input price'), | ||
| value: | ||
| laneEnabled.image && lanePrices.image | ||
| ? `$${lanePrices.image}` | ||
| : t('Empty'), | ||
| }, | ||
| { | ||
| key: 'audio', | ||
| label: t('Audio input price'), | ||
| value: | ||
| laneEnabled.audioInput && lanePrices.audioInput | ||
| ? `$${lanePrices.audioInput}` | ||
| : t('Empty'), | ||
| }, | ||
| { | ||
| key: 'audioCompletion', | ||
| label: t('Audio output price'), | ||
| value: | ||
| laneEnabled.audioOutput && lanePrices.audioOutput | ||
| ? `$${lanePrices.audioOutput}` | ||
| : t('Empty'), | ||
| }, | ||
| ] | ||
| } |
There was a problem hiding this comment.
Hardcoded untranslated labels in preview rows.
Lines 221 and 234 use hardcoded English strings 'BillingMode' and 'ModelPrice' without wrapping them in t(), while all other labels in the same function correctly use t(). This breaks i18n consistency and prevents these labels from being translated.
🌐 Wrap hardcoded labels with t()
if (mode === 'tiered_expr') {
const effectiveExpr = combineBillingExpr(billingExpr, requestRuleExpr)
return [
- { key: 'mode', label: 'BillingMode', value: 'tiered_expr' },
+ { key: 'mode', label: t('BillingMode'), value: 'tiered_expr' },
{
key: 'expr',
label: t('Expression'),
value: effectiveExpr || t('Empty'),
multiline: true,
},
]
}
if (mode === 'per-request') {
return [
{
key: 'price',
- label: 'ModelPrice',
+ label: t('ModelPrice'),
value: values.price || t('Empty'),
},
]
}📝 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.
| export function buildPreviewRows( | |
| values: ModelPricingFormValues, | |
| mode: PricingMode, | |
| billingExpr: string, | |
| requestRuleExpr: string, | |
| promptPrice: string, | |
| lanePrices: Record<LaneKey, string>, | |
| laneEnabled: Record<LaneKey, boolean>, | |
| t: (key: string) => string | |
| ): PreviewRow[] { | |
| if (mode === 'tiered_expr') { | |
| const effectiveExpr = combineBillingExpr(billingExpr, requestRuleExpr) | |
| return [ | |
| { key: 'mode', label: 'BillingMode', value: 'tiered_expr' }, | |
| { | |
| key: 'expr', | |
| label: t('Expression'), | |
| value: effectiveExpr || t('Empty'), | |
| multiline: true, | |
| }, | |
| ] | |
| } | |
| if (mode === 'per-request') { | |
| return [ | |
| { | |
| key: 'price', | |
| label: 'ModelPrice', | |
| value: values.price || t('Empty'), | |
| }, | |
| ] | |
| } | |
| return [ | |
| { | |
| key: 'inputPrice', | |
| label: t('Input price'), | |
| value: promptPrice ? `$${promptPrice}` : t('Empty'), | |
| }, | |
| { | |
| key: 'completion', | |
| label: t('Completion price'), | |
| value: | |
| laneEnabled.completion && lanePrices.completion | |
| ? `$${lanePrices.completion}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'cache', | |
| label: t('Cache read price'), | |
| value: | |
| laneEnabled.cache && lanePrices.cache | |
| ? `$${lanePrices.cache}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'createCache', | |
| label: t('Cache write price'), | |
| value: | |
| laneEnabled.createCache && lanePrices.createCache | |
| ? `$${lanePrices.createCache}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'image', | |
| label: t('Image input price'), | |
| value: | |
| laneEnabled.image && lanePrices.image | |
| ? `$${lanePrices.image}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'audio', | |
| label: t('Audio input price'), | |
| value: | |
| laneEnabled.audioInput && lanePrices.audioInput | |
| ? `$${lanePrices.audioInput}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'audioCompletion', | |
| label: t('Audio output price'), | |
| value: | |
| laneEnabled.audioOutput && lanePrices.audioOutput | |
| ? `$${lanePrices.audioOutput}` | |
| : t('Empty'), | |
| }, | |
| ] | |
| } | |
| export function buildPreviewRows( | |
| values: ModelPricingFormValues, | |
| mode: PricingMode, | |
| billingExpr: string, | |
| requestRuleExpr: string, | |
| promptPrice: string, | |
| lanePrices: Record<LaneKey, string>, | |
| laneEnabled: Record<LaneKey, boolean>, | |
| t: (key: string) => string | |
| ): PreviewRow[] { | |
| if (mode === 'tiered_expr') { | |
| const effectiveExpr = combineBillingExpr(billingExpr, requestRuleExpr) | |
| return [ | |
| { key: 'mode', label: t('BillingMode'), value: 'tiered_expr' }, | |
| { | |
| key: 'expr', | |
| label: t('Expression'), | |
| value: effectiveExpr || t('Empty'), | |
| multiline: true, | |
| }, | |
| ] | |
| } | |
| if (mode === 'per-request') { | |
| return [ | |
| { | |
| key: 'price', | |
| label: t('ModelPrice'), | |
| value: values.price || t('Empty'), | |
| }, | |
| ] | |
| } | |
| return [ | |
| { | |
| key: 'inputPrice', | |
| label: t('Input price'), | |
| value: promptPrice ? `$${promptPrice}` : t('Empty'), | |
| }, | |
| { | |
| key: 'completion', | |
| label: t('Completion price'), | |
| value: | |
| laneEnabled.completion && lanePrices.completion | |
| ? `$${lanePrices.completion}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'cache', | |
| label: t('Cache read price'), | |
| value: | |
| laneEnabled.cache && lanePrices.cache | |
| ? `$${lanePrices.cache}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'createCache', | |
| label: t('Cache write price'), | |
| value: | |
| laneEnabled.createCache && lanePrices.createCache | |
| ? `$${lanePrices.createCache}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'image', | |
| label: t('Image input price'), | |
| value: | |
| laneEnabled.image && lanePrices.image | |
| ? `$${lanePrices.image}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'audio', | |
| label: t('Audio input price'), | |
| value: | |
| laneEnabled.audioInput && lanePrices.audioInput | |
| ? `$${lanePrices.audioInput}` | |
| : t('Empty'), | |
| }, | |
| { | |
| key: 'audioCompletion', | |
| label: t('Audio output price'), | |
| value: | |
| laneEnabled.audioOutput && lanePrices.audioOutput | |
| ? `$${lanePrices.audioOutput}` | |
| : t('Empty'), | |
| }, | |
| ] | |
| } |
🤖 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/system-settings/models/model-pricing-core.ts` around
lines 208 - 296, In buildPreviewRows fix two hardcoded labels by replacing
'BillingMode' and 'ModelPrice' with translated strings using the t(...) helper:
in the mode === 'tiered_expr' branch change the label for the row with key
'mode' to t('BillingMode'), and in the mode === 'per-request' branch change the
label for the row with key 'price' to t('ModelPrice') so those preview rows are
localized like the others.
| { | ||
| id: 'actions', | ||
| header: () => <div className='text-right'>{t('Actions')}</div>, | ||
| cell: ({ row }) => ( | ||
| <div className='flex justify-end gap-2'> | ||
| <Button | ||
| variant='ghost' | ||
| size='sm' | ||
| onClick={() => onEdit(row.original)} | ||
| > | ||
| <Pencil /> | ||
| </Button> | ||
| <Button | ||
| variant='ghost' | ||
| size='sm' | ||
| onClick={() => onDelete(row.original.name)} | ||
| > | ||
| <Trash2 /> | ||
| </Button> | ||
| </div> | ||
| ), | ||
| enableHiding: false, | ||
| }, | ||
| ] |
There was a problem hiding this comment.
Add aria-labels to icon-only action buttons.
The edit and delete buttons contain only icons without accessible text. Add aria-label props so screen reader users understand each button's purpose.
♿ Proposed fix to add accessibility labels
<div className='flex justify-end gap-2'>
<Button
variant='ghost'
size='sm'
onClick={() => onEdit(row.original)}
+ aria-label={t('Edit model')}
>
<Pencil />
</Button>
<Button
variant='ghost'
size='sm'
onClick={() => onDelete(row.original.name)}
+ aria-label={t('Delete model')}
>
<Trash2 />
</Button>
</div>As per coding guidelines, ensure keyboard operability and use ARIA attributes when necessary; icon buttons require aria-labels for screen readers.
🤖 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/system-settings/models/model-ratio-table-columns.tsx`
around lines 137 - 160, The action column (id: 'actions') renders icon-only
Buttons for edit and delete which lack accessible labels; update the two Button
components (the ones using Pencil and Trash2) to include descriptive aria-label
props (e.g., aria-label="Edit ratio" and aria-label="Delete ratio") and ensure
they still call onEdit(row.original) and onDelete(row.original.name)
respectively so keyboard and screen-reader users can identify and operate them.
Source: Coding guidelines
| AutoGroups: createJsonStringField(t, { | ||
| predicate: (parsed) => | ||
| Array.isArray(parsed) && | ||
| parsed.every((item) => typeof item === 'string'), | ||
| predicateMessage: 'Expected a JSON array of group identifiers', | ||
| }) | ||
| if (!result.valid) { | ||
| ctx.addIssue({ | ||
| code: z.ZodIssueCode.custom, | ||
| message: result.message || 'Invalid JSON array', | ||
| }) | ||
| } | ||
| }), | ||
| DefaultUseAutoGroup: z.boolean(), | ||
| GroupSpecialUsableGroup: z.string().superRefine((value, ctx) => { | ||
| const result = validateJsonString(value) | ||
| if (!result.valid) { | ||
| ctx.addIssue({ | ||
| code: z.ZodIssueCode.custom, | ||
| message: result.message || 'Invalid JSON', | ||
| }) | ||
| } | ||
| }), | ||
| }) | ||
| }), |
There was a problem hiding this comment.
Translate the AutoGroups validation error message.
The predicateMessage on line 123 is a hardcoded English string that will be displayed to users when validation fails. Wrap it in t() to support internationalization.
🌐 Proposed fix to add i18n support
AutoGroups: createJsonStringField(t, {
predicate: (parsed) =>
Array.isArray(parsed) &&
parsed.every((item) => typeof item === 'string'),
- predicateMessage: 'Expected a JSON array of group identifiers',
+ predicateMessage: t('Expected a JSON array of group identifiers'),
}),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.
| AutoGroups: createJsonStringField(t, { | |
| predicate: (parsed) => | |
| Array.isArray(parsed) && | |
| parsed.every((item) => typeof item === 'string'), | |
| predicateMessage: 'Expected a JSON array of group identifiers', | |
| }) | |
| if (!result.valid) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: result.message || 'Invalid JSON array', | |
| }) | |
| } | |
| }), | |
| DefaultUseAutoGroup: z.boolean(), | |
| GroupSpecialUsableGroup: z.string().superRefine((value, ctx) => { | |
| const result = validateJsonString(value) | |
| if (!result.valid) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: result.message || 'Invalid JSON', | |
| }) | |
| } | |
| }), | |
| }) | |
| }), | |
| AutoGroups: createJsonStringField(t, { | |
| predicate: (parsed) => | |
| Array.isArray(parsed) && | |
| parsed.every((item) => typeof item === 'string'), | |
| predicateMessage: t('Expected a JSON array of group identifiers'), | |
| }), |
🤖 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/system-settings/models/ratio-settings-card.tsx`
around lines 119 - 124, The predicateMessage on the AutoGroups field is a
hardcoded English string; wrap it in the i18n function so it uses translations
by replacing predicateMessage: 'Expected a JSON array of group identifiers' with
predicateMessage: t('...') and ensure the t function from useTranslation() is in
scope (or passed into this module) — update the AutoGroups createJsonStringField
invocation to use predicateMessage: t('expectedJsonArrayOfGroupIdentifiers') (or
an appropriate key) and add/import/obtain const { t } = useTranslation() where
other translations are used in this file so the key resolves.
Source: Coding guidelines
| const missingCommaLine = | ||
| isMissingCommaError && position.line && position.line > 1 | ||
| ? position.line - 1 | ||
| : undefined |
There was a problem hiding this comment.
missingCommaLine is off by one for same-line comma omissions.
The current logic always reports position.line - 1, which can misdirect users when the missing comma is on the same line as the parse failure token.
Suggested fix
- const missingCommaLine =
- isMissingCommaError && position.line && position.line > 1
- ? position.line - 1
- : undefined
+ const missingCommaLine =
+ isMissingCommaError && position.line ? position.line : undefined📝 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.
| const missingCommaLine = | |
| isMissingCommaError && position.line && position.line > 1 | |
| ? position.line - 1 | |
| : undefined | |
| const missingCommaLine = | |
| isMissingCommaError && position.line ? position.line : undefined |
🤖 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/system-settings/models/utils.ts` around lines 111 -
114, The computed missingCommaLine currently always subtracts one from
position.line which misreports same-line comma omissions; update the logic
around missingCommaLine (in the block that uses isMissingCommaError and
position) to return position.line when the missing comma is on the same line
(detectable via position.column > 0 or other token/column info) and only use
position.line - 1 when the comma should be on the previous line; keep the
overall conditional on isMissingCommaError and preserve undefined otherwise so
references to missingCommaLine and isMissingCommaError in this module continue
to work.
Merge pull request QuantumNous#5275 from QuantumNous/fix/model-pricing-draft-save
Merge pull request QuantumNous#5275 from QuantumNous/fix/model-pricing-draft-save
Merge pull request QuantumNous#5275 from QuantumNous/fix/model-pricing-draft-save
Important
📝 变更描述 / Description
概述
改动说明
效果
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
Summary by CodeRabbit
New Features
Refactor
Localization