@bitkyc08/opencodex 2.77.0 → 2.78.0

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 (222) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-BtfG5id-.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
  5. package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  15. package/src/adapters/devin/trajectory.ts +49 -0
  16. package/src/adapters/devin.ts +13 -0
  17. package/src/adapters/ollama-native.ts +35 -19
  18. package/src/claude/intercept/client-hello.ts +103 -0
  19. package/src/claude/intercept/model-bindings.ts +3 -2
  20. package/src/claude/intercept/picker-listener.ts +279 -66
  21. package/src/cli/access-audio-input.ts +84 -0
  22. package/src/cli/access-audio-live.ts +99 -0
  23. package/src/cli/access-audio.ts +123 -0
  24. package/src/cli/access-data-client.ts +127 -0
  25. package/src/cli/access-data-plane.ts +104 -0
  26. package/src/cli/access-data-response.ts +76 -0
  27. package/src/cli/access.ts +65 -2
  28. package/src/cli/account-api.ts +30 -6
  29. package/src/cli/account-auth.ts +111 -42
  30. package/src/cli/account-key-quota.ts +118 -0
  31. package/src/cli/account-main.ts +11 -4
  32. package/src/cli/account-next-actions.ts +63 -0
  33. package/src/cli/account-policy-dto.ts +68 -0
  34. package/src/cli/account-policy.ts +167 -0
  35. package/src/cli/account-target.ts +8 -0
  36. package/src/cli/account.ts +34 -8
  37. package/src/cli/agent-runtime-settings.ts +130 -0
  38. package/src/cli/agent-settings.ts +101 -0
  39. package/src/cli/agent.ts +19 -2
  40. package/src/cli/alias.ts +7 -2
  41. package/src/cli/aside-profiles.ts +2 -2
  42. package/src/cli/capabilities-access-remote.ts +147 -0
  43. package/src/cli/capabilities-accounts.ts +195 -0
  44. package/src/cli/capabilities-agents-routing.ts +414 -0
  45. package/src/cli/capabilities-base.ts +783 -0
  46. package/src/cli/capabilities-command.ts +1 -0
  47. package/src/cli/capabilities-integrations.ts +264 -0
  48. package/src/cli/capabilities-lab.ts +214 -0
  49. package/src/cli/capabilities-observe-system.ts +662 -0
  50. package/src/cli/capabilities-provider-models.ts +302 -0
  51. package/src/cli/capabilities.ts +30 -1199
  52. package/src/cli/capability-types.ts +54 -0
  53. package/src/cli/catalog-command-result.ts +57 -0
  54. package/src/cli/chatgpt-command.ts +25 -2
  55. package/src/cli/claude-agent-startup-sync.ts +17 -0
  56. package/src/cli/claude-desktop-profile.ts +100 -0
  57. package/src/cli/claude-desktop.ts +39 -6
  58. package/src/cli/codex-shim-autorestore.ts +2 -1
  59. package/src/cli/codex-shim-readiness.ts +23 -3
  60. package/src/cli/combo-input.ts +88 -0
  61. package/src/cli/combo-result.ts +52 -0
  62. package/src/cli/combo-stats.ts +123 -0
  63. package/src/cli/combo.ts +37 -14
  64. package/src/cli/companion-timeline.ts +142 -0
  65. package/src/cli/companion-usage.ts +146 -0
  66. package/src/cli/companion.ts +12 -3
  67. package/src/cli/config-command.ts +24 -8
  68. package/src/cli/dispatch.ts +52 -79
  69. package/src/cli/doctor.ts +22 -4
  70. package/src/cli/help-catalog.ts +31 -14
  71. package/src/cli/help-recovery.ts +2 -2
  72. package/src/cli/help.ts +25 -11
  73. package/src/cli/index.ts +38 -27
  74. package/src/cli/injection-follow.ts +35 -0
  75. package/src/cli/inspect.ts +5 -0
  76. package/src/cli/integration-aside-sync.ts +69 -0
  77. package/src/cli/integration-cursor.ts +106 -0
  78. package/src/cli/integration-input.ts +60 -0
  79. package/src/cli/integration-journal.ts +49 -0
  80. package/src/cli/integration-plan-dto.ts +156 -0
  81. package/src/cli/integration-preview.ts +215 -0
  82. package/src/cli/integrations.ts +24 -18
  83. package/src/cli/json-input.ts +134 -0
  84. package/src/cli/link.ts +32 -4
  85. package/src/cli/local-sync-result.ts +41 -0
  86. package/src/cli/log-follow.ts +84 -0
  87. package/src/cli/log-view-filter.ts +146 -0
  88. package/src/cli/logout-command.ts +44 -0
  89. package/src/cli/model-picker-ordering.ts +137 -0
  90. package/src/cli/models-custom-input.ts +107 -0
  91. package/src/cli/models-custom-runtime.ts +112 -0
  92. package/src/cli/models-order.ts +194 -0
  93. package/src/cli/models-runtime-subcommands.ts +2 -0
  94. package/src/cli/models-runtime.ts +7 -0
  95. package/src/cli/models.ts +66 -81
  96. package/src/cli/observe-stream.ts +142 -0
  97. package/src/cli/observe.ts +75 -34
  98. package/src/cli/provider-batch.ts +77 -0
  99. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  100. package/src/cli/provider-result.ts +124 -0
  101. package/src/cli/provider-runtime.ts +15 -12
  102. package/src/cli/provider-settings.ts +143 -0
  103. package/src/cli/provider.ts +112 -34
  104. package/src/cli/registry.ts +62 -20
  105. package/src/cli/remote-workspace-hub.ts +83 -0
  106. package/src/cli/remote-workspace.ts +6 -1
  107. package/src/cli/restart-failure.ts +27 -0
  108. package/src/cli/root.ts +12 -0
  109. package/src/cli/route-policy-write.ts +137 -0
  110. package/src/cli/route-policy.ts +8 -0
  111. package/src/cli/runtime-api.ts +56 -79
  112. package/src/cli/secret-args.ts +149 -0
  113. package/src/cli/settings-result.ts +30 -0
  114. package/src/cli/storage.ts +77 -7
  115. package/src/cli/system-command.ts +60 -2
  116. package/src/cli/system-restart-client.ts +20 -3
  117. package/src/cli/system-settings-parity.ts +83 -0
  118. package/src/cli/update-restart-candidate.ts +21 -0
  119. package/src/cli/update-restart-child.ts +81 -0
  120. package/src/cli/update-restart-home.ts +38 -0
  121. package/src/cli/update-restart-transport.ts +193 -0
  122. package/src/cli/update-restart.ts +260 -0
  123. package/src/cli/usage-model-search.ts +58 -0
  124. package/src/cli/usage-report.ts +13 -5
  125. package/src/cli/v2-input.ts +66 -0
  126. package/src/cli/v2-local-output.ts +37 -0
  127. package/src/cli/v2-runtime.ts +90 -0
  128. package/src/cli/v2.ts +102 -144
  129. package/src/clients/config-export/dsh.ts +21 -0
  130. package/src/clients/config-export.ts +7 -1
  131. package/src/codex/catalog/build-entries.ts +1 -1
  132. package/src/codex/catalog/bundled.ts +9 -2
  133. package/src/codex/catalog/heal-observation.ts +66 -0
  134. package/src/codex/catalog/publication-observer.ts +19 -0
  135. package/src/codex/catalog/remote.ts +13 -15
  136. package/src/codex/catalog/restore.ts +19 -6
  137. package/src/codex/catalog/retained-sync.ts +58 -10
  138. package/src/codex/catalog/routed-removal.ts +116 -0
  139. package/src/codex/catalog/write-audit-contract.ts +21 -0
  140. package/src/codex/catalog/write-audit.ts +171 -0
  141. package/src/codex/catalog-self-heal.ts +284 -0
  142. package/src/codex/catalog-write-serialization.ts +71 -7
  143. package/src/codex/codex-home-owner.ts +124 -0
  144. package/src/codex/convergence.ts +55 -15
  145. package/src/codex/features.ts +8 -1
  146. package/src/codex/inject/restore.ts +46 -4
  147. package/src/codex/inject.ts +11 -1
  148. package/src/codex/internal/catalog-writer.ts +120 -13
  149. package/src/codex/journal.ts +49 -13
  150. package/src/codex/management-convergence.ts +3 -0
  151. package/src/codex/quota-types.ts +3 -0
  152. package/src/codex/quota.ts +13 -3
  153. package/src/codex/refresh.ts +20 -9
  154. package/src/codex/runtime.ts +7 -1
  155. package/src/codex/shim-diagnostics.ts +80 -0
  156. package/src/codex/shim-fingerprint.ts +7 -2
  157. package/src/codex/shim-migration.ts +153 -0
  158. package/src/codex/shim-overlay.ts +509 -0
  159. package/src/codex/shim-path-resolution.ts +14 -2
  160. package/src/codex/shim-probe.ts +4 -3
  161. package/src/codex/shim-state-file.ts +93 -7
  162. package/src/codex/shim.ts +15 -68
  163. package/src/codex/sync.ts +24 -4
  164. package/src/combos/failover.ts +19 -0
  165. package/src/combos/index.ts +1 -0
  166. package/src/combos/resolve.ts +12 -0
  167. package/src/config/atomic-write.ts +13 -8
  168. package/src/config/diagnostics.ts +19 -1
  169. package/src/config/load-degrade.ts +3 -0
  170. package/src/config/schema/chatgpt-desktop.ts +22 -0
  171. package/src/config/schema/config-schema.ts +32 -14
  172. package/src/config/schema/leaf-validators.ts +3 -2
  173. package/src/generated/compatibility-version.json +445 -129
  174. package/src/integrations/merge.ts +13 -5
  175. package/src/integrations/mutation-plan.ts +46 -3
  176. package/src/integrations/omp-yaml-source.ts +181 -1
  177. package/src/integrations/ownership-policy.ts +66 -0
  178. package/src/integrations/registry.ts +50 -1
  179. package/src/integrations/state.ts +46 -21
  180. package/src/integrations/target.ts +25 -2
  181. package/src/integrations/writer.ts +89 -31
  182. package/src/lib/package-tree-integrity.ts +42 -1
  183. package/src/lib/proxy-env.ts +3 -0
  184. package/src/lib/test-home-guard.ts +2 -1
  185. package/src/oauth/anthropic-account-refusal.ts +19 -8
  186. package/src/oauth/anthropic-routing.ts +17 -12
  187. package/src/oauth/anthropic.ts +27 -2
  188. package/src/oauth/index.ts +6 -8
  189. package/src/oauth/pool-settings-capability.ts +9 -2
  190. package/src/protocols/plan-snapshot.ts +1 -1
  191. package/src/protocols/settings.ts +28 -4
  192. package/src/server/caller-session-identity.ts +32 -0
  193. package/src/server/claude-messages.ts +11 -4
  194. package/src/server/index/serve-options.ts +12 -7
  195. package/src/server/index.ts +1 -1
  196. package/src/server/management/agent-settings-routes.ts +6 -5
  197. package/src/server/management/context.ts +8 -1
  198. package/src/server/management/cursor-integration-routes.ts +3 -3
  199. package/src/server/management/integration-routes.ts +5 -5
  200. package/src/server/management/model-routes.ts +3 -3
  201. package/src/server/management/native-integration-routes.ts +2 -2
  202. package/src/server/management/oauth-account-routes.ts +96 -30
  203. package/src/server/management/route-registry.ts +25 -24
  204. package/src/server/messages-native-eligibility.ts +3 -6
  205. package/src/server/messages-native-oauth.ts +82 -35
  206. package/src/server/messages-native.ts +78 -16
  207. package/src/server/responses/adapter-dispatch.ts +53 -28
  208. package/src/server/responses/core-auth.ts +10 -4
  209. package/src/server/responses/core-combo.ts +46 -6
  210. package/src/service/cli.ts +6 -1
  211. package/src/service/ownership-mutation-lease.d.mts +26 -0
  212. package/src/service/ownership-mutation-lease.mjs +76 -1
  213. package/src/types/config.ts +2 -0
  214. package/src/types/provider.ts +10 -10
  215. package/src/update/index.ts +2 -1
  216. package/src/update/update-failure-guidance.d.mts +6 -0
  217. package/src/update/update-failure-guidance.mjs +30 -5
  218. package/gui/dist/assets/App-COdEeqSj.css +0 -1
  219. package/gui/dist/assets/App-DEpCJEP_.js +0 -54
  220. package/gui/dist/assets/index-Cd7ittMv.css +0 -1
  221. package/gui/dist/assets/index-DwJKFJm5.js +0 -94
  222. package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
