@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -0,0 +1,372 @@
1
+ import { loadConfig, saveConfig } from "../config";
2
+ import {
3
+ CODEX_REASONING_LEVELS,
4
+ configuredReasoningEfforts,
5
+ isDeclaredReasoningEffort,
6
+ mapReasoningEffort,
7
+ reasoningEffortMapFor,
8
+ } from "../reasoning-effort";
9
+ import { findLiveProxy } from "../server/proxy-liveness";
10
+ import { modelInList, type OcxConfig } from "../types";
11
+ import {
12
+ CliUsageError,
13
+ printData,
14
+ rejectArgs,
15
+ runCliAction,
16
+ runtimeRequest,
17
+ takeFlag,
18
+ takeOption,
19
+ type RuntimeApiDeps,
20
+ } from "./runtime-api";
21
+
22
+ export const EFFORT_USAGE = `Usage:
23
+ ocx effort [status] [--json]
24
+ ocx effort <low|medium|high|xhigh|max|ultra|none|minimal|-> [--json]
25
+ ocx effort set [--main <level|->] [--subagent <level|->] [--injection <level|->] [--json]
26
+ ocx effort clear [--json]
27
+ ocx effort model <provider/model|model> [--json]
28
+
29
+ Note: 'ocx effort clear' resets main and subagent caps but keeps delegation
30
+ injection effort. Use 'ocx effort set --injection -' to clear injection effort.`;
31
+
32
+ function clearable(value: string | undefined): string | null | undefined {
33
+ return value === "-" ? null : value;
34
+ }
35
+
36
+ function validateEffortLevel(level: string | null | undefined, label: string): string | null | undefined {
37
+ if (level === undefined || level === null) return level;
38
+ const trimmed = level.trim();
39
+ if (trimmed === "-" || trimmed === "") return null;
40
+ if (!isDeclaredReasoningEffort(trimmed)) {
41
+ throw new CliUsageError(
42
+ `unknown reasoning effort "${trimmed}" for ${label} (allowed: ${CODEX_REASONING_LEVELS.map(l => l.effort).join(", ")}, none, minimal, -)`,
43
+ EFFORT_USAGE,
44
+ );
45
+ }
46
+ return trimmed;
47
+ }
48
+
49
+ interface EffortCapsResponse {
50
+ effortCap: string | null;
51
+ subagentEffortCap: string | null;
52
+ efforts: string[];
53
+ }
54
+
55
+ interface InjectionResponse {
56
+ effort?: string | null;
57
+ }
58
+
59
+ async function getLiveStatus(deps: RuntimeApiDeps): Promise<{
60
+ effortCap: string | null;
61
+ subagentEffortCap: string | null;
62
+ injectionEffort: string | null;
63
+ efforts: string[];
64
+ source: "runtime";
65
+ }> {
66
+ // Propagate API failures once live mode is selected — do not swallow 401/500 into null (#3528 review).
67
+ const [caps, injection] = await Promise.all([
68
+ runtimeRequest<EffortCapsResponse>("/api/effort-caps", {}, deps),
69
+ runtimeRequest<InjectionResponse>("/api/injection-model", {}, deps),
70
+ ]);
71
+ return {
72
+ effortCap: caps.effortCap ?? null,
73
+ subagentEffortCap: caps.subagentEffortCap ?? null,
74
+ injectionEffort: injection?.effort ?? null,
75
+ efforts: caps.efforts ?? CODEX_REASONING_LEVELS.map(l => l.effort),
76
+ source: "runtime",
77
+ };
78
+ }
79
+
80
+ function getOfflineStatus(): {
81
+ effortCap: string | null;
82
+ subagentEffortCap: string | null;
83
+ injectionEffort: string | null;
84
+ efforts: string[];
85
+ source: "config";
86
+ } {
87
+ const config = loadConfig();
88
+ return {
89
+ effortCap: config.effortCap ?? null,
90
+ subagentEffortCap: config.subagentEffortCap ?? null,
91
+ injectionEffort: config.injectionEffort ?? null,
92
+ efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
93
+ source: "config",
94
+ };
95
+ }
96
+
97
+ async function status(wantsJson: boolean, deps: RuntimeApiDeps): Promise<void> {
98
+ let data: {
99
+ effortCap: string | null;
100
+ subagentEffortCap: string | null;
101
+ injectionEffort: string | null;
102
+ efforts: string[];
103
+ source: "runtime" | "config";
104
+ };
105
+
106
+ const liveFinder = deps.findLiveProxy ?? findLiveProxy;
107
+ const live = await liveFinder().catch(() => null);
108
+ const isLive = Boolean(live || deps.baseUrl);
109
+
110
+ if (isLive) {
111
+ // Once live proxy / baseUrl is selected, propagate API failures; do not silently substitute local config (#3528 review).
112
+ data = await getLiveStatus(deps);
113
+ } else {
114
+ data = getOfflineStatus();
115
+ }
116
+
117
+ const lines = [
118
+ `Reasoning effort status (${data.source === "runtime" ? "live proxy" : "offline config"}):`,
119
+ ` Main agent effort cap: ${data.effortCap ?? "(unset — no cap)"}`,
120
+ ` Subagent effort cap: ${data.subagentEffortCap ?? "(unset — no cap)"}`,
121
+ ` Subagent injection effort: ${data.injectionEffort ?? "(unset — inherits parent session)"}`,
122
+ "",
123
+ "Supported Codex reasoning effort ladder:",
124
+ ...CODEX_REASONING_LEVELS.map(l => ` - ${l.effort.padEnd(8)} ${l.description}`),
125
+ ];
126
+
127
+ printData(data, wantsJson, lines);
128
+ }
129
+
130
+ async function setEffort(
131
+ options: {
132
+ main?: string | null;
133
+ subagent?: string | null;
134
+ injection?: string | null;
135
+ },
136
+ wantsJson: boolean,
137
+ deps: RuntimeApiDeps,
138
+ ): Promise<void> {
139
+ const validatedMain = validateEffortLevel(options.main, "--main");
140
+ const validatedSubagent = validateEffortLevel(options.subagent, "--subagent");
141
+ const validatedInjection = validateEffortLevel(options.injection, "--injection");
142
+
143
+ if (validatedMain === undefined && validatedSubagent === undefined && validatedInjection === undefined) {
144
+ throw new CliUsageError("at least one effort option (--main, --subagent, or --injection) is required", EFFORT_USAGE);
145
+ }
146
+
147
+ // Probe live proxy BEFORE any mutation attempt
148
+ const liveFinder = deps.findLiveProxy ?? findLiveProxy;
149
+ const live = await liveFinder().catch(() => null);
150
+ const isLive = Boolean(live || deps.baseUrl);
151
+
152
+ if (isLive) {
153
+ // Live update path: once live proxy / baseUrl is selected, HTTP 4xx/5xx or transport
154
+ // errors must never fall through to silent offline config writes (#3528 review).
155
+ const capsBody: Record<string, unknown> = {};
156
+ if (validatedMain !== undefined) capsBody.effortCap = validatedMain;
157
+ if (validatedSubagent !== undefined) capsBody.subagentEffortCap = validatedSubagent;
158
+
159
+ let capsCommitted = false;
160
+ let injectionCommitted = false;
161
+
162
+ if (Object.keys(capsBody).length > 0) {
163
+ await runtimeRequest<EffortCapsResponse>("/api/effort-caps", {
164
+ method: "PUT",
165
+ body: JSON.stringify(capsBody),
166
+ }, deps);
167
+ capsCommitted = true;
168
+ }
169
+
170
+ if (validatedInjection !== undefined) {
171
+ try {
172
+ await runtimeRequest<InjectionResponse>("/api/injection-model", {
173
+ method: "PUT",
174
+ body: JSON.stringify({ effort: validatedInjection }),
175
+ }, deps);
176
+ injectionCommitted = true;
177
+ } catch (err) {
178
+ if (capsCommitted) {
179
+ const errMsg = err instanceof Error ? err.message : String(err);
180
+ throw new Error(`effort caps were updated on live proxy, but injection effort failed: ${errMsg}`);
181
+ }
182
+ throw err;
183
+ }
184
+ }
185
+
186
+ // Accurately reflect live state: query fresh live status so unchanged caps are never serialized as null.
187
+ // If the read fails after a successful PUT, wrap with explicit partial-application error (#3528 review).
188
+ let finalStatus: { effortCap: string | null; subagentEffortCap: string | null; injectionEffort: string | null; efforts: string[] };
189
+ try {
190
+ finalStatus = await getLiveStatus(deps);
191
+ } catch (err) {
192
+ if (capsCommitted || injectionCommitted) {
193
+ const errMsg = err instanceof Error ? err.message : String(err);
194
+ throw new Error(`live state was updated, but verifying live status failed: ${errMsg}`);
195
+ }
196
+ throw err;
197
+ }
198
+
199
+ const result = {
200
+ ok: true,
201
+ effortCap: finalStatus.effortCap,
202
+ subagentEffortCap: finalStatus.subagentEffortCap,
203
+ injectionEffort: finalStatus.injectionEffort,
204
+ source: "runtime" as const,
205
+ };
206
+
207
+ printData(result, wantsJson, [
208
+ "Effort caps updated on live proxy:",
209
+ ...(validatedMain !== undefined ? [` Main agent effort cap: ${validatedMain ?? "(cleared)"}`] : []),
210
+ ...(validatedSubagent !== undefined ? [` Subagent effort cap: ${validatedSubagent ?? "(cleared)"}`] : []),
211
+ ...(validatedInjection !== undefined ? [` Subagent injection effort: ${validatedInjection ?? "(cleared)"}`] : []),
212
+ ]);
213
+ return;
214
+ }
215
+
216
+ // Offline persistence path: only reached when no live proxy was found before mutation
217
+ const config = loadConfig();
218
+ if (validatedMain !== undefined) {
219
+ if (validatedMain === null) delete config.effortCap;
220
+ else config.effortCap = validatedMain;
221
+ }
222
+ if (validatedSubagent !== undefined) {
223
+ if (validatedSubagent === null) delete config.subagentEffortCap;
224
+ else config.subagentEffortCap = validatedSubagent;
225
+ }
226
+ if (validatedInjection !== undefined) {
227
+ if (validatedInjection === null) delete config.injectionEffort;
228
+ else config.injectionEffort = validatedInjection;
229
+ }
230
+ saveConfig(config);
231
+
232
+ const result = {
233
+ ok: true,
234
+ effortCap: config.effortCap ?? null,
235
+ subagentEffortCap: config.subagentEffortCap ?? null,
236
+ injectionEffort: config.injectionEffort ?? null,
237
+ source: "config" as const,
238
+ };
239
+
240
+ printData(result, wantsJson, [
241
+ "[offline] Effort caps updated in config.json (proxy is not running; changes will take effect on next start):",
242
+ ...(validatedMain !== undefined ? [` Main agent effort cap: ${validatedMain ?? "(cleared)"}`] : []),
243
+ ...(validatedSubagent !== undefined ? [` Subagent effort cap: ${validatedSubagent ?? "(cleared)"}`] : []),
244
+ ...(validatedInjection !== undefined ? [` Subagent injection effort: ${validatedInjection ?? "(cleared)"}`] : []),
245
+ ]);
246
+ }
247
+
248
+ function inspectModelEffort(modelTarget: string, wantsJson: boolean): void {
249
+ // Reject leading or trailing slash selectors before lookup (#3528 review)
250
+ if (modelTarget.startsWith("/") || modelTarget.endsWith("/")) {
251
+ throw new CliUsageError(
252
+ `invalid model selector "${modelTarget}" (must be <provider/model> or <model> without leading or trailing slashes)`,
253
+ EFFORT_USAGE,
254
+ );
255
+ }
256
+
257
+ const config = loadConfig();
258
+ let providerName = "";
259
+ let modelId = modelTarget;
260
+
261
+ const slashIndex = modelTarget.indexOf("/");
262
+ if (slashIndex > 0) {
263
+ providerName = modelTarget.slice(0, slashIndex);
264
+ modelId = modelTarget.slice(slashIndex + 1);
265
+ } else {
266
+ providerName = config.defaultProvider || "openai";
267
+ }
268
+
269
+ const provider = config.providers[providerName];
270
+ if (!provider) {
271
+ throw new CliUsageError(
272
+ `Provider "${providerName}" is not configured. Configured providers: ${Object.keys(config.providers).join(", ")}`,
273
+ EFFORT_USAGE,
274
+ );
275
+ }
276
+
277
+ const isReasoningDisabled = modelInList(provider.noReasoningModels, modelId);
278
+ const efforts = configuredReasoningEfforts(provider, modelId);
279
+ const wireMap = reasoningEffortMapFor(provider, modelId);
280
+
281
+ // Derive sample ladder directly from canonical CODEX_REASONING_LEVELS (#3528 review)
282
+ const mappedExamples: Record<string, string | undefined> = {};
283
+ for (const { effort } of CODEX_REASONING_LEVELS) {
284
+ mappedExamples[effort] = mapReasoningEffort(provider, modelId, effort);
285
+ }
286
+
287
+ const result = {
288
+ provider: providerName,
289
+ model: modelId,
290
+ reasoningDisabled: isReasoningDisabled,
291
+ supportedEfforts: efforts ?? null,
292
+ wireMap: wireMap ?? null,
293
+ mappedTiers: mappedExamples,
294
+ };
295
+
296
+ const lines = [
297
+ `Reasoning effort configuration for ${providerName}/${modelId}:`,
298
+ ` Reasoning disabled: ${isReasoningDisabled ? "yes (noReasoningModels)" : "no"}`,
299
+ ` Supported ladder: ${efforts ? efforts.join(", ") : "(default / unconstrained)"}`,
300
+ ` Wire mapping overrides: ${wireMap ? JSON.stringify(wireMap) : "(standard provider mapping)"}`,
301
+ " Sample wire translations:",
302
+ ...Object.entries(mappedExamples).map(([req, wire]) => ` ${req.padEnd(8)} -> ${wire ?? "(omitted/unsupported)"}`),
303
+ ];
304
+
305
+ printData(result, wantsJson, lines);
306
+ }
307
+
308
+ export async function handleEffortCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
309
+ return runCliAction(async () => {
310
+ const args = [...argv];
311
+ const wantsJson = takeFlag(args, "--json");
312
+
313
+ if (args.length === 0) {
314
+ await status(wantsJson, deps);
315
+ return;
316
+ }
317
+
318
+ const rawFirst = args[0]!;
319
+ const first = rawFirst.toLowerCase();
320
+
321
+ if (first === "status") {
322
+ args.shift();
323
+ rejectArgs(args, EFFORT_USAGE);
324
+ await status(wantsJson, deps);
325
+ return;
326
+ }
327
+
328
+ if (first === "clear" || first === "unset") {
329
+ args.shift();
330
+ rejectArgs(args, EFFORT_USAGE);
331
+ await setEffort({ main: null, subagent: null }, wantsJson, deps);
332
+ return;
333
+ }
334
+
335
+ if (first === "set") {
336
+ args.shift();
337
+ const main = clearable(takeOption(args, "--main"));
338
+ const subagent = clearable(takeOption(args, "--subagent"));
339
+ const injection = clearable(takeOption(args, "--injection"));
340
+ rejectArgs(args, EFFORT_USAGE);
341
+ await setEffort({ main, subagent, injection }, wantsJson, deps);
342
+ return;
343
+ }
344
+
345
+ if (first === "model") {
346
+ args.shift();
347
+ const target = args.shift();
348
+ if (!target) throw new CliUsageError("model identifier (<provider/model> or <model>) is required", EFFORT_USAGE);
349
+ rejectArgs(args, EFFORT_USAGE);
350
+ inspectModelEffort(target, wantsJson);
351
+ return;
352
+ }
353
+
354
+ // Shorthand: check if first argument is an effort level or "-"
355
+ if (first === "-" || isDeclaredReasoningEffort(first)) {
356
+ args.shift();
357
+ rejectArgs(args, EFFORT_USAGE);
358
+ await setEffort({ main: first === "-" ? null : first }, wantsJson, deps);
359
+ return;
360
+ }
361
+
362
+ // Shorthand model inspection: preserve raw casing and check slash boundaries (#3528 review)
363
+ if (rawFirst.includes("/")) {
364
+ args.shift();
365
+ rejectArgs(args, EFFORT_USAGE);
366
+ inspectModelEffort(rawFirst, wantsJson);
367
+ return;
368
+ }
369
+
370
+ throw new CliUsageError(`unknown effort command or level "${rawFirst}"`, EFFORT_USAGE);
371
+ });
372
+ }
@@ -68,13 +68,6 @@ type ExportProxyModelRow = OpencodeProxyModelRow & {
68
68
  defaultReasoningEffort?: string;
69
69
  };
