feat: unify Codex third-party providers into stable "custom" history bucket

Codex filters resume history by `model_provider`, so switching between
provider-specific ids like `rightcode` and `aihubmix` made past sessions
appear to vanish. Collapse all third-party providers into a single
stable bucket so cross-switch history stays visible.

- Normalize live `model_provider` to "custom" on every Codex write
  (reserved built-in ids like openai/ollama are preserved).
- Add device-level one-shot migration that rewrites historical JSONL
  session files and the `state_5.sqlite` threads table from legacy
  provider ids into the "custom" bucket. Backs up originals under
  `~/.cc-switch/backups/codex-history-provider-migration-v1/` and uses
  the SQLite Backup API for the state DB.
- Record completion in `settings.json` under `localMigrations` so the
  migration is strictly idempotent across launches.
- Update Codex provider preset templates to emit `model_provider = "custom"`
  out of the box.
This commit is contained in:
Jason
2026-05-20 17:10:38 +08:00
parent 2a4651a21e
commit b44f83f7c5
11 changed files with 771 additions and 121 deletions
+12 -17
View File
@@ -46,21 +46,16 @@ export function generateThirdPartyConfig(
baseUrl: string,
modelName = "gpt-5.4",
): string {
// 清理供应商名称,确保符合TOML键名规范
const cleanProviderName =
providerName
.toLowerCase()
.replace(/[^a-z0-9_]/g, "_")
.replace(/^_+|_+$/g, "") || "custom";
const tomlString = (value: string) => JSON.stringify(value);
return `model_provider = "${cleanProviderName}"
model = "${modelName}"
return `model_provider = "custom"
model = ${tomlString(modelName)}
model_reasoning_effort = "high"
disable_response_storage = true
[model_providers.${cleanProviderName}]
name = "${cleanProviderName}"
base_url = "${baseUrl}"
[model_providers.custom]
name = ${tomlString(providerName)}
base_url = ${tomlString(baseUrl)}
wire_api = "responses"
requires_openai_auth = true`;
}
@@ -120,12 +115,12 @@ export const codexProviderPresets: CodexProviderPreset[] = [
category: "third_party",
isOfficial: true,
auth: generateThirdPartyAuth(""),
config: `model_provider = "azure"
config: `model_provider = "custom"
model = "gpt-5.4"
model_reasoning_effort = "high"
disable_response_storage = true
[model_providers.azure]
[model_providers.custom]
name = "Azure OpenAI"
base_url = "https://YOUR_RESOURCE_NAME.openai.azure.com/openai"
env_key = "OPENAI_API_KEY"
@@ -438,14 +433,14 @@ requires_openai_auth = true`,
auth: {
OPENAI_API_KEY: "",
},
config: `model_provider = "e-flowcode"
config: `model_provider = "custom"
model = "gpt-5.4"
model_reasoning_effort = "high"
disable_response_storage = true
personality = "pragmatic"
[model_providers.e-flowcode]
name = "e-flowcode"
[model_providers.custom]
name = "E-FlowCode"
base_url = "https://e-flowcode.cc/v1"
wire_api = "responses"
requires_openai_auth = true
@@ -485,7 +480,7 @@ model_reasoning_effort = "medium"
disable_response_storage = true
[model_providers.custom]
name = "custom"
name = "PIPELLM"
wire_api = "responses"
requires_openai_auth = true
base_url = "https://cc-api.pipellm.ai/v1"`,
+15
View File
@@ -56,6 +56,21 @@ export const settingsSchema = z.object({
.optional(),
})
.optional(),
// 本机自动迁移状态(后端维护,前端保存设置时应透传)
localMigrations: z
.object({
codexThirdPartyHistoryProviderBucketV1: z
.object({
completedAt: z.string(),
targetProviderId: z.string(),
sourceProviderIds: z.array(z.string()).optional(),
migratedJsonlFiles: z.number().optional(),
migratedStateRows: z.number().optional(),
})
.optional(),
})
.optional(),
});
export type SettingsFormData = z.infer<typeof settingsSchema>;
+11
View File
@@ -354,6 +354,17 @@ export interface Settings {
// Windows: "cmd" | "powershell" | "wt"
// Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty"
preferredTerminal?: string;
// ===== 本机自动迁移状态 =====
localMigrations?: {
codexThirdPartyHistoryProviderBucketV1?: {
completedAt: string;
targetProviderId: string;
sourceProviderIds?: string[];
migratedJsonlFiles?: number;
migratedStateRows?: number;
};
};
}
export interface SessionMeta {