@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.
@@ -34,6 +34,9 @@ function validateProvidersShape(input) {
34
34
  if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
35
35
  throw new Error(`Provider "${name}" is missing a valid apiKey.`);
36
36
  }
37
+ if (provider.model !== undefined && typeof provider.model !== "string") {
38
+ throw new Error(`Provider "${name}" has an invalid model.`);
39
+ }
37
40
  if (provider.baseUrl !== undefined && typeof provider.baseUrl !== "string") {
38
41
  throw new Error(`Provider "${name}" has an invalid baseUrl.`);
39
42
  }
@@ -55,6 +58,7 @@ function validateProvidersShape(input) {
55
58
  providers[name] = cleanProviderRecord({
56
59
  profile: provider.profile,
57
60
  apiKey: provider.apiKey,
61
+ model: provider.model,
58
62
  baseUrl: provider.baseUrl,
59
63
  note: provider.note,
60
64
  tags: provider.tags,
@@ -71,6 +75,9 @@ function cleanProviderRecord(record) {
71
75
  profile: record.profile.trim(),
72
76
  apiKey: record.apiKey.trim(),
73
77
  };
78
+ if (record.model && record.model.trim() !== "") {
79
+ next.model = record.model.trim();
80
+ }
74
81
  if (record.baseUrl && record.baseUrl.trim() !== "") {
75
82
  next.baseUrl = record.baseUrl.trim();
76
83
  }
@@ -90,26 +90,26 @@ function getStorageRoles(args) {
90
90
  };
91
91
  }
92
92
  /**
93
- * Compares the live active profile against managed providers to detect drift.
93
+ * Compares the live active model_provider against managed providers to detect drift.
94
94
  */
95
- function inspectLiveStateDrift(currentProfile, providers) {
96
- if (currentProfile === null) {
95
+ function inspectLiveStateDrift(currentModelProvider, providers) {
96
+ if (currentModelProvider === null) {
97
97
  return {
98
- currentProfile,
98
+ currentModelProvider,
99
99
  mappedProvider: null,
100
100
  mappedProviders: [],
101
- profileMapped: false,
101
+ modelProviderMapped: false,
102
102
  providerResolvable: false,
103
103
  canBackfillActiveProvider: false,
104
- reason: providers ? "profile-missing" : "config-missing",
104
+ reason: providers ? "model-provider-missing" : "config-missing",
105
105
  };
106
106
  }
107
107
  if (!providers) {
108
108
  return {
109
- currentProfile,
109
+ currentModelProvider,
110
110
  mappedProvider: null,
111
111
  mappedProviders: [],
112
- profileMapped: false,
112
+ modelProviderMapped: false,
113
113
  providerResolvable: false,
114
114
  canBackfillActiveProvider: false,
115
115
  reason: "providers-missing",
@@ -117,16 +117,16 @@ function inspectLiveStateDrift(currentProfile, providers) {
117
117
  }
118
118
  const mappedProviders = [];
119
119
  for (const [name, provider] of Object.entries(providers.providers)) {
120
- if (provider.profile === currentProfile) {
120
+ if (provider.profile === currentModelProvider) {
121
121
  mappedProviders.push(name);
122
122
  }
123
123
  }
124
124
  if (mappedProviders.length === 1) {
125
125
  return {
126
- currentProfile,
126
+ currentModelProvider,
127
127
  mappedProvider: mappedProviders[0],
128
128
  mappedProviders,
129
- profileMapped: true,
129
+ modelProviderMapped: true,
130
130
  providerResolvable: true,
131
131
  canBackfillActiveProvider: false,
132
132
  reason: "ok",
@@ -134,20 +134,20 @@ function inspectLiveStateDrift(currentProfile, providers) {
134
134
  }
135
135
  if (mappedProviders.length > 1) {
136
136
  return {
137
- currentProfile,
137
+ currentModelProvider,
138
138
  mappedProvider: null,
139
139
  mappedProviders,
140
- profileMapped: true,
140
+ modelProviderMapped: true,
141
141
  providerResolvable: false,
142
142
  canBackfillActiveProvider: false,
143
143
  reason: "shared-profile",
144
144
  };
145
145
  }
146
146
  return {
147
- currentProfile,
147
+ currentModelProvider,
148
148
  mappedProvider: null,
149
149
  mappedProviders: [],
150
- profileMapped: false,
150
+ modelProviderMapped: false,
151
151
  providerResolvable: false,
152
152
  canBackfillActiveProvider: true,
153
153
  reason: "provider-unmapped",
@@ -16,8 +16,9 @@ function buildSetupDrafts(profiles, detailsByProfile, runtimeByProfile) {
16
16
  return {
17
17
  providerName,
18
18
  record: (0, providers_1.cleanProviderRecord)({
19
- profile,
19
+ profile: runtime?.modelProvider ?? profile,
20
20
  apiKey: detail.apiKey ?? "",
21
+ model: runtime?.model,
21
22
  baseUrl: detail.baseUrl ?? runtime?.baseUrl,
22
23
  note: detail.note,
23
24
  tags: detail.tags,
@@ -71,6 +72,7 @@ function collectMigrateAdoptability(document, providers) {
71
72
  adoptableProfileDetails.push({
72
73
  name: view.name,
73
74
  model: view.model,
75
+ modelProvider: view.modelProvider,
74
76
  baseUrl: view.baseUrl,
75
77
  });
76
78
  continue;
@@ -70,8 +70,8 @@ function canPrompt(runtime, jsonMode) {
70
70
  */
71
71
  async function promptForProviderSelection(runtime, providersPath, configPath, message) {
72
72
  const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
73
- const currentProfile = fs.existsSync(configPath) ? (0, config_repo_1.readStructuredConfig)(configPath).activeProfile : null;
74
- const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
73
+ const currentModelProvider = fs.existsSync(configPath) ? (0, config_repo_1.readStructuredConfig)(configPath).currentModelProvider : null;
74
+ const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentModelProvider, providers);
75
75
  const choices = Object.entries(providers.providers)
76
76
  .sort(([left], [right]) => left.localeCompare(right))
77
77
  .map(([providerName, provider]) => {
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MIN_SUPPORTED_CODEX_VERSION = void 0;
4
+ /**
5
+ * Minimum Codex CLI version supported by the managed runtime workflow.
6
+ */
7
+ exports.MIN_SUPPORTED_CODEX_VERSION = "0.134.0";
@@ -73,12 +73,12 @@ function readStructuredConfig(configPath) {
73
73
  }
74
74
  }
75
75
  /**
76
- * Reads the active top-level profile from config.toml.
76
+ * Reads the active top-level model_provider route from config.toml.
77
77
  */
78
78
  function readCurrentProfile(configPath) {
79
- const profile = readStructuredConfig(configPath).activeProfile ?? (0, config_1.parseTopLevelProfile)(readConfigFile(configPath));
79
+ const profile = readStructuredConfig(configPath).currentModelProvider;
80
80
  if (!profile) {
81
- throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No top-level profile is set in config.toml.", {
81
+ throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No top-level model_provider is set in config.toml.", {
82
82
  file: configPath,
83
83
  });
84
84
  }
@@ -91,18 +91,10 @@ function listConfigProfiles(configPath) {
91
91
  return new Set(readStructuredConfig(configPath).profiles.map((profile) => profile.name));
92
92
  }
93
93
  /**
94
- * Verifies that a provider's target profile exists before a switch operation proceeds.
94
+ * Loads config.toml for commands that project one model_provider route.
95
95
  */
96
96
  function ensureProfileExists(configPath, profile, provider) {
97
- const document = readStructuredConfig(configPath);
98
- if (!document.profiles.some((entry) => entry.name === profile)) {
99
- throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Profile "${profile}" does not exist in config.toml.`, {
100
- file: configPath,
101
- provider,
102
- profile,
103
- });
104
- }
105
- return document;
97
+ return readStructuredConfig(configPath);
106
98
  }
107
99
  /**
108
100
  * Resolves one profile view and enforces the managed model_provider contract.
@@ -166,7 +158,7 @@ function requireModelProviderRuntimeSection(document, profile) {
166
158
  */
167
159
  function updateTopLevelProfile(configPath, configContent, profile) {
168
160
  (0, fs_utils_1.writeTextFileAtomic)(configPath, (0, config_1.applyPatchOperations)(configContent, (0, config_1.planConfigMutation)((0, config_1.parseStructuredConfig)(configContent), {
169
- setActiveProfile: profile,
161
+ setLegacyProfile: profile,
170
162
  }).operations));
171
163
  }
172
164
  /**
@@ -0,0 +1,33 @@
1
+ # codex-switch v0.1.1 Design
2
+
3
+ ## Scope
4
+
5
+ - Support Codex `0.134.0+` only.
6
+ - Treat top-level `model` and `model_provider` as the runtime routing source of truth.
7
+ - Treat legacy top-level `profile` and legacy `[profiles.*]` sections as adopt-only and diagnostic inputs.
8
+ - Project provider auth through `auth.json` with `OPENAI_API_KEY`.
9
+ - Do not write `env_key` or `env_key_instructions` into managed `[model_providers.<id>]` sections.
10
+
11
+ ## Command Contract
12
+
13
+ - `--profile <name>` remains a CLI alias for the stored `model_provider` id.
14
+ - `--model <name>` stores the provider default switch model in `providers.json`.
15
+ - `switch` writes top-level `model` and `model_provider`, repairs `[model_providers.<id>]`, rewrites `auth.json`, and removes the targeted legacy route selector/profile section.
16
+ - `add` and `edit` repair `[model_providers.<id>]` and scrub `env_key` / `env_key_instructions`.
17
+ - `remove --switch-to <provider-name>` switches by managed provider name, not by profile id.
18
+
19
+ ## Persistence
20
+
21
+ - `providers.json` keeps `profile` as the persisted `model_provider` id alias.
22
+ - `providers.json` adds `model` for default route projection.
23
+ - Managed `[model_providers.<id>]` sections write:
24
+ - `base_url`
25
+ - `name`
26
+ - `requires_openai_auth = true`
27
+ - `wire_api = "responses"`
28
+
29
+ ## Legacy Handling
30
+
31
+ - `config show` and `config list-profiles` expose legacy profile inspection views.
32
+ - `migrate` remains a legacy adoption helper and does not modify the active top-level route.
33
+ - `doctor` flags missing top-level route fields, legacy selectors/sections, and legacy `env_key` wiring in the active model provider section.
@@ -137,6 +137,12 @@ model_reasoning_effort = "high"
137
137
  approval_policy = "never"
138
138
  ```
139
139
 
140
+ Important `codex-switch` note:
141
+
142
+ - official Codex still supports profiles
143
+ - `codex-switch` `0.1.1` does not treat top-level `profile` as the recommended managed runtime selector
144
+ - in `codex-switch`, legacy `profile` and `[profiles.*]` are mainly inspect-and-adopt inputs for `migrate`, `doctor`, and `config` inspection flows
145
+
140
146
  ## 4. `config.toml` by topic
141
147
 
142
148
  ### 4.1 Model, reasoning, and response style
@@ -194,6 +200,8 @@ Core keys:
194
200
  - `model_providers.<id>.env_http_headers`
195
201
  - `model_providers.<id>.query_params`
196
202
  - `model_providers.<id>.request_max_retries`
203
+ - `model_providers.<id>.wire_api`
204
+ - `model_providers.<id>.requires_openai_auth`
197
205
 
198
206
  Authentication options for custom providers include:
199
207
 
@@ -214,6 +222,22 @@ Related Bedrock keys:
214
222
  - `model_providers.amazon-bedrock.aws.profile`
215
223
  - `model_providers.amazon-bedrock.aws.region`
216
224
 
225
+ #### `codex-switch` 0.1.1 managed projection
226
+
227
+ When `codex-switch` manages a direct OpenAI-compatible route for Codex `0.134.0+`, it intentionally projects a narrower runtime shape than the full official provider schema:
228
+
229
+ - top-level `model` is the active model selector
230
+ - top-level `model_provider` is the active route selector
231
+ - projected `[model_providers.<id>]` keeps `base_url`
232
+ - projected `[model_providers.<id>]` fixes `wire_api = "responses"`
233
+ - projected `[model_providers.<id>]` fixes `requires_openai_auth = true`
234
+ - projected runtime config does not keep `env_key`
235
+ - projected runtime config does not keep `env_key_instructions`
236
+
237
+ Authentication is projected through `auth.json` with `OPENAI_API_KEY`, not through `env_key` in the managed runtime config.
238
+
239
+ That is a `codex-switch` product decision, not a limitation of Codex itself. If you hand-write or independently manage Codex config, `env_key` remains a valid official mechanism.
240
+
217
241
  ### 4.3.1 `openai_base_url` vs custom providers
218
242
 
219
243
  If you only want to point the built-in OpenAI provider to a proxy, router, or residency-specific endpoint, use:
@@ -539,6 +563,23 @@ env_key = "OPENAI_API_KEY"
539
563
  http_headers = { "X-Team" = "platform" }
540
564
  ```
541
565
 
566
+ This is an official Codex-style custom provider example.
567
+
568
+ If you are using `codex-switch` managed direct-provider projection instead, the runtime projection is intentionally narrower:
569
+
570
+ ```toml
571
+ model = "gpt-5.4"
572
+ model_provider = "proxy"
573
+
574
+ [model_providers.proxy]
575
+ name = "proxy"
576
+ base_url = "https://proxy.example.com/v1"
577
+ wire_api = "responses"
578
+ requires_openai_auth = true
579
+ ```
580
+
581
+ In that managed projection, `OPENAI_API_KEY` is expected in `auth.json` rather than through `env_key` in `config.toml`.
582
+
542
583
  ### 6.4 Locked-down shell environment
543
584
 
544
585
  ```toml
@@ -149,6 +149,12 @@ model_reasoning_effort = "high"
149
149
  approval_policy = "never"
150
150
  ```
151
151
 
152
+ 对 `codex-switch` 0.1.1 的补充说明:
153
+
154
+ - 官方 Codex 仍然支持 profiles
155
+ - `codex-switch` 不再把顶层 `profile` 视为推荐的受管 runtime selector
156
+ - 在 `codex-switch` 里,legacy `profile` 和 `[profiles.*]` 主要用于 `migrate`、`doctor` 和 `config` 检视的 adopt / inspect 场景
157
+
152
158
  ## 4. `config.toml` 主题整理
153
159
 
154
160
  ### 4.1 模型、推理强度与输出风格
@@ -206,6 +212,8 @@ Codex 把“当前使用哪个 provider”和“provider 怎么定义”拆开
206
212
  - `model_providers.<id>.env_http_headers`
207
213
  - `model_providers.<id>.query_params`
208
214
  - `model_providers.<id>.request_max_retries`
215
+ - `model_providers.<id>.wire_api`
216
+ - `model_providers.<id>.requires_openai_auth`
209
217
 
210
218
  自定义 provider 的认证方式包括:
211
219
 
@@ -229,6 +237,22 @@ Bedrock 相关 key:
229
237
  - `model_providers.amazon-bedrock.aws.profile`
230
238
  - `model_providers.amazon-bedrock.aws.region`
231
239
 
240
+ #### `codex-switch` 0.1.1 的受管投影
241
+
242
+ 当 `codex-switch` 为 Codex `0.134.0+` 管理一个 direct OpenAI-compatible route 时,它会有意把运行态投影限制在一组更窄的字段上:
243
+
244
+ - 顶层 `model` 是活动模型选择器
245
+ - 顶层 `model_provider` 是活动路由选择器
246
+ - 投影后的 `[model_providers.<id>]` 保留 `base_url`
247
+ - 投影后的 `[model_providers.<id>]` 固定写 `wire_api = "responses"`
248
+ - 投影后的 `[model_providers.<id>]` 固定写 `requires_openai_auth = true`
249
+ - 受管运行态投影不会保留 `env_key`
250
+ - 受管运行态投影不会保留 `env_key_instructions`
251
+
252
+ 认证信息会通过 `auth.json` 里的 `OPENAI_API_KEY` 投影,而不是通过运行态 `config.toml` 中的 `env_key`。
253
+
254
+ 这属于 `codex-switch` 的产品约束,不是 Codex 官方能力限制。如果你是手工维护或独立维护 Codex config,`env_key` 仍然是官方支持的方式。
255
+
232
256
  ### 4.3.1 `openai_base_url` 和自定义 provider 的区别
233
257
 
234
258
  如果你只是想把内置 `openai` provider 指向公司网关、路由层或者某个数据驻留 endpoint,官方建议直接用:
@@ -568,6 +592,23 @@ env_key = "OPENAI_API_KEY"
568
592
  http_headers = { "X-Team" = "platform" }
569
593
  ```
570
594
 
595
+ 这段是官方 Codex 风格的自定义 provider 示例。
596
+
597
+ 如果你走的是 `codex-switch` 的受管 direct-provider 投影,运行态会被有意收窄为:
598
+
599
+ ```toml
600
+ model = "gpt-5.4"
601
+ model_provider = "proxy"
602
+
603
+ [model_providers.proxy]
604
+ name = "proxy"
605
+ base_url = "https://proxy.example.com/v1"
606
+ wire_api = "responses"
607
+ requires_openai_auth = true
608
+ ```
609
+
610
+ 在这种受管投影下,`OPENAI_API_KEY` 预期写在 `auth.json`,而不是通过 `config.toml` 里的 `env_key` 暴露。
611
+
571
612
  ### 6.4 最小暴露的 shell 环境策略
572
613
 
573
614
  ```toml