@minniexcode/codex-switch 0.0.12 → 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.
Files changed (36) hide show
  1. package/README.AI.md +37 -6
  2. package/README.CN.md +45 -11
  3. package/README.md +45 -13
  4. package/dist/app/add-provider.js +22 -24
  5. package/dist/app/edit-provider.js +34 -55
  6. package/dist/app/get-current-profile.js +15 -3
  7. package/dist/app/get-status.js +11 -8
  8. package/dist/app/list-config-profiles.js +3 -1
  9. package/dist/app/list-providers.js +10 -4
  10. package/dist/app/remove-provider.js +52 -19
  11. package/dist/app/run-doctor.js +29 -28
  12. package/dist/app/setup-codex.js +3 -3
  13. package/dist/app/show-config.js +3 -1
  14. package/dist/app/switch-provider.js +36 -5
  15. package/dist/cli/output.js +36 -18
  16. package/dist/commands/handlers.js +2 -2
  17. package/dist/commands/help.js +3 -3
  18. package/dist/commands/registry.js +35 -30
  19. package/dist/domain/config.js +250 -185
  20. package/dist/domain/providers.js +23 -0
  21. package/dist/domain/runtime-state.js +15 -15
  22. package/dist/domain/setup.js +3 -1
  23. package/dist/interaction/interactive.js +2 -2
  24. package/dist/runtime/codex-version.js +7 -0
  25. package/dist/storage/config-repo.js +6 -14
  26. package/docs/Design/codex-switch-v0.1.0-design.md +152 -0
  27. package/docs/Design/codex-switch-v0.1.1-design.md +33 -0
  28. package/docs/PRD/codex-switch-prd-v0.1.0.md +217 -205
  29. package/docs/Reference/codex-config-reference.md +41 -0
  30. package/docs/Reference/codex-config-reference.zh-CN.md +41 -0
  31. package/docs/Tests/testing.md +31 -78
  32. package/docs/cli-usage.md +86 -27
  33. package/docs/codex-switch-command-design.md +649 -649
  34. package/docs/codex-switch-product-overview.md +81 -80
  35. package/docs/codex-switch-technical-architecture.md +1115 -1115
  36. package/package.json +51 -51
package/README.AI.md CHANGED
@@ -6,12 +6,13 @@ This file summarizes the current operational contract for AI agents, automation
6
6
 
7
7
  - Package: `@minniexcode/codex-switch`
8
8
  - CLI name: `codexs`
9
- - Current repository version: `0.0.12`
10
- - Version status: development-version software, not a stable `0.1.0` release yet
9
+ - Current repository version: `0.1.1`
10
+ - Version status: stable release line
11
+ - Runtime contract target: Codex `0.134.0+`
11
12
 
12
13
  ## Product Role
13
14
 
14
- `codex-switch` is a local-first TypeScript CLI that manages provider and profile state for Codex while keeping tool-managed state separate from the target Codex runtime.
15
+ `codex-switch` is a local-first TypeScript CLI that manages provider and model-provider routing state for Codex while keeping tool-managed state separate from the target Codex runtime.
15
16
 
16
17
  The managed source of truth is the tool home. Runtime files under the target Codex directory are projected outputs, not the main registry.
17
18
 
@@ -21,7 +22,7 @@ Direct provider workflow:
21
22
 
22
23
  ```bash
23
24
  codexs init
24
- codexs add <provider> --profile <name> --api-key <key>
25
+ codexs add <provider> --model <model> --api-key <key> [--base-url <url>]
25
26
  codexs switch <provider>
26
27
  codexs status
27
28
  codexs doctor
@@ -32,7 +33,7 @@ GitHub Copilot workflow:
32
33
  ```bash
33
34
  codexs init
34
35
  codexs login copilot
35
- codexs add <provider> --copilot --profile <name>
36
+ codexs add <provider> --copilot --model <model>
36
37
  codexs switch <provider>
37
38
  codexs status
38
39
  codexs doctor
@@ -47,6 +48,36 @@ codexs migrate
47
48
 
48
49
  `migrate` is not a fresh-install default. It is an advanced adopt helper for existing runtime state.
49
50
 