70
70
 
71
- /** Same authoritativeness rule the serializers apply, for the degraded-count line. */
72
- function hasContextLimit(model: ExportModel): boolean {
73
- return typeof model.contextWindow === "number"
74
- && Number.isFinite(model.contextWindow)
75
- && model.contextWindow > 0;
76
- }
77
-
78
71
  /**
79
72
  * Export rows from proxy `/api/models` rows.
80
73
  *
@@ -106,6 +99,7 @@ export function exportModelsFromProxyRows(
106
99
  id: entry.id ?? entry.namespaced,
107
100
  };
108
101
  if (entry.native) model.native = true;
102
+ if (entry.fastRowAvailable !== undefined) model.fastRowAvailable = entry.fastRowAvailable;
109
103
  if (entry.displayName) model.displayName = entry.displayName;
110
104
  if (entry.contextWindow !== undefined) model.contextWindow = entry.contextWindow;
111
105
  if (entry.reasoningEfforts && entry.reasoningEfforts.length > 0) {
@@ -195,7 +189,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
195
189
  // stderr, so `--json` stdout stays a standalone JSON document.
196
190
  if (out !== undefined && wantsJson) console.error(`Wrote ${out}`);
197
191
 
198
- const degraded = models.filter(model => !hasContextLimit(model)).length;
192
+ const { modelCount, modelsWithoutLimits } = spec.summarize(clientConfig);
199
193
  // `--json` keeps emitting the DOCUMENT at the top level as JSON for scripts;
200
194
  // `--out` is the path that writes the selected client's native format.
201
195
  // Format metadata rides in the human lines below.
@@ -206,7 +200,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
206
200
  `Destination: ${spec.destination(process.env)}`,
207
201
  "Merge this generated configuration into that file; do not replace it.",
208
202
  `Before launching: ${spec.exportHint}`,
209
- `${models.length} model${models.length === 1 ? "" : "s"}; ${degraded} omit context limits (the client applies its own defaults).`,
203
+ `${modelCount} model${modelCount === 1 ? "" : "s"}; ${modelsWithoutLimits} omit context limits (the client applies its own defaults).`,
210
204
  ]);
211
205
  });
212
206
  }
package/src/cli/help.ts CHANGED
@@ -66,6 +66,7 @@ Usage:
66
66
  ocx alias <sub> Short names for providers and models (list, set, rm, defaults)
67
67
  ocx combo <sub> Combo routing strategies and failover
68
68
  ocx agent <sub> Subagents, injection, effort caps, and sidecars
69
+ ocx effort [sub] Inspect and configure reasoning effort caps and defaults
69
70
  ocx observe <sub> Logs, usage, storage, memory, and debug data
70
71
  ocx inspect <sub> Effective config, catalog, analytics, pacing, client-config
71
72
  ocx route <sub> Routing features (combo, policy)
package/src/cli/index.ts CHANGED
@@ -1,5 +1,18 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn } from "node:child_process";
3
+ import { homedir } from "node:os";
4
+
5
+ // Best-effort recovery for runtime execution and spawned children if launched
6
+ // from an unlinked/deleted working directory (runs after hoisted ESM module imports).
7
+ try {
8
+ process.cwd();
9
+ } catch {
10
+ try {
11
+ process.chdir(homedir());
12
+ } catch {
13
+ /* best-effort */
14
+ }
15
+ }
3
16
  import { currentExternalCodexModelProvider, restoreNativeCodex, restoreNativeCodexAsync, shouldInjectApiAuthHeader } from "../codex/inject";