@@ -0,0 +1,112 @@
1
+ import { isValidProviderName } from "../config/provider-name";
2
+ import { isDeclaredReasoningEffort } from "../reasoning-effort";
3
+ import { resolveSlugSelection } from "../providers/slug-codec";
4
+ import { printCatalogResult, runCatalogAction } from "./catalog-command-result";
5
+ import { serializeManagementJson } from "./json-input";
6
+ import { parseCustomModelAddInput } from "./models-custom-input";
7
+ import {
8
+ CliUsageError, RuntimeApiError, runtimeBaseUrl, runtimeRequest, takeFlag,
9
+ type RuntimeApiDeps,
10
+ } from "./runtime-api";
11
+
12
+ function record(value: unknown): value is Record<string, unknown> {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value);
14
+ }
15
+
16
+ function identity(value: unknown): value is string {
17
+ return typeof value === "string" && value.trim().length > 0 && value.trim() === value;
18
+ }
19
+
20
+ function modelIdentity(value: unknown): { id: string; provider: string; modelId: string } {
21
+ if (!record(value) || !identity(value.id) || !identity(value.provider)
22
+ || !isValidProviderName(value.provider) || !identity(value.modelId)) {
23
+ throw new Error("Invalid custom model identity");
24
+ }
25
+ return { id: value.id, provider: value.provider, modelId: value.modelId };
26
+ }
27
+
28
+ /** Rebuild the public stored-entry DTO; never copy arbitrary response properties. */
29
+ function addedModel(value: unknown, provider: string, modelId: string): Record<string, unknown> {
30
+ const result: Record<string, unknown> = modelIdentity(value);
31
+ if (!record(value) || result.provider !== provider || result.modelId !== modelId
32
+ || !identity(value.addedAt) || !Number.isFinite(Date.parse(value.addedAt))) {
33
+ throw new Error("Invalid custom model response");
34
+ }
35
+ result.addedAt = value.addedAt;
36
+ if (value.displayName !== undefined) {
37
+ if (!identity(value.displayName) || value.displayName.includes("/")) throw new Error("Invalid display name response");
38
+ result.displayName = value.displayName;
39
+ }
40
+ if (value.contextWindow !== undefined) {
41
+ if (typeof value.contextWindow !== "number" || !Number.isSafeInteger(value.contextWindow) || value.contextWindow <= 0) {
42
+ throw new Error("Invalid context response");
43
+ }
44
+ result.contextWindow = value.contextWindow;
45
+ }
46
+ for (const key of ["inputModalities", "reasoningEfforts"] as const) {
47
+ const values = value[key];
48
+ if (values === undefined) continue;
49
+ if (!Array.isArray(values) || !values.every(item => typeof item === "string"
50
+ && (key === "inputModalities" ? ["text", "image", "audio"].includes(item) : isDeclaredReasoningEffort(item)))
51
+ || new Set(values).size !== values.length || (key === "inputModalities" && values.length === 0)) {
52
+ throw new Error("Invalid model metadata response");
53
+ }
54
+ result[key] = [...values];
55
+ }
56
+ if (value.defaultReasoningEffort !== undefined) {
57
+ if (typeof value.defaultReasoningEffort !== "string" || !Array.isArray(result.reasoningEfforts)
58
+ || !result.reasoningEfforts.includes(value.defaultReasoningEffort)) throw new Error("Invalid default effort response");
59
+ result.defaultReasoningEffort = value.defaultReasoningEffort;
60
+ }
61
+ return result;
62
+ }
63
+
64
+ function resolveRemoval(value: unknown, target: string): { id: string; provider: string; modelId: string } {
65
+ if (!Array.isArray(value)) throw new Error("Invalid custom model roster");
66
+ const rows = value.map(modelIdentity);
67
+ if (new Set(rows.map(row => row.id)).size !== rows.length) throw new Error("Duplicate stored model IDs");
68
+ const byId = rows.find(row => row.id === target);
69
+ if (byId) return byId;
70
+ const separator = target.indexOf("/");
71
+ const provider = separator >= 0 ? target.slice(0, separator) : undefined;
72
+ const roster = provider === undefined ? [] : rows.filter(row => row.provider === provider);
73
+ const admitted = new Set(provider === undefined ? []
74
+ : resolveSlugSelection(provider, target, roster.map(row => row.modelId)).matched);
75
+ const matches = roster.filter(row => admitted.has(row.modelId));
76
+ if (matches.length === 0) throw new RuntimeApiError("Custom model not found", 404, null);
77
+ if (matches.length !== 1) throw new CliUsageError("Ambiguous custom model selector; use the complete stored ID");
78
+ return matches[0]!;
79
+ }
80
+
81
+ export async function handleModelsCustomRuntimeCommand(
82
+ sub: "add" | "remove", argv: string[], deps: RuntimeApiDeps = {},
83
+ ): Promise<number> {
84
+ return runCatalogAction(async () => {
85
+ const args = [...argv];
86
+ const wantsJson = takeFlag(args, "--json");
87
+ if (sub === "add") {
88
+ const input = parseCustomModelAddInput(args);
89
+ const body = serializeManagementJson(input);
90
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
91
+ const response = await runtimeRequest("/api/custom-models", { method: "POST", redirect: "error", body }, pinned);
92
+ const result = addedModel(response, input.provider, input.modelId);
93
+ return printCatalogResult(result, record(response) ? response.catalogRefresh : undefined, wantsJson,
94
+ [`Saved custom model ${input.provider}/${input.modelId} (${result.id}).`]);
95
+ }
96
+ const confirmed = takeFlag(args, "--yes");
97
+ if (!confirmed) throw new CliUsageError("Live custom model removal requires --yes");
98
+ if (args.length !== 1 || !args[0]?.trim() || args[0].startsWith("-")) {
99
+ throw new CliUsageError("A complete stored ID or provider/model selector is required");
100
+ }
101
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
102
+ const roster = await runtimeRequest("/api/custom-models", { redirect: "error" }, pinned);
103
+ const selected = resolveRemoval(roster, args[0].trim());
104
+ if (selected.id === "." || selected.id === "..") throw new Error("Stored ID is not addressable as a route segment");
105
+ const response = await runtimeRequest(`/api/custom-models/${encodeURIComponent(selected.id)}`, {
106
+ method: "DELETE", redirect: "error",
107
+ }, pinned);
108
+ if (!record(response) || response.ok !== true) throw new Error("Invalid custom model removal response");
109
+ return printCatalogResult({ ok: true, ...selected }, response.catalogRefresh, wantsJson,
110
+ [`Removed custom model ${selected.provider}/${selected.modelId} (${selected.id}).`]);
111
+ });
112
+ }
@@ -0,0 +1,194 @@
1
+ import { isValidProviderName } from "../config/provider-name";
2
+ import { modelDisplayNamesConfigError } from "../config/provider-validation";
3
+ import { isValidModelDiscoveryModelId } from "../providers/model-discovery-limits";
4
+ import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
5
+ import { printCatalogResult, runCatalogAction } from "./catalog-command-result";
6
+ import { serializeManagementJson } from "./json-input";
7
+ import {
8
+ customPickerRows, isModelPickerUsage, isPickerOrderSaved, isPickerOrderSettings,
9
+ modelPickerOrder, normalizePickerIds, pickerIdentityCoverage,
10
+ type PickerModelIdentity, type PickerOrderSettings, type ModelPickerUsage,
11
+ } from "./model-picker-ordering";
12
+ import {
13
+ CliUsageError, RuntimeApiError, printData, runtimeBaseUrl, runtimeRequest,
14
+ takeFlag, takeOptionWithSyntax, type RuntimeApiDeps,
15
+ } from "./runtime-api";
16
+
17
+ const ORDER_USAGE = "Usage: ocx models order status|reset|set (--models CSV | --mode default|alphabetical|provider|most-used) [--json]";
18
+ const DISPLAY_USAGE = "Usage: ocx models display-name PROVIDER/MODEL (--set TEXT | --clear) [--json]";
19
+ const SETTINGS = "/api/subagent-models";
20
+ function record(value: unknown): value is Record<string, unknown> {
21
+ return value !== null && typeof value === "object" && !Array.isArray(value);
22
+ }
23
+ function stringList(value: unknown): value is string[] {
24
+ return Array.isArray(value) && value.every(item => typeof item === "string");
25
+ }
26
+ function unusable(): never { throw new Error("Invalid model management response"); }
27
+
28
+ async function readSettings(deps: RuntimeApiDeps): Promise<PickerOrderSettings> {
29
+ const value = await runtimeRequest(SETTINGS, { redirect: "error" }, deps);
30
+ if (!isPickerOrderSettings(value) || value.pickerAvailable.some(id => !id.includes("/"))
31
+ || new Set(value.pickerAvailable).size !== value.pickerAvailable.length) unusable();
32
+ return { pickerAvailable: value.pickerAvailable, pickerOrder: value.pickerOrder,
33
+ pickerOrderMode: value.pickerOrderMode, ...(value.chosen === undefined ? {} : { chosen: value.chosen }) };
34
+ }
35
+ async function readIdentities(deps: RuntimeApiDeps): Promise<PickerModelIdentity[]> {
36
+ const rows = await runtimeRequest("/api/models", { redirect: "error" }, deps);
37
+ if (!Array.isArray(rows)) unusable();
38
+ return rows.map(row => {
39
+ if (!record(row) || typeof row.provider !== "string" || !row.provider.trim()
40
+ || !isValidModelDiscoveryModelId(row.id) || !isValidModelDiscoveryModelId(row.namespaced)) unusable();
41
+ return { provider: row.provider, id: row.id, namespaced: row.namespaced };
42
+ });
43
+ }
44
+ function snapshot(settings: PickerOrderSettings, identities: PickerModelIdentity[]): string {
45
+ return JSON.stringify([settings, identities]);
46
+ }
47
+ function assertRouted(settings: PickerOrderSettings): void {
48
+ if (settings.pickerOrder.some(id => !id.includes("/"))) {
49
+ throw new CliUsageError("Saved order includes native models. Run ocx models order reset before replacing it.");
50
+ }
51
+ if (settings.pickerAvailable.length === 0) throw new CliUsageError("No routed picker candidates are available.");
52
+ }
53
+ function manualOrder(csv: string, settings: PickerOrderSettings, identities: PickerModelIdentity[]): string[] {
54
+ const tokens = csv.split(",").map(id => id.trim());
55
+ const order = tokens.map(token => {
56
+ if (!token) throw new CliUsageError("--models cannot contain blank entries", ORDER_USAGE);
57
+ const result = normalizePickerIds([token], settings.pickerAvailable, identities);
58
+ if (result.length !== 1) throw new CliUsageError("--models contains an unknown or ambiguous model", ORDER_USAGE);
59
+ return result[0]!;
60
+ });
61
+ if (new Set(order).size !== order.length || order.length !== settings.pickerAvailable.length) {
62
+ throw new CliUsageError("--models must contain every routed candidate exactly once", ORDER_USAGE);
63
+ }
64
+ const draft = customPickerRows(settings, identities);
65
+ if (!draft) throw new CliUsageError("Featured state or model identities are incomplete. Read status before trying again.");
66
+ if (!draft.fixed.every((id, index) => order[index] === id)) {
67
+ throw new CliUsageError("--models must keep the current featured models as its exact leading prefix.");
68
+ }
69
+ return order;
70
+ }
71
+ async function readUsage(deps: RuntimeApiDeps): Promise<ModelPickerUsage[]> {
72
+ const value = await runtimeRequest("/api/usage?range=all&surface=all", { redirect: "error" }, deps);
73
+ if (!record(value) || (value.usageIncomplete !== undefined && value.usageIncomplete !== false)
74
+ || !isModelPickerUsage(value.models)) unusable();
75
+ return value.models;
76
+ }
77
+ function printOrderSave(value: unknown, expected: string[] | null, mode: string | null, wantsJson: boolean): number {
78
+ if (!record(value) || value.ok !== true || !isPickerOrderSaved(value)
79
+ || !stringList(value.applied) || (value.force !== null && typeof value.force !== "string")
80
+ || JSON.stringify(value.pickerOrder) !== JSON.stringify(expected ?? []) || value.pickerOrderMode !== mode) unusable();
81
+ return printCatalogResult({ ok: true, pickerOrder: value.pickerOrder, pickerOrderMode: value.pickerOrderMode,
82
+ applied: value.applied, force: value.force }, value.catalogRefresh, wantsJson,
83
+ ["Picker order saved.", `Order: ${value.pickerOrder.join(", ") || "default"}`]);
84
+ }
85
+
86
+ export async function handleModelsOrderCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
87
+ return runCatalogAction(async () => {
88
+ const args = [...argv];
89
+ const sub = args.shift();
90
+ const wantsJson = takeFlag(args, "--json");
91
+ const csv = takeOptionWithSyntax(args, "--models")?.value;
92
+ const mode = takeOptionWithSyntax(args, "--mode")?.value;
93
+ if (args.length || !["status", "reset", "set"].includes(sub ?? "")
94
+ || (sub !== "set" && (csv !== undefined || mode !== undefined))
95
+ || (sub === "set" && ((csv === undefined) === (mode === undefined)))
96
+ || (mode !== undefined && !["default", "alphabetical", "provider", "most-used"].includes(mode))) {
97
+ throw new CliUsageError("Invalid picker order arguments", ORDER_USAGE);
98
+ }
99
+ // Refuse lexical manual-input failures before discovering a target.
100
+ if (csv !== undefined) {
101
+ const tokens = csv.split(",").map(id => id.trim());
102
+ if (tokens.some(id => !id) || new Set(tokens).size !== tokens.length) {
103
+ throw new CliUsageError("--models requires unique nonblank entries", ORDER_USAGE);
104
+ }
105
+ }
106
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
107
+ if (sub === "status") {
108
+ const settings = await readSettings(pinned);
109
+ printData(settings, wantsJson, [
110
+ `Saved mode: ${settings.pickerOrderMode ?? (settings.pickerOrder.length ? "custom" : "default")}`,
111
+ `Saved order: ${settings.pickerOrder.join(", ") || "default"}`,
112
+ `Routed candidates: ${settings.pickerAvailable.join(", ") || "none"}`,
113
+ `Featured: ${settings.chosen === undefined ? "unknown" : settings.chosen.join(", ") || "none"}`,
114
+ ]);
115
+ return 0;
116
+ }
117
+ let order: string[] | null = null;
118
+ let savedMode: string | null = null;
119
+ if (sub !== "reset" && mode !== "default") {
120
+ const settings = await readSettings(pinned);
121
+ assertRouted(settings);
122
+ const identities = await readIdentities(pinned);
123
+ if (!pickerIdentityCoverage(settings.pickerAvailable, identities)) {
124
+ throw new CliUsageError("Model identities are incomplete or ambiguous; picker order was not changed.");
125
+ }
126
+ if (csv !== undefined) {
127
+ order = manualOrder(csv, settings, identities);
128
+ const freshSettings = await readSettings(pinned);
129
+ const freshIdentities = await readIdentities(pinned);
130
+ if (snapshot(settings, identities) !== snapshot(freshSettings, freshIdentities)) {
131
+ throw new RuntimeApiError("Picker state changed", 409, null);
132
+ }
133
+ } else {
134
+ const preset = mode as "alphabetical" | "provider" | "most-used";
135
+ order = modelPickerOrder(preset, settings.pickerAvailable, preset === "most-used" ? await readUsage(pinned) : [], identities);
136
+ savedMode = preset;
137
+ }
138
+ }
139
+ const value = await runtimeRequest(SETTINGS, { method: "PUT", redirect: "error",
140
+ body: serializeManagementJson({ pickerOrder: order, pickerOrderMode: savedMode }) }, pinned);
141
+ return printOrderSave(value, order, savedMode, wantsJson);
142
+ });
143
+ }
144
+
145
+ function displayReceipt(value: unknown, provider: string, modelId: string, label: string | null, partial: boolean): Record<string, unknown> {
146
+ if (!record(value) || value.provider !== provider || value.modelId !== modelId
147
+ || value.displayNameOverride !== label || (partial ? value.saved !== true : value.ok !== true)) unusable();
148
+ const projected: Record<string, unknown> = { ...(partial ? { saved: true } : { ok: true }), provider, modelId,
149
+ displayNameOverride: value.displayNameOverride };
150
+ if (partial) {
151
+ if (normalizeCatalogDisposition(value.catalogRefresh)?.status !== "failed") unusable();
152
+ } else {
153
+ if (typeof value.displayName !== "string" || !value.displayName.trim()
154
+ || typeof value.displayNameSource !== "string"
155
+ || !["operator", "provider", "fallback"].includes(value.displayNameSource)) unusable();
156
+ projected.displayName = value.displayName;
157
+ projected.displayNameSource = value.displayNameSource;
158
+ }
159
+ return projected;
160
+ }
161
+ export async function handleModelsDisplayNameCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
162
+ return runCatalogAction(async () => {
163
+ const args = [...argv];
164
+ const selector = args.shift() ?? "";
165
+ const wantsJson = takeFlag(args, "--json");
166
+ const set = takeOptionWithSyntax(args, "--set")?.value;
167
+ const clear = takeFlag(args, "--clear");
168
+ const slash = selector.indexOf("/");
169
+ const provider = selector.slice(0, slash), modelId = selector.slice(slash + 1);
170
+ if (args.length || slash < 1 || !isValidProviderName(provider) || !isValidModelDiscoveryModelId(modelId)
171
+ || (set !== undefined) === clear) throw new CliUsageError("Invalid display-name arguments", DISPLAY_USAGE);
172
+ const label = set === undefined ? null : set.trim();
173
+ if (label !== null && modelDisplayNamesConfigError({ [modelId]: label }) !== null) {
174
+ throw new CliUsageError("Display name must be a valid nonblank label without slashes or control characters", DISPLAY_USAGE);
175
+ }
176
+ const body = serializeManagementJson({ modelId, displayName: label });
177
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
178
+ let value: unknown;
179
+ let partial = false;
180
+ try {
181
+ value = await runtimeRequest(`/api/providers/${encodeURIComponent(provider)}/model-display-names`, {
182
+ method: "PUT", redirect: "error", body,
183
+ }, pinned);
184
+ } catch (error) {
185
+ if (!(error instanceof RuntimeApiError) || error.status !== 503) throw error;
186
+ value = error.body;
187
+ partial = true;
188
+ }
189
+ const result = displayReceipt(value, provider, modelId, label, partial);
190
+ const refresh = (value as Record<string, unknown>).catalogRefresh;
191
+ return printCatalogResult(result, refresh, wantsJson,
192
+ [`Display name saved for ${provider}/${modelId}: ${label ?? "override cleared"}.`]);
193
+ });
194
+ }
@@ -28,6 +28,8 @@ export const MODELS_RUNTIME_SUBCOMMANDS = [
28
28
  "new-arrivals",
29
29
  "context",
30
30
  "shadow",
31
+ "order",
32
+ "display-name",
31
33
  ] as const;