51
+ ## Runtime Route Contract
52
+
53
+ For Codex `0.134.0+`, the live route is selected by:
54
+
55
+ - top-level `model`
56
+ - top-level `model_provider`
57
+
58
+ Important implications for automation:
59
+
60
+ - treat `model_provider` as the active provider selector
61
+ - treat `--profile` as an alias for a managed `model_provider` id
62
+ - do not describe top-level `profile` or `[profiles.*]` as the primary runtime path
63
+ - managed direct-provider projection does not keep `env_key` or `env_key_instructions`
64
+ - managed provider projection fixes `wire_api = "responses"` and `requires_openai_auth = true`
65
+
66
+ Expected managed direct-provider projection:
67
+
68
+ ```toml
69
+ model = "gpt-5.5"
70
+ model_provider = "proxy"
71
+
72
+ [model_providers.proxy]
73
+ name = "proxy"
74
+ base_url = "https://proxy.example.com/v1"
75
+ wire_api = "responses"
76
+ requires_openai_auth = true
77
+ ```
78
+
79
+ Authentication remains projected through `auth.json` with `OPENAI_API_KEY`.
80
+
50
81
  ## Important Paths
51
82
 
52
83
  Tool home:
@@ -99,7 +130,7 @@ Important behavioral constraints:
99
130
  - `login copilot` requires a real TTY and does not support `--json`.
100
131
  - `login copilot` currently installs the local Copilot SDK when needed, tries the bundled runtime CLI first, falls back to `PATH` when necessary, and rechecks auth readiness before reporting success.
101
132
  - `add --copilot` assumes SDK install and upstream Copilot auth are already ready.
102
- - `migrate` still depends on interactive profile selection and provider-detail collection in this release.
133
+ - `migrate` remains interactive when provider adoption requires human input.
103
134
  - `status` is the main dual-path summary command.
104
135
  - `doctor` is the deeper repair-oriented diagnostic command.
105
136
 
package/README.CN.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # @minniexcode/codex-switch
2
2
 
3
- `@minniexcode/codex-switch` 是一个本地优先的 CLI,用来安全地管理和切换 Codex 的 provider 与 profile 配置。
3
+ `@minniexcode/codex-switch` 是一个本地优先的 CLI,用来安全地管理和切换 Codex 的 provider 与 model-provider 路由配置。
4
4
 
5
5
  它把 `codex-switch` 自己的工具状态和目标 Codex runtime 明确分开,让 provider 管理、备份与 runtime 投影有一套受管流程,而不是依赖手工改文件。
6
6
 
7
7
  ## 版本定位
8
8
 
9
- 当前包版本:`0.0.12`
9
+ 当前包版本:`0.1.1`
10
10
 
11
- 当前仍处于开发版本阶段。这个版本的重点不是继续扩命令面,而是把主工作流、帮助文案和实际行为统一到同一套契约上。
11
+ 这是当前稳定发布线。`0.1.1` 的目标是把公开文档和 Codex `0.134.0+` 的 runtime contract 对齐,也就是用顶层 `model` 与 `model_provider` 选择活动路由。
12
12
 
13
13
  ## 安装
14
14
 
@@ -34,7 +34,7 @@ Direct provider 主路径:
34
34
 
35
35
  ```bash
36
36
  codexs init
37
- codexs add my-provider --profile my-provider --api-key sk-xxx
37
+ codexs add my-provider --model gpt-5.5 --base-url https://gateway.example.com/v1 --api-key sk-xxx
38
38
  codexs switch my-provider
39
39
  codexs status
40
40
  codexs doctor
@@ -45,7 +45,7 @@ GitHub Copilot 主路径:
45
45
  ```bash
46
46
  codexs init
47
47
  codexs login copilot
48
- codexs add copilot-main --copilot --profile copilot-main
48
+ codexs add copilot-main --copilot --model gpt-4.1
49
49
  codexs switch copilot-main
50
50
  codexs status
51
51
  codexs doctor
@@ -56,9 +56,43 @@ codexs doctor
56
56
  - `init` 负责初始化 `codex-switch` 的 tool home 与受管状态文件。
57
57
  - `login copilot` 负责上游 Copilot onboarding 和登录可用性检查。
58
58
  - `add --copilot` 不负责替你登录,它假设上游 Copilot 已经 ready。
59
+ - `switch` 会把选中的 provider 投影到目标 Codex runtime 的顶层 `model` 与 `model_provider`。
59
60
  - `status` 是切换后的主读取命令。
60
61
  - `doctor` 是主诊断命令,用于解释问题和下一步修复动作。
61
62
 
