mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
Enable Codex goals in provider templates (#3089)
* Enable Codex goals in provider templates * feat: add Codex goal mode toggle - Remove forced goals=true from Codex provider presets and custom templates. - Add a Codex provider editor switch that updates [features].goals on demand. - Update docs, i18n, and regression coverage for the optional Goal mode flow. --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -268,10 +268,13 @@ requires_openai_auth = true
|
||||
| `model` | Yes | Model to use (e.g., `gpt-5.2`, `gpt-4o`) |
|
||||
| `model_reasoning_effort` | No | Reasoning effort: `low` / `medium` / `high` |
|
||||
| `disable_response_storage` | No | Whether to disable response storage |
|
||||
| `goals` | No | Enables Codex `/goal` mode when set under `[features]`; the provider editor can toggle this field for providers that need it |
|
||||
| `base_url` | Yes | API endpoint URL |
|
||||
| `wire_api` | No | API protocol type (usually `responses`) |
|
||||
| `requires_openai_auth` | No | Whether to use OpenAI authentication |
|
||||
|
||||
If Codex recognizes `/goal` but cannot set a goal after switching to an API-key or custom endpoint provider, enable the provider editor's **Goal mode** switch. CC Switch writes `goals = true` under `[features]` for that provider's `~/.codex/config.toml`; restart Codex afterward. If Goal mode asks for approval more often than expected, refresh the Codex settings page and check that `approval_policy` and `sandbox_mode` still match your intended permission level.
|
||||
|
||||
|
||||
### Gemini Configuration Format
|
||||
|
||||
|
||||
@@ -268,10 +268,13 @@ requires_openai_auth = true
|
||||
| `model` | はい | 使用するモデル(例:`gpt-5.2`、`gpt-4o`) |
|
||||
| `model_reasoning_effort` | いいえ | 推論強度:`low` / `medium` / `high` |
|
||||
| `disable_response_storage` | いいえ | レスポンス保存を無効にするかどうか |
|
||||
| `goals` | いいえ | `[features]` 配下で `true` にすると Codex の `/goal` モードを有効化します。必要なプロバイダーでは、プロバイダー編集画面のスイッチで切り替えられます |
|
||||
| `base_url` | はい | API エンドポイントアドレス |
|
||||
| `wire_api` | いいえ | API プロトコルタイプ(通常 `responses`) |
|
||||
| `requires_openai_auth` | いいえ | OpenAI 認証方式を使用するかどうか |
|
||||
|
||||
API Key またはカスタムエンドポイントのプロバイダーへ切り替えた後、Codex が `/goal` を認識しているのに目標を設定できない場合は、プロバイダー編集画面の **Goal mode** スイッチを有効にしてください。CC Switch はそのプロバイダーの `~/.codex/config.toml` の `[features]` に `goals = true` を書き込みます。その後、Codex を再起動してください。Goal mode 有効化後に想定より頻繁に承認を求められる場合は、Codex の設定画面を再読み込みし、`approval_policy` と `sandbox_mode` が意図した権限と一致しているか確認してください。
|
||||
|
||||
|
||||
### Gemini 設定形式
|
||||
|
||||
|
||||
@@ -268,10 +268,13 @@ requires_openai_auth = true
|
||||
| `model` | 是 | 使用的模型(如 `gpt-5.2`、`gpt-4o`) |
|
||||
| `model_reasoning_effort` | 否 | 推理强度:`low` / `medium` / `high` |
|
||||
| `disable_response_storage` | 否 | 是否禁用响应存储 |
|
||||
| `goals` | 否 | 放在 `[features]` 下并设为 `true` 后启用 Codex `/goal` 模式;需要时可在供应商编辑区域通过开关启用 |
|
||||
| `base_url` | 是 | API 端点地址 |
|
||||
| `wire_api` | 否 | API 协议类型(通常为 `responses`) |
|
||||
| `requires_openai_auth` | 否 | 是否使用 OpenAI 认证方式 |
|
||||
|
||||
如果切换到 API Key 或自定义端点供应商后,Codex 能识别 `/goal`,但无法设置目标,请在供应商编辑区域打开 **Goal mode** 开关。CC Switch 会为该供应商在 `~/.codex/config.toml` 的 `[features]` 下写入 `goals = true`,之后请重启 Codex。如果启用 Goal mode 后比预期更频繁地请求批准,请刷新 Codex 设置页,并确认 `approval_policy` 与 `sandbox_mode` 仍符合你的权限预期。
|
||||
|
||||
|
||||
### Gemini 配置格式
|
||||
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
// NOTE: Codex 1M 上下文 UI 已暂时隐藏(详见下方 CodexConfigSection 内 JSX 注释)。
|
||||
// 如需恢复,请同时:
|
||||
// - 在下方 React import 中加回 `useMemo`
|
||||
// - 取消下面 `@/utils/providerConfigUtils` import 的注释
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import JsonEditor from "@/components/JsonEditor";
|
||||
import {
|
||||
isCodexGoalModeEnabled,
|
||||
setCodexGoalMode,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
/*
|
||||
import {
|
||||
extractCodexTopLevelInt,
|
||||
@@ -144,6 +153,18 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const goalModeEnabled = useMemo(
|
||||
() => isCodexGoalModeEnabled(localValue),
|
||||
[localValue],
|
||||
);
|
||||
|
||||
const handleGoalModeToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
handleLocalChange(setCodexGoalMode(localValueRef.current || "", checked));
|
||||
},
|
||||
[handleLocalChange],
|
||||
);
|
||||
|
||||
// Codex 1M 上下文相关状态/回调暂时禁用——见同文件下方 JSX 注释处的恢复说明。
|
||||
/*
|
||||
// Parse toggle states from TOML text
|
||||
@@ -217,7 +238,7 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<label
|
||||
htmlFor="codexConfig"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
@@ -225,15 +246,27 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
{t("codexConfig.configToml")}
|
||||
</label>
|
||||
|
||||
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useCommonConfig}
|
||||
onChange={(e) => onCommonConfigToggle(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||
/>
|
||||
{t("codexConfig.writeCommonConfig")}
|
||||
</label>
|
||||
<div className="flex flex-wrap items-center justify-end gap-x-4 gap-y-1">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={goalModeEnabled}
|
||||
onChange={(e) => handleGoalModeToggle(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||
/>
|
||||
{t("codexConfig.enableGoalMode")}
|
||||
</label>
|
||||
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useCommonConfig}
|
||||
onChange={(e) => onCommonConfigToggle(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||
/>
|
||||
{t("codexConfig.writeCommonConfig")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
|
||||
@@ -1102,6 +1102,7 @@
|
||||
"editCommonConfig": "Edit Common Config",
|
||||
"editCommonConfigTitle": "Edit Codex Common Config Snippet",
|
||||
"commonConfigHint": "This snippet will be appended to the end of config.toml when 'Write Common Config' is checked",
|
||||
"enableGoalMode": "Enable Goal mode",
|
||||
"apiUrlLabel": "API Request URL",
|
||||
"extractFromCurrent": "Extract from Editor",
|
||||
"extractNoCommonConfig": "No common config available to extract from editor",
|
||||
|
||||
@@ -1102,6 +1102,7 @@
|
||||
"editCommonConfig": "共通設定を編集",
|
||||
"editCommonConfigTitle": "Codex 共通設定スニペットを編集",
|
||||
"commonConfigHint": "「共通設定を書き込む」がオンの場合、config.toml の末尾に追記されます",
|
||||
"enableGoalMode": "Goal mode を有効化",
|
||||
"apiUrlLabel": "API リクエスト URL",
|
||||
"extractFromCurrent": "編集内容から抽出",
|
||||
"extractNoCommonConfig": "編集内容から抽出できる共通設定がありません",
|
||||
|
||||
@@ -1074,6 +1074,7 @@
|
||||
"editCommonConfig": "編輯通用設定",
|
||||
"editCommonConfigTitle": "編輯 Codex 通用設定片段",
|
||||
"commonConfigHint": "該片段會在勾選「寫入通用設定」時附加到 config.toml 末尾",
|
||||
"enableGoalMode": "啟用 Goal mode",
|
||||
"apiUrlLabel": "API 請求位址",
|
||||
"extractFromCurrent": "從編輯內容擷取",
|
||||
"extractNoCommonConfig": "目前編輯內容沒有可擷取的通用設定",
|
||||
|
||||
@@ -1102,6 +1102,7 @@
|
||||
"editCommonConfig": "编辑通用配置",
|
||||
"editCommonConfigTitle": "编辑 Codex 通用配置片段",
|
||||
"commonConfigHint": "该片段会在勾选'写入通用配置'时追加到 config.toml 末尾",
|
||||
"enableGoalMode": "启用 Goal mode",
|
||||
"apiUrlLabel": "API 请求地址",
|
||||
"extractFromCurrent": "从编辑内容提取",
|
||||
"extractNoCommonConfig": "当前编辑内容没有可提取的通用配置",
|
||||
|
||||
@@ -410,6 +410,9 @@ const TOML_WIRE_API_PATTERN =
|
||||
/^\s*wire_api\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_MODEL_PROVIDER_LINE_PATTERN =
|
||||
/^\s*model_provider\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_GOALS_FEATURE_PATTERN = /^\s*goals\s*=\s*(true|false)\s*(?:#.*)?$/;
|
||||
const TOML_GOALS_FEATURE_REPLACE_PATTERN =
|
||||
/^(\s*goals\s*=\s*)(true|false)(\s*(?:#.*)?)$/;
|
||||
const CODEX_RESERVED_MODEL_PROVIDER_IDS = new Set([
|
||||
"amazon-bedrock",
|
||||
"openai",
|
||||
@@ -422,6 +425,7 @@ const CODEX_RESERVED_MODEL_PROVIDER_IDS = new Set([
|
||||
interface TomlSectionRange {
|
||||
bodyEndIndex: number;
|
||||
bodyStartIndex: number;
|
||||
headerLineIndex: number;
|
||||
}
|
||||
|
||||
interface TomlAssignmentMatch {
|
||||
@@ -458,6 +462,7 @@ const getTomlSectionRange = (
|
||||
return {
|
||||
bodyStartIndex: headerLineIndex + 1,
|
||||
bodyEndIndex: index,
|
||||
headerLineIndex,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -468,6 +473,7 @@ const getTomlSectionRange = (
|
||||
return {
|
||||
bodyStartIndex: headerLineIndex + 1,
|
||||
bodyEndIndex: lines.length,
|
||||
headerLineIndex,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -639,6 +645,14 @@ const getTopLevelModelProviderLineIndex = (lines: string[]): number => {
|
||||
return -1;
|
||||
};
|
||||
|
||||
const hasTomlSectionBodyContent = (
|
||||
lines: string[],
|
||||
sectionRange: TomlSectionRange,
|
||||
): boolean =>
|
||||
lines
|
||||
.slice(sectionRange.bodyStartIndex, sectionRange.bodyEndIndex)
|
||||
.some((line) => line.trim() !== "");
|
||||
|
||||
const TOML_BASIC_STRING_ESCAPES: Record<string, string> = {
|
||||
'"': '\\"',
|
||||
"\\": "\\\\",
|
||||
@@ -797,6 +811,103 @@ export const setCodexWireApi = (
|
||||
return finalizeTomlText(lines);
|
||||
};
|
||||
|
||||
export const isCodexGoalModeEnabled = (
|
||||
configText: string | undefined | null,
|
||||
): boolean => {
|
||||
try {
|
||||
const raw = typeof configText === "string" ? configText : "";
|
||||
const text = normalizeTomlText(raw);
|
||||
if (!text) return false;
|
||||
|
||||
try {
|
||||
const parsed = parseToml(text) as Record<string, any>;
|
||||
return parsed.features?.goals === true;
|
||||
} catch {
|
||||
// Fall back to line scanning while the user is editing invalid TOML.
|
||||
}
|
||||
|
||||
const lines = text.split("\n");
|
||||
const featureRange = getTomlSectionRange(lines, "features");
|
||||
if (!featureRange) return false;
|
||||
|
||||
const index = findTomlLineInRange(
|
||||
lines,
|
||||
TOML_GOALS_FEATURE_PATTERN,
|
||||
featureRange.bodyStartIndex,
|
||||
featureRange.bodyEndIndex,
|
||||
);
|
||||
if (index === -1) return false;
|
||||
|
||||
return lines[index].match(TOML_GOALS_FEATURE_PATTERN)?.[1] === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const setCodexGoalMode = (
|
||||
configText: string,
|
||||
enabled: boolean,
|
||||
): string => {
|
||||
const normalizedText = normalizeTomlText(configText);
|
||||
const lines = normalizedText ? normalizedText.split("\n") : [];
|
||||
let featureRange = getTomlSectionRange(lines, "features");
|
||||
|
||||
if (featureRange) {
|
||||
const goalLineIndex = findTomlLineInRange(
|
||||
lines,
|
||||
TOML_GOALS_FEATURE_REPLACE_PATTERN,
|
||||
featureRange.bodyStartIndex,
|
||||
featureRange.bodyEndIndex,
|
||||
);
|
||||
|
||||
if (enabled) {
|
||||
if (goalLineIndex !== -1) {
|
||||
lines[goalLineIndex] = lines[goalLineIndex].replace(
|
||||
TOML_GOALS_FEATURE_REPLACE_PATTERN,
|
||||
"$1true$3",
|
||||
);
|
||||
} else {
|
||||
lines.splice(
|
||||
getTomlSectionInsertIndex(lines, featureRange),
|
||||
0,
|
||||
"goals = true",
|
||||
);
|
||||
}
|
||||
return finalizeTomlText(lines);
|
||||
}
|
||||
|
||||
if (goalLineIndex !== -1) {
|
||||
lines.splice(goalLineIndex, 1);
|
||||
featureRange = getTomlSectionRange(lines, "features");
|
||||
if (featureRange && !hasTomlSectionBodyContent(lines, featureRange)) {
|
||||
lines.splice(
|
||||
featureRange.headerLineIndex,
|
||||
featureRange.bodyEndIndex - featureRange.headerLineIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
return finalizeTomlText(lines);
|
||||
}
|
||||
|
||||
if (!enabled) return normalizedText;
|
||||
|
||||
const topLevelEndIndex = getTopLevelEndIndex(lines);
|
||||
const sectionLines: string[] = [];
|
||||
if (topLevelEndIndex > 0 && lines[topLevelEndIndex - 1].trim() !== "") {
|
||||
sectionLines.push("");
|
||||
}
|
||||
sectionLines.push("[features]", "goals = true");
|
||||
if (
|
||||
topLevelEndIndex < lines.length &&
|
||||
lines[topLevelEndIndex]?.trim() !== ""
|
||||
) {
|
||||
sectionLines.push("");
|
||||
}
|
||||
|
||||
lines.splice(topLevelEndIndex, 0, ...sectionLines);
|
||||
return finalizeTomlText(lines);
|
||||
};
|
||||
|
||||
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
|
||||
export const extractCodexBaseUrl = (
|
||||
configText: string | undefined | null,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import CodexConfigEditor from "@/components/providers/forms/CodexConfigEditor";
|
||||
import GeminiConfigEditor from "@/components/providers/forms/GeminiConfigEditor";
|
||||
import { isCodexGoalModeEnabled } from "@/utils/providerConfigUtils";
|
||||
|
||||
vi.mock("@/components/common/FullScreenPanel", () => ({
|
||||
FullScreenPanel: ({
|
||||
@@ -68,7 +69,9 @@ describe("Common config modals", () => {
|
||||
expect(screen.queryByTestId("common-config-panel")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: /codexConfig.editCommonConfig|编辑通用配置/ }),
|
||||
screen.getByRole("button", {
|
||||
name: /codexConfig.editCommonConfig|编辑通用配置/,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("common-config-panel")).toBeInTheDocument();
|
||||
@@ -82,6 +85,52 @@ describe("Common config modals", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("toggles Codex Goal mode in config.toml from the provider editor", () => {
|
||||
const onConfigChange = vi.fn();
|
||||
const configValue = [
|
||||
'model_provider = "custom"',
|
||||
'model = "gpt-5.4"',
|
||||
"",
|
||||
"[model_providers.custom]",
|
||||
'name = "custom"',
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
render(
|
||||
<CodexConfigEditor
|
||||
authValue="{}"
|
||||
configValue={configValue}
|
||||
onAuthChange={() => {}}
|
||||
onConfigChange={onConfigChange}
|
||||
useCommonConfig={false}
|
||||
onCommonConfigToggle={() => {}}
|
||||
commonConfigSnippet=""
|
||||
onCommonConfigSnippetChange={() => true}
|
||||
onCommonConfigErrorClear={() => {}}
|
||||
commonConfigError=""
|
||||
authError=""
|
||||
configError=""
|
||||
/>,
|
||||
);
|
||||
|
||||
const goalToggle = screen.getByRole("checkbox", {
|
||||
name: "codexConfig.enableGoalMode",
|
||||
});
|
||||
|
||||
expect(goalToggle).not.toBeChecked();
|
||||
|
||||
fireEvent.click(goalToggle);
|
||||
|
||||
const enabledConfig = onConfigChange.mock.lastCall?.[0] ?? "";
|
||||
expect(isCodexGoalModeEnabled(enabledConfig)).toBe(true);
|
||||
|
||||
fireEvent.click(goalToggle);
|
||||
|
||||
const disabledConfig = onConfigChange.mock.lastCall?.[0] ?? "";
|
||||
expect(isCodexGoalModeEnabled(disabledConfig)).toBe(false);
|
||||
expect(disabledConfig).not.toContain("goals = true");
|
||||
});
|
||||
|
||||
it("keeps the Gemini common config modal closed after user closes it with an error present", async () => {
|
||||
render(
|
||||
<GeminiConfigEditor
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
import { getCodexCustomTemplate } from "@/config/codexTemplates";
|
||||
|
||||
describe("Codex custom templates", () => {
|
||||
it("does not force Codex Goal mode in the custom provider template", () => {
|
||||
const template = getCodexCustomTemplate();
|
||||
const parsed = parseToml(template.config) as {
|
||||
features?: { goals?: boolean };
|
||||
model_providers?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
expect(template.auth).toEqual({ OPENAI_API_KEY: "" });
|
||||
expect(parsed.features?.goals).toBeUndefined();
|
||||
expect(parsed.model_providers?.custom).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -24,26 +24,25 @@ describe("TheRouter provider presets", () => {
|
||||
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe(
|
||||
"anthropic/claude-sonnet-4.6",
|
||||
);
|
||||
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe(
|
||||
"anthropic/claude-opus-4.7",
|
||||
);
|
||||
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe("anthropic/claude-opus-4.7");
|
||||
});
|
||||
|
||||
it("uses the OpenAI-compatible v1 endpoint for Codex", () => {
|
||||
const preset = codexProviderPresets.find((item) => item.name === "TheRouter");
|
||||
const preset = codexProviderPresets.find(
|
||||
(item) => item.name === "TheRouter",
|
||||
);
|
||||
|
||||
expect(preset).toBeDefined();
|
||||
expect(preset?.websiteUrl).toBe("https://therouter.ai");
|
||||
expect(preset?.apiKeyUrl).toBe("https://dashboard.therouter.ai");
|
||||
expect(preset?.category).toBe("aggregator");
|
||||
expect(preset?.endpointCandidates).toEqual([
|
||||
"https://api.therouter.ai/v1",
|
||||
]);
|
||||
expect(preset?.endpointCandidates).toEqual(["https://api.therouter.ai/v1"]);
|
||||
expect(preset?.auth).toEqual({ OPENAI_API_KEY: "" });
|
||||
expect(preset?.config).toContain('model_provider = "custom"');
|
||||
expect(preset?.config).toContain("[model_providers.custom]");
|
||||
expect(preset?.config).toContain('name = "therouter"');
|
||||
expect(preset?.config).toContain('model = "openai/gpt-5.3-codex"');
|
||||
expect(preset?.config).not.toContain("goals = true");
|
||||
expect(preset?.config).toContain(
|
||||
'base_url = "https://api.therouter.ai/v1"',
|
||||
);
|
||||
@@ -51,7 +50,9 @@ describe("TheRouter provider presets", () => {
|
||||
});
|
||||
|
||||
it("uses the Gemini-native root endpoint for Gemini", () => {
|
||||
const preset = geminiProviderPresets.find((item) => item.name === "TheRouter");
|
||||
const preset = geminiProviderPresets.find(
|
||||
(item) => item.name === "TheRouter",
|
||||
);
|
||||
|
||||
expect(preset).toBeDefined();
|
||||
expect(preset?.websiteUrl).toBe("https://therouter.ai");
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
extractCodexExperimentalBearerToken,
|
||||
extractCodexModelName,
|
||||
extractCodexTopLevelInt,
|
||||
isCodexGoalModeEnabled,
|
||||
removeCodexTopLevelField,
|
||||
setCodexBaseUrl,
|
||||
setCodexGoalMode,
|
||||
setCodexModelName,
|
||||
setCodexTopLevelInt,
|
||||
updateCodexExperimentalBearerToken,
|
||||
@@ -195,6 +197,83 @@ describe("Codex TOML utils", () => {
|
||||
expect(removed).toContain("[model_providers.custom]");
|
||||
});
|
||||
|
||||
it("adds Goal mode under the top-level features table", () => {
|
||||
const input = [
|
||||
'model_provider = "custom"',
|
||||
'model = "gpt-5.4"',
|
||||
"",
|
||||
"[model_providers.custom]",
|
||||
'name = "custom"',
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const output = setCodexGoalMode(input, true);
|
||||
|
||||
expect(isCodexGoalModeEnabled(output)).toBe(true);
|
||||
expect(output).toContain(
|
||||
'model = "gpt-5.4"\n\n[features]\ngoals = true\n\n[model_providers.custom]',
|
||||
);
|
||||
});
|
||||
|
||||
it("removes Goal mode without deleting other feature flags", () => {
|
||||
const input = [
|
||||
'model_provider = "custom"',
|
||||
"",
|
||||
"[features]",
|
||||
"goals = true",
|
||||
"experimental_resume = true",
|
||||
"",
|
||||
"[model_providers.custom]",
|
||||
'name = "custom"',
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const output = setCodexGoalMode(input, false);
|
||||
|
||||
expect(isCodexGoalModeEnabled(output)).toBe(false);
|
||||
expect(output).toContain("[features]\nexperimental_resume = true");
|
||||
expect(output).not.toMatch(/^\s*goals\s*=/m);
|
||||
});
|
||||
|
||||
it("removes the features table when disabling the only Goal mode flag", () => {
|
||||
const input = [
|
||||
'model_provider = "custom"',
|
||||
"",
|
||||
"[features]",
|
||||
"goals = true",
|
||||
"",
|
||||
"[model_providers.custom]",
|
||||
'name = "custom"',
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const output = setCodexGoalMode(input, false);
|
||||
|
||||
expect(isCodexGoalModeEnabled(output)).toBe(false);
|
||||
expect(output).not.toContain("[features]");
|
||||
expect(output).toContain("[model_providers.custom]");
|
||||
});
|
||||
|
||||
it("preserves feature-section comments when disabling Goal mode", () => {
|
||||
const input = [
|
||||
'model_provider = "custom"',
|
||||
"",
|
||||
"[features]",
|
||||
"# Keep this note",
|
||||
"goals = true",
|
||||
"",
|
||||
"[model_providers.custom]",
|
||||
'name = "custom"',
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const output = setCodexGoalMode(input, false);
|
||||
|
||||
expect(isCodexGoalModeEnabled(output)).toBe(false);
|
||||
expect(output).toContain("[features]\n# Keep this note");
|
||||
expect(output).not.toMatch(/^\s*goals\s*=/m);
|
||||
});
|
||||
|
||||
// P3 回归: 不能在 config 没用 bearer token 模式时, 误为它新增一行
|
||||
it("updateCodexExperimentalBearerToken leaves config without the token alone", () => {
|
||||
const input = [
|
||||
@@ -251,10 +330,7 @@ describe("Codex TOML utils", () => {
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const updated = updateCodexExperimentalBearerToken(
|
||||
input,
|
||||
'abc"def\\ghi',
|
||||
);
|
||||
const updated = updateCodexExperimentalBearerToken(input, 'abc"def\\ghi');
|
||||
|
||||
expect(updated).toContain(
|
||||
'experimental_bearer_token = "abc\\"def\\\\ghi" # vendor token',
|
||||
|
||||
Reference in New Issue
Block a user