@minniexcode/codex-switch 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,13 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getCurrentProfile = getCurrentProfile;
4
4
  const config_repo_1 = require("../storage/config-repo");
5
+ const providers_repo_1 = require("../storage/providers-repo");
5
6
  /**
6
- * Returns the currently active top-level Codex profile.
7
+ * Returns the currently active top-level Codex route.
7
8
  */
8
- function getCurrentProfile(configPath) {
9
+ function getCurrentProfile(configPath, providersPath) {
10
+ const document = (0, config_repo_1.readStructuredConfig)(configPath);
11
+ const providers = providersPath ? (0, providers_repo_1.readProvidersFileIfExists)(providersPath) : null;
12
+ const providerCandidates = document.currentModelProvider && providers
13
+ ? Object.entries(providers.providers)
14
+ .filter(([, provider]) => provider.profile === document.currentModelProvider)
15
+ .map(([name]) => name)
16
+ .sort()
17
+ : [];
9
18
  return {
10
19
  data: {
11
- profile: (0, config_repo_1.readCurrentProfile)(configPath),
20
+ model: document.currentModel,
21
+ modelProvider: document.currentModelProvider,
22
+ provider: providerCandidates.length === 1 ? providerCandidates[0] : null,
23
+ profile: document.currentModelProvider,
12
24
  },
13
25
  };
14
26
  }
@@ -51,7 +51,8 @@ const runtime_state_repo_1 = require("../storage/runtime-state-repo");
51
51
  async function getStatus(codexDir, configPath, providersPath, authPath, options) {
52
52
  const configExists = fs.existsSync(configPath);
53
53
  const providersExists = fs.existsSync(providersPath);
54
- let currentProfile = null;
54
+ let currentModelProvider = null;
55
+ let currentModel = null;
55
56
  const warnings = [];
56
57
  const providers = providersExists ? (0, providers_repo_1.readProvidersFile)(providersPath) : null;
57
58
  let configViews = [];
@@ -59,14 +60,15 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
59
60
  const authState = (0, auth_repo_1.readAuthFileState)(authPath);
60
61
  if (configExists) {
61
62
  const document = (0, config_repo_1.readStructuredConfig)(configPath);
62
- currentProfile = document.activeProfile;
63
+ currentModel = document.currentModel;
64
+ currentModelProvider = document.currentModelProvider;
63
65
  configViews = (0, config_1.buildManagedProfileViews)(document, providers);
64
66
  consistencyIssues = (0, config_1.collectConfigConsistencyIssues)(document, providers);
65
- if (!currentProfile) {
66
- warnings.push("config.toml exists but has no top-level profile.");
67
+ if (!currentModelProvider) {
68
+ warnings.push("config.toml exists but has no top-level model_provider.");
67
69
  }
68
70
  }
69
- const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
71
+ const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentModelProvider, providers);
70
72
  const activeProviderCandidates = liveState.mappedProviders;
71
73
  const activeProvider = liveState.providerResolvable && providers && liveState.mappedProvider
72
74
  ? providers.providers[liveState.mappedProvider]
@@ -111,7 +113,7 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
111
113
  warnings.push("Current config profile is not mapped in providers.json. Backfill would be required before treating live state as managed.");
112
114
  }
113
115
  if (liveState.reason === "shared-profile") {
114
- warnings.push(`Current config profile "${currentProfile}" is shared by multiple providers in providers.json, so the active provider cannot be resolved uniquely.`);
116
+ warnings.push(`Current model provider "${currentModelProvider}" is shared by multiple providers in providers.json, so the active provider cannot be resolved uniquely.`);
115
117
  }
116
118
  if (runtimeStateInspection.exists && !runtimeStateInspection.valid) {
117
119
  warnings.push(`Copilot bridge runtime state is unreadable: ${runtimeStateInspection.parseError ?? "unknown parse failure"}`);
@@ -130,8 +132,9 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
130
132
  }),
131
133
  configExists,
132
134
  providersExists,
133
- currentProfile,
134
- currentProfileMapped: liveState.profileMapped,
135
+ currentModelProvider,
136
+ currentModelProviderMapped: liveState.modelProviderMapped,
137
+ currentModel,
135
138
  provider: liveState.mappedProvider,
136
139
  activeProviderResolvable: liveState.providerResolvable,
137
140
  activeProviderCandidates,
@@ -22,7 +22,9 @@ function listConfigProfilesView(args) {
22
22
  }));