63
+ ## Runtime 路由模型
64
+
65
+ 对于 Codex `0.134.0+`,活动 runtime route 由 `config.toml` 顶层的 `model` 和 `model_provider` 决定。
66
+
67
+ `codex-switch` 按这套 contract 管理运行态:
68
+
69
+ - 顶层 `model` 表示当前活动模型
70
+ - 顶层 `model_provider` 表示当前活动 provider route
71
+ - 受管的 `[model_providers.<id>]` 是 runtime provider 定义投影
72
+ - `--profile` 只作为受管 `model_provider` id 的 alias,不再是主 runtime selector
73
+
74
+ Direct provider 的运行态投影会写入:
75
+
76
+ - 顶层 `model`
77
+ - 顶层 `model_provider`
78
+ - `[model_providers.<id>]`
79
+ - 带 `OPENAI_API_KEY` 的 `auth.json`
80
+
81
+ 受管 direct provider 投影不会再保留 `env_key` 或 `env_key_instructions`。`switch`、`add` 和 `edit` 会在写入活动路由前清理这些旧字段。
82
+
83
+ 对受管的 OpenAI-compatible route,投影后的 provider 结构固定为:
84
+
85
+ ```toml
86
+ model = "gpt-5.5"
87
+ model_provider = "my-provider"
88
+
89
+ [model_providers.my-provider]
90
+ name = "my-provider"
91
+ base_url = "https://gateway.example.com/v1"
92
+ wire_api = "responses"
93
+ requires_openai_auth = true
94
+ ```
95
+
62
96
  ## Advanced Adopt 路径
63
97
 
64
98
  如果你已经有现成的 Codex runtime 状态,希望把它 adopt 到受管 `providers.json`,再使用:
@@ -82,11 +116,11 @@ codexs current
82
116
  codexs status
83
117
  codexs config show [profile]
84
118
  codexs config list-profiles
85
- codexs add <provider> --profile <name> --api-key <key>
86
- codexs add <provider> --copilot --profile <name>
119
+ codexs add <provider> --model <model> --api-key <key> [--base-url <url>]
120
+ codexs add <provider> --copilot --model <model>
87
121
  codexs edit <provider>
88
122
  codexs switch <provider>
89
- codexs remove <provider> [--force] [--switch-to <profile>]
123
+ codexs remove <provider> [--force] [--switch-to <provider>]
90
124
  codexs import <file>
91
125
  codexs export <file> [--force]
92
126
  codexs bridge start [provider]
@@ -151,7 +185,7 @@ tool home:
151
185
  当前实现边界:
152
186
 
153
187
  - `login copilot` 必须运行在真实 TTY 下,不支持 `--json`。
154
- - `migrate` 在当前版本仍然依赖交互式 profile 选择和 provider 细节补全。
188
+ - `migrate` 在需要人工补齐 adopt 信息时仍然保持交互式语义。
155
189
  - 自动化调用应尽量显式传参,并优先使用 `--json`。
156
190
 
157
191
  ## 本地开发
@@ -171,8 +205,8 @@ npm pack --dry-run
171
205
  - [详细 CLI 文档](./docs/cli-usage.md)
172
206
  - [产品概览](./docs/codex-switch-product-overview.md)
173
207
  - [测试说明](./docs/Tests/testing.md)
174
- - [PRD 0.0.12](./docs/PRD/codex-switch-prd-v0.0.12.md)
175
- - [Release Gate PRD 0.1.0](./docs/PRD/codex-switch-prd-v0.1.0.md)
208
+ - [Release PRD 0.1.1](./docs/PRD/codex-switch-prd-v0.1.1.md)
209
+ - [Release Design 0.1.1](./docs/Design/codex-switch-v0.1.1-design.md)
176
210
 
177
211
  ## License
178
212
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @minniexcode/codex-switch
2
2
 
3
- `@minniexcode/codex-switch` is a local-first CLI for managing and switching Codex provider and profile configuration safely.
3
+ `@minniexcode/codex-switch` is a local-first CLI for managing and switching Codex provider and model-provider routing safely.
4
4
 
5
5
  It keeps `codex-switch` tool state separate from the target Codex runtime, so provider management, backup flow, and runtime projection stay explicit instead of relying on manual file edits.
6
6
 
@@ -8,9 +8,9 @@ Chinese version: [README.CN.md](./README.CN.md)
8
8
 