4
17
  import { stripGrokConfig } from "../grok/inject";
5
18
  import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../update/stop-contract.mjs";
package/src/cli/init.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import * as readline from "node:readline";
2
+ import { modelSelectionGuidance } from "./model-selection-guidance";
3
+ import { initializeProviderModelSelection } from "../providers/initial-model-selection";
2
4
  import { existsSync, readFileSync, unlinkSync } from "node:fs";
3
5
  import { injectCodexConfig } from "../codex/inject";
4
6
  import { classifyOpenAiTierBackup, getConfigPath, getDefaultConfig, isValidProviderName, preserveOpenAiTierRollbackSnapshot, saveConfig } from "../config";
@@ -160,6 +162,7 @@ export async function runInit(): Promise<void> {
160
162
  const portStr = await prompt.ask("\nProxy port [10100]: ");
161
163
  const port = parseInt(portStr, 10) || 10100;
162
164
 
165
+ initializeProviderModelSelection(providerName, providerConfig);
163
166
  const config: OcxConfig = {
164
167
  ...getDefaultConfig(),
165
168
  port,
@@ -198,6 +201,7 @@ export async function runInit(): Promise<void> {
198
201
  }
199
202
 
200
203
  console.log(`\nšŸš€ Setup complete! Run 'ocx start' to start the proxy.`);
204
+ for (const line of modelSelectionGuidance(providerName)) console.log(line);
201
205
  } catch (error) {
202
206
  const message = error instanceof Error ? error.message : String(error);
203
207
  if (/stdin (closed|reached EOF)/i.test(message)) {
@@ -0,0 +1,30 @@
1
+ /** Existing model-management commands; use the exact ID from `live`, including native IDs. */
2
+ export function modelSelectionNextSteps(provider: string, afterLogin = false) {
3
+ const name = provider === "codex" || provider === "chatgpt" ? "openai" : provider;
4
+ return {
5
+ provider: name,
6
+ afterLogin,
7
+ requiresRunningProxy: true,
8
+ commands: {
9
+ list: `ocx models live --provider ${name}`,
10
+ enable: 'ocx models enable "<model-id-from-list>"',
11
+ disable: 'ocx models disable "<model-id-from-list>"',
12
+ enableNative: 'ocx models enable "<model-id-from-list>" --native',
13
+ disableNative: 'ocx models disable "<model-id-from-list>" --native',
14
+ enableAll: `ocx models provider ${name} on`,
15
+ disableAll: `ocx models provider ${name} off`,
16
+ },
17
+ };
18
+ }
19
+
20
+ export function modelSelectionGuidance(provider: string, afterLogin = false): string[] {
21
+ const next = modelSelectionNextSteps(provider, afterLogin);
22
+ return [
23
+ afterLogin ? "After login completes, manage model switches with:" : "Manage model switches (the provider stays active):",
24
+ " Start the proxy first if needed: ocx start",
25
+ " Replace <model-id-from-list> with an exact ID printed by the list command.",
26
+ " For rows marked native, use the --native variants (including IDs containing /).",
27
+ ...Object.values(next.commands).map(command => ` ${command}`),
28
+ " If initial discovery is still pending, check the provider connection and retry: ocx sync",
29
+ ];
30
+ }
@@ -36,6 +36,7 @@ type ModelRow = {
36
36
  namespaced?: string;
37
37
  native?: boolean;
38
38
  disabled?: boolean;
39
+ initialSelectionPending?: boolean;
39
40
  custom?: boolean;
40
41
  customId?: string;
41
42
  displayName?: string;
@@ -49,7 +50,7 @@ async function live(argv: string[], deps: RuntimeApiDeps): Promise<void> {
49
50
  const rows = await runtimeRequest<ModelRow[]>("/api/models", {}, deps);
50
51
  const filtered = provider ? rows.filter(row => row.provider === provider) : rows;
51
52
  printData(filtered, wantsJson, filtered.map(row => {
52
- const flags = [row.native ? "native" : "routed", row.custom ? "custom" : "", row.disabled ? "disabled" : "enabled"].filter(Boolean);
53
+ const flags = [row.native ? "native" : "routed", row.custom ? "custom" : "", row.initialSelectionPending ? "initial discovery pending" : row.disabled ? "disabled" : "enabled"].filter(Boolean);
53
54
  return `${row.namespaced ?? `${row.provider}/${row.id}`} [${flags.join(", ")}]`;
54
55
  }));
55
56
  }
@@ -323,7 +324,7 @@ async function shadow(argv: string[], deps: RuntimeApiDeps): Promise<void> {
323
324
 
324
325
  export async function handleModelsRuntimeCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
325
326
  // The dispatch below and MODELS_RUNTIME_SUBCOMMANDS must name the same set;
326
- // tests/cli-models-runtime-dispatch.test.ts fails if they drift (#3094).
327
+ // tests/cli/cli-models-runtime-dispatch.test.ts fails if they drift (#3094).
327
328
  if (!isModelsRuntimeSubcommand(sub)) return null;
328
329
  let action: (() => Promise<void>) | undefined;
329
330
  if (sub === "live") action = () => live(argv, deps);
package/src/cli/models.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { syncModelsToCodex } from "../codex/sync";
7
+ import { configuredContextWindow } from "../codex/catalog/provider-fetch";
7
8
  import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
8
9
  import {
9
10
  canonicalizeReasoningEfforts,
@@ -86,6 +87,11 @@ interface ModelEntry {
86
87
  reasoningEfforts: string[] | null;
87
88
  }
88
89
 
90
+ /**
91
+ * Collect static configured models for all providers or one selected provider.
92
+ * Keep each provider's default model first and resolve metadata through shared helpers.
93
+ * Live-discovered models are not fetched by this listing.
94
+ */
89
95
  function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[] {
90
96
  const entries: ModelEntry[] = [];
91
97
  const providers = providerFilter
@@ -95,10 +101,9 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
95
101
  for (const [provName, prov] of Object.entries(providers)) {
96
102
  if (!prov) continue;
97
103
  const seen = new Set<string>();
98
- const contextWindows = prov.modelContextWindows ?? {};
99
104
  const inputModalities = prov.modelInputModalities ?? {};
100
- const globalContext = prov.contextWindow ?? null;
101
105
 
106
+ /** Append one model with resolved metadata, ignoring duplicates within this provider. */
102
107
  const addModel = (model: string, isDefault: boolean) => {
103
108
  if (seen.has(model)) return;
104
109
  seen.add(model);
@@ -124,7 +129,7 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
124
129
  provider: provName,
125
130
  model,
126
131
  isDefault,
127
- contextWindow: modelRecordValue(contextWindows, model) ?? globalContext,
132
+ contextWindow: configuredContextWindow(prov, model) ?? null,
128
133
  inputModalities: modalities,
129
134
  reasoningEfforts: efforts,
130
135
  });
@@ -88,6 +88,8 @@ export interface OpencodeRoutedModel {
88
88
 
89
89
  /** Row shape from authenticated GET /api/models on the running proxy. */
90
90
  export interface OpencodeProxyModelRow {
91
+ /** Hub-resolved availability, independent of the launcher's local Fast setting. */
92
+ fastRowAvailable?: boolean;
91
93
  provider?: string;
92
94
  id?: string;
93
95
  namespaced?: string;
@@ -389,6 +391,7 @@ export function opencodeCatalogFromProxyRows(
389
391
  id: row.id,
390
392
  contextWindow: row.contextWindow,
391
393
  displayName: row.displayNameSource === "fallback" ? undefined : row.displayName,
394
+ ...(typeof row.fastRowAvailable === "boolean" ? { fastRowAvailable: row.fastRowAvailable } : {}),
392
395
  ...(Array.isArray(row.reasoningEfforts) && row.reasoningEfforts.length > 0
393
396
  ? { reasoningEfforts: [...row.reasoningEfforts] }
394
397
  : {}),
@@ -648,7 +651,7 @@ export async function cmdOpencode(args: string[]): Promise<number> {
648
651
  const catalog = opencodeCatalogFromProxyRows(proxyModels, config);
649
652
  const blocks = buildOpencodeProviderBlocksFromCatalog(live.port, catalog, live.hostname, config);
650
653
  const baseUrl = blocks.v1.options.baseURL;
651
- const modelCount = catalog.length;
654
+ const modelCount = Object.keys(blocks.v1.models).length;
652
655
  console.error(`āœ… opencode wired to ${baseUrl} — ${modelCount} model(s) under provider \`${OPENCODE_PROVIDER_ID}\`.`);
653
656
  console.error(" Your existing opencode config files are left untouched; only the runtime provider blocks are injected.");
654
657
  const providerOverride = opencodeProviderOverridePath(process.cwd());