mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:27:03 +08:00
refactor(codex): make the first row of the model list the default model
With a model list, the separate default model field duplicated the list: the first row is now the default model, as in the Aggregation layout, and a help tip next to "Model list" says so. Renaming or removing the first row updates the model; an older card whose default model sits further down shows that row first without counting as an edit. Cards without a list keep the default model field below the empty list. They rely on Codex's built-in /model menu, and adding a list row just to set the model would shrink that menu to the listed entries. Adding the first row to an empty list seeds it with the current default model.
This commit is contained in:
1 parent
0dae89e25c
commit
9f7ac1fa67
7 files changed
+233
-158
No files matched your search
@@ -240,8 +240,8 @@ function createCatalogRow(seed?: Partial<CodexCatalogModel>): CodexCatalogRow {
|
||||
};
|
||||
}
|
||||
|
||||
// Stack 布局里第一行就是默认模型(`model`)。直连表单里默认模型可以是列表的任意一行,
|
||||
// 这样的行在 Stack 布局里挪到最前面;不在列表里时不动。
|
||||
// 有模型列表时第一行就是默认模型(`model`)。旧数据里默认模型可以是列表的任意一行,
|
||||
// 这样的行挪到最前面显示;不在列表里时不动。
|
||||
function withDefaultModelFirst<T extends CodexCatalogModel>(
|
||||
models: T[],
|
||||
defaultModel: string,
|
||||
@@ -609,6 +609,9 @@ export function CodexFormFields({
|
||||
// 对应词条在 grokBuild.* 下。
|
||||
const isGrokBuild = appId === "grokbuild";
|
||||
const canEditCatalog = Boolean(onCatalogModelsChange);
|
||||
// 有模型列表的卡(两种布局):列表第一行就是默认模型,没有单独的默认模型框;列表为空时
|
||||
// 直连布局才显示默认模型框。Grok Build 没有列表,官方卡不显示列表。
|
||||
const firstRowIsDefault = canEditCatalog && category !== "official";
|
||||
const canEditReasoning = Boolean(onCodexChatReasoningChange);
|
||||
const supportsThinking =
|
||||
codexChatReasoning.supportsThinking === true ||
|
||||
@@ -652,7 +655,7 @@ export function CodexFormFields({
|
||||
|
||||
// 显示用的顺序。挪动默认那一行不算修改:没动过列表时保存仍是原来的顺序。
|
||||
const orderCatalogModels = (models: CodexCatalogModel[]) =>
|
||||
variant === "stack" ? withDefaultModelFirst(models, codexModel) : models;
|
||||
firstRowIsDefault ? withDefaultModelFirst(models, codexModel) : models;
|
||||
|
||||
const [catalogRows, setCatalogRows] = useState<CodexCatalogRow[]>(() =>
|
||||
orderCatalogModels(catalogModels).map((m) => createCatalogRow(m)),
|
||||
@@ -693,7 +696,7 @@ export function CodexFormFields({
|
||||
// 挪到第一行,行 id 不变,也不回传父组件。编辑中 `model` 只会跟着第一行变,这里不会动。
|
||||
// 放在子 → 父 effect 之后:同一次渲染里改了的行先回传,再看要不要挪。
|
||||
useEffect(() => {
|
||||
if (variant !== "stack") return;
|
||||
if (!firstRowIsDefault) return;
|
||||
const ordered = withDefaultModelFirst(catalogRows, codexModel);
|
||||
if (ordered === catalogRows) return;
|
||||
lastSentModelsRef.current = ordered.map(
|
||||
@@ -701,7 +704,7 @@ export function CodexFormFields({
|
||||
);
|
||||
setCatalogRows(ordered);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [codexModel, variant]);
|
||||
}, [codexModel, firstRowIsDefault]);
|
||||
|
||||
const handleReasoningThinkingChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
@@ -927,21 +930,53 @@ export function CodexFormFields({
|
||||
[catalogRowsRef, commitCatalogRows, fillModelMetadata],
|
||||
);
|
||||
|
||||
const handleAddCatalogRow = useCallback(() => {
|
||||
if (!onCatalogModelsChange) return;
|
||||
setCatalogRows((current) => [...current, createCatalogRow()]);
|
||||
}, [onCatalogModelsChange]);
|
||||
|
||||
// Stack 布局没有默认模型字段,第一行就是 `model`(加载时已挪到最前面):它不在列表里时
|
||||
// 没有这样的行;没填时保存会用第一行。
|
||||
// 第一行就是 `model`(加载时已挪到最前面):它不在列表里时没有这样的行;没填时保存会用
|
||||
// 第一行。
|
||||
const trimmedDefaultModel = codexModel.trim();
|
||||
const stackDefaultIndex = trimmedDefaultModel
|
||||
const defaultRowIndex = trimmedDefaultModel
|
||||
? catalogRows.findIndex((row) => row.model.trim() === trimmedDefaultModel)
|
||||
: 0;
|
||||
// Stack 布局里默认模型那一行就代表 `model`:改它的名字、删掉它,`model` 当场跟着变,
|
||||
// 两种布局共用这份状态。没有这样的行时是 -1。
|
||||
// 默认模型那一行就代表 `model`:改它的名字、删掉它,`model` 当场跟着变。没有这样的行时
|
||||
// 是 -1。
|
||||
const linkedDefaultIndex =
|
||||
variant === "stack" && trimmedDefaultModel ? stackDefaultIndex : -1;
|
||||
firstRowIsDefault && trimmedDefaultModel ? defaultRowIndex : -1;
|
||||
|
||||
// 列表从空变成有行时,当前默认模型排第一行(已在新增的行里就挪到最前,否则补一行),
|
||||
// 默认模型框随之隐藏,默认值不会悄悄换掉。
|
||||
const seedWithDefaultModel = useCallback(
|
||||
(additions: CodexCatalogRow[]): CodexCatalogRow[] => {
|
||||
if (!firstRowIsDefault || !trimmedDefaultModel) return additions;
|
||||
const ordered = withDefaultModelFirst(additions, trimmedDefaultModel);
|
||||
if (ordered[0]?.model.trim() === trimmedDefaultModel) return ordered;
|
||||
return [
|
||||
createCatalogRow({
|
||||
model: trimmedDefaultModel,
|
||||
displayName: trimmedDefaultModel,
|
||||
}),
|
||||
...additions,
|
||||
];
|
||||
},
|
||||
[firstRowIsDefault, trimmedDefaultModel],
|
||||
);
|
||||
|
||||
const handleAddCatalogRow = useCallback(() => {
|
||||
if (!onCatalogModelsChange) return;
|
||||
const rows = catalogRowsRef.current;
|
||||
const added =
|
||||
rows.length === 0
|
||||
? seedWithDefaultModel([createCatalogRow()])
|
||||
: [createCatalogRow()];
|
||||
commitCatalogRows([...rows, ...added]);
|
||||
for (const row of added) {
|
||||
if (row.model) fillCatalogRowMetadata(row.rowId, row.model);
|
||||
}
|
||||
}, [
|
||||
catalogRowsRef,
|
||||
commitCatalogRows,
|
||||
fillCatalogRowMetadata,
|
||||
onCatalogModelsChange,
|
||||
seedWithDefaultModel,
|
||||
]);
|
||||
|
||||
const handleUpdateCatalogRow = useCallback(
|
||||
(index: number, patch: Partial<CodexCatalogModel>) => {
|
||||
@@ -1000,19 +1035,26 @@ export function CodexFormFields({
|
||||
[catalogRows, linkedDefaultIndex, onModelChange],
|
||||
);
|
||||
|
||||
// 批量勾选拉取到的模型加入列表(Stack 布局)。
|
||||
// 批量勾选拉取到的模型加入列表。
|
||||
const handleAddFetchedCatalogRows = useCallback(
|
||||
(modelIds: string[]) => {
|
||||
const current = catalogRowsRef.current;
|
||||
const configured = new Set(current.map((row) => row.model.trim()));
|
||||
const additions = modelIds
|
||||
const picked = modelIds
|
||||
.filter((id) => !configured.has(id))
|
||||
.map((id) => createCatalogRow({ model: id, displayName: id }));
|
||||
if (additions.length === 0) return;
|
||||
if (picked.length === 0) return;
|
||||
const additions =
|
||||
current.length === 0 ? seedWithDefaultModel(picked) : picked;
|
||||
commitCatalogRows([...current, ...additions]);
|
||||
for (const row of additions) fillCatalogRowMetadata(row.rowId, row.model);
|
||||
},
|
||||
[catalogRowsRef, commitCatalogRows, fillCatalogRowMetadata],
|
||||
[
|
||||
catalogRowsRef,
|
||||
commitCatalogRows,
|
||||
fillCatalogRowMetadata,
|
||||
seedWithDefaultModel,
|
||||
],
|
||||
);
|
||||
|
||||
// 默认模型下拉建议 = 模型映射的"实际请求模型"列 ∪ 拉取到的 /models 列表
|
||||
@@ -1050,17 +1092,17 @@ export function CodexFormFields({
|
||||
model: trimmedDefaultModel,
|
||||
displayName: trimmedDefaultModel,
|
||||
});
|
||||
// Stack 布局里默认模型排第一位。
|
||||
// 默认模型排第一位。
|
||||
const rows = catalogRowsRef.current;
|
||||
commitCatalogRows(variant === "stack" ? [row, ...rows] : [...rows, row]);
|
||||
commitCatalogRows(firstRowIsDefault ? [row, ...rows] : [...rows, row]);
|
||||
fillCatalogRowMetadata(row.rowId, trimmedDefaultModel);
|
||||
}, [
|
||||
catalogRowsRef,
|
||||
commitCatalogRows,
|
||||
fillCatalogRowMetadata,
|
||||
firstRowIsDefault,
|
||||
onCatalogModelsChange,
|
||||
trimmedDefaultModel,
|
||||
variant,
|
||||
]);
|
||||
|
||||
// 子代理模型不在模型映射里:Codex 拿不到它的窗口和推理档位;经路由转成 Chat / Anthropic
|
||||
@@ -1669,6 +1711,28 @@ export function CodexFormFields({
|
||||
</>
|
||||
);
|
||||
|
||||
// 默认模型(`model`)不在列表里:没有哪一行是默认,提示并给出加入列表(排第一行)。
|
||||
const defaultModelNotInListNotice = isDefaultModelOutsideCatalog && (
|
||||
<p className="flex flex-wrap items-center gap-x-2 text-xs leading-relaxed text-fg-2">
|
||||
{t("codexConfig.defaultModelNotInList", {
|
||||
model: trimmedDefaultModel,
|
||||
defaultValue:
|
||||
"默认模型 {{model}} 不在列表里:Codex 默认仍请求它,但 /model 里没有这一项。",
|
||||
})}
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="h-auto p-0 text-xs"
|
||||
onClick={handleAddDefaultModelToCatalog}
|
||||
>
|
||||
{t("codexConfig.addToModelList", {
|
||||
defaultValue: "加入列表",
|
||||
})}
|
||||
</Button>
|
||||
</p>
|
||||
);
|
||||
|
||||
if (variant === "stack") {
|
||||
const showFormatFields = shouldShowSpeedTest && !isXaiOauthPreset;
|
||||
const showReasoning = isChatFormat && canEditReasoning;
|
||||
@@ -1722,26 +1786,7 @@ export function CodexFormFields({
|
||||
onAdd={handleAddFetchedCatalogRows}
|
||||
/>
|
||||
)}
|
||||
{isDefaultModelOutsideCatalog && (
|
||||
<p className="flex flex-wrap items-center gap-x-2 text-xs leading-relaxed text-fg-2">
|
||||
{t("codexConfig.stackDefaultNotInList", {
|
||||
model: trimmedDefaultModel,
|
||||
defaultValue:
|
||||
"默认模型 {{model}} 不在列表里:Codex 默认仍请求它,但 /model 里没有这一项。",
|
||||
})}
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="h-auto p-0 text-xs"
|
||||
onClick={handleAddDefaultModelToCatalog}
|
||||
>
|
||||
{t("codexConfig.addToModelList", {
|
||||
defaultValue: "加入列表",
|
||||
})}
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
{defaultModelNotInListNotice}
|
||||
{catalogRows.length > 0 ? (
|
||||
renderCatalogRows()
|
||||
) : (
|
||||
@@ -1826,16 +1871,28 @@ export function CodexFormFields({
|
||||
{endpointSection}
|
||||
|
||||
{/* 模型列表 —— 生成 Codex 的模型目录(model_catalog_json):/model 菜单列哪些模型,
|
||||
各自的窗口和思考等级。留空不生成。默认模型、子代理从这里选,所以排在它们前面。 */}
|
||||
{category !== "official" && canEditCatalog && (
|
||||
各自的窗口和思考等级。留空不生成。第一行是默认模型,子代理也从这里选。 */}
|
||||
{firstRowIsDefault && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<FormLabel>
|
||||
{t("codexConfig.modelListTitle", {
|
||||
defaultValue: "模型列表",
|
||||
})}
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<FormLabel>
|
||||
{t("codexConfig.modelListTitle", {
|
||||
defaultValue: "模型列表",
|
||||
})}
|
||||
</FormLabel>
|
||||
<HelpTip
|
||||
title={t("providerForm.stackModelDefault", {
|
||||
defaultValue: "默认模型",
|
||||
})}
|
||||
>
|
||||
{t("codexConfig.modelListDefaultHelp", {
|
||||
defaultValue:
|
||||
"第一行是默认模型,Codex 启动时用它。列表为空时,可以在下方单独填写默认模型。",
|
||||
})}
|
||||
</HelpTip>
|
||||
</div>
|
||||
{renderCatalogActionButtons(
|
||||
handleAddCatalogRow,
|
||||
t("codexConfig.addCatalogModel", {
|
||||
@@ -1858,92 +1915,76 @@ export function CodexFormFields({
|
||||
onAdd={handleAddFetchedCatalogRows}
|
||||
/>
|
||||
)}
|
||||
{defaultModelNotInListNotice}
|
||||
{catalogRows.length > 0 && renderCatalogRows()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 默认模型 —— config.toml 顶层 model,Codex 启动时默认请求的模型。
|
||||
实时写回 TOML;留空则删行(有模型列表时保存回退为列表第一行)。 */}
|
||||
{category !== "official" && onModelChange && (
|
||||
<div className="space-y-1.5">
|
||||
<FormLabel htmlFor="codexDefaultModel">
|
||||
{t("codexConfig.defaultModelLabel", { defaultValue: "默认模型" })}
|
||||
</FormLabel>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
id="codexDefaultModel"
|
||||
value={codexModel}
|
||||
onChange={(event) => onModelChange(event.target.value)}
|
||||
placeholder={
|
||||
isGrokBuild
|
||||
? t("grokBuild.defaultModelPlaceholder", {
|
||||
defaultValue: "例如: grok-4.5",
|
||||
})
|
||||
: t("codexConfig.defaultModelPlaceholder", {
|
||||
defaultValue: "例如: gpt-5.6",
|
||||
})
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
{/* 有模型列表时用列表上的「获取模型列表」,这里不再放一个。 */}
|
||||
{!canEditCatalog && (
|
||||
<HoverTip content={t("providerForm.fetchModels")}>
|
||||
<Button
|
||||
aria-label={t("providerForm.fetchModels")}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleFetchModels}
|
||||
disabled={isFetchingModels}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isFetchingModels ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</HoverTip>
|
||||
)}
|
||||
{defaultModelSuggestions.length > 0 && (
|
||||
<ModelDropdown
|
||||
models={defaultModelSuggestions}
|
||||
onSelect={(id) => onModelChange(id)}
|
||||
{/* 默认模型 —— config.toml 顶层 model,Codex 启动时默认请求的模型。实时写回 TOML;
|
||||
留空则删行。有模型列表时第一行就是它,不显示这个框(Grok Build 没有列表,一直显示)。 */}
|
||||
{category !== "official" &&
|
||||
onModelChange &&
|
||||
(!firstRowIsDefault || catalogRows.length === 0) && (
|
||||
<div className="space-y-1.5">
|
||||
<FormLabel htmlFor="codexDefaultModel">
|
||||
{t("codexConfig.defaultModelLabel", { defaultValue: "默认模型" })}
|
||||
</FormLabel>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
id="codexDefaultModel"
|
||||
value={codexModel}
|
||||
onChange={(event) => onModelChange(event.target.value)}
|
||||
placeholder={
|
||||
isGrokBuild
|
||||
? t("grokBuild.defaultModelPlaceholder", {
|
||||
defaultValue: "例如: grok-4.5",
|
||||
})
|
||||
: t("codexConfig.defaultModelPlaceholder", {
|
||||
defaultValue: "例如: gpt-5.6",
|
||||
})
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs leading-relaxed text-fg-2">
|
||||
{isGrokBuild
|
||||
? t("grokBuild.defaultModelHint", {
|
||||
defaultValue:
|
||||
"Grok Build 默认请求的模型,随时可改,无需等待预设更新。",
|
||||
})
|
||||
: t("codexConfig.defaultModelHint", {
|
||||
defaultValue:
|
||||
"Codex 默认请求的模型,随时可改,无需等待预设更新。留空时用模型列表第一行。",
|
||||
})}
|
||||
</p>
|
||||
{isDefaultModelOutsideCatalog && (
|
||||
<p className="flex flex-wrap items-center gap-x-2 text-xs leading-relaxed text-fg-2">
|
||||
{t("codexConfig.defaultModelNotInCatalog", {
|
||||
defaultValue:
|
||||
"该模型不在模型列表中,Codex 的 /model 菜单不会列出它(直接请求仍然有效)。",
|
||||
})}
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="h-auto p-0 text-xs"
|
||||
onClick={handleAddDefaultModelToCatalog}
|
||||
>
|
||||
{t("codexConfig.addToModelList", {
|
||||
defaultValue: "加入列表",
|
||||
})}
|
||||
</Button>
|
||||
{/* 有模型列表时用列表上的「获取模型列表」,这里不再放一个。 */}
|
||||
{!canEditCatalog && (
|
||||
<HoverTip content={t("providerForm.fetchModels")}>
|
||||
<Button
|
||||
aria-label={t("providerForm.fetchModels")}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleFetchModels}
|
||||
disabled={isFetchingModels}
|
||||
className="shrink-0"
|
||||
>
|
||||
{isFetchingModels ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</HoverTip>
|
||||
)}
|
||||
{defaultModelSuggestions.length > 0 && (
|
||||
<ModelDropdown
|
||||
models={defaultModelSuggestions}
|
||||
onSelect={(id) => onModelChange(id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs leading-relaxed text-fg-2">
|
||||
{isGrokBuild
|
||||
? t("grokBuild.defaultModelHint", {
|
||||
defaultValue:
|
||||
"Grok Build 默认请求的模型,随时可改,无需等待预设更新。",
|
||||
})
|
||||
: t("codexConfig.defaultModelHint", {
|
||||
defaultValue:
|
||||
"Codex 默认请求的模型,随时可改,无需等待预设更新。添加模型列表后,列表第一行就是默认模型。",
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 子代理 —— config.toml 的 [agents] default_subagent_model / default_subagent_reasoning_effort。
|
||||
留空删键,子代理跟随主模型;聚合模式下不写进 live。 */}
|
||||
|
||||
@@ -1912,8 +1912,7 @@
|
||||
"modelMetadataAdvancedHint": "Overrides Codex metadata for unknown models; clearing a field removes the matching config.toml entry.",
|
||||
"defaultModelLabel": "Default Model",
|
||||
"defaultModelPlaceholder": "e.g. gpt-5.6-sol",
|
||||
"defaultModelHint": "The model Codex requests by default; change it anytime without waiting for a preset update. If left empty, the first row of the model list is used.",
|
||||
"defaultModelNotInCatalog": "This model is not in the model list, so the Codex /model menu won't list it (direct requests still work).",
|
||||
"defaultModelHint": "The model Codex requests by default; change it anytime without waiting for a preset update. Once you add a model list, its first row is the default model.",
|
||||
"subagentModelLabel": "Sub-agent Model",
|
||||
"subagentModelPlaceholder": "Leave empty to follow the main model",
|
||||
"subagentReasoningEffortLabel": "Sub-agent reasoning effort",
|
||||
@@ -1921,11 +1920,12 @@
|
||||
"subagentModelHint": "The model and reasoning effort Codex sub-agents use by default. When empty, sub-agents use the main agent's; with only a model set, they use that model's default effort. Not applied in Aggregation mode, where sub-agents follow the main model.",
|
||||
"subagentModelNotInCatalog": "This model is not in the model list: Codex can't get its context window, and when routing converts requests to Chat / Anthropic it is replaced by the default model.",
|
||||
"modelListHint": "These models appear in Codex's /model menu with their own context window and reasoning levels. Restart Codex after changes.",
|
||||
"modelListDefaultHelp": "The first row is the default model Codex uses at startup. With an empty list, you can set the default model separately below.",
|
||||
"addCatalogModel": "Add manually",
|
||||
"modelListTitle": "Models",
|
||||
"stackModelListHint": "These models appear in Codex's /model menu; picking one sends requests straight to this provider. Restart Codex after changes.",
|
||||
"stackModelDefaultHelp": "The first row is this provider's default model: while this provider is the default, Codex uses it by default.",
|
||||
"stackDefaultNotInList": "The default model {{model}} is not in the list: Codex still requests it by default, but /model won't show it.",
|
||||
"defaultModelNotInList": "The default model {{model}} is not in the list: Codex still requests it by default, but /model won't show it.",
|
||||
"addToModelList": "Add to list",
|
||||
"modelListEmpty": "No models configured: in Aggregation mode only this provider's default model (the model in config.toml) is published.",
|
||||
"stackAdvancedHint": "Reasoning, Anthropic-specific options, custom User-Agent and request overrides; usually no need to change.",
|
||||
|
||||
@@ -1912,8 +1912,7 @@
|
||||
"modelMetadataAdvancedHint": "未知のモデル向けに Codex メタデータを上書きします。空にすると対応する config.toml 設定を削除します。",
|
||||
"defaultModelLabel": "デフォルトモデル",
|
||||
"defaultModelPlaceholder": "例: gpt-5.6-sol",
|
||||
"defaultModelHint": "Codex がデフォルトでリクエストするモデルです。プリセットの更新を待たずにいつでも変更できます。空欄のときはモデル一覧の先頭行が使われます。",
|
||||
"defaultModelNotInCatalog": "このモデルはモデル一覧にないため、Codex の /model メニューには表示されません(直接リクエストは有効です)。",
|
||||
"defaultModelHint": "Codex がデフォルトでリクエストするモデルです。プリセットの更新を待たずにいつでも変更できます。モデル一覧を追加すると、その先頭行がデフォルトモデルになります。",
|
||||
"subagentModelLabel": "サブエージェントモデル",
|
||||
"subagentModelPlaceholder": "空欄ならメインモデルに従う",
|
||||
"subagentReasoningEffortLabel": "サブエージェントの推論強度",
|
||||
@@ -1921,11 +1920,12 @@
|
||||
"subagentModelHint": "Codex が起動するサブエージェントのデフォルトのモデルと推論強度です。空欄ならメインエージェントと同じものを使い、モデルだけ指定するとそのモデルのデフォルト強度を使います。集約モードでは適用されず、サブエージェントはメインモデルに従います。",
|
||||
"subagentModelNotInCatalog": "このモデルはモデル一覧にありません。Codex はコンテキストウィンドウを取得できず、ルーティングで Chat / Anthropic 形式に変換する際はデフォルトモデルに置き換えられます。",
|
||||
"modelListHint": "これらのモデルは、それぞれのコンテキストウィンドウと思考レベル付きで Codex の /model に表示されます。変更後は Codex を再起動してください。",
|
||||
"modelListDefaultHelp": "先頭の行がデフォルトモデルで、Codex の起動時に使われます。一覧が空のときは、下でデフォルトモデルを個別に設定できます。",
|
||||
"addCatalogModel": "手動で追加",
|
||||
"modelListTitle": "モデル一覧",
|
||||
"stackModelListHint": "これらのモデルは Codex の /model に表示され、選ぶとリクエストがこのプロバイダーへ直接送られます。変更後は Codex を再起動してください。",
|
||||
"stackModelDefaultHelp": "先頭の行がこのプロバイダーのデフォルトモデルです。このプロバイダーがデフォルトのとき、Codex はこれを既定で使います。",
|
||||
"stackDefaultNotInList": "デフォルトモデル {{model}} は一覧にありません。Codex は既定でこれをリクエストしますが、/model には表示されません。",
|
||||
"defaultModelNotInList": "デフォルトモデル {{model}} は一覧にありません。Codex は既定でこれをリクエストしますが、/model には表示されません。",
|
||||
"addToModelList": "一覧に追加",
|
||||
"modelListEmpty": "モデル未設定:集約モードではこのプロバイダーのデフォルトモデル(config.toml の model)だけが公開されます。",
|
||||
"stackAdvancedHint": "思考能力、Anthropic 固有の項目、カスタム User-Agent とリクエスト上書き。通常は変更不要です。",
|
||||
|
||||
@@ -1907,8 +1907,7 @@
|
||||
"promptCacheRoutingHint": "自動模式僅對已確認相容的上游傳送 prompt_cache_key;開啟可用於其他相容閘道,關閉可避免嚴格閘道因未知欄位回傳 400。只使用客戶端提供的穩定工作階段 ID。",
|
||||
"defaultModelLabel": "預設模型",
|
||||
"defaultModelPlaceholder": "例如: gpt-5.6-sol",
|
||||
"defaultModelHint": "Codex 預設請求的模型,隨時可改,無需等待軟體更新供應商範本。留空時使用模型列表第一行。",
|
||||
"defaultModelNotInCatalog": "該模型不在模型列表中,Codex 的 /model 選單不會列出它(直接請求仍然有效)。",
|
||||
"defaultModelHint": "Codex 預設請求的模型,隨時可改,無需等待軟體更新供應商範本。加入模型列表後,列表第一行就是預設模型。",
|
||||
"subagentModelLabel": "子代理模型",
|
||||
"subagentModelPlaceholder": "留空則跟隨主模型",
|
||||
"subagentReasoningEffortLabel": "子代理推理強度",
|
||||
@@ -1916,11 +1915,12 @@
|
||||
"subagentModelHint": "Codex 派出的子代理預設使用的模型和推理強度。留空時子代理沿用主代理的;只填模型時使用該模型自己的預設強度。聚合模式下不生效,子代理跟隨主模型。",
|
||||
"subagentModelNotInCatalog": "該模型不在模型列表中:Codex 取得不到它的上下文視窗;經路由轉成 Chat / Anthropic 協定時會被換成預設模型。",
|
||||
"modelListHint": "這些模型會出現在 Codex 的 /model 裡,帶上各自的上下文視窗和思考等級。修改後需要重新啟動 Codex。",
|
||||
"modelListDefaultHelp": "第一行是預設模型,Codex 啟動時使用它。列表為空時,可以在下方單獨填寫預設模型。",
|
||||
"addCatalogModel": "手動新增",
|
||||
"modelListTitle": "模型列表",
|
||||
"stackModelListHint": "這些模型會出現在 Codex 的 /model 裡,選中後請求直達這家。修改後需要重新啟動 Codex。",
|
||||
"stackModelDefaultHelp": "第一行是這家的預設模型:這家被設為預設時,Codex 預設使用它。",
|
||||
"stackDefaultNotInList": "預設模型 {{model}} 不在清單裡:Codex 預設仍請求它,但 /model 裡沒有這一項。",
|
||||
"defaultModelNotInList": "預設模型 {{model}} 不在清單裡:Codex 預設仍請求它,但 /model 裡沒有這一項。",
|
||||
"addToModelList": "加入清單",
|
||||
"modelListEmpty": "未設定模型:聚合模式下只發布這家的預設模型(config.toml 的 model)。",
|
||||
"stackAdvancedHint": "思考能力、Anthropic 專有項、自訂 User-Agent 與請求覆寫,一般無需修改。",
|
||||
|
||||
@@ -1912,8 +1912,7 @@
|
||||
"modelMetadataAdvancedHint": "用于未知模型的 Codex 元数据覆盖;清空字段会从 config.toml 删除对应配置。",
|
||||
"defaultModelLabel": "默认模型",
|
||||
"defaultModelPlaceholder": "例如: gpt-5.6-sol",
|
||||
"defaultModelHint": "Codex 默认请求的模型,随时可改,无需等待预设更新。留空时用模型列表第一行。",
|
||||
"defaultModelNotInCatalog": "该模型不在模型列表中,Codex 的 /model 菜单不会列出它(直接请求仍然有效)。",
|
||||
"defaultModelHint": "Codex 默认请求的模型,随时可改,无需等待预设更新。添加模型列表后,列表第一行就是默认模型。",
|
||||
"subagentModelLabel": "子代理模型",
|
||||
"subagentModelPlaceholder": "留空则跟随主模型",
|
||||
"subagentReasoningEffortLabel": "子代理推理强度",
|
||||
@@ -1921,11 +1920,12 @@
|
||||
"subagentModelHint": "Codex 派出的子代理默认用的模型和推理强度。留空时子代理沿用主代理的;只填模型时用该模型自己的默认强度。聚合模式下不生效,子代理跟随主模型。",
|
||||
"subagentModelNotInCatalog": "该模型不在模型列表中:Codex 拿不到它的上下文窗口;经路由转成 Chat / Anthropic 协议时会被换成默认模型。",
|
||||
"modelListHint": "这些模型会出现在 Codex 的 /model 里,带上各自的上下文窗口和思考等级。修改后需要重启 Codex。",
|
||||
"modelListDefaultHelp": "第一行是默认模型,Codex 启动时用它。列表为空时,可以在下方单独填写默认模型。",
|
||||
"addCatalogModel": "手动添加",
|
||||
"modelListTitle": "模型列表",
|
||||
"stackModelListHint": "这些模型会出现在 Codex 的 /model 里,选中后请求直达这家。修改后需要重启 Codex。",
|
||||
"stackModelDefaultHelp": "第一行是这家的默认模型:这家被设为默认时,Codex 默认用它。",
|
||||
"stackDefaultNotInList": "默认模型 {{model}} 不在列表里:Codex 默认仍请求它,但 /model 里没有这一项。",
|
||||
"defaultModelNotInList": "默认模型 {{model}} 不在列表里:Codex 默认仍请求它,但 /model 里没有这一项。",
|
||||
"addToModelList": "加入列表",
|
||||
"modelListEmpty": "未配置模型:聚合模式下只发布这家的默认模型(config.toml 的 model)。",
|
||||
"stackAdvancedHint": "思考能力、Anthropic 专有项、自定义 User-Agent 与请求覆盖,一般无需修改。",
|
||||
|
||||
@@ -107,30 +107,64 @@ describe("CodexFormFields sub-agent defaults", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const rowModels = () =>
|
||||
screen
|
||||
.getAllByPlaceholderText("例如: deepseek-v4-flash")
|
||||
.map((input) => (input as HTMLInputElement).value);
|
||||
|
||||
describe("CodexFormFields direct layout", () => {
|
||||
it("shows the model list outside the advanced section, before the default model", () => {
|
||||
it("lets the list own the default model: no separate field, list outside the advanced section", () => {
|
||||
renderCodexForm({
|
||||
catalogModels: [{ model: "gpt-a" }],
|
||||
onCatalogModelsChange: vi.fn(),
|
||||
});
|
||||
|
||||
const listTitle = screen.getByText("模型列表");
|
||||
expect(listTitle).toBeVisible();
|
||||
expect(screen.getByText("模型列表")).toBeVisible();
|
||||
expect(screen.getByRole("button", { name: "默认模型" })).toBeVisible();
|
||||
expect(screen.queryByText("上游格式")).toBeNull();
|
||||
const defaultModel = document.getElementById("codexDefaultModel")!;
|
||||
expect(
|
||||
listTitle.compareDocumentPosition(defaultModel) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
// 列表上已有「获取模型列表」,默认模型旁不再放第二个。
|
||||
expect(document.getElementById("codexDefaultModel")).toBeNull();
|
||||
expect(
|
||||
screen.getAllByRole("button", { name: /providerForm\.fetchModels/ }),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps the fetch button on the default model when there is no list (Grok Build)", () => {
|
||||
it("shows the default model field below an empty list, and seeds the first row with it", () => {
|
||||
const onCatalogModelsChange = vi.fn();
|
||||
renderCodexForm({ catalogModels: [], onCatalogModelsChange });
|
||||
|
||||
const listTitle = screen.getByText("模型列表");
|
||||
const defaultModel = document.getElementById("codexDefaultModel")!;
|
||||
expect(
|
||||
listTitle.compareDocumentPosition(defaultModel) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "手动添加" }));
|
||||
expect(rowModels()).toEqual(["gpt-a", ""]);
|
||||
expect(document.getElementById("codexDefaultModel")).toBeNull();
|
||||
const rows = onCatalogModelsChange.mock.calls.at(-1)?.[0] ?? [];
|
||||
expect(rows.map((row: { model: string }) => row.model)).toEqual([
|
||||
"gpt-a",
|
||||
"",
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows an older card's default row first without reporting a change", () => {
|
||||
const onCatalogModelsChange = vi.fn();
|
||||
renderCodexForm({
|
||||
codexModel: "gpt-b",
|
||||
catalogModels: [{ model: "gpt-a" }, { model: "gpt-b" }],
|
||||
onCatalogModelsChange,
|
||||
});
|
||||
|
||||
expect(rowModels()).toEqual(["gpt-b", "gpt-a"]);
|
||||
expect(onCatalogModelsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the default model field and its fetch button without a list (Grok Build)", () => {
|
||||
renderCodexForm();
|
||||
expect(screen.queryByText("模型列表")).toBeNull();
|
||||
expect(document.getElementById("codexDefaultModel")).not.toBeNull();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "providerForm.fetchModels" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
@@ -212,7 +212,9 @@ describe("Codex Copilot provider form", () => {
|
||||
|
||||
it("defaults to GPT-6 Astra and shows all five preset model mappings", () => {
|
||||
renderForm();
|
||||
expect(screen.getAllByDisplayValue("gpt-6-astra")).toHaveLength(2);
|
||||
// 有模型列表时第一行就是默认模型,没有单独的默认模型框。
|
||||
expect(screen.getAllByDisplayValue("gpt-6-astra")).toHaveLength(1);
|
||||
expect(document.getElementById("codexDefaultModel")).toBeNull();
|
||||
for (const model of [
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra",
|
||||
@@ -277,9 +279,6 @@ describe("Codex Copilot provider form", () => {
|
||||
screen.getByRole("button", { name: "select-copilot-account" }),
|
||||
);
|
||||
await selectFormat("openai_responses");
|
||||
fireEvent.change(screen.getByLabelText("默认模型"), {
|
||||
target: { value: "gpt-5.6-luna" },
|
||||
});
|
||||
fireEvent.change(screen.getByDisplayValue("gpt-5.6-sol"), {
|
||||
target: { value: "custom-copilot-model" },
|
||||
});
|
||||
@@ -305,7 +304,7 @@ describe("Codex Copilot provider form", () => {
|
||||
const settings = JSON.parse(saved.settingsConfig);
|
||||
expect(settings.auth).toEqual({});
|
||||
expect(settings.config).toContain(liveToml);
|
||||
expect(settings.config).toContain('model = "gpt-5.6-luna"');
|
||||
expect(settings.config).toContain('model = "gpt-6-astra"');
|
||||
expect(settings.config).toContain('wire_api = "responses"');
|
||||
expect(settings.modelCatalog.models).toEqual(
|
||||
preset.modelCatalog?.map((model) =>
|
||||
@@ -416,7 +415,8 @@ describe("Codex Copilot provider form", () => {
|
||||
expect(screen.getByTestId("selected-copilot-account")).toHaveTextContent(
|
||||
accountId ?? "default",
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText("默认模型"), {
|
||||
// 没有模型列表的卡才有默认模型框(「?」按钮也叫默认模型)。
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "默认模型" }), {
|
||||
target: { value: "gpt-5.6-luna" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "save" }));
|
||||
|
||||
Reference in new issue
Block a user