23
23
  return {
24
24
  data: {
25
- activeProfile: document.activeProfile,
25
+ currentModel: document.currentModel,
26
+ currentModelProvider: document.currentModelProvider,
27
+ legacyProfile: document.legacyProfile,
26
28
  profiles,
27
29
  count: profiles.length,
28
30
  },
@@ -45,13 +45,18 @@ const providers_repo_1 = require("../storage/providers-repo");
45
45
  function listProviders(providersPath, configPath) {
46
46
  const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
47
47
  const names = Object.keys(providers.providers).sort();
48
- const currentProfile = configPath && fs.existsSync(configPath)
49
- ? (0, config_repo_1.readStructuredConfig)(configPath).activeProfile
48
+ const currentModelProvider = configPath && fs.existsSync(configPath)
49
+ ? (0, config_repo_1.readStructuredConfig)(configPath).currentModelProvider
50
50
  : null;
51
- const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
51
+ const currentModel = configPath && fs.existsSync(configPath)
52
+ ? (0, config_repo_1.readStructuredConfig)(configPath).currentModel
53
+ : null;
54
+ const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentModelProvider, providers);
52
55
  const items = names.map((name) => ({
53
56
  name,
54
57
  profile: providers.providers[name].profile,
58
+ modelProvider: providers.providers[name].profile,
59
+ model: providers.providers[name].model ?? null,
55
60
  providerType: (0, providers_1.isCopilotBridgeProvider)(providers.providers[name]) ? "copilot" : "direct",
56
61
  isActive: liveState.providerResolvable && liveState.mappedProvider === name,
57
62
  note: providers.providers[name].note ?? null,
@@ -61,7 +66,8 @@ function listProviders(providersPath, configPath) {
61
66
  data: {
62
67
  providers: items,
63
68
  count: items.length,
64
- currentProfile,
69
+ currentModel,
70
+ currentModelProvider,
65
71
  activeProvider: liveState.mappedProvider,
66
72
  activeProviderResolvable: liveState.providerResolvable,
67
73
  activeProviderCandidates: liveState.mappedProviders,
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.removeProvider = removeProvider;
4
4
  const errors_1 = require("../domain/errors");
5
- const config_1 = require("../domain/config");
6
5
  const config_repo_1 = require("../storage/config-repo");
7
6
  const providers_repo_1 = require("../storage/providers-repo");
7
+ const providers_1 = require("../domain/providers");
8
8
  const run_mutation_1 = require("./run-mutation");
9
9
  /**
10
10
  * Removes a provider from the managed providers registry.
@@ -17,22 +17,48 @@ function removeProvider(args) {
17
17
  throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", `Provider "${args.providerName}" was not found.`);
18
18
  }
19
19
  const nextProviders = { ...providers.providers };
20
- // Delete against a copied object so the original parsed state stays untouched.
21
20
  delete nextProviders[args.providerName];
22
- const remainingLinksByProfile = new Map();
23
- for (const [name, provider] of Object.entries(nextProviders)) {
24
- const list = remainingLinksByProfile.get(provider.profile) ?? [];
25
- list.push(name);
26
- remainingLinksByProfile.set(provider.profile, list);
21
+ const activeModelProvider = document.currentModelProvider;
22
+ const linkedProviders = Object.entries(providers.providers)
23
+ .filter(([, provider]) => provider.profile === activeModelProvider)
24
+ .map(([name]) => name)
25
+ .sort();
26
+ const removingActiveProvider = activeModelProvider === current.profile && linkedProviders.length === 1;
27
+ const switchTargetName = args.switchToProvider ?? null;
28
+ const switchTarget = switchTargetName ? nextProviders[switchTargetName] ?? null : null;
29
+ if (removingActiveProvider && !switchTargetName) {
30
+ throw (0, errors_1.cliError)("PROFILE_IN_USE", `Provider "${args.providerName}" is the active route and requires --switch-to <provider-name>.`, {
31
+ provider: args.providerName,
32
+ activeModelProvider,
33
+ linkedProviders,
34
+ });
35
+ }
36
+ if (switchTargetName && !switchTarget) {
37
+ throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", `Provider "${switchTargetName}" was not found.`, {
38
+ provider: switchTargetName,
39
+ availableProviders: Object.keys(nextProviders).sort(),
40
+ });
41
+ }
42
+ const switchTargetModel = switchTarget?.model ?? document.currentModel ?? null;
43
+ if (switchTargetName && !switchTargetModel) {
44
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Provider "${switchTargetName}" has no model to switch with.`, {
45
+ provider: switchTargetName,
46
+ suggestion: "Run `codexs edit <provider> --model <name>` first.",
47
+ });
48
+ }
49
+ const switchTargetProjection = switchTarget
50
+ ? (0, providers_1.isCopilotBridgeProvider)(switchTarget)
51
+ ? (0, providers_1.buildCopilotModelProviderProjection)(switchTarget.runtime)
52
+ : switchTarget.baseUrl
53
+ ? (0, providers_1.buildDirectModelProviderProjection)(switchTarget.profile, switchTarget.baseUrl)
54
+ : null
55
+ : null;
56
+ if (switchTargetName && !switchTargetProjection) {
57
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Provider "${switchTargetName}" requires base_url before it can become active.`, {
58
+ provider: switchTargetName,
59
+ suggestion: "Run `codexs edit <provider> --base-url <url>` first.",
60
+ });
27
61
  }
28
- const lifecycle = (0, config_1.planProfileLifecycleOutcome)({
29
- providerName: args.providerName,
30
- oldProfile: current.profile,
31
- newProfile: null,
32
- activeProfile: document.activeProfile,
33
- remainingLinksByProfile,
34
- switchToProfile: args.switchToProfile ?? null,
35
- });
36
62
  return (0, run_mutation_1.runMutation)({
37
63
  lockPath: args.lockPath,
38
64
  backupsDir: args.backupsDir,
@@ -44,17 +70,24 @@ function removeProvider(args) {
44
70
  ],
45
71
  mutate: () => {
46
72
  const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {
47
- deleteProfiles: lifecycle.deletedProfileSections,
48
- setActiveProfile: lifecycle.nextActiveProfile,
73
+ setCurrentModel: switchTarget ? switchTargetModel : undefined,
74
+ setCurrentModelProvider: switchTarget ? switchTarget.profile : undefined,
75
+ upsertModelProviders: switchTarget && switchTargetProjection
76
+ ? { [switchTarget.profile]: switchTargetProjection }
77
+ : undefined,
78
+ deleteLegacyProfile: Boolean(switchTarget),
79
+ deleteLegacyProfilesByName: switchTarget ? [switchTarget.profile] : [],
80
+ scrubModelProviderEnvKeys: switchTarget ? [switchTarget.profile] : [],
49
81
  });
50
82
  (0, providers_repo_1.writeProvidersFile)(args.providersPath, { providers: nextProviders });
51
83
  (0, config_repo_1.applyConfigMutation)(args.configPath, document, configPlan);
52
84
  return {
53
85
  provider: args.providerName,
86
+ switchedTo: switchTargetName,
54
87
  createdProfileSections: configPlan.createdProfileSections,
55
88
  deletedProfileSections: configPlan.deletedProfileSections,
56
- keptSharedProfiles: lifecycle.keptSharedProfiles,
57
- switchedActiveProfile: lifecycle.switchedActiveProfile,
89
+ keptSharedProfiles: [],
90
+ switchedActiveProfile: Boolean(switchTarget),
58
91
  adoptedProfiles: [],
59
92
  repairedProfiles: [],
60
93
  };
@@ -47,12 +47,13 @@ const copilot_installer_1 = require("../runtime/copilot-installer");
47
47
  const copilot_bridge_1 = require("../runtime/copilot-bridge");
48
48
  const copilot_adapter_1 = require("../runtime/copilot-adapter");
49
49
  const runtime_state_repo_1 = require("../storage/runtime-state-repo");
50
+ const codex_version_1 = require("../runtime/codex-version");
50
51
  /**
51
52
  * Performs consistency checks across config.toml, providers.json, and the local Codex CLI.
52
53
  */
53
54
  async function runDoctor(args) {
54
55
  const issues = [];
55
- let currentProfile = null;
56
+ let currentModelProvider = null;
56
57
  let providers = null;
57
58
  let document = null;
58
59
  if (!fs.existsSync(args.configPath)) {
@@ -64,11 +65,11 @@ async function runDoctor(args) {
64
65
  }
65
66
  else {
66
67
  document = (0, config_repo_1.readStructuredConfig)(args.configPath);
67
- currentProfile = document.activeProfile;
68
- if (!currentProfile) {
68
+ currentModelProvider = document.currentModelProvider;
69
+ if (!currentModelProvider) {
69
70
  issues.push({
70
- code: "PROFILE_NOT_FOUND",
71
- message: "config.toml has no top-level profile.",
71
+ code: "MODEL_PROVIDER_MISSING",
72
+ message: "config.toml has no top-level model_provider.",
72
73
  file: args.configPath,
73
74
  });
74
75
  }
@@ -118,8 +119,8 @@ async function runDoctor(args) {
118
119
  message: `Copilot bridge runtime state is unreadable: ${runtimeStateInspection.parseError ?? "unknown parse failure"}`,
119
120
  });
120
121
  }
121
- if (document?.activeProfile && providers) {
122
- const matches = (0, providers_1.findProvidersByProfile)(providers, document.activeProfile);
122
+ if (document?.currentModelProvider && providers) {
123
+ const matches = (0, providers_1.findProvidersByProfile)(providers, document.currentModelProvider);
123
124
  if (matches.length === 1) {
124
125
  const activeProvider = providers.providers[matches[0]];
125
126
  if ((0, providers_1.isCopilotBridgeProvider)(activeProvider)) {
@@ -163,11 +164,11 @@ async function runDoctor(args) {
163
164
  ...runtimeState,
164
165
  });
165
166
  }
166
- else if (!document?.activeProfile || runtimeProvider.profile !== document.activeProfile) {
167
+ else if (!document?.currentModelProvider || runtimeProvider.profile !== document.currentModelProvider) {
167
168
  issues.push({
168
169
  code: "BRIDGE_STATE_STALE",
169
- message: "Copilot bridge runtime state exists for a provider that is not the current active profile.",
170
- activeProfile: document?.activeProfile ?? null,
170
+ message: "Copilot bridge runtime state exists for a provider that is not the current active model_provider.",
171
+ activeModelProvider: document?.currentModelProvider ?? null,
171
172
  runtimeProvider: runtimeState.provider,
172
173
  runtimeProfile: runtimeProvider.profile,
173
174
  ...runtimeState,
@@ -175,8 +176,8 @@ async function runDoctor(args) {
175
176
  }
176
177
  }
177
178
  // Drift inspection still runs when files are missing so status output can explain partial state.
178
- const drift = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
179
- const codexCheck = (0, codex_probe_1.probeCodexRuntime)();
179
+ const drift = (0, runtime_state_1.inspectLiveStateDrift)(currentModelProvider, providers);
180
+ const codexCheck = (0, codex_probe_1.probeCodexRuntime)(codex_version_1.MIN_SUPPORTED_CODEX_VERSION);
180
181
  if (!codexCheck.ok) {
181
182
  const message = codexCheck.reason === "missing"
182
183
  ? "codex CLI is not available on PATH."
@@ -229,32 +230,28 @@ function mapBridgeDiagnosticCode(cause) {
229
230
  */
230
231
  function renderConfigIssueMessage(issue) {
231
232
  switch (issue.code) {
232
- case "ORPHANED_PROFILE_REFERENCE":
233
- return `Profile "${issue.profile}" is referenced by providers but missing from config.toml.`;
234
- case "UNMANAGED_ACTIVE_PROFILE":
235
- return `Active profile "${issue.profile}" is not mapped by providers.json.`;
236
- case "SHARED_PROFILE_REFERENCE":
237
- return `Profile "${issue.profile}" is shared by multiple providers.`;
238
- case "ORPHANED_PROFILE_SECTION":
239
- return `Profile section "${issue.profile}" is not linked to any provider.`;
233
+ case "MODEL_MISSING":
234
+ return "Top-level model is missing from config.toml.";
240
235
  case "MODEL_PROVIDER_MISSING":
241
- return `Profile "${issue.profile}" is missing model_provider.`;
242
- case "MODEL_PROVIDER_NAME_MISMATCH":
243
- return `Profile "${issue.profile}" must use matching model_provider name "${issue.profile}", found "${issue.modelProvider}".`;
236
+ return "Top-level model_provider is missing from config.toml.";
244
237
  case "MODEL_PROVIDER_SECTION_MISSING":
245
- return `Model provider section "${issue.modelProvider}" for profile "${issue.profile}" is missing from config.toml.`;
238
+ return `Model provider section "${issue.modelProvider}" is missing from config.toml.`;
246
239
  case "MODEL_PROVIDER_BASE_URL_MISSING":
247
- return `Model provider section "${issue.modelProvider}" for profile "${issue.profile}" is missing base_url.`;
240
+ return `Model provider section "${issue.modelProvider}" is missing base_url.`;
241
+ case "LEGACY_PROFILE_SELECTOR":
242
+ return `Legacy top-level profile selector "${issue.profile}" is still present.`;
243
+ case "LEGACY_PROFILE_SECTION":
244
+ return `Legacy profile section "${issue.profile}" is still present.`;
245
+ case "LEGACY_MODEL_PROVIDER_ENV_KEY":
246
+ return `Model provider "${issue.modelProvider}" still contains legacy env_key wiring.`;
248
247
  case "PROVIDER_BASE_URL_MISMATCH":
249
248
  return issue.providerType === "direct"
250
- ? `Direct provider "${issue.provider}" baseUrl does not match config.toml model provider "${issue.profile}" base_url.`
249
+ ? `Direct provider "${issue.provider}" baseUrl does not match config.toml model provider "${issue.modelProvider}" base_url.`
251
250
  : String(issue.code ?? "UNKNOWN_ISSUE");
252
- case "ACTIVE_PROVIDER_UNRESOLVED":
253
- return `Active profile "${issue.profile}" maps to multiple providers, so the active managed provider cannot be resolved uniquely.`;
254
251
  case "AUTH_JSON_INVALID":
255
252
  return String(issue.message ?? issue.reason ?? "auth.json is invalid.");
256
253
  case "DESTRUCTIVE_REMOVE_BLOCKED":
257
- return `Provider "${issue.provider}" cannot be removed while "${issue.activeProfile}" remains active.`;
254
+ return `Provider "${issue.provider}" cannot be removed while "${issue.activeModelProvider}" remains active.`;
258
255
  default:
259
256
  return String(issue.code ?? "UNKNOWN_ISSUE");
260
257
  }
@@ -42,9 +42,9 @@ const codex_cli_1 = require("../runtime/codex-cli");
42
42
  const config_repo_1 = require("../storage/config-repo");
43
43
  const fs_utils_1 = require("../storage/fs-utils");
44
44
  const providers_repo_1 = require("../storage/providers-repo");
45
+ const codex_version_1 = require("../runtime/codex-version");
45
46
  const run_doctor_1 = require("./run-doctor");
46
47
  const run_mutation_1 = require("./run-mutation");
47
- const MIN_CODEX_VERSION = "0.0.1";
48
48
  /**
49
49
  * Migrates unmanaged Codex config profiles into a managed providers.json registry.
50
50
  */
@@ -55,10 +55,10 @@ async function migrateCodex(args) {
55
55
  cause: available.cause,
56
56
  });
57
57
  }
58
- const version = (0, codex_cli_1.checkCodexVersion)(MIN_CODEX_VERSION);
58
+ const version = (0, codex_cli_1.checkCodexVersion)(codex_version_1.MIN_SUPPORTED_CODEX_VERSION);
59
59
  if (!version.ok) {
60
60
  throw (0, errors_1.cliError)("CODEX_VERSION_UNSUPPORTED", "codex CLI version is below the supported minimum.", {
61
- minimumVersion: MIN_CODEX_VERSION,
61
+ minimumVersion: codex_version_1.MIN_SUPPORTED_CODEX_VERSION,
62
62
  currentVersion: version.currentVersion ?? null,
63
63
  cause: version.cause,
64
64
  });
@@ -27,7 +27,9 @@ function showConfig(args) {
27
27
  }
28
28
  return {
29
29
  data: {
30
- activeProfile: document.activeProfile,
30
+ currentModel: document.currentModel,
31
+ currentModelProvider: document.currentModelProvider,
32
+ legacyProfile: document.legacyProfile,
31
33
  selectedProfile,
32
34
  profiles: profiles.map((profile) => ({
33
35
  ...profile,
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.switchProvider = switchProvider;
4
4
  const errors_1 = require("../domain/errors");
5
- const providers_1 = require("../domain/providers");
6
5
  const config_repo_1 = require("../storage/config-repo");
7
6
  const auth_repo_1 = require("../storage/auth-repo");
8
7
  const providers_repo_1 = require("../storage/providers-repo");
@@ -10,8 +9,9 @@ const copilot_bridge_1 = require("../runtime/copilot-bridge");
10
9
  const copilot_installer_1 = require("../runtime/copilot-installer");
11
10
  const copilot_adapter_1 = require("../runtime/copilot-adapter");
12
11
  const run_mutation_1 = require("./run-mutation");
12
+ const providers_1 = require("../domain/providers");
13
13
  /**
14
- * Switches the active Codex profile to the target provider.
14
+ * Switches the active Codex route to the target provider.
15
15
  */
16
16
  async function switchProvider(args) {
17
17
  const providers = (0, providers_repo_1.readProvidersFile)(args.providersPath);
@@ -21,7 +21,15 @@ async function switchProvider(args) {
21
21
  availableProviders: Object.keys(providers.providers).sort(),
22
22
  });
23
23
  }
24
- const document = (0, config_repo_1.ensureProfileExists)(args.configPath, provider.profile, args.providerName);
24
+ const document = (0, config_repo_1.readStructuredConfig)(args.configPath);
25
+ const resolvedModel = provider.model ?? document.currentModel;
26
+ if (!resolvedModel) {
27
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Provider "${args.providerName}" has no model to switch with.`, {
28
+ provider: args.providerName,
29
+ modelProvider: provider.profile,
30
+ suggestion: "Run `codexs edit <provider> --model <name>` or `codexs add <provider> --model <name>`.",
31
+ });
32
+ }
25
33
  if ((0, providers_1.isCopilotBridgeProvider)(provider)) {
26
34
  const installStatus = (0, copilot_installer_1.probeCopilotSdkInstall)(args.runtimesDir);
27
35
  if (!installStatus.installed) {
@@ -55,10 +63,14 @@ async function switchProvider(args) {
55
63
  ],
56
64
  mutate: () => {
57
65
  const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {
58
- setActiveProfile: provider.profile,
66
+ setCurrentModel: resolvedModel,
67
+ setCurrentModelProvider: provider.profile,
59
68
  upsertModelProviders: {
60
69
  [provider.profile]: (0, providers_1.buildCopilotModelProviderProjection)(nextProvider.runtime),
61
70
  },
71
+ deleteLegacyProfile: true,
72
+ deleteLegacyProfilesByName: [provider.profile],
73
+ scrubModelProviderEnvKeys: [provider.profile],
62
74
  });
63
75
  if (bridge.portChanged) {
64
76
  (0, providers_repo_1.writeProvidersFile)(args.providersPath, {
@@ -72,6 +84,8 @@ async function switchProvider(args) {
72
84
  (0, auth_repo_1.writeOpenAiApiKeyAuth)(args.authPath, provider.apiKey);
73
85
  return {
74
86
  provider: args.providerName,
87
+ model: resolvedModel,
88
+ modelProvider: nextProvider.profile,
75
89
  profile: nextProvider.profile,
76
90
  portChanged: bridge.portChanged,
77
91
  bridgePort: bridge.port,
@@ -96,13 +110,30 @@ async function switchProvider(args) {
96
110
  { absolutePath: args.configPath, relativePath: "config.toml" },
97
111
  ],
98
112
  mutate: () => {
113
+ const directBaseUrl = provider.baseUrl?.trim() ?? "";
114
+ if (!directBaseUrl) {
115
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Provider "${args.providerName}" requires base_url before switching.`, {
116
+ provider: args.providerName,
117
+ modelProvider: provider.profile,
118
+ suggestion: "Run `codexs edit <provider> --base-url <url>`.",
119
+ });
120
+ }
99
121
  const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {
100
- setActiveProfile: provider.profile,
122
+ setCurrentModel: resolvedModel,
123
+ setCurrentModelProvider: provider.profile,
124
+ upsertModelProviders: {
125
+ [provider.profile]: (0, providers_1.buildDirectModelProviderProjection)(provider.profile, directBaseUrl),
126
+ },
127
+ deleteLegacyProfile: true,
128
+ deleteLegacyProfilesByName: [provider.profile],
129
+ scrubModelProviderEnvKeys: [provider.profile],
101
130
  });
102
131
  (0, config_repo_1.applyConfigMutation)(args.configPath, document, configPlan);
103
132
  (0, auth_repo_1.writeOpenAiApiKeyAuth)(args.authPath, provider.apiKey);
104
133
  return {
105
134
  provider: args.providerName,
135
+ model: resolvedModel,
136
+ modelProvider: provider.profile,
106
137
  profile: provider.profile,
107
138
  };
108
139
  },
@@ -85,11 +85,15 @@ function renderHumanSuccess(command, data, warnings) {
85
85
  lines.push("No providers configured.");
86
86
  }
87
87
  else {
88
- const currentProfile = typeof data?.currentProfile === "string" ? data.currentProfile : null;
88
+ const currentModel = typeof data?.currentModel === "string" ? data.currentModel : null;
89
+ const currentModelProvider = typeof data?.currentModelProvider === "string" ? data.currentModelProvider : null;
89
90
  const activeProviderResolvable = data?.activeProviderResolvable !== false;
90
91
  const activeCandidates = Array.isArray(data?.activeProviderCandidates) ? data?.activeProviderCandidates : [];
91
- if (currentProfile) {
92
- lines.push(`Current profile: ${currentProfile}`);
92
+ if (currentModel) {
93
+ lines.push(`Current model: ${currentModel}`);
94
+ }
95
+ if (currentModelProvider) {
96
+ lines.push(`Current model provider: ${currentModelProvider}`);
93
97
  if (!activeProviderResolvable && activeCandidates.length > 1) {
94
98
  lines.push(`Current provider: ambiguous (${activeCandidates.join(", ")})`);
95
99
  }
@@ -103,7 +107,7 @@ function renderHumanSuccess(command, data, warnings) {
103
107
  : "";
104
108
  const note = provider.note ? ` note=${provider.note}` : "";
105
109
  const current = provider.isActive ? " current" : "";
106
- lines.push(`${provider.name} [${String(provider.providerType ?? "direct")}]${current} -> ${provider.profile}${tags}${note}`);
110
+ lines.push(`${provider.name} [${String(provider.providerType ?? "direct")}]${current} -> ${provider.modelProvider}${provider.model ? ` model=${provider.model}` : ""}${tags}${note}`);
107
111
  }
108
112
  }
109
113
  break;
@@ -125,13 +129,18 @@ function renderHumanSuccess(command, data, warnings) {
125
129
  break;
126
130
  }
127
131
  case "current":
128
- lines.push(`Current profile: ${String(data?.profile ?? "")}`);
132
+ lines.push(`Current model: ${String(data?.model ?? "")}`);
133
+ lines.push(`Current model provider: ${String(data?.modelProvider ?? "")}`);
134
+ if (data?.provider) {
135
+ lines.push(`Managed provider: ${String(data.provider)}`);
136
+ }
129
137
  break;
130
138
  case "status":
131
139
  lines.push("Status summary:");
132
140
  lines.push(` target runtime: ${String(data?.codexDir ?? "")}`);
133
141
  lines.push(` tool home: ${String(data?.storage?.toolHome?.root ?? "")}`);
134
- lines.push(` current profile: ${String(data?.currentProfile ?? "(none)")}`);
142
+ lines.push(` current model: ${String(data?.currentModel ?? "(none)")}`);
143
+ lines.push(` current model provider: ${String(data?.currentModelProvider ?? "(none)")}`);
135
144
  lines.push(` mapped provider: ${renderStatusMappedProvider(data)}`);
136
145
  lines.push(` provider path: ${renderStatusProviderPath(data)}`);
137
146
  lines.push(` runtime health: ${renderStatusHealth(data)}`);
@@ -139,7 +148,9 @@ function renderHumanSuccess(command, data, warnings) {
139
148
  lines.push(` next step: ${renderStatusNextStep(data, warnings)}`);
140
149
  break;
141
150
  case "config-show": {
142
- lines.push(`activeProfile: ${String(data?.activeProfile ?? "")}`);
151
+ lines.push(`currentModel: ${String(data?.currentModel ?? "")}`);
152
+ lines.push(`currentModelProvider: ${String(data?.currentModelProvider ?? "")}`);
153
+ lines.push(`legacyProfile: ${String(data?.legacyProfile ?? "")}`);
143
154
  const profiles = data?.profiles ?? [];
144
155
  for (const profile of profiles) {
145
156
  lines.push(`${String(profile.name)} managed=${String(profile.managed)} active=${String(profile.isActive)} source=${String(profile.source)} model=${String(profile.model ?? "")} modelProvider=${String(profile.modelProvider ?? "")} baseUrl=${String(profile.baseUrl ?? "")}`);
@@ -154,7 +165,8 @@ function renderHumanSuccess(command, data, warnings) {
154
165
  break;
155
166
  }
156
167
  case "switch":
157
- lines.push(`Switched to provider ${String(data?.provider ?? "")} using profile ${String(data?.profile ?? "")}.`);
168
+ lines.push(`Switched to provider ${String(data?.provider ?? "")} using model provider ${String(data?.modelProvider ?? data?.profile ?? "")}.`);
169
+ lines.push(`Model: ${String(data?.model ?? "")}`);
158
170
  lines.push(`Backup: ${String(data?.backupPath ?? "")}`);
159
171
  break;
160
172
  case "import":
@@ -183,7 +195,7 @@ function renderHumanSuccess(command, data, warnings) {
183
195
  }
184
196
  lines.push(`login launched: ${String(data?.loginLaunched ?? false)}`);
185
197
  lines.push(`auth ready: ${String(data?.authReady ?? false)}`);
186
- lines.push("next step: run `codexs add <provider> --copilot --profile <name>` and then `codexs switch <provider>`.");
198
+ lines.push("next step: run `codexs add <provider> --copilot --profile <model-provider-id>` and then `codexs switch <provider>`.");
187
199
  break;
188
200
  case "migrate":
189
201
  lines.push(`Migrated providers in ${String(data?.codexDir ?? "")} using ${String(data?.strategy ?? "")}.`);
@@ -262,12 +274,6 @@ function renderStatusHealth(data) {
262
274
  if (!activeProviderResolvable || liveState.reason === "shared-profile") {
263
275
  return "active provider ambiguous";
264
276
  }
265
- if (issues.some((issue) => issue.code === "UNMANAGED_ACTIVE_PROFILE")) {
266
- return "active profile unmanaged";
267
- }
268
- if (issues.some((issue) => issue.code === "ACTIVE_PROVIDER_UNRESOLVED")) {
269
- return "active provider ambiguous";
270
- }
271
277
  if (issues.some((issue) => issue.code === "PROVIDER_BASE_URL_MISMATCH")) {
272
278
  return "provider projection drift";
273
279
  }
@@ -317,6 +323,9 @@ function renderStatusNextStep(data, warnings) {
317
323
  if (!data?.provider) {
318
324
  return "run `codexs switch <provider>` after adding or adopting a managed provider";
319
325
  }
326
+ if (Array.isArray(data?.issues) && (data?.issues).some((issue) => issue.code === "LEGACY_PROFILE_SELECTOR" || issue.code === "LEGACY_PROFILE_SECTION" || issue.code === "LEGACY_MODEL_PROVIDER_ENV_KEY")) {
327
+ return "run `codexs switch <provider>` to reproject the active route and clean legacy fields";
328
+ }
320
329
  return "run `codexs doctor` if you need a deeper diagnostic pass";
321
330
  }
322
331
  /**
@@ -337,10 +346,11 @@ function renderDoctorIssueNextStep(issue) {
337
346
  case "BRIDGE_HEALTHCHECK_FAILED":
338
347
  return "reselect the provider with `codexs switch <provider>` or inspect bridge state";
339
348
  case "UNMANAGED_ACTIVE_PROFILE":
340
- return "switch to a managed provider or adopt the active profile with `codexs migrate`";
341
- case "ACTIVE_PROVIDER_UNRESOLVED":
342
- case "SHARED_PROFILE_REFERENCE":
343
- return "make provider-to-profile mappings unique before relying on current-provider detection";
349
+ return "switch to a managed provider or adopt the active route with `codexs migrate`";
350
+ case "LEGACY_PROFILE_SELECTOR":
351
+ case "LEGACY_PROFILE_SECTION":
352
+ case "LEGACY_MODEL_PROVIDER_ENV_KEY":
353
+ return "rerun `codexs switch <provider>` to project top-level model/model_provider and remove legacy fields";
344
354
  case "PROVIDER_BASE_URL_MISMATCH":
345
355
  return "rerun `codexs edit <provider> --base-url <url>` or `codexs switch <provider>` to repair the runtime projection";
346
356
  default:
@@ -95,7 +95,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
95
95
  });
96
96
  }
97
97
  case "current":
98
- return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
98
+ return (0, get_current_profile_1.getCurrentProfile)(paths.configPath, paths.providersPath);
99
99
  case "status":
100
100
  return (0, get_status_1.getStatus)(paths.codexDir, paths.configPath, paths.providersPath, paths.authPath, {
101
101
  runtimeDir: paths.runtimeDir,
@@ -495,7 +495,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
495
495
  providersPath: paths.providersPath,
496
496
  configPath: paths.configPath,
497
497
  providerName,
498
- switchToProfile,
498
+ switchToProvider: switchToProfile,
499
499
  });
500
500
  }
501
501
  case "doctor":
@@ -29,7 +29,7 @@ function buildHelpText(commandName) {
29
29
  return [
30
30
  "codex-switch",
31
31
  "",
32
- "Manage and switch local Codex provider/profile configuration safely.",
32
+ "Manage and switch local Codex provider/model-provider routing safely.",
33
33
  "Primary workflows: direct providers use init -> add -> switch -> status -> doctor.",
34
34
  "Primary workflows: Copilot providers use init -> login copilot -> add --copilot -> switch -> status -> doctor.",
35
35
  "Advanced adopt flows use migrate only when you already have Codex runtime state to import.",
@@ -65,12 +65,12 @@ function buildHelpText(commandName) {
65
65
  "",
66
66
  "Examples:",
67
67
  " codexs init",
68
- " codexs add packycode --profile packycode --api-key sk-xxx",
68
+ " codexs add packycode --profile packycode --model gpt-5 --api-key sk-xxx --base-url https://api.example/v1",
69
69
  " codexs switch packycode",
70
70
  " codexs status",
71
71
  " codexs doctor",
72
72
  " codexs login copilot",
73
- " codexs add copilot-main --copilot --profile copilot-main",
73
+ " codexs add copilot-main --copilot --profile copilot-main --model gpt-5",
74
74
  " codexs migrate",
75
75
  " codexs config show",
76
76
  " codexs backups list",