From a94bee041de984a3b69416cdf61a382cf7a19700 Mon Sep 17 00:00:00 2001 From: sechmachine Date: Sat, 10 Oct 2026 07:03:15 +0700 Subject: [PATCH 1/9] feat(catalog): global auto-review override for the Models settings panel --- gui/src/i18n/de.ts | 2 + gui/src/i18n/en.ts | 2 + gui/src/i18n/fr.ts | 2 + gui/src/i18n/ja.ts | 2 + gui/src/i18n/ko.ts | 2 + gui/src/i18n/pt.ts | 2 + gui/src/i18n/ru.ts | 2 + gui/src/i18n/tr.ts | 2 + gui/src/i18n/vi.ts | 2 + gui/src/i18n/zh-TW.ts | 2 + gui/src/i18n/zh.ts | 2 + gui/src/pages/Models.tsx | 46 +++++++++- gui/src/pages/models-settings-summary.ts | 6 ++ gui/src/pages/models-shared.ts | 27 ++++++ .../models-auto-review-override.test.tsx | 56 ++++++++++++ src/codex/catalog/auto-review.ts | 23 ++++- src/config/load-degrade.ts | 6 ++ src/config/provider-validation.ts | 20 +++++ src/config/schema/config-schema.ts | 4 + src/config/schema/leaf-validators.ts | 8 ++ src/server/management/config-routes.ts | 25 ++++++ src/server/management/route-registry.ts | 2 + src/types/config.ts | 13 +++ .../catalog-auto-review-override.test.ts | 89 +++++++++++++++++++ tests/config/config-load-degrade.test.ts | 18 ++++ .../management-auto-review-settings.test.ts | 82 +++++++++++++++++ 26 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 gui/tests/models-auto-review-override.test.tsx create mode 100644 tests/codex-integration/catalog-auto-review-override.test.ts create mode 100644 tests/server/management-auto-review-settings.test.ts diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index d1484ec0dc3..3b842ac51a5 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -479,6 +479,8 @@ export const de: Record = { "dash.shadowCallTooltip": "Die Codex-App ruft im Hintergrund ein Hilfsmodell für Titelgenerierung, Commit-Nachrichten und Skill-Orchestrierung auf. Das Modell wechselt zwischen Client-Versionen, daher fängt opencodex diesen Satz ab: {models}.", "models.shadowCallIntercept": "Shadow-Call-Abfangen", "models.shadowCallInterceptHint": "Fängt die Hintergrund-Hilfsaufrufe der Codex-App ({models}) ab und leitet sie an das gewählte Modell um.", + "models.autoReviewOverride": "Auto-Review-Überschreibung", + "models.autoReviewOverrideHint": "Legt fest, welches Modell Codex für die automatische Prüfung (Approval-Reviewer) verwendet. Wird beim Katalog-Abgleich auf alle Zeilen angewendet; ein autoReviewModel auf Anbieterebene hat weiterhin Vorrang.", "dash.sidecarBackend": "Backend", "dash.sidecarModel": "Modell", "dash.backendAuto": "Automatisch", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index a5bd623dfa3..2f89618c5f4 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -497,6 +497,8 @@ export const en = { "dash.shadowCallTooltip": "Codex App makes background helper calls for thread title generation, commit message generation, and skill orchestration. The helper model changed across client versions, so opencodex intercepts every model in this set: {models}. Enable this to redirect those calls to your chosen model.", "models.shadowCallIntercept": "Shadow Call Intercept", "models.shadowCallInterceptHint": "Intercepts Codex App's background helper calls ({models}) for titles and commit messages and redirects them to your chosen model.", + "models.autoReviewOverride": "Auto-review override", + "models.autoReviewOverrideHint": "Chooses the model Codex uses for auto-review (the approval reviewer). Applied to every catalog row at sync; a provider-scoped autoReviewModel still takes precedence on its own rows.", "dash.sidecarBackend": "Backend", "dash.sidecarModel": "Model", "dash.backendAuto": "Auto", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 325dffd175c..7f36acab550 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -487,6 +487,8 @@ export const fr: Record = { "dash.shadowCallTooltip": "L’application Codex effectue des appels auxiliaires en arrière-plan pour générer les titres de fils, les messages de commit et orchestrer les compétences. Le modèle auxiliaire ayant changé selon les versions du client, opencodex intercepte tous les modèles de cet ensemble : {models}. Activez cette option pour rediriger ces appels vers le modèle choisi.", "models.shadowCallIntercept": "Interception des appels fantômes", "models.shadowCallInterceptHint": "Intercepte les appels auxiliaires en arrière-plan de l’application Codex ({models}) pour les titres et les messages de commit, puis les redirige vers le modèle choisi.", + "models.autoReviewOverride": "Remplacement de la revue automatique", + "models.autoReviewOverrideHint": "Choisit le modèle que Codex utilise pour la revue automatique (l'approbateur). Appliqué à chaque ligne du catalogue à la synchronisation ; un autoReviewModel défini au niveau du fournisseur reste prioritaire sur ses lignes.", "dash.sidecarBackend": "Moteur", "dash.sidecarModel": "Modèle", "dash.backendAuto": "Auto", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 12dc523cd69..40b2c0b4521 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -488,6 +488,8 @@ export const ja: Record = { "dash.shadowCallTooltip": "Codex App はスレッドタイトル生成、コミットメッセージ生成、スキルオーケストレーションをバックグラウンドで呼び出します。使われるモデルはクライアントのバージョンによって変わるため、opencodex は {models} をまとめて傍受します。これをオンにすると、それらの呼び出しを選択したモデルにリダイレクトします。", "models.shadowCallIntercept": "シャドウコール傍受", "models.shadowCallInterceptHint": "Codex App のバックグラウンドヘルパー呼び出し({models}: タイトル、コミットメッセージ)を傍受し、選択したモデルにリダイレクトします。", + "models.autoReviewOverride": "自動レビューの上書き", + "models.autoReviewOverrideHint": "Codex の自動レビュー(承認レビュアー)が使うモデルを指定します。カタログ同期時にすべての行へ適用され、プロバイダー固有の autoReviewModel がその行では引き続き優先されます。", "dash.sidecarBackend": "バックエンド", "dash.sidecarModel": "モデル", "dash.backendAuto": "自動", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 499b65bcf64..79e18cf76a7 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -483,6 +483,8 @@ export const ko: Record = { "dash.shadowCallTooltip": "Codex 앱은 스레드 제목 자동 생성, 커밋 메시지 생성, 스킬 오케스트레이션 같은 내부 작업을 백그라운드로 호출합니다. 이때 쓰는 모델은 클라이언트 버전마다 달라서 opencodex는 {models}를 모두 가로챕니다. 이 설정을 켜면 해당 호출이 선택한 모델로 넘어갑니다.", "models.shadowCallIntercept": "쉐도우 호출 가로채기", "models.shadowCallInterceptHint": "Codex 앱의 백그라운드 호출({models}, 제목·커밋 메시지)을 가로채 선택한 모델로 바꿉니다.", + "models.autoReviewOverride": "자동 검토 재정의", + "models.autoReviewOverrideHint": "Codex의 자동 검토(승인 검토자)에 사용할 모델을 지정합니다. 카탈로그 동기화 시 모든 행에 적용되며, 공급자별 autoReviewModel이 해당 행에서 계속 우선합니다.", "dash.sidecarBackend": "백엔드", "dash.sidecarModel": "모델", "dash.backendAuto": "자동", diff --git a/gui/src/i18n/pt.ts b/gui/src/i18n/pt.ts index 098b8ec9d3b..c5228ec818e 100644 --- a/gui/src/i18n/pt.ts +++ b/gui/src/i18n/pt.ts @@ -501,6 +501,8 @@ export const pt: Record = { "dash.shadowCallTooltip": "O Codex App faz chamadas auxiliares em segundo plano para geração de título de thread, geração de mensagem de commit e orquestração de skills. O modelo auxiliar mudou entre versões do cliente, então o opencodex intercepta todos os modelos deste conjunto: {models}. Ative para redirecionar essas chamadas ao modelo escolhido.", "models.shadowCallIntercept": "Shadow Call Intercept", "models.shadowCallInterceptHint": "Intercepta as chamadas auxiliares em segundo plano do Codex App ({models}) para títulos e mensagens de commit e as redireciona ao modelo escolhido.", + "models.autoReviewOverride": "Substituição da revisão automática", + "models.autoReviewOverrideHint": "Define o modelo que o Codex usa na revisão automática (revisor de aprovação). Aplicado a todas as linhas do catálogo na sincronização; um autoReviewModel no provedor continua tendo prioridade nas próprias linhas.", "dash.sidecarBackend": "Backend", "dash.sidecarModel": "Modelo", "dash.backendAuto": "Automático", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index c1b349abe7c..1764f4efdf0 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -488,6 +488,8 @@ export const ru: Record = { "dash.shadowCallTooltip": "Codex App в фоновом режиме вызывает служебную модель для генерации заголовков тредов, сообщений коммитов и оркестрации навыков. Эта модель менялась между версиями клиента, поэтому opencodex перехватывает весь набор: {models}. Включите функцию, чтобы перенаправлять такие вызовы на выбранную вами модель.", "models.shadowCallIntercept": "Перехват теневых вызовов", "models.shadowCallInterceptHint": "Перехватывает фоновые служебные вызовы Codex App ({models}: заголовки, сообщения коммитов) и перенаправляет их на выбранную вами модель.", + "models.autoReviewOverride": "Переопределение авто-проверки", + "models.autoReviewOverrideHint": "Задаёт модель, которую Codex использует для автоматической проверки (ревьюера подтверждений). Применяется ко всем строкам каталога при синхронизации; autoReviewModel на уровне провайдера по-прежнему имеет приоритет.", "dash.sidecarBackend": "Бэкенд", "dash.sidecarModel": "Модель", "dash.backendAuto": "Авто", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 002b64024bf..874a332063f 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -489,6 +489,8 @@ export const tr: Record = { "dash.shadowCallTooltip": "Codex App arka planda başlık ve commit mesajı oluşturmak için yardımcı çağrılar yapar (orijinal {models}). Etkinleştirerek bunları seçtiğiniz modele yönlendirebilirsiniz.", "models.shadowCallIntercept": "Gölge Çağrı Yakalama", "models.shadowCallInterceptHint": "Codex App'in başlıklar ve commit mesajları için yaptığı arka plan çağrılarını ({models}) yakalar ve seçtiğiniz modele yönlendirir.", + "models.autoReviewOverride": "Otomatik inceleme geçersiz kılma", + "models.autoReviewOverrideHint": "Codex’in otomatik incelemesi (onay gözden geçiren) için kullanılacak modeli seçer. Katalog eşitlemesinde tüm satırlara uygulanır; sağlayıcıya özel autoReviewModel kendi satırlarında yine önceliklidir.", "dash.sidecarBackend": "Arka uç", "dash.sidecarModel": "Model", "dash.backendAuto": "Otomatik", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 5c66b31cc03..9f541b41fe0 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -487,6 +487,8 @@ export const vi: Record = { "dash.shadowCallTooltip": "Ứng dụng Codex thực hiện các lệnh gọi helper ngầm để tạo tiêu đề chuỗi, tạo commit message và điều phối các kĩ năng. Helper model có thể thay đổi giữa các phiên bản client, do đó opencodex chặn mọi model trong tập hợp này: {models}. Hãy bật tính năng này để chuyển hướng các lệnh gọi đó đến model bạn chọn.", "models.shadowCallIntercept": "Shadow Call Intercept", "models.shadowCallInterceptHint": "Chặn các lệnh gọi helper nền ({models}) của ứng dụng Codex để tạo tiêu đề và commit messages, và chuyển hướng chúng đến model bạn chọn.", + "models.autoReviewOverride": "Ghi đè duyệt tự động", + "models.autoReviewOverrideHint": "Chọn model Codex dùng cho việc duyệt tự động (trình duyệt phê duyệt). Áp dụng cho mọi hàng trong catalog khi đồng bộ; autoReviewModel ở cấp nhà cung cấp vẫn được ưu tiên trên các hàng đó.", "dash.sidecarBackend": "Backend", "dash.sidecarModel": "Model", "dash.backendAuto": "Tự động", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index baf13ebb18a..64c09eafc2e 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -364,6 +364,8 @@ export const zhTW: Record = { "dash.shadowCallTooltip": "Codex 應用會為執行緒標題生成、提交訊息生成與技能編排發出背景 helper 呼叫。helper 模型因客戶端版本而異,因此 opencodex 攔截此集合中的每個模型:{models}。啟用此選項可將這些呼叫重定向到您選擇的模型。", "models.shadowCallIntercept": "影子呼叫攔截", "models.shadowCallInterceptHint": "攔截 Codex 應用的背景 helper 呼叫({models})以生成標題與提交訊息,並將它們重定向到您選擇的模型。", + "models.autoReviewOverride": "自動審查覆寫", + "models.autoReviewOverrideHint": "指定 Codex 自動審查(核准審查模型)所使用的模型。同步型錄時套用到所有資料列;提供者層級的 autoReviewModel 在這些資料列上仍具優先權。", "dash.sidecarBackend": "後端", "dash.sidecarModel": "模型", "dash.backendAuto": "自動", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index e86031fa907..e9d6a21a3c5 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -483,6 +483,8 @@ export const zh: Record = { "dash.shadowCallTooltip": "Codex 应用会在后台调用辅助模型来生成线程标题、提交消息以及进行技能编排。该模型随客户端版本变化,因此 opencodex 会同时拦截这些模型:{models}。启用此选项可将这些调用重定向到您选择的模型。", "models.shadowCallIntercept": "影子调用拦截", "models.shadowCallInterceptHint": "拦截 Codex 应用的后台辅助调用({models})并重定向到所选模型。", + "models.autoReviewOverride": "自动审查覆盖", + "models.autoReviewOverrideHint": "指定 Codex 自动审查(审批审查模型)使用的模型。同步目录时应用于所有行;提供方级的 autoReviewModel 在这些行上仍然优先。", "dash.sidecarBackend": "后端", "dash.sidecarModel": "模型", "dash.backendAuto": "自动", diff --git a/gui/src/pages/Models.tsx b/gui/src/pages/Models.tsx index 6c0a446fb81..ba6c94c1753 100644 --- a/gui/src/pages/Models.tsx +++ b/gui/src/pages/Models.tsx @@ -76,6 +76,8 @@ import { freeOnlyInForce, modelPricingKnown, REASONING_EFFORT_LEVELS, + routedModelOptions, + type AutoReviewData, type ModelRow, type ProviderContextCapsResponse, type ShadowCallData, @@ -453,6 +455,8 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c const hoverTimerRef = useRef | null>(null); const [shadowCall, setShadowCall] = useState(null); const [shadowCallSaving, setShadowCallSaving] = useState(false); + const [autoReview, setAutoReview] = useState(null); + const [autoReviewSaving, setAutoReviewSaving] = useState(false); // App owns the in-session view mode; fallback to persisted mode for isolated renders/tests. const [selectedProvider, setSelectedProvider] = useState(null); @@ -474,6 +478,11 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c ); }, [models, shadowCall?.model, shadowCall?.sourceModels, shadowModelOptions]); + const autoReviewOptions = useMemo( + () => [{ value: "", label: "—" }, ...routedModelOptions(shadowModelOptions, models, autoReview?.model)], + [models, shadowModelOptions, autoReview?.model], + ); + const loadShadowCall = useCallback(async () => { const bounded = createBoundedFetch(15_000); try { @@ -484,6 +493,16 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c finally { bounded.clear(); } }, [apiBase]); + const loadAutoReview = useCallback(async () => { + const bounded = createBoundedFetch(15_000); + try { + const r = await fetch(`${apiBase}/api/auto-review-settings`, { signal: bounded.signal }); + const data = await readJsonIfOk(r); + if (data) setAutoReview(data); + } catch { /* old server / network: keep the row disabled */ } + finally { bounded.clear(); } + }, [apiBase]); + const loadV2 = useCallback(async () => { // Never let a toggle in flight be clobbered by the poll (same single-flight rule as models). if (v2BusyRef.current) return; @@ -722,6 +741,7 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c if (!catalogActive) return; const timeout = window.setTimeout(() => { void loadShadowCall(); + void loadAutoReview(); void loadV2(); // Preset previews belong to the same tab. Loaded once rather than polled: the rules are // shipped code and the catalog poll above already refreshes the rows they describe. @@ -747,7 +767,7 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c // gui/.oxlintrc.json (override) and gui/doctor.config.json (ignore.overrides). An in-file // react-doctor-disable comment was tried and removed - it changed nothing, and // react/react-compiler penalises a component for carrying suppressions at all. - }, [catalogActive, loadShadowCall, loadV2]); + }, [catalogActive, loadShadowCall, loadAutoReview, loadV2]); const groups = useMemo( () => buildProviderModelGroups(models, providers), @@ -1084,6 +1104,21 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c } }; + const saveAutoReview = async (patch: Partial) => { + if (!autoReview || autoReviewSaving) return; + setAutoReviewSaving(true); + setAutoReview({ ...autoReview, ...patch }); + try { + await fetch(`${apiBase}/api/auto-review-settings`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(patch), + }); + } finally { + setAutoReviewSaving(false); + } + }; + /** * Both v2 surface writes adopt the response directly instead of calling * `loadV2()`. `loadV2` returns early while `v2BusyRef` is still held by the @@ -1967,6 +2002,14 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c +
+ {t("models.autoReviewOverride")} ⓘ + void saveAutoReview({ enabled: !autoReview?.enabled })} disabled={!autoReview || autoReviewSaving} label={t("models.autoReviewOverride")} /> +
+ { setShadowCall(c => c ? { ...c, model: v } : c); void saveShadowCall({ model: v }); }} disabled={!shadowCall || shadowCallSaving || !shadowCall.enabled} label={t("models.shadowCallIntercept")} /> +
+ {t("models.shadowCallIntercept")} ⓘ +
+ {t("models.shadowCallOriginal", { models: shadowSourceModelBadge(shadowCall?.sourceModels) })} + void saveShadowCall({ enabled: !shadowCall?.enabled })} disabled={!shadowCall || shadowCallSaving} label={t("models.shadowCallIntercept")} /> +
+ { setAutoReview(c => c ? { ...c, model: v } : c); void saveAutoReview({ model: v }); }} disabled={!autoReview || autoReviewSaving || !autoReview.enabled} label={t("models.autoReviewOverride")} /> +
+ {t("models.autoReviewOverride")} ⓘ +
+ void saveAutoReview({ enabled: !autoReview?.enabled })} disabled={!autoReview || autoReviewSaving} label={t("models.autoReviewOverride")} /> +
+ { setAutoReview(c => c ? { ...c, model: v } : c); void saveAutoReview({ model: v }); }} disabled={!autoReview || autoReviewSaving || !autoReview.enabled} label={t("models.autoReviewOverride")} /> + { setShadowCall(c => c ? { ...c, model: v } : c); void saveShadowCall({ model: v }); }} disabled={!shadowCall || shadowCallSaving || !shadowCall.enabled} label={t("models.shadowCallIntercept")} /> -
+ + void setDefaultAliases(!aliases.defaults.global)} label={t("models.useDefaultAliasesGlobal")} /> + + + ⓘ} busy={!shadowCall}> + {t("models.shadowCallOriginal", { models: shadowSourceModelBadge(shadowCall?.sourceModels) })} + void saveShadowCall({ enabled: !shadowCall?.enabled })} disabled={!shadowCall || shadowCallSaving} label={t("models.shadowCallIntercept")} /> +
+ void saveAutoReview({ model: v })} disabled={!autoReview || autoReviewSaving || !autoReview.enabled} label={t("models.autoReviewOverride")} /> -
+ ⓘ} busy={!autoReview}> + void saveAutoReview({ enabled: !autoReview?.enabled })} disabled={!autoReview || autoReviewSaving} label={t("models.autoReviewOverride")} /> +
+