9
9
  ## Version
10
10
 
11
- Current package version: `0.0.12`
11
+ Current package version: `0.1.1`
12
12
 
13
- This repository is still on a development-version line. The current release focuses on making the primary workflows, help text, and operational boundaries consistent with the implementation.
13
+ This is the current stable release line. `0.1.1` aligns the public docs with the Codex `0.134.0+` runtime contract where top-level `model` and `model_provider` select the active route.
14
14
 
15
15
  ## Install
16
16
 
@@ -36,7 +36,7 @@ Direct provider workflow:
36
36
 
37
37
  ```bash
38
38
  codexs init
39
- codexs add my-provider --profile my-provider --api-key sk-xxx
39
+ codexs add my-provider --model gpt-5.5 --base-url https://gateway.example.com/v1 --api-key sk-xxx
40
40
  codexs switch my-provider
41
41
  codexs status
42
42
  codexs doctor
@@ -47,7 +47,7 @@ GitHub Copilot workflow:
47
47
  ```bash
48
48
  codexs init
49
49
  codexs login copilot
50
- codexs add copilot-main --copilot --profile copilot-main
50
+ codexs add copilot-main --copilot --model gpt-4.1
51
51
  codexs switch copilot-main
52
52
  codexs status
53
53
  codexs doctor
@@ -58,9 +58,43 @@ Notes:
58
58
  - `init` prepares the `codex-switch` tool home and managed state.
59
59
  - `login copilot` handles upstream Copilot onboarding and auth readiness.
60
60
  - `add --copilot` does not perform login for you; it assumes Copilot login is already ready.
61
+ - `switch` projects the selected provider into the target Codex runtime as top-level `model` plus `model_provider`.
61
62
  - `status` is the main read command after switching.
62
63
  - `doctor` is the main repair-oriented diagnostic command.
63
64
 
65
+ ## Runtime Routing Model
66
+
67
+ For Codex `0.134.0+`, the active runtime route is selected through top-level `model` and `model_provider` in `config.toml`.
68
+
69
+ `codex-switch` treats that route as the runtime contract:
70
+
71
+ - top-level `model` selects the active model id
72
+ - top-level `model_provider` selects the active provider route
73
+ - managed `[model_providers.<id>]` entries are the projected runtime provider definitions
74
+ - `--profile` is only an alias for the managed `model_provider` id, not the primary runtime selector
75
+
76
+ Direct-provider projection writes:
77
+
78
+ - top-level `model`
79
+ - top-level `model_provider`
80
+ - `[model_providers.<id>]`
81
+ - `auth.json` with `OPENAI_API_KEY`
82
+
83
+ Managed direct-provider projection does not keep `env_key` or `env_key_instructions` in the generated runtime config. `switch`, `add`, and `edit` clean old legacy projection fields before writing the active route.
84
+
85
+ For managed OpenAI-compatible routes, the projected provider entry keeps the fixed runtime shape:
86
+
87
+ ```toml
88
+ model = "gpt-5.5"
89
+ model_provider = "my-provider"
90
+
91
+ [model_providers.my-provider]
92
+ name = "my-provider"
93
+ base_url = "https://gateway.example.com/v1"
94
+ wire_api = "responses"
95
+ requires_openai_auth = true
96
+ ```
97
+
64
98
  ## Advanced Adopt Workflow
65
99
 
66
100
  Use `migrate` only when you already have Codex runtime state that should be adopted into managed `providers.json` state:
@@ -84,11 +118,11 @@ codexs current
84
118
  codexs status
85
119
  codexs config show [profile]
86
120
  codexs config list-profiles
87
- codexs add <provider> --profile <name> --api-key <key>
88
- codexs add <provider> --copilot --profile <name>
121
+ codexs add <provider> --model <model> --api-key <key> [--base-url <url>]
122
+ codexs add <provider> --copilot --model <model>
89
123
  codexs edit <provider>
90
124
  codexs switch <provider>
91
- codexs remove <provider> [--force] [--switch-to <profile>]
125
+ codexs remove <provider> [--force] [--switch-to <provider>]
92
126
  codexs import <file>
93
127
  codexs export <file> [--force]
94
128
  codexs bridge start [provider]
@@ -103,8 +137,6 @@ codexs doctor
103
137
 
104
138
  ## Runtime Model
105
139
 
106
- `codex-switch` uses a dual-path model.
107
-
108
140
  Tool home:
