mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
feat(codex): add remote compaction toggle for third-party providers
Write model_providers name as "OpenAI" to let Codex attempt remote compaction through compatible endpoints. Hidden for official providers.
This commit is contained in:
@@ -7,6 +7,10 @@ interface CodexConfigEditorProps {
|
||||
|
||||
configValue: string;
|
||||
|
||||
providerName?: string;
|
||||
|
||||
showRemoteCompaction?: boolean;
|
||||
|
||||
onAuthChange: (value: string) => void;
|
||||
|
||||
onConfigChange: (value: string) => void;
|
||||
@@ -37,6 +41,8 @@ interface CodexConfigEditorProps {
|
||||
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
authValue,
|
||||
configValue,
|
||||
providerName,
|
||||
showRemoteCompaction,
|
||||
onAuthChange,
|
||||
onConfigChange,
|
||||
onAuthBlur,
|
||||
@@ -72,6 +78,8 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
<CodexConfigSection
|
||||
value={configValue}
|
||||
onChange={onConfigChange}
|
||||
providerName={providerName}
|
||||
showRemoteCompaction={showRemoteCompaction}
|
||||
useCommonConfig={useCommonConfig}
|
||||
onCommonConfigToggle={onCommonConfigToggle}
|
||||
onEditCommonConfig={() => setIsCommonConfigModalOpen(true)}
|
||||
|
||||
@@ -12,7 +12,9 @@ import { useTranslation } from "react-i18next";
|
||||
import JsonEditor from "@/components/JsonEditor";
|
||||
import {
|
||||
isCodexGoalModeEnabled,
|
||||
isCodexRemoteCompactionEnabled,
|
||||
setCodexGoalMode,
|
||||
setCodexRemoteCompaction,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
/*
|
||||
import {
|
||||
@@ -98,6 +100,8 @@ export const CodexAuthSection: React.FC<CodexAuthSectionProps> = ({
|
||||
interface CodexConfigSectionProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
providerName?: string;
|
||||
showRemoteCompaction?: boolean;
|
||||
useCommonConfig: boolean;
|
||||
onCommonConfigToggle: (checked: boolean) => void;
|
||||
onEditCommonConfig: () => void;
|
||||
@@ -111,6 +115,8 @@ interface CodexConfigSectionProps {
|
||||
export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
providerName,
|
||||
showRemoteCompaction = true,
|
||||
useCommonConfig,
|
||||
onCommonConfigToggle,
|
||||
onEditCommonConfig,
|
||||
@@ -157,6 +163,10 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
() => isCodexGoalModeEnabled(localValue),
|
||||
[localValue],
|
||||
);
|
||||
const remoteCompactionEnabled = useMemo(
|
||||
() => isCodexRemoteCompactionEnabled(localValue),
|
||||
[localValue],
|
||||
);
|
||||
|
||||
const handleGoalModeToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
@@ -165,6 +175,19 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
[handleLocalChange],
|
||||
);
|
||||
|
||||
const handleRemoteCompactionToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
handleLocalChange(
|
||||
setCodexRemoteCompaction(
|
||||
localValueRef.current || "",
|
||||
checked,
|
||||
providerName,
|
||||
),
|
||||
);
|
||||
},
|
||||
[handleLocalChange, providerName],
|
||||
);
|
||||
|
||||
// Codex 1M 上下文相关状态/回调暂时禁用——见同文件下方 JSX 注释处的恢复说明。
|
||||
/*
|
||||
// Parse toggle states from TOML text
|
||||
@@ -257,6 +280,21 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||
{t("codexConfig.enableGoalMode")}
|
||||
</label>
|
||||
|
||||
{showRemoteCompaction && (
|
||||
<label
|
||||
className="inline-flex cursor-pointer items-center gap-2 text-sm text-muted-foreground"
|
||||
title={t("codexConfig.remoteCompactionHint")}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={remoteCompactionEnabled}
|
||||
onChange={(e) => handleRemoteCompactionToggle(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.enableRemoteCompaction")}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -2163,6 +2163,8 @@ function ProviderFormFull({
|
||||
<CodexConfigEditor
|
||||
authValue={codexAuth}
|
||||
configValue={codexConfig}
|
||||
providerName={form.watch("name")}
|
||||
showRemoteCompaction={category !== "official"}
|
||||
onAuthChange={setCodexAuth}
|
||||
onConfigChange={handleCodexConfigChange}
|
||||
useCommonConfig={useCodexCommonConfigFlag}
|
||||
|
||||
@@ -1103,6 +1103,8 @@
|
||||
"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",
|
||||
"enableRemoteCompaction": "Enable remote compaction",
|
||||
"remoteCompactionHint": "When enabled, the active model_providers entry name is written as OpenAI so Codex can try remote compaction.",
|
||||
"apiUrlLabel": "API Request URL",
|
||||
"extractFromCurrent": "Extract from Editor",
|
||||
"extractNoCommonConfig": "No common config available to extract from editor",
|
||||
|
||||
@@ -1103,6 +1103,8 @@
|
||||
"editCommonConfigTitle": "Codex 共通設定スニペットを編集",
|
||||
"commonConfigHint": "「共通設定を書き込む」がオンの場合、config.toml の末尾に追記されます",
|
||||
"enableGoalMode": "Goal mode を有効化",
|
||||
"enableRemoteCompaction": "リモート圧縮を有効化",
|
||||
"remoteCompactionHint": "有効にすると、現在の model_providers エントリの name を OpenAI に設定し、Codex がリモート圧縮を試行できるようにします。",
|
||||
"apiUrlLabel": "API リクエスト URL",
|
||||
"extractFromCurrent": "編集内容から抽出",
|
||||
"extractNoCommonConfig": "編集内容から抽出できる共通設定がありません",
|
||||
|
||||
@@ -1075,6 +1075,8 @@
|
||||
"editCommonConfigTitle": "編輯 Codex 通用設定片段",
|
||||
"commonConfigHint": "該片段會在勾選「寫入通用設定」時附加到 config.toml 末尾",
|
||||
"enableGoalMode": "啟用 Goal mode",
|
||||
"enableRemoteCompaction": "啟用遠端壓縮",
|
||||
"remoteCompactionHint": "開啟後會將目前 model_providers 條目的 name 寫為 OpenAI,讓 Codex 嘗試使用遠端壓縮。",
|
||||
"apiUrlLabel": "API 請求位址",
|
||||
"extractFromCurrent": "從編輯內容擷取",
|
||||
"extractNoCommonConfig": "目前編輯內容沒有可擷取的通用設定",
|
||||
|
||||
@@ -1103,6 +1103,8 @@
|
||||
"editCommonConfigTitle": "编辑 Codex 通用配置片段",
|
||||
"commonConfigHint": "该片段会在勾选'写入通用配置'时追加到 config.toml 末尾",
|
||||
"enableGoalMode": "启用 Goal mode",
|
||||
"enableRemoteCompaction": "启用远程压缩",
|
||||
"remoteCompactionHint": "开启后会将当前 model_providers 条目的 name 写为 OpenAI,使 Codex 尝试使用远程压缩。",
|
||||
"apiUrlLabel": "API 请求地址",
|
||||
"extractFromCurrent": "从编辑内容提取",
|
||||
"extractNoCommonConfig": "当前编辑内容没有可提取的通用配置",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isCodexRemoteCompactionEnabled,
|
||||
setCodexRemoteCompaction,
|
||||
} from "./providerConfigUtils";
|
||||
|
||||
describe("Codex remote compaction config helpers", () => {
|
||||
it("enables remote compaction by naming the active custom provider OpenAI", () => {
|
||||
const input = `model_provider = "custom"
|
||||
model = "gpt-5.4"
|
||||
|
||||
[model_providers.custom]
|
||||
name = "AIHubMix"
|
||||
base_url = "https://aihubmix.example/v1"
|
||||
wire_api = "responses"
|
||||
|
||||
[model_providers.backup]
|
||||
name = "Backup"
|
||||
base_url = "https://backup.example/v1"
|
||||
`;
|
||||
|
||||
const result = setCodexRemoteCompaction(input, true, "AIHubMix");
|
||||
|
||||
expect(isCodexRemoteCompactionEnabled(result)).toBe(true);
|
||||
expect(result).toContain(`[model_providers.custom]\nname = "OpenAI"`);
|
||||
expect(result).toContain(`[model_providers.backup]\nname = "Backup"`);
|
||||
});
|
||||
|
||||
it("disables remote compaction by restoring the provider display name", () => {
|
||||
const input = `model_provider = "custom"
|
||||
|
||||
[model_providers.custom]
|
||||
name = "OpenAI"
|
||||
base_url = "https://aihubmix.example/v1"
|
||||
wire_api = "responses"
|
||||
`;
|
||||
|
||||
const result = setCodexRemoteCompaction(input, false, "AIHubMix");
|
||||
|
||||
expect(isCodexRemoteCompactionEnabled(result)).toBe(false);
|
||||
expect(result).toContain(`name = "AIHubMix"`);
|
||||
});
|
||||
|
||||
it("does not rewrite reserved built-in providers", () => {
|
||||
const input = `model_provider = "openai"
|
||||
model = "gpt-5"
|
||||
`;
|
||||
|
||||
expect(setCodexRemoteCompaction(input, true, "OpenAI")).toBe(input);
|
||||
expect(isCodexRemoteCompactionEnabled(input)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -410,6 +410,10 @@ 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_PROVIDER_NAME_PATTERN =
|
||||
/^\s*name\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
|
||||
const TOML_PROVIDER_NAME_REPLACE_PATTERN =
|
||||
/^(\s*name\s*=\s*)(?:"(?:\\.|[^"\\\r\n])*"|'[^'\r\n]*')(\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*(?:#.*)?)$/;
|
||||
@@ -670,6 +674,9 @@ const escapeTomlBasicString = (value: string): string =>
|
||||
return `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
||||
});
|
||||
|
||||
const tomlBasicString = (value: string): string =>
|
||||
`"${escapeTomlBasicString(value)}"`;
|
||||
|
||||
const CODEX_CHAT_WIRE_API_VALUES = new Set([
|
||||
"chat",
|
||||
"chat_completions",
|
||||
@@ -908,6 +915,106 @@ export const setCodexGoalMode = (
|
||||
return finalizeTomlText(lines);
|
||||
};
|
||||
|
||||
export const isCodexRemoteCompactionEnabled = (
|
||||
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>;
|
||||
const providerId =
|
||||
typeof parsed.model_provider === "string"
|
||||
? parsed.model_provider.trim()
|
||||
: "";
|
||||
if (!providerId || !isCustomCodexModelProviderId(providerId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parsed.model_providers?.[providerId]?.name === "OpenAI";
|
||||
} catch {
|
||||
// Fall back to line scanning while the user is editing invalid TOML.
|
||||
}
|
||||
|
||||
const lines = text.split("\n");
|
||||
const targetSectionName = getCodexCustomProviderSectionName(text);
|
||||
if (!targetSectionName) return false;
|
||||
|
||||
const sectionRange = getTomlSectionRange(lines, targetSectionName);
|
||||
if (!sectionRange) return false;
|
||||
|
||||
const nameAssignment = findTomlAssignmentInRange(
|
||||
lines,
|
||||
TOML_PROVIDER_NAME_PATTERN,
|
||||
sectionRange.bodyStartIndex,
|
||||
sectionRange.bodyEndIndex,
|
||||
targetSectionName,
|
||||
);
|
||||
|
||||
return nameAssignment?.value === "OpenAI";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const setCodexRemoteCompaction = (
|
||||
configText: string,
|
||||
enabled: boolean,
|
||||
fallbackProviderName?: string,
|
||||
): string => {
|
||||
const normalizedText = normalizeTomlText(configText);
|
||||
const lines = normalizedText ? normalizedText.split("\n") : [];
|
||||
const targetSectionName = getCodexCustomProviderSectionName(normalizedText);
|
||||
|
||||
if (!targetSectionName) {
|
||||
return normalizedText;
|
||||
}
|
||||
|
||||
let targetSectionRange = getTomlSectionRange(lines, targetSectionName);
|
||||
const replacementName = enabled
|
||||
? "OpenAI"
|
||||
: fallbackProviderName?.trim() ||
|
||||
getCodexModelProviderName(normalizedText) ||
|
||||
"custom";
|
||||
const replacementLine = `name = ${tomlBasicString(replacementName)}`;
|
||||
|
||||
if (targetSectionRange) {
|
||||
const nameLine = findTomlLineInRange(
|
||||
lines,
|
||||
TOML_PROVIDER_NAME_REPLACE_PATTERN,
|
||||
targetSectionRange.bodyStartIndex,
|
||||
targetSectionRange.bodyEndIndex,
|
||||
);
|
||||
|
||||
if (nameLine !== -1) {
|
||||
lines[nameLine] = lines[nameLine].replace(
|
||||
TOML_PROVIDER_NAME_REPLACE_PATTERN,
|
||||
`$1${tomlBasicString(replacementName)}$2`,
|
||||
);
|
||||
return finalizeTomlText(lines);
|
||||
}
|
||||
|
||||
lines.splice(
|
||||
getTomlSectionInsertIndex(lines, targetSectionRange),
|
||||
0,
|
||||
replacementLine,
|
||||
);
|
||||
return finalizeTomlText(lines);
|
||||
}
|
||||
|
||||
if (!enabled) return normalizedText;
|
||||
|
||||
if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push(`[${targetSectionName}]`, replacementLine);
|
||||
targetSectionRange = getTomlSectionRange(lines, targetSectionName);
|
||||
return targetSectionRange ? finalizeTomlText(lines) : normalizedText;
|
||||
};
|
||||
|
||||
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
|
||||
export const extractCodexBaseUrl = (
|
||||
configText: string | undefined | null,
|
||||
|
||||
Reference in New Issue
Block a user