32
34
 
33
35
  export type ModelsRuntimeSubcommand = (typeof MODELS_RUNTIME_SUBCOMMANDS)[number];
@@ -24,6 +24,9 @@ import { MAX_COST4_RATE } from "../usage/expected-prices";
24
24
  import { isValidCost4Rate } from "../usage/user-cost-overlays";
25
25
 
26
26
  const USAGE = `Usage:
27
+ ocx models display-name <provider/raw-model> (--set <text> | --clear) [--json]
28
+ ocx models order status|reset [--json]
29
+ ocx models order set (--models <csv> | --mode <default|alphabetical|provider|most-used>) [--json]
27
30
  ocx models live [--provider <name>] [--free-only] [--json]
28
31
  ocx models price <provider/model> [--json]
29
32
  ocx models set-price <provider/model> --input N --output N [--cache-read N] [--cache-write N] [--json]
@@ -564,6 +567,10 @@ async function shadow(argv: string[], deps: RuntimeApiDeps): Promise<void> {
564
567
  }
565
568
 
566
569
  export async function handleModelsRuntimeCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
570
+ if (sub === "order" || sub === "display-name") {
571
+ const { handleModelsOrderCommand, handleModelsDisplayNameCommand } = await import("./models-order");
572
+ return sub === "order" ? handleModelsOrderCommand(argv, deps) : handleModelsDisplayNameCommand(argv, deps);
573
+ }
567
574
  // The dispatch below and MODELS_RUNTIME_SUBCOMMANDS must name the same set;
568
575
  // tests/cli/cli-models-runtime-dispatch.test.ts fails if they drift (#3094).
569
576
  if (!isModelsRuntimeSubcommand(sub)) return null;
package/src/cli/models.ts CHANGED
@@ -8,9 +8,7 @@ import { syncModelsToCodex } from "../codex/sync";
8
8
  import { configuredContextWindow } from "../codex/catalog/provider-fetch";
9
9
  import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
10
10
  import {
11
- canonicalizeReasoningEfforts,
12
11
  configuredReasoningEfforts,
13
- isDeclaredReasoningEffort,
14
12
  modelRecordValue,
15
13
  } from "../reasoning-effort";
16
14
  import { encodedModelIdCollides, resolveSlugSelection, routedSlug } from "../providers/slug-codec";
@@ -18,66 +16,19 @@ import { isModelsRuntimeSubcommand } from "./models-runtime-subcommands";
18
16
  import { knownModelIdsForProvider } from "../router";
19
17
  import { findLiveProxy } from "../server/proxy-liveness";
20
18
  import { modelInList, type OcxConfig, type OcxCustomModel } from "../types";
19
+ import type { RuntimeApiDeps } from "./runtime-api";
20
+ import { projectLocalSyncResult, type LocalSyncResult } from "./local-sync-result";
21
21
 
22
- const ADD_USAGE = "Usage: ocx models add <provider> <modelId> [--display-name <name>] [--context-window <tokens>] [--modalities text,image,audio] [--reasoning-efforts <none,minimal,low,medium,high,xhigh,max,ultra>] [--default-reasoning-effort <level>]";
23
- const REMOVE_USAGE = "Usage: ocx models remove <customId|provider/modelId> [--yes]";
22
+ export interface ModelsCommandDeps extends RuntimeApiDeps { syncModels?: typeof syncModelsToCodex; }
23
+
24
+ const ADD_USAGE = "Usage: ocx models add <provider> <modelId> [--display-name <name>] [--context-window <tokens>] [--modalities text,image,audio] [--reasoning-efforts <none,minimal,low,medium,high,xhigh,max,ultra>] [--default-reasoning-effort <level>] [--live] [--json]";
25
+ const REMOVE_USAGE = "Usage: ocx models remove <customId|provider/modelId> [--yes] [--live] [--json]";
24
26
  const LIST_CUSTOM_USAGE = "Usage: ocx models list-custom [--json]";
25
27
  const ALLOWED_MODALITIES = new Set(["text", "image", "audio"]);
26
28
 
27
- /**
28
- * Parse and validate the reasoning flags shared by `ocx models add` (offline path).
29
- * "-" means "inherit" and omits the field entirely; "" means an explicit empty ladder
30
- * ("no reasoning" override, the same state the dashboard stores for the toggle-off
31
- * checkbox set). Malformed CSV like `low,,high` or `,,` is rejected instead of being
32
- * silently normalized. Values are canonicalized into Codex ladder order so the stored
33
- * config matches what the API stores.
34
- */
35
- export function parseReasoningArgs(
36
- reasoningEffortsValue: string | undefined,
37
- defaultEffortValue: string | undefined,
38
- ): { reasoningEfforts?: string[]; defaultReasoningEffort?: string; error?: string } {
39
- if (reasoningEffortsValue === undefined && defaultEffortValue === undefined) return {};
40
- let reasoningEfforts: string[] | undefined;
41
- if (reasoningEffortsValue !== undefined) {
42
- const trimmed = reasoningEffortsValue.trim();
43
- if (trimmed === "-") {
44
- reasoningEfforts = undefined;
45
- } else if (trimmed === "") {
46
- // Explicit no-reasoning override, exactly like the API's [] / the dashboard's
47
- // uncheck-all state.
48
- reasoningEfforts = [];
49
- } else {
50
- const parts = trimmed.split(",").map(value => value.trim());
51
- if (parts.some(part => part === "")) {
52
- return { error: "--reasoning-efforts must be comma-separated values from none, minimal, low, medium, high, xhigh, max, ultra (\"\" for no reasoning, \"-\" to inherit)" };
53
- }
54
- const invalid = parts.filter(value => !isDeclaredReasoningEffort(value));
55
- if (invalid.length > 0) {
56
- return { error: `unsupported reasoning effort: ${invalid.join(", ")} (allowed: none, minimal, low, medium, high, xhigh, max, ultra)` };
57
- }
58
- reasoningEfforts = canonicalizeReasoningEfforts(parts);
59
- }
60
- }
61
- let defaultReasoningEffort: string | undefined;
62
- if (defaultEffortValue !== undefined) {
63
- const trimmed = defaultEffortValue.trim();
64
- if (trimmed === "-") {
65
- defaultReasoningEffort = undefined;
66
- } else {
67
- if (!isDeclaredReasoningEffort(trimmed)) {
68
- return { error: `unsupported reasoning effort: ${trimmed} (allowed: none, minimal, low, medium, high, xhigh, max, ultra)` };
69
- }
70
- if (!reasoningEfforts || reasoningEfforts.length === 0) {
71
- return { error: "--default-reasoning-effort requires --reasoning-efforts" };
72
- }
73
- if (!reasoningEfforts.includes(trimmed)) {
74
- return { error: `--default-reasoning-effort "${trimmed}" is not in the declared reasoning efforts` };
75
- }
76
- defaultReasoningEffort = trimmed;
77
- }
78
- }
79
- return { reasoningEfforts, defaultReasoningEffort };
80
- }
29
+ import { parseReasoningArgs } from "./models-custom-input";
30
+ import { redactSecretArgs } from "./secret-args";
31
+ export { parseReasoningArgs } from "./models-custom-input";
81
32
 
82
33
  interface ModelEntry {
83
34
  provider: string;
@@ -173,29 +124,44 @@ function fail(message: string, usage?: string): never {
173
124
 
174
125
  function rejectUnexpectedArgs(args: string[], usage: string): void {
175
126
  if (args.length === 0) return;
176
- const unknown = args.filter(arg => arg.startsWith("-"));
127
+ const shown = redactSecretArgs(args);
128
+ const unknown = shown.filter(arg => arg.startsWith("-"));
177
129
  fail(
178
130
  unknown.length > 0
179
131
  ? `Unknown flag(s): ${unknown.join(", ")}`
180
- : `Unexpected argument(s): ${args.join(", ")}`,
132
+ : `Unexpected argument(s): ${shown.join(", ")}`,
181
133
  usage,
182
134
  );
183
135
  }
184
136
 
185
- async function syncCustomModelsIfLive(): Promise<void> {
186
- const live = await findLiveProxy();
187
- if (!live) return;
188
- const synced = await syncModelsToCodex(live.port).catch(error => {
189
- console.error(`Warning: custom model saved, but catalog sync failed: ${error instanceof Error ? error.message : String(error)}`);
190
- return null;
191
- });
192
- if (synced?.status === "skipped") {
193
- console.log("Custom model saved; Codex integration is OFF, so its catalog was not changed.");
137
+ async function syncCustomModelsIfLive(config: OcxConfig, deps: ModelsCommandDeps): Promise<LocalSyncResult> {
138
+ try {
139
+ const live = await (deps.findLiveProxy ?? findLiveProxy)();
140
+ if (!live) return { status: "not-attempted", ok: false };
141
+ return projectLocalSyncResult(await (deps.syncModels ?? syncModelsToCodex)(live.port, config, null));
142
+ } catch {
143
+ return { status: "failed", ok: false };
194
144
  }
195
145
  }
196
146
 
197
- async function handleCustomAdd(args: string[]): Promise<void> {
147
+ function printCustomMutation(action: "added" | "removed", model: OcxCustomModel, sync: LocalSyncResult, wantsJson: boolean): void {
148
+ const complete = sync.configApplied === true && sync.ok;
149
+ if (sync.status !== "not-attempted" && (!sync.ok || sync.status === "refused")) process.exitCode = 1;
150
+ if (wantsJson) {
151
+ console.log(JSON.stringify({ action,
152
+ model: action === "added" ? model : { id: model.id, provider: model.provider, modelId: model.modelId },
153
+ needsSync: !complete, sync }, null, 2));
154
+ return;
155
+ }
156
+ if (sync.warning) console.log(`Warning: ${sync.warning}`);
157
+ console.log(`${action === "added" ? "Added" : "Removed"} custom model ${routedSlug(model.provider, model.modelId)}${action === "added" ? ` (${model.id})` : ""}.`);
158
+ if (sync.status === "skipped") console.log("Custom model saved; client sync was skipped by the integration policy.");
159
+ else if (!complete && sync.status !== "not-attempted") console.log("Custom model saved; client/catalog sync remains incomplete. Inspect the target before retrying.");
160
+ }
161
+
162
+ async function handleCustomAdd(args: string[], deps: ModelsCommandDeps): Promise<void> {
198
163
  const rest = [...args];
164
+ const wantsJson = consumeFlag(rest, "--json");
199
165
  const provider = rest.shift()?.trim() ?? "";
200
166
  const modelId = rest.shift()?.trim() ?? "";
201
167
  const displayNameValue = consumeFlagValue(rest, "--display-name");
@@ -260,8 +226,8 @@ async function handleCustomAdd(args: string[]): Promise<void> {
260
226
  };
261
227
  config.customModels = [...existing, entry];
262
228
  saveConfig(config);
263
- await syncCustomModelsIfLive();
264
- console.log(`Added custom model ${slug} (${entry.id}).`);
229
+ const sync = await syncCustomModelsIfLive(config, deps);
230
+ printCustomMutation("added", entry, sync, wantsJson);
265
231
  }
266
232
 
267
233
  async function confirmCustomRemoval(model: OcxCustomModel): Promise<boolean> {
@@ -277,12 +243,18 @@ async function confirmCustomRemoval(model: OcxCustomModel): Promise<boolean> {
277
243
  }
278
244
  }
279
245
 
280
- async function handleCustomRemove(args: string[]): Promise<void> {
246
+ async function handleCustomRemove(args: string[], deps: ModelsCommandDeps): Promise<void> {
281
247
  const rest = [...args];
248
+ const wantsJson = consumeFlag(rest, "--json");
282
249
  const confirmed = consumeFlag(rest, "--yes");
283
250
  const target = rest.shift()?.trim() ?? "";
284
251
  rejectUnexpectedArgs(rest, REMOVE_USAGE);
285
252
  if (!target) fail("custom model id or provider/modelId is required", REMOVE_USAGE);
253
+ if (wantsJson && !confirmed) {
254
+ console.error("Error: JSON removal requires --yes; it does not prompt.");
255
+ process.exitCode = 2;
256
+ return;
257
+ }
286
258
 
287
259
  const config = loadConfig();
288
260
  const existing = config.customModels ?? [];
@@ -329,8 +301,8 @@ async function handleCustomRemove(args: string[]): Promise<void> {
329
301
  const next = existing.filter((_, modelIndex) => modelIndex !== index);
330
302
  config.customModels = next.length > 0 ? next : undefined;
331
303
  saveConfig(config);
332
- await syncCustomModelsIfLive();
333
- console.log(`Removed custom model ${routedSlug(model.provider, model.modelId)}.`);
304
+ const sync = await syncCustomModelsIfLive(config, deps);
305
+ printCustomMutation("removed", model, sync, wantsJson);
334
306
  }
335
307
 
336
308
  function customModelCells(model: OcxCustomModel): string[] {
@@ -384,11 +356,12 @@ function handleConfiguredModels(args: string[]): void {
384
356
  const providerFilter = consumeFlagValue(restArgs, "--provider");
385
357
 
386
358
  if (restArgs.length > 0) {
387
- const unknown = restArgs.filter(a => a.startsWith("-"));
359
+ const shown = redactSecretArgs(restArgs);
360
+ const unknown = shown.filter(a => a.startsWith("-"));
388
361
  if (unknown.length > 0) {
389
362
  console.error(`Unknown flag(s): ${unknown.join(", ")}`);
390
363
  } else {
391
- console.error(`Unexpected argument(s): ${restArgs.join(", ")}`);
364
+ console.error(`Unexpected argument(s): ${shown.join(", ")}`);
392
365
  }
393
366
  console.error("Usage: ocx models [--provider <name>] [--json]");
394
367
  process.exit(1);
@@ -442,14 +415,26 @@ function handleConfiguredModels(args: string[]): void {
442
415
  console.log("Note: providers with liveModels may have additional models at runtime.");
443
416
  }
444
417
 
445
- export async function handleModels(args: string[]): Promise<void> {
418
+ export async function handleModels(args: string[], deps: ModelsCommandDeps = {}): Promise<void> {
446
419
  const [subcommand, ...rest] = args;
420
+ const live = rest.filter(arg => arg === "--live" || arg.startsWith("--live="));
421
+ if (live.length) {
422
+ if (live.length !== 1 || live[0] !== "--live" || (subcommand !== "add" && subcommand !== "remove")) {
423
+ console.error("Error: --live is a single boolean flag for models add or remove.");
424
+ process.exitCode = 2;
425
+ return;
426
+ }
427
+ rest.splice(rest.indexOf("--live"), 1);
428
+ const { handleModelsCustomRuntimeCommand } = await import("./models-custom-runtime");
429
+ process.exitCode = await handleModelsCustomRuntimeCommand(subcommand, rest, deps);
430
+ return;
431
+ }
447
432
  if (subcommand === "add") {
448
- await handleCustomAdd(rest);
433
+ await handleCustomAdd(rest, deps);
449
434
  return;
450
435
  }
451
436
  if (subcommand === "remove") {
452
- await handleCustomRemove(rest);
437
+ await handleCustomRemove(rest, deps);
453
438
  return;
454
439
  }
455
440
  if (subcommand === "list-custom") {
@@ -458,7 +443,7 @@ export async function handleModels(args: string[]): Promise<void> {
458
443
  }
459
444
  if (isModelsRuntimeSubcommand(subcommand)) {
460
445
  const { handleModelsRuntimeCommand } = await import("./models-runtime");
461
- const code = await handleModelsRuntimeCommand(subcommand!, rest);
446
+ const code = await handleModelsRuntimeCommand(subcommand!, rest, deps);
462
447
  if (code !== null) process.exitCode = code;
463
448
  return;
464
449
  }