109
141
 
110
142
  ```text
@@ -155,7 +187,7 @@ Global flags:
155
187
  Operational limits:
156
188
 
157
189
  - `login copilot` requires a real TTY and does not support `--json`.
158
- - `migrate` still depends on interactive profile selection and provider-detail collection in this release.
190
+ - `migrate` remains interactive when provider adoption requires human input.
159
191
  - Automation should pass explicit arguments and prefer `--json` for stable parsing.
160
192
 
161
193
  ## Local Development
@@ -175,8 +207,8 @@ npm pack --dry-run
175
207
  - [Detailed CLI Usage](./docs/cli-usage.md)
176
208
  - [Testing Guide](./docs/Tests/testing.md)
177
209
  - [Product Overview](./docs/codex-switch-product-overview.md)
178
- - [PRD 0.0.12](./docs/PRD/codex-switch-prd-v0.0.12.md)
179
- - [Release Gate PRD 0.1.0](./docs/PRD/codex-switch-prd-v0.1.0.md)
210
+ - [Release PRD 0.1.1](./docs/PRD/codex-switch-prd-v0.1.1.md)
211
+ - [Release Design 0.1.1](./docs/Design/codex-switch-v0.1.1-design.md)
180
212
 
181
213
  ## License
182
214
 
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.addProvider = addProvider;
37
37
  const crypto = __importStar(require("node:crypto"));
38
- const config_1 = require("../domain/config");
39
38
  const providers_1 = require("../domain/providers");
40
39
  const errors_1 = require("../domain/errors");
41
40
  const config_repo_1 = require("../storage/config-repo");
@@ -91,36 +90,32 @@ async function addProvider(args) {
91
90
  }
92
91
  }
93
92
  const document = (0, config_repo_1.readStructuredConfig)(args.configPath);
94
- const existingProfile = document.profiles.find((profile) => profile.name === args.profile);
95
93
  const existingModelProvider = document.modelProviders.find((entry) => entry.name === args.profile);
96
- if (!existingProfile && !args.createProfile) {
97
- throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Profile "${args.profile}" does not exist in config.toml.`, {
98
- profile: args.profile,
94
+ const inheritedModel = document.currentModel ?? undefined;
95
+ const providerModel = args.model ?? inheritedModel;
96
+ if (!providerModel) {
97
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Provider "${args.providerName}" requires a model.`, {
99
98
  provider: args.providerName,
99
+ modelProvider: args.profile,
100
+ missingFields: ["model"],
101
+ suggestion: "Pass `--model <name>` or set a top-level model in config.toml first.",
102
+ });
103
+ }
104
+ const directBaseUrl = args.baseUrl;
105
+ if (!args.copilot && (!directBaseUrl || directBaseUrl.trim() === "") && !existingModelProvider) {
106
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${args.profile}" requires base_url.`, {
107
+ profile: args.profile,
108
+ modelProvider: args.profile,
109
+ missingFields: ["base_url"],
100
110
  });
101
111
  }
102
- const upsertProfiles = !existingProfile && args.createProfile
103
- ? {
104
- [args.profile]: (0, config_1.validateManagedProfileCreation)(args.profile, {
105
- model: args.model ?? undefined,
106
- modelProvider: args.profile,
107
- }),
108
- }
109
- : undefined;
110
112
  const upsertModelProviders = args.copilot
111
113
  ? {
112
114
  [args.profile]: (0, providers_1.buildCopilotModelProviderProjection)(runtime),
113
115
  }
114
- : !existingModelProvider && args.createProfile
115
- ? {
116
- [args.profile]: {
117
- baseUrl: args.baseUrl ?? undefined,
118
- },
119
- }
120
- : undefined;
121
- if (existingProfile) {
122
- (0, config_repo_1.requireManagedProfileRuntime)(document, providers, args.profile);
123
- }
116
+ : {
117
+ [args.profile]: (0, providers_1.buildDirectModelProviderProjection)(args.profile, (directBaseUrl ?? existingModelProvider?.baseUrl ?? "").trim()),
118
+ };
124
119
  const apiKey = args.copilot ? args.bridgeApiKey ?? crypto.randomBytes(24).toString("hex") : args.apiKey;
125
120
  const baseUrl = args.copilot ? (0, providers_1.buildCopilotBridgeBaseUrl)(runtime) : args.baseUrl ?? undefined;
