fix(codex): load context defaults from CPA model definitions

- Resolve context limits from core model definitions, aliases, and configuration
- Preserve user overrides and display context sources and fallback hints
- Refresh defaults from CPA when restoring all model settings
- Keep restore-all available without customizations and remove its icon
- Allow saving restored defaults even when values remain unchanged
- Add regression coverage for context resolution and default restoration
This commit is contained in:
lzt404 committed 2026-09-07 19:28:21 +08:00
1 parent ce9944c316
commit 40ba85d6f0
10 files changed
+452 -66

No files matched your search

+10 -5
View File
@@ -65,7 +65,7 @@ export const jaOverrides = {
'config.software.terminal.auto': '自動選択',
'agents.catalog.button': 'モデル一覧の設定',
'agents.catalog.title': 'Codex モデル一覧',
'agents.catalog.subtitle': '利用可能なモデルのパラメーターを編集し、生成される Codex モデル一覧に保存します。',
'agents.catalog.subtitle': 'コンテキストの既定値は CPA コア API から取得します。モデルのパラメーターを編集し、Codex モデル一覧に保存できます。',
'agents.catalog.search': 'モデル名または ID を検索',
'agents.catalog.official': '公式テンプレート',
'agents.catalog.fallback': '標準テンプレート',
@@ -76,6 +76,11 @@ export const jaOverrides = {
'agents.catalog.displayName': '表示名',
'agents.catalog.description': 'モデルの説明',
'agents.catalog.context': 'コンテキストウィンドウ(トークン)',
'agents.catalog.contextSource.definition': 'コンテキストの取得元:CPA コア API のモデル定義。',
'agents.catalog.contextSource.configuration': 'コンテキストの取得元:CPA コアでこのモデルに設定された max-context-length。',
'agents.catalog.contextSource.compatibility': 'CPA にこのモデル固有のコンテキスト定義がありません。現在の値は互換カタログの値であり、汎用テンプレートの既定値である可能性があります。',
'agents.catalog.contextSource.template': 'コアに有効なコンテキスト情報がありません。ソフトウェアのテンプレートの代替値を使用しています。',
'agents.catalog.contextSource.customized': 'コンテキストの取得元:このモデルのカスタム設定。',
'agents.catalog.maximum': '最大コンテキストウィンドウ(トークン)',
'agents.catalog.percent': '有効コンテキスト比率(%)',
'agents.catalog.compact': '自動圧縮のしきい値(トークン)',
@@ -89,10 +94,10 @@ export const jaOverrides = {
'agents.catalog.visible': 'Codex のモデル一覧に表示',
'agents.catalog.parallel': '並列ツール呼び出しを許可',
'agents.catalog.capabilityHint': 'Fast は常に選択可能です。推論・画像・ツール機能の動作は上流モデルの対応状況によります。',
'agents.catalog.resetModel': 'このテンプレートを復元',
'agents.catalog.resetAll': 'すべて公式テンプレートに復元',
'agents.catalog.resetHint': '専用テンプレートがあるモデルは公式設定に、それ以外はアプリの標準設定に戻ります。保存すると反映されます。',
'agents.catalog.restored': '下書きをテンプレートに戻しました。保存すると反映されます。',
'agents.catalog.resetModel': 'このモデルの既定値に戻す',
'agents.catalog.resetAll': 'すべて既定値に戻す',
'agents.catalog.resetHint': 'コンテキストは CPA コア API の値を既定値とし、有効なデータがない場合のみテンプレートの値を使用します。その他の項目はモデルのテンプレートに戻ります。保存後に適用されます。',
'agents.catalog.restored': '下書きを既定値に戻しました。保存すると適用されます。',
'agents.catalog.saved': '保存しました。管理対象の一覧が同期されます。Codex を再起動して変更を読み込んでください。',
'agents.catalog.syncFailed': '設定は保存しましたが、一覧の同期に失敗しました:{error}',
'agents.catalog.changed': 'モデル一覧またはテンプレートが更新されました。再読み込みしてから編集してください。',
+10 -5
View File
@@ -15,7 +15,7 @@ export const en: Record<MessageKey, string> = {
'usage.unit.tokens': 'Token',
'agents.catalog.button': 'Model list settings',
'agents.catalog.title': 'Codex model catalog',
'agents.catalog.subtitle': 'Customize parameters for currently available models and save them to the generated Codex catalog.',
'agents.catalog.subtitle': 'Default context settings come from the CPA core API. Customize model parameters and save them to the Codex catalog.',
'agents.catalog.search': 'Search model names or IDs',
'agents.catalog.official': 'Official template',
'agents.catalog.fallback': 'Default template',
@@ -26,6 +26,11 @@ export const en: Record<MessageKey, string> = {
'agents.catalog.displayName': 'Display name',
'agents.catalog.description': 'Description',
'agents.catalog.context': 'Context window (tokens)',
'agents.catalog.contextSource.definition': 'Context source: model definitions from the CPA core API.',
'agents.catalog.contextSource.configuration': 'Context source: max-context-length configured for this model in the CPA core.',
'agents.catalog.contextSource.compatibility': 'CPA provides no independent context definition for this model. The current value comes from its compatibility catalog and may be a generic template default.',
'agents.catalog.contextSource.template': 'The core provides no valid context data. The current value is a fallback from the application template.',
'agents.catalog.contextSource.customized': 'Context source: your custom settings for this model.',
'agents.catalog.maximum': 'Maximum context window (tokens)',
'agents.catalog.percent': 'Effective context (%)',
'agents.catalog.compact': 'Auto-compaction threshold (tokens)',
@@ -39,10 +44,10 @@ export const en: Record<MessageKey, string> = {
'agents.catalog.visible': 'Show in the Codex model list',
'agents.catalog.parallel': 'Allow parallel tool calls',
'agents.catalog.capabilityHint': 'Fast is always available. Reasoning, image and tool capabilities depend on the upstream model.',
'agents.catalog.resetModel': 'Restore this template',
'agents.catalog.resetAll': 'Restore all official templates',
'agents.catalog.resetHint': 'Models with dedicated templates return to official settings; other models return to application defaults. Save to apply.',
'agents.catalog.restored': 'Templates restored in the draft. Save to apply.',
'agents.catalog.resetModel': 'Restore model defaults',
'agents.catalog.resetAll': 'Restore all defaults',
'agents.catalog.resetHint': 'Context windows default to CPA core API values, with template values used only when valid data is missing. Other parameters revert to the model template. Save to apply.',
'agents.catalog.restored': 'Defaults restored in the draft. Save to apply.',
'agents.catalog.saved': 'Saved. Managed catalogs are synchronized; restart Codex to load the changes.',
'agents.catalog.syncFailed': 'Settings saved, but catalog synchronization failed: {error}',
'agents.catalog.changed': 'The model list or templates have changed. Reload before editing again.',
+10 -5
View File
@@ -14,7 +14,7 @@ export const zhCN = {
'usage.unit.tokens': 'Token',
'agents.catalog.button': '模型列表设置',
'agents.catalog.title': 'Codex 模型列表',
'agents.catalog.subtitle': '自定义当前可用模型的参数,保存后用于生成 Codex 模型列表。',
'agents.catalog.subtitle': '默认上下文参数读取自 CPA 内核 API;可自定义当前模型参数并保存到 Codex 模型列表。',
'agents.catalog.search': '搜索模型名称或 ID',
'agents.catalog.official': '官方模板',
'agents.catalog.fallback': '默认模板',
@@ -25,6 +25,11 @@ export const zhCN = {
'agents.catalog.displayName': '显示名称',
'agents.catalog.description': '模型说明',
'agents.catalog.context': '上下文窗口(tokens)',
'agents.catalog.contextSource.definition': '上下文来源:CPA 内核的模型定义 API。',
'agents.catalog.contextSource.configuration': '上下文来源:CPA 内核中为此模型配置的 max-context-length。',
'agents.catalog.contextSource.compatibility': 'CPA 未提供此模型的独立上下文定义,当前值来自兼容目录,可能是通用模板默认值。',
'agents.catalog.contextSource.template': '内核未提供有效的上下文数据,当前使用软件模板的后备值。',
'agents.catalog.contextSource.customized': '上下文来源:你为此模型保存或编辑的自定义配置。',
'agents.catalog.maximum': '最大上下文窗口(tokens)',
'agents.catalog.percent': '有效上下文比例(%)',
'agents.catalog.compact': '自动压缩阈值(tokens)',
@@ -38,10 +43,10 @@ export const zhCN = {
'agents.catalog.visible': '在 Codex 模型列表中显示',
'agents.catalog.parallel': '允许并行工具调用',
'agents.catalog.capabilityHint': 'Fast 始终可选;思考等级、图片与工具能力是否生效取决于上游模型。',
'agents.catalog.resetModel': '恢复此模型模板',
'agents.catalog.resetAll': '全部恢复官方模板',
'agents.catalog.resetHint': '有专属模板的模型恢复官方配置,其余恢复软件默认配置。恢复操作保存后生效。',
'agents.catalog.restored': '模板已恢复到草稿,点击保存后生效。',
'agents.catalog.resetModel': '恢复此模型默认值',
'agents.catalog.resetAll': '全部恢复默认值',
'agents.catalog.resetHint': '上下文窗口默认使用 CPA 内核 API 返回值,仅在缺少有效数据时使用模板值;其他参数恢复模型模板。保存后生效。',
'agents.catalog.restored': '默认值已恢复到草稿,点击保存后生效。',
'agents.catalog.saved': '已保存。已托管的模型目录会同步更新,重启 Codex 后加载新配置。',
'agents.catalog.syncFailed': '设置已保存,但目录同步失败:{error}',
'agents.catalog.changed': '模型列表或模板已更新,请重新加载后再编辑。',
+21 -15
View File
@@ -4,6 +4,7 @@ import { Check, LoaderCircle, RefreshCw, RotateCcw, Search, X } from 'lucide-rea
import { useI18n } from '../i18n';
import {
cloneCodexModelConfiguration,
codexContextSourceHint,
codexReasoningEfforts,
sameCodexModelConfiguration,
toggleCodexReasoningLevel,
@@ -36,23 +37,29 @@ export function CodexModelCatalogDialog({ onClose, onSaved }: CodexModelCatalogD
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [defaultsRestored, setDefaultsRestored] = useState(false);
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const [discardConfirmOpen, setDiscardConfirmOpen] = useState(false);
const load = useCallback(async () => {
const load = useCallback(async (restoreDefaults = false) => {
setLoading(true);
setError('');
setNotice('');
try {
const next = await invoke<CodexCatalogEditorSnapshot>('get_codex_model_catalog_editor');
setSnapshot(next);
setModels(cloneModels(next));
setModels(cloneModels(next).map((model) => restoreDefaults
? { ...model, configuration: cloneCodexModelConfiguration(model.defaults) }
: model));
setDefaultsRestored(restoreDefaults);
setSelectedSlug((current) => next.models.some((model) => model.slug === current)
? current
: next.models[0]?.slug ?? '');
return true;
} catch (requestError) {
setError(String(requestError));
return false;
} finally {
setLoading(false);
}
@@ -63,12 +70,13 @@ export function CodexModelCatalogDialog({ onClose, onSaved }: CodexModelCatalogD
}, [load]);
const dirty = useMemo(() => {
if (defaultsRestored) return true;
if (!snapshot || snapshot.models.length !== models.length) return false;
return models.some((model) => {
const saved = snapshot.models.find((candidate) => candidate.slug === model.slug);
return !saved || !sameCodexModelConfiguration(model.configuration, saved.configuration);
});
}, [models, snapshot]);
}, [defaultsRestored, models, snapshot]);
const filteredModels = useMemo(() => {
const query = search.trim().toLocaleLowerCase();
@@ -130,17 +138,13 @@ export function CodexModelCatalogDialog({ onClose, onSaved }: CodexModelCatalogD
setNotice(t('agents.catalog.restored'));
};
const restoreAll = () => {
setModels((current) => current.map((model) => ({
...model,
configuration: cloneCodexModelConfiguration(model.defaults),
})));
setError('');
setNotice(t('agents.catalog.restored'));
const restoreAll = async () => {
if (loading || saving) return;
if (await load(true)) setNotice(t('agents.catalog.restored'));
};
const save = async () => {
if (!snapshot || saving) return;
if (!snapshot || loading || saving) return;
for (const model of models) {
const validationKey = validateCodexModelConfiguration(model.configuration);
if (validationKey) {
@@ -165,6 +169,7 @@ export function CodexModelCatalogDialog({ onClose, onSaved }: CodexModelCatalogD
});
setSnapshot(result.snapshot);
setModels(cloneModels(result.snapshot));
setDefaultsRestored(false);
setNotice(result.synchronizationError
? t('agents.catalog.syncFailed', { error: result.synchronizationError })
: t('agents.catalog.saved'));
@@ -231,7 +236,7 @@ export function CodexModelCatalogDialog({ onClose, onSaved }: CodexModelCatalogD
</aside>
<main className="codex-catalog-editor">
{activeModel ? (
{activeModel && !loading ? (
<>
<div className="codex-catalog-model-heading">
<div>
@@ -276,6 +281,7 @@ export function CodexModelCatalogDialog({ onClose, onSaved }: CodexModelCatalogD
<label className="codex-catalog-switch"><input type="checkbox" checked={activeModel.configuration.visibility === 'list'} onChange={(event) => updateField('visibility', event.currentTarget.checked ? 'list' : 'hide')} /><span>{t('agents.catalog.visible')}</span></label>
<label className="codex-catalog-switch"><input type="checkbox" checked={activeModel.configuration.supports_parallel_tool_calls} onChange={(event) => updateField('supports_parallel_tool_calls', event.currentTarget.checked)} /><span>{t('agents.catalog.parallel')}</span></label>
</div>
<p className="codex-catalog-hint" role="note">{t(codexContextSourceHint(activeModel))}</p>
<p className="codex-catalog-hint">{t('agents.catalog.capabilityHint')}</p>
</>
) : !loading ? <div className="codex-catalog-state">{t('agents.catalog.empty')}</div> : null}
@@ -289,11 +295,11 @@ export function CodexModelCatalogDialog({ onClose, onSaved }: CodexModelCatalogD
{!error && !notice ? <span>{dirty ? t('agents.catalog.unsaved') : t('agents.catalog.saveHint')}</span> : null}
</div>
<div>
<button type="button" className="secondary-button" onClick={restoreAll} disabled={!models.some((model) => !sameCodexModelConfiguration(model.configuration, model.defaults)) || saving}>
<RotateCcw size={15} />{t('agents.catalog.resetAll')}
<button type="button" className="secondary-button" onClick={() => void restoreAll()} disabled={loading || saving}>
{t('agents.catalog.resetAll')}
</button>
<button type="button" className="secondary-button" onClick={requestClose} disabled={saving}>{t('common.cancel')}</button>
<button type="button" className="primary-button" onClick={() => void save()} disabled={!snapshot || !dirty || saving}>
<button type="button" className="primary-button" onClick={() => void save()} disabled={!snapshot || !dirty || loading || saving}>
{saving ? <LoaderCircle size={16} className="spin" /> : null}{saving ? t('common.saving') : t('common.save')}
</button>
</div>
+9
View File
@@ -25,6 +25,7 @@ export type CodexModelConfiguration = {
export type CodexCatalogEditorModel = {
slug: string;
hasOfficialTemplate: boolean;
contextSource: "definition" | "configuration" | "compatibility" | "template";
customized: boolean;
configuration: CodexModelConfiguration;
defaults: CodexModelConfiguration;
@@ -35,6 +36,14 @@ export type CodexCatalogEditorSnapshot = {
models: CodexCatalogEditorModel[];
};
export function codexContextSourceHint(model: CodexCatalogEditorModel): MessageKey {
if (model.configuration.context_window !== model.defaults.context_window
|| model.configuration.max_context_window !== model.defaults.max_context_window) {
return "agents.catalog.contextSource.customized";
}
return `agents.catalog.contextSource.${model.contextSource ?? "template"}`;
}
export type CodexCatalogEditorSaveResult = {
snapshot: CodexCatalogEditorSnapshot;
synchronizationError: string | null;