126
121
  const next = {
@@ -129,6 +124,7 @@ async function addProvider(args) {
129
124
  [args.providerName]: (0, providers_1.cleanProviderRecord)({
130
125
  profile: args.profile,
131
126
  apiKey,
127
+ model: providerModel,
132
128
  baseUrl,
133
129
  note: args.note ?? undefined,
134
130
  tags: args.tags,
@@ -147,14 +143,16 @@ async function addProvider(args) {
147
143
  ],
148
144
  mutate: () => {
149
145
  const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {
150
- upsertProfiles,
151
146
  upsertModelProviders,
147
+ scrubModelProviderEnvKeys: [args.profile],
152
148
  });
153
149
  // Persist only the normalized provider payload so later reads are deterministic.
154
150
  (0, providers_repo_1.writeProvidersFile)(args.providersPath, next);
155
151
  (0, config_repo_1.applyConfigMutation)(args.configPath, document, configPlan);
156
152
  return {
157
153
  provider: args.providerName,
154
+ model: providerModel,
155
+ modelProvider: args.profile,
158
156
  profile: args.profile,
159
157
  runtimeKind: runtime?.kind ?? null,
160
158
  createdProfileSections: configPlan.createdProfileSections,
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.editProvider = editProvider;
4
4
  const errors_1 = require("../domain/errors");
5
- const config_1 = require("../domain/config");
6
5
  const providers_1 = require("../domain/providers");
7
6
  const config_repo_1 = require("../storage/config-repo");
8
7
  const fs_utils_1 = require("../storage/fs-utils");
@@ -24,6 +23,7 @@ function editProvider(args) {
24
23
  }
25
24
  const updatedFields = [];
26
25
  const nextProfile = args.profile ?? current.profile;
26
+ const nextModel = args.model === null ? undefined : args.model ?? current.model;
27
27
  if (args.profile !== undefined && args.profile !== current.profile) {
28
28
  updatedFields.push("profile");
29
29
  }
@@ -33,6 +33,9 @@ function editProvider(args) {
33
33
  if (args.baseUrl !== undefined && (args.baseUrl ?? undefined) !== current.baseUrl) {
34
34
  updatedFields.push("baseUrl");
35
35
  }
36
+ if (args.model !== undefined && args.model !== current.model) {
37
+ updatedFields.push("model");
38
+ }
36
39
  if (args.note !== undefined && (args.note ?? undefined) !== current.note) {
37
40
  updatedFields.push("note");
38
41
  }
@@ -41,73 +44,46 @@ function editProvider(args) {
41
44
  }
42
45
  const oldProfile = current.profile;
43
46
  const newProfile = nextProfile;
44
- const targetSection = document.profiles.find((profile) => profile.name === newProfile) ?? null;
45
47
  const targetModelProviderSection = document.modelProviders.find((entry) => entry.name === newProfile) ?? null;
46
- const targetProfileExists = Boolean(targetSection);
47
- let upsertProfiles;
48
48
  let upsertModelProviders;
49
- if (!targetProfileExists) {
50
- if (!args.createProfile) {
51
- throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Profile "${newProfile}" does not exist in config.toml.`, {
49
+ const resolvedBaseUrl = (args.baseUrl ?? current.baseUrl ?? targetModelProviderSection?.baseUrl ?? "").trim();
50
+ if (!current.runtime) {
51
+ if (!resolvedBaseUrl) {
52
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${newProfile}" requires base_url.`, {
52
53
  profile: newProfile,
53
- provider: args.providerName,
54
+ modelProvider: newProfile,
55
+ missingFields: ["base_url"],
54
56
  });
55
57
  }
56
- upsertProfiles = {
57
- [newProfile]: (0, config_1.validateManagedProfileCreation)(newProfile, {
58
- model: args.model ?? undefined,
59
- modelProvider: newProfile,
60
- }),
58
+ upsertModelProviders = {
59
+ [newProfile]: (0, providers_1.buildDirectModelProviderProjection)(newProfile, resolvedBaseUrl),
61
60
  };
61
+ }
62
+ else if (targetModelProviderSection || args.profile !== undefined) {
62
63
  upsertModelProviders = {
64
+ ...(upsertModelProviders ?? {}),
63
65
  [newProfile]: {
64
- baseUrl: args.baseUrl ?? undefined,
66
+ ...(current.runtime
67
+ ? {
68
+ baseUrl: current.baseUrl ?? targetModelProviderSection?.baseUrl ?? "",
69
+ name: "copilot",
70
+ requiresOpenAiAuth: true,
71
+ wireApi: "responses",
72
+ }
73
+ : (0, providers_1.buildDirectModelProviderProjection)(newProfile, resolvedBaseUrl)),
65
74
  },
66
75
  };
67
76
  }
68
- else {
69
- (0, config_repo_1.requireManagedProfileRuntime)(document, providers, newProfile);
70
- }
71
77
  const nextRecord = (0, providers_1.cleanProviderRecord)({
72
78
  profile: newProfile,
73
79
  apiKey: args.apiKey ?? current.apiKey,
80
+ model: nextModel,
74
81
  baseUrl: args.baseUrl === null ? undefined : args.baseUrl ?? current.baseUrl,
75
82
  note: args.note === null ? undefined : args.note ?? current.note,
76
83
  tags: args.tags ?? current.tags,
84
+ runtime: current.runtime,
77
85
  });
78
- if (targetProfileExists && args.model !== undefined) {
79
- upsertProfiles = {
80
- [newProfile]: {
81
- ...(args.model !== undefined && args.model !== null ? { model: args.model } : {}),
82
- },
83
- };
84
- if (args.model !== undefined && targetSection?.model !== args.model && !updatedFields.includes("model")) {
85
- updatedFields.push("model");
86
- }
87
- }
88
- // Compute profile link ownership after the edit so lifecycle planning can decide whether sections stay, move, or delete.
89
- const remainingLinksByProfile = new Map();
90
- for (const [name, provider] of Object.entries(providers.providers)) {
91
- if (name === args.providerName) {
92
- continue;
93
- }
94
- const list = remainingLinksByProfile.get(provider.profile) ?? [];
95
- list.push(name);
96
- remainingLinksByProfile.set(provider.profile, list);
97
- }
98
- if (newProfile !== oldProfile) {
99
- const list = remainingLinksByProfile.get(newProfile) ?? [];
100
- list.push(args.providerName);
101
- remainingLinksByProfile.set(newProfile, list);
102
- }
103
- const lifecycle = (0, config_1.planProfileLifecycleOutcome)({
104
- providerName: args.providerName,
105
- oldProfile,
106
- newProfile,
107
- activeProfile: document.activeProfile,
108
- remainingLinksByProfile,
109
- switchToProfile: args.switchToProfile ?? null,
110
- });
86
+ const isActive = document.currentModelProvider === oldProfile;
111
87
  return (0, run_mutation_1.runMutation)({
112
88
  lockPath: args.lockPath,
113
89
  backupsDir: args.backupsDir,
@@ -119,10 +95,12 @@ function editProvider(args) {
119
95
  ],
120
96
  mutate: () => {
121
97
  const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {
122
- upsertProfiles,
123
98
  upsertModelProviders,
124
- deleteProfiles: lifecycle.deletedProfileSections,
125
- setActiveProfile: lifecycle.nextActiveProfile,
99
+ setCurrentModel: isActive ? nextModel ?? document.currentModel : undefined,
100
+ setCurrentModelProvider: isActive ? newProfile : undefined,
101
+ deleteLegacyProfile: isActive,
102
+ deleteLegacyProfilesByName: isActive ? [newProfile] : [],
103
+ scrubModelProviderEnvKeys: [newProfile],
126
104
  });
127
105
  const nextProviders = {
128
106
  providers: {
@@ -135,12 +113,13 @@ function editProvider(args) {
135
113
  (0, config_repo_1.applyConfigMutation)(args.configPath, document, configPlan);
136
114
  return {
137
115
  provider: args.providerName,
116
+ modelProvider: newProfile,
138
117
  updatedFields,
139
118
  createdProfileSections: configPlan.createdProfileSections,
140
119
  createdModelProviderSections: configPlan.createdModelProviderSections,
141
120
  deletedProfileSections: configPlan.deletedProfileSections,
142
- keptSharedProfiles: lifecycle.keptSharedProfiles,
143
- switchedActiveProfile: lifecycle.switchedActiveProfile,
121
+ keptSharedProfiles: [],
122
+ switchedActiveProfile: isActive && newProfile !== oldProfile,
144
123
  adoptedProfiles: [],
145
124
  repairedProfiles: [],
146
125
  };
@@ -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
  },