@bitkyc08/opencodex 2.76.0 → 2.77.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 (189) hide show
  1. package/gui/dist/assets/App-COdEeqSj.css +1 -0
  2. package/gui/dist/assets/App-DEpCJEP_.js +54 -0
  3. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
  4. package/gui/dist/assets/index-Cd7ittMv.css +1 -0
  5. package/gui/dist/assets/index-DwJKFJm5.js +94 -0
  6. package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
  7. package/gui/dist/index.html +4 -3
  8. package/package.json +1 -1
  9. package/src/adapters/antigravity-refusal.ts +28 -0
  10. package/src/adapters/cursor/protobuf-request.ts +6 -0
  11. package/src/adapters/cursor/request-builder.ts +3 -0
  12. package/src/adapters/cursor/structured-output.ts +13 -0
  13. package/src/adapters/cursor/types.ts +2 -0
  14. package/src/adapters/ollama-native.ts +29 -2
  15. package/src/adapters/openai-responses/passthrough.ts +133 -103
  16. package/src/adapters/openai-responses/request-strips.ts +10 -4
  17. package/src/adapters/zed.ts +26 -4
  18. package/src/bridge/response-json.ts +14 -14
  19. package/src/bridge/sse.ts +15 -5
  20. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  21. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  22. package/src/claude/agents-inject.ts +4 -43
  23. package/src/claude/gateway-cache.ts +16 -13
  24. package/src/claude/inbound-model-options.ts +9 -2
  25. package/src/claude/intercept/cli-catalog.ts +27 -16
  26. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  27. package/src/claude/intercept/picker-budget.ts +41 -0
  28. package/src/claude/outbound.ts +2 -0
  29. package/src/claude/subagent-force-status.ts +53 -0
  30. package/src/claude/subagent-model.ts +93 -0
  31. package/src/cli/account-api.ts +3 -0
  32. package/src/cli/account.ts +3 -1
  33. package/src/cli/agent.ts +10 -0
  34. package/src/cli/capabilities.ts +9 -0
  35. package/src/cli/chatgpt-command.ts +10 -7
  36. package/src/cli/claude.ts +22 -6
  37. package/src/cli/codex-shim-readiness.ts +89 -0
  38. package/src/cli/connect.ts +19 -0
  39. package/src/cli/dispatch.ts +3 -3
  40. package/src/cli/export-command.ts +20 -9
  41. package/src/cli/gui.ts +8 -3
  42. package/src/cli/help-catalog.ts +84 -0
  43. package/src/cli/help-models-context.ts +17 -0
  44. package/src/cli/help-navigation.ts +50 -0
  45. package/src/cli/help-recovery.ts +66 -0
  46. package/src/cli/help.ts +52 -8
  47. package/src/cli/models-runtime.ts +2 -1
  48. package/src/cli/opencode.ts +54 -10
  49. package/src/cli/provider.ts +3 -29
  50. package/src/cli/registry.ts +29 -5
  51. package/src/cli/root.ts +30 -9
  52. package/src/cli/runtime-api.ts +1 -1
  53. package/src/cli/tray-proxy.ts +3 -2
  54. package/src/clients/config-export/contracts.ts +100 -0
  55. package/src/clients/config-export/droid.ts +54 -2
  56. package/src/clients/config-export/kilo.ts +12 -2
  57. package/src/clients/config-export/model-metadata.ts +10 -4
  58. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  59. package/src/clients/config-export.ts +46 -66
  60. package/src/clients/omo-role-models.ts +17 -3
  61. package/src/codex/account-credit-use.ts +4 -2
  62. package/src/codex/account-runtime-state.ts +21 -0
  63. package/src/codex/account-store.ts +11 -47
  64. package/src/codex/account-usability.ts +11 -8
  65. package/src/codex/auth-api/account-list.ts +73 -63
  66. package/src/codex/auth-api/main-account-probe.ts +25 -12
  67. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  68. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  69. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  70. package/src/codex/auth-context.ts +30 -11
  71. package/src/codex/catalog/aggregation.ts +3 -0
  72. package/src/codex/catalog/model-hints.ts +2 -1
  73. package/src/codex/catalog/model-visibility.ts +6 -3
  74. package/src/codex/catalog/parsing.ts +2 -0
  75. package/src/codex/catalog/provider-models.ts +13 -4
  76. package/src/codex/catalog/retained-sync.ts +2 -1
  77. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  78. package/src/codex/convergence.ts +2 -1
  79. package/src/codex/main-account.ts +144 -11
  80. package/src/codex/quota-refresh-outcome.ts +17 -2
  81. package/src/codex/quota-types.ts +33 -1
  82. package/src/codex/quota.ts +56 -15
  83. package/src/codex/routing/cooldown-math.ts +10 -4
  84. package/src/codex/routing/selection.ts +11 -4
  85. package/src/codex/routing.ts +9 -9
  86. package/src/codex/shim-path-resolution.ts +133 -0
  87. package/src/codex/shim.ts +39 -39
  88. package/src/codex/subagent-model-fallback.ts +2 -1
  89. package/src/combos/failover.ts +82 -1
  90. package/src/combos/jev.ts +13 -3
  91. package/src/config/diagnostics.ts +4 -0
  92. package/src/config/load-degrade.ts +4 -0
  93. package/src/config/provider-validation.ts +3 -0
  94. package/src/config/schema/compaction-triggers.ts +8 -0
  95. package/src/config/schema/config-schema.ts +11 -0
  96. package/src/config/schema/leaf-validators.ts +5 -1
  97. package/src/config/subagent-models.ts +12 -0
  98. package/src/generated/compatibility-version.json +245 -153
  99. package/src/grok/session-identity.ts +33 -0
  100. package/src/integrations/catalog-refresh.ts +1 -1
  101. package/src/integrations/mutation-plan.ts +4 -2
  102. package/src/integrations/state.ts +68 -6
  103. package/src/integrations/writer.ts +18 -4
  104. package/src/lib/gui-pair-capability.ts +17 -0
  105. package/src/lib/provider-client-headers.ts +62 -0
  106. package/src/oauth/anthropic-continuity.ts +65 -0
  107. package/src/oauth/anthropic-identity.ts +75 -0
  108. package/src/oauth/anthropic-routing.ts +3 -3
  109. package/src/oauth/anthropic.ts +7 -1
  110. package/src/oauth/generic-account-failover.ts +21 -0
  111. package/src/oauth/health.ts +32 -5
  112. package/src/oauth/index.ts +36 -27
  113. package/src/oauth/local-token-detect.ts +7 -0
  114. package/src/oauth/refresh-policy.ts +2 -0
  115. package/src/oauth/store.ts +55 -6
  116. package/src/oauth/types.ts +9 -0
  117. package/src/protocols/encoders/messages.ts +4 -3
  118. package/src/providers/antigravity-effort-families.ts +75 -0
  119. package/src/providers/antigravity-models.ts +54 -16
  120. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  121. package/src/providers/derive.ts +2 -0
  122. package/src/providers/model-rename-fields.ts +3 -0
  123. package/src/providers/new-model-policy.ts +48 -10
  124. package/src/providers/registry/entries-extended.ts +3 -0
  125. package/src/providers/registry/model-ids.ts +2 -0
  126. package/src/providers/registry/model-seeds.ts +5 -0
  127. package/src/providers/registry/types.ts +4 -0
  128. package/src/responses/compaction.ts +19 -0
  129. package/src/router.ts +6 -0
  130. package/src/server/auth-cors.ts +9 -0
  131. package/src/server/chat-completions.ts +10 -0
  132. package/src/server/claude-messages.ts +18 -11
  133. package/src/server/droid-reasoning-default.ts +50 -0
  134. package/src/server/github-copilot-responses-repair.ts +3 -6
  135. package/src/server/grok-responses-control-frame.ts +2 -2
  136. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  137. package/src/server/gui-session.ts +33 -11
  138. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  139. package/src/server/index/serve-options.ts +2 -1
  140. package/src/server/management/agent-settings-routes.ts +6 -129
  141. package/src/server/management/config-routes.ts +1 -1
  142. package/src/server/management/integration-routes.ts +64 -6
  143. package/src/server/management/link-routes.ts +20 -13
  144. package/src/server/management/model-row-export-metadata.ts +150 -0
  145. package/src/server/management/model-rows.ts +148 -9
  146. package/src/server/management/route-registry.ts +4 -3
  147. package/src/server/management/shared.ts +19 -5
  148. package/src/server/management/subagent-model-routes.ts +214 -0
  149. package/src/server/relay.ts +14 -44
  150. package/src/server/request-log-generation-window.ts +38 -0
  151. package/src/server/request-log.ts +13 -0
  152. package/src/server/responses/adapter-dispatch.ts +100 -1
  153. package/src/server/responses/codex-auth-error.ts +10 -2
  154. package/src/server/responses/combo-stream-preflight.ts +29 -3
  155. package/src/server/responses/compaction-routing.ts +10 -1
  156. package/src/server/responses/core-auth.ts +57 -26
  157. package/src/server/responses/core-codex-account.ts +6 -3
  158. package/src/server/responses/core-combo-failure.ts +5 -1
  159. package/src/server/responses/core-combo-native.ts +7 -1
  160. package/src/server/responses/core-combo.ts +18 -1
  161. package/src/server/responses/core-normalize.ts +7 -0
  162. package/src/server/responses/core-options.ts +5 -0
  163. package/src/server/responses/fetch-helpers.ts +9 -0
  164. package/src/server/responses/passthrough-delivery.ts +1 -1
  165. package/src/server/responses/policy-fallback.ts +1 -1
  166. package/src/server/responses/request-prepare.ts +5 -0
  167. package/src/server/responses/request-spend.ts +30 -30
  168. package/src/server/responses-custom-tool-repair.ts +2 -2
  169. package/src/server/responses-hosted-image-display.ts +70 -9
  170. package/src/server/responses-terminal-repair.ts +15 -2
  171. package/src/server/sse-frame-buffer.ts +38 -36
  172. package/src/server/sse-payload-rewrite.ts +78 -49
  173. package/src/server/ws-bridge.ts +1 -11
  174. package/src/service/windows-taskxml.ts +1 -2
  175. package/src/service-manager-probe.ts +36 -2
  176. package/src/types/config.ts +4 -0
  177. package/src/types/provider.ts +18 -0
  178. package/src/update/npm-invocation.mjs +35 -3
  179. package/src/update/proxy-liveness-probe.mjs +38 -8
  180. package/src/usage/expected-prices.ts +9 -0
  181. package/src/usage/log.ts +7 -0
  182. package/src/usage/summary.ts +82 -1
  183. package/src/web-search/loop.ts +11 -8
  184. package/src/web-search/run-turn-loop.ts +10 -7
  185. package/gui/dist/assets/App-CddorHdA.js +0 -51
  186. package/gui/dist/assets/App-DMudtISW.css +0 -1
  187. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  188. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  189. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
package/src/cli/gui.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { OcxConfig } from "../types";
2
- import { canonicalGuiBrowserOrigin } from "../lib/gui-pair-capability";
2
+ import { canonicalGuiBrowserOrigin, standaloneGuiPairingOrigin } from "../lib/gui-pair-capability";
3
3
  import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
4
4
  import {
5
5
  requestBoundGuiPairingGrant,
@@ -23,7 +23,7 @@ export interface GuiCommandDeps extends RuntimeApiDeps {
23
23
  }
24
24
 
25
25
  function allowedPairingOrigin(origin: string, config: OcxConfig): boolean {
26
- if (config.runtimeRole !== "hub") return false;
26
+ if (config.runtimeRole !== "hub") return standaloneGuiPairingOrigin(config) === origin;
27
27
  if (canonicalGuiBrowserOrigin(config.hub?.managementPublicOrigin) === origin) return true;
28
28
  return (config.corsAllowOrigins ?? []).some(value => canonicalGuiBrowserOrigin(value) === origin);
29
29
  }
@@ -62,7 +62,7 @@ export async function runGuiCommand(args: string[], deps: GuiCommandDeps): Promi
62
62
  }
63
63
  const config = deps.loadConfig();
64
64
  if (!allowedPairingOrigin(canonicalOrigin, config)) {
65
- console.error("The pairing origin is not enabled by hub.managementPublicOrigin or corsAllowOrigins.");
65
+ console.error("Pairing requires the standalone configured literal loopback origin, or an allowed hub origin.");
66
66
  return 1;
67
67
  }
68
68
  const target = await (deps.findLiveProxy ?? findLiveProxy)();
@@ -70,6 +70,11 @@ export async function runGuiCommand(args: string[], deps: GuiCommandDeps): Promi
70
70
  console.error("No running attested OpenCodex proxy is available for GUI pairing.");
71
71
  return 1;
72
72
  }
73
+ if ((config.runtimeRole ?? "standalone") === "standalone"
74
+ && (target.port !== config.port || (target.hostname ?? "127.0.0.1") !== (config.hostname ?? "127.0.0.1"))) {
75
+ console.error("Standalone pairing requires the running proxy to use its configured loopback address and port.");
76
+ return 1;
77
+ }
73
78
  const result = await (deps.requestPairingGrant ?? requestBoundGuiPairingGrant)(target, canonicalOrigin, {
74
79
  ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
75
80
  });
@@ -0,0 +1,84 @@
1
+ import { CAPABILITIES, type Capability } from "./capabilities";
2
+ import { CLI_COMMANDS, findCommand, type CliCommandEntry } from "./registry";
3
+
4
+ export type HelpResolution =
5
+ | { kind: "entry"; entry: CliCommandEntry; canonicalName: string; children: readonly Capability[] }
6
+ | { kind: "capability"; path: string[]; capability: Capability }
7
+ | { kind: "prefix"; path: string[]; children: readonly Capability[] }
8
+ | { kind: "models-context"; path: string[] }
9
+ | { kind: "unavailable"; path: string[]; parent?: string[] };
10
+
11
+ function startsWithPath(command: readonly string[], prefix: readonly string[]): boolean {
12
+ return prefix.length <= command.length && prefix.every((token, index) => token === command[index]);
13
+ }
14
+
15
+ const DOCUMENTED_PATHS: readonly (readonly string[])[] = [
16
+ ...CAPABILITIES.filter(capability => !findCommand(capability.command[0])?.hidden).map(capability => capability.command),
17
+ ["models", "context"],
18
+ ];
19
+ const MAX_DOCUMENTED_DEPTH = Math.max(...DOCUMENTED_PATHS.map(path => path.length));
20
+
21
+ function canonicalEntry(entry: CliCommandEntry): CliCommandEntry {
22
+ return CLI_COMMANDS.find(candidate => candidate.aliases?.includes(entry.name)) ?? entry;
23
+ }
24
+
25
+ export interface HelpRecoveryCandidate {
26
+ names: string[];
27
+ path: string[];
28
+ }
29
+
30
+ /** Matching names may include aliases; destinations are always catalog-owned. */
31
+ export function helpRecoveryCandidates(parent: readonly string[] = []): HelpRecoveryCandidate[] {
32
+ const candidates = new Map<string, HelpRecoveryCandidate>();
33
+ if (parent.length === 0) {
34
+ for (const entry of CLI_COMMANDS) {
35
+ if (entry.hidden) continue;
36
+ const canonical = canonicalEntry(entry);
37
+ if (canonical.hidden) continue;
38
+ const candidate = candidates.get(canonical.name) ?? { names: [], path: [canonical.name] };
39
+ candidate.names = [...new Set([...candidate.names, entry.name, ...(entry.aliases ?? [])])];
40
+ candidates.set(canonical.name, candidate);
41
+ }
42
+ } else {
43
+ if (parent.length >= MAX_DOCUMENTED_DEPTH) return [];
44
+ const entry = findCommand(parent[0]);
45
+ if (!entry || entry.hidden) return [];
46
+ const prefix = [canonicalEntry(entry).name, ...parent.slice(1)];
47
+ for (const documented of DOCUMENTED_PATHS) {
48
+ if (documented.length <= prefix.length || !startsWithPath(documented, prefix)) continue;
49
+ const path = documented.slice(0, prefix.length + 1);
50
+ candidates.set(path.join(" "), { names: [path[path.length - 1]], path });
51
+ }
52
+ }
53
+ return [...candidates.values()];
54
+ }
55
+
56
+ /** Static declarations describe help coverage, never the runtime's complete grammar. */
57
+ export function resolveHelpPath(requested: readonly string[]): HelpResolution {
58
+ const entry = requested[0] ? findCommand(requested[0]) : undefined;
59
+ if (!entry) return { kind: "unavailable", path: [...requested] };
60
+ // Keep exact-name alias entries for root help; nested topics use the owner.
61
+ const canonical = canonicalEntry(entry);
62
+ const declared = CAPABILITIES.filter(capability => !findCommand(capability.command[0])?.hidden);
63
+ if (requested.length === 1) return {
64
+ kind: "entry", entry, canonicalName: canonical.name,
65
+ children: entry.hidden ? [] : declared.filter(candidate => candidate.command.length > 1 && candidate.command[0] === canonical.name),
66
+ };
67
+ const path = [canonical.name, ...requested.slice(1)];
68
+ if (path.length === 2 && path[0] === "models" && path[1] === "context") {
69
+ return { kind: "models-context", path };
70
+ }
71
+ const capability = declared.find(candidate => candidate.command.length === path.length && startsWithPath(candidate.command, path));
72
+ if (capability) return { kind: "capability", path, capability };
73
+ const children = declared.filter(candidate => startsWithPath(candidate.command, path));
74
+ if (children.length) return { kind: "prefix", path, children };
75
+
76
+ for (let length = Math.min(path.length - 1, MAX_DOCUMENTED_DEPTH); length > 1; length--) {
77
+ const parent = path.slice(0, length);
78
+ if ((length === 2 && parent[0] === "models" && parent[1] === "context")
79
+ || declared.some(candidate => startsWithPath(candidate.command, parent))) {
80
+ return { kind: "unavailable", path, parent };
81
+ }
82
+ }
83
+ return { kind: "unavailable", path, parent: [canonical.name] };
84
+ }
@@ -0,0 +1,17 @@
1
+ /** Exact runtime usage row, including its indentation. No runtime imports. */
2
+ export const MODELS_CONTEXT_USAGE = " ocx models context <status|value <tokens> [--set-all]|provider <name> on [--value <tokens>]|provider <name> off|all <on|off>> [--json]";
3
+
4
+ export const MODELS_CONTEXT_DETAILS = [
5
+ "Manage context caps for routed providers.",
6
+ "status: read current settings without changing them (the default action).",
7
+ "value <tokens>: set a positive token cap for future toggles; --set-all also applies it to every routed provider.",
8
+ "provider <name> on|off: enable or disable a provider cap; on accepts --value <tokens> for that provider only.",
9
+ "all on|off: enable or disable caps for every routed provider.",
10
+ "--json: print the result as JSON.",
11
+ "",
12
+ "Examples:",
13
+ " ocx models context status --json",
14
+ " ocx models context value 128000",
15
+ " ocx models context provider openai on --value 128000",
16
+ " ocx models context all off",
17
+ ] as const;
@@ -0,0 +1,50 @@
1
+ import { findCommand } from "./registry";
2
+
3
+ // Presentation owns selection/order only; names and summaries belong to registry.
4
+ const ACTION_GROUPS = [
5
+ { title: "Start here", commands: ["setup", "start", "status"] },
6
+ { title: "Common tasks", commands: ["doctor", "logs", "usage"] },
7
+ ] as const;
8
+
9
+ const FAMILY_GROUPS = [
10
+ ["provider", "account", "models", "alias"],
11
+ ["combo", "route", "agent", "effort"],
12
+ ["observe", "inspect", "access", "api"],
13
+ ["config", "system", "integration"],
14
+ ] as const;
15
+
16
+ function publicCommand(name: string) {
17
+ const entry = findCommand(name);
18
+ if (!entry || entry.hidden) throw new Error(`Invalid public help group command: ${name}`);
19
+ return entry;
20
+ }
21
+
22
+ function actionLines(name: string): string[] {
23
+ const entry = publicCommand(name);
24
+ const prefix = ` ocx ${entry.name}`.padEnd(16);
25
+ const lines: string[] = [];
26
+ let line = prefix;
27
+ for (const word of entry.summary.split(/\s+/)) {
28
+ if (line.trim() && line.length + word.length + 1 > 80 && line !== prefix) {
29
+ lines.push(line);
30
+ line = " ".repeat(prefix.length);
31
+ }
32
+ line += `${line.endsWith(" ") ? "" : " "}${word}`;
33
+ }
34
+ lines.push(line);
35
+ return lines;
36
+ }
37
+
38
+ export function renderRootHelp(): string {
39
+ const lines = ["opencodex (ocx) — Universal provider proxy for Codex", "Usage: ocx <command> [options]"];
40
+ for (const group of ACTION_GROUPS) {
41
+ lines.push("", `${group.title}:`, ...group.commands.flatMap(actionLines));
42
+ }
43
+ lines.push("", "Explore:");
44
+ for (const group of FAMILY_GROUPS) lines.push(` ${group.map(name => publicCommand(name).name).join(" ")}`);
45
+ lines.push("", "More help:",
46
+ " ocx help <command> Command usage and declared topics",
47
+ " ocx help --all Complete command reference",
48
+ " ocx capabilities --json Declared capabilities for scripts");
49
+ return lines.join("\n");
50
+ }
@@ -0,0 +1,66 @@
1
+ import { helpRecoveryCandidates, resolveHelpPath } from "./help-catalog";
2
+ import { redactSecretString } from "../lib/redact";
3
+
4
+ const MAX_TOKEN_LENGTH = 64;
5
+ const MAX_PATH_DEPTH = 8;
6
+ const MAX_SUGGESTIONS = 3;
7
+
8
+ function safeToken(value: string | undefined): value is string {
9
+ return value !== undefined && value.length >= 3 && value.length <= MAX_TOKEN_LENGTH
10
+ && /^[a-z][a-z0-9-]*$/i.test(value) && redactSecretString(value) === value;
11
+ }
12
+
13
+ /** Banded edit distance with adjacent transposition, over already bounded tokens. */
14
+ function distance(left: string, right: string, limit: number): number {
15
+ if (Math.abs(left.length - right.length) > limit) return limit + 1;
16
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
17
+ let older = previous;
18
+ for (let i = 1; i <= left.length; i++) {
19
+ const current: number[] = Array(right.length + 1).fill(limit + 1);
20
+ current[0] = i;
21
+ for (let j = Math.max(1, i - limit); j <= Math.min(right.length, i + limit); j++) {
22
+ current[j] = Math.min(current[j - 1] + 1, previous[j] + 1,
23
+ previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1));
24
+ if (i > 1 && j > 1 && left[i - 1] === right[j - 2] && left[i - 2] === right[j - 1]) {
25
+ current[j] = Math.min(current[j], older[j - 2] + 1);
26
+ }
27
+ }
28
+ older = previous;
29
+ previous = current;
30
+ }
31
+ return previous[right.length];
32
+ }
33
+
34
+ /** Return complete help destinations, never user-supplied trailing operands. */
35
+ export function suggestHelpPaths(path: readonly string[]): string[][] {
36
+ if (!path.length || path.length > MAX_PATH_DEPTH || !path.every(safeToken)) return [];
37
+ const result = resolveHelpPath(path);
38
+ if (result.kind !== "unavailable") return [];
39
+ const parent = result.parent ?? [];
40
+ const token = path[parent.length].toLowerCase();
41
+ const limit = token.length < 6 ? 1 : 2;
42
+ return helpRecoveryCandidates(parent).map(candidate => ({
43
+ path: candidate.path,
44
+ name: candidate.path.join(" "),
45
+ distance: Math.min(...candidate.names.filter(safeToken).map(name => distance(token, name.toLowerCase(), limit))),
46
+ })).filter(candidate => candidate.distance <= limit)
47
+ .sort((a, b) => a.distance - b.distance || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
48
+ .slice(0, MAX_SUGGESTIONS).map(candidate => candidate.path);
49
+ }
50
+
51
+ export function formatHelpRecovery(path: readonly string[]): string {
52
+ const result = resolveHelpPath(path);
53
+ const parent = result.kind === "unavailable" ? result.parent : undefined;
54
+ const lines = [parent ? "Detailed help unavailable for the requested topic."
55
+ : safeToken(path[0]) ? `Unknown command: ${path[0]}` : "Unknown command."];
56
+ const suggestions = suggestHelpPaths(path);
57
+ if (suggestions.length) {
58
+ lines.push("Did you mean:", ...suggestions.map(suggestion => ` ocx help ${suggestion.join(" ")}`));
59
+ }
60
+ lines.push(parent ? `See: ocx help ${parent.join(" ")}` : "See: ocx help --all");
61
+ return lines.join("\n");
62
+ }
63
+
64
+ export function printUnknownCommand(command: string | undefined): void {
65
+ console.error(formatHelpRecovery(command === undefined ? [] : [command]));
66
+ }
package/src/cli/help.ts CHANGED
@@ -1,4 +1,7 @@
1
- import { findCommand } from "./registry";
1
+ import { resolveHelpPath } from "./help-catalog";
2
+ import { MODELS_CONTEXT_DETAILS, MODELS_CONTEXT_USAGE } from "./help-models-context";
3
+ import { renderRootHelp } from "./help-navigation";
4
+ import { formatHelpRecovery } from "./help-recovery";
2
5
  import { packageVersion as readPackageVersion } from "../lib/package-version";
3
6
 
4
7
  /**
@@ -17,6 +20,10 @@ export function printVersion(): void {
17
20
  }
18
21
 
19
22
  export function printUsage(): void {
23
+ console.log(renderRootHelp());
24
+ }
25
+
26
+ export function printFullUsage(): void {
20
27
  console.log(`opencodex (ocx) — Universal provider proxy for Codex
21
28
 
22
29
  Usage:
@@ -115,13 +122,50 @@ export function hasHelpFlag(values: string[]): boolean {
115
122
  return values.some(value => value === "--help" || value === "-h" || value === "help");
116
123
  }
117
124
 
118
- export function printSubcommandUsage(name: string | undefined): void {
119
- const entry = name ? findCommand(name) : undefined;
120
- if (!entry) {
121
- console.error(`Unknown command: ${name ?? ""}`.trim());
122
- printUsage();
125
+ export function printSubcommandUsage(
126
+ name: string | undefined,
127
+ path?: readonly string[],
128
+ options: { fallbackToParent?: boolean; write?: (text: string) => void } = {},
129
+ ): void {
130
+ const write = options.write ?? console.log;
131
+ const result = resolveHelpPath(path ?? (name ? [name] : []));
132
+ if (result.kind === "unavailable") {
133
+ // Appended flags may follow runtime operands. An explicit `help <path>`
134
+ // requests that exact detail instead, so only the CLI head enables fallback.
135
+ if (options.fallbackToParent && result.parent) {
136
+ printSubcommandUsage(result.parent[0], result.parent, { write });
137
+ return;
138
+ }
139
+ console.error(formatHelpRecovery(result.path));
123
140
  process.exit(1);
124
141
  }
125
- console.log(`Usage: ${entry.usage}\n\n${entry.summary}`);
126
- if (entry.details?.length) console.log(`\n${entry.details.join("\n")}`);
142
+ if (result.kind === "entry") {
143
+ write(`Usage: ${result.entry.usage}\n\n${result.entry.summary}`);
144
+ if (result.entry.details?.length) write(`\n${result.entry.details.join("\n")}`);
145
+ if (result.children.length) {
146
+ write("\nDeclared commands (incomplete):");
147
+ for (const child of result.children) write(` ocx help ${child.command.join(" ")} ${child.summary}`);
148
+ }
149
+ if (result.canonicalName === "models") write("\nContext cap help: ocx help models context");
150
+ if (result.entry.name !== result.canonicalName) write(`\nCanonical help: ocx help ${result.canonicalName}`);
151
+ return;
152
+ }
153
+ if (result.kind === "models-context") {
154
+ write(`Usage:\n${MODELS_CONTEXT_USAGE}\n\n${MODELS_CONTEXT_DETAILS.join("\n")}`);
155
+ } else if (result.kind === "capability") {
156
+ const { capability } = result;
157
+ write(`Command: ocx ${result.path.join(" ")}\n\n${capability.summary}`);
158
+ if (capability.flags.length) {
159
+ write("\nDeclared flags:");
160
+ for (const flag of capability.flags) {
161
+ write(` ${flag.name}${flag.value && flag.value !== "boolean" ? ` <${flag.value}>` : ""}${flag.required ? " (required)" : ""} ${flag.summary}`);
162
+ }
163
+ }
164
+ if (capability.details?.length) write(`\n${capability.details.join("\n")}`);
165
+ write("\nCapability metadata is incomplete; this is not the full operand grammar.");
166
+ } else {
167
+ write(`Command group: ocx ${result.path.join(" ")}\n\nDeclared commands (incomplete):`);
168
+ for (const child of result.children) write(` ocx ${child.command.join(" ")} ${child.summary}`);
169
+ }
170
+ write(`\nParent help: ocx help ${result.path.slice(0, -1).join(" ")}`);
127
171
  }
@@ -14,6 +14,7 @@ import {
14
14
  type RuntimeApiDeps,
15
15
  } from "./runtime-api";
16
16
  import { isModelsRuntimeSubcommand } from "./models-runtime-subcommands";
17
+ import { MODELS_CONTEXT_USAGE } from "./help-models-context";
17
18
  import { isValidProviderName } from "../config/provider-name";
18
19
  import { isValidModelDiscoveryModelId } from "../providers/model-discovery-limits";
19
20
  import { redactSecretString } from "../lib/redact";
@@ -42,7 +43,7 @@ const USAGE = `Usage:
42
43
  ocx models preset apply <provider> [--all] [--json]
43
44
  ocx models new-policy [on|off] [--provider <name>] [--json]
44
45
  ocx models new-arrivals [--json]
45
- ocx models context <status|value <tokens> [--set-all]|provider <name> on [--value <tokens>]|provider <name> off|all <on|off>> [--json]
46
+ ${MODELS_CONTEXT_USAGE}
46
47
  ocx models shadow <status|set> [model|-] [--enabled <on|off>] [--json]
47
48
 
48
49
  Prices are USD per 1M tokens. Omitted cache rates default to 0.
@@ -39,6 +39,7 @@ import type {
39
39
  OpencodeProviderBlocks,
40
40
  OpencodeV2ProviderBlock,
41
41
  } from "../clients/config-export";
42
+ import type { EffectiveModelExportMetadata } from "../clients/config-export/contracts";
42
43
  import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog";
43
44
  import { commandInvocation } from "../lib/win-exec";
44
45
  import { configuredAdminToken, opencodeCatalogToken } from "../lib/admin-secrets";
@@ -106,12 +107,30 @@ export interface OpencodeProxyModelRow {
106
107
  contextWindow?: number;
107
108
  /** Authoritative output limit (CatalogModel.maxOutputTokens); optional. */
108
109
  maxOutputTokens?: number;
110
+ /** Authoritative input ceiling (CatalogModel.maxInputTokens); optional. */
111
+ maxInputTokens?: number;
112
+ /**
113
+ * Derived on the hub from known evidence; absent means unknown, never a negative. An
114
+ * explicit `false` from the hub is evidence too and is carried through unchanged.
115
+ */
116
+ supportsTools?: boolean;
117
+ supportsReasoning?: boolean;
118
+ supportsReasoningSummaries?: boolean;
109
119
  /** Declared input modalities from `/api/models`; carried into opencode model capabilities. */
110
120
  inputModalities?: string[];
111
121
  /** Declared effort ladder from `/api/models`; carried into opencode model variants. */
112
122
  reasoningEfforts?: string[];
113
123
  /** Declared default effort from `/api/models`. */
114
124
  defaultReasoningEffort?: string;
125
+ /**
126
+ * Effective export metadata `/api/models` attaches to every row it produces. Custom row
127
+ * fields are the operator's stored overrides, so this object is the authoritative
128
+ * projection a client consumes; routed and native rows carry their declared default in it
129
+ * because the row-level default is the picker's preference order. When present, an absent
130
+ * field inside means "no declared value" — never "fall back to the row" — and every field
131
+ * below still works for rows without it (older hubs, hand-built rows).
132
+ */
133
+ exportMetadata?: EffectiveModelExportMetadata;
115
134
  }
116
135
 
117
136
  const PROJECT_CONFIG_FILENAMES = ["opencode.json", "opencode.jsonc"] as const;
@@ -324,6 +343,12 @@ export async function fetchOpencodeProxyModels(
324
343
  /**
325
344
  * Visible OpenCode catalog entries from proxy /api/models rows. Disabled rows are omitted;
326
345
  * native rows are omitted in Codex Direct mode.
346
+ *
347
+ * Custom rows carry their effective projection in `exportMetadata`; when it is present it
348
+ * wins over the row's own fields, which for a custom row are the operator's stored overrides
349
+ * rather than the resolved capabilities. Routed/native projections also distinguish declared
350
+ * defaults from picker preferences; older hubs without the field retain their row values. The empty-vs-undefined ladder distinction
351
+ * survives: a row declaring `reasoningEfforts: []` exports `[]`, not an absent key.
327
352
  */
328
353
  export function opencodeCatalogFromProxyRows(
329
354
  rows: readonly OpencodeProxyModelRow[],
@@ -343,24 +368,43 @@ export function opencodeCatalogFromProxyRows(
343
368
  && !visibleRouted.has(row)) continue;
344
369
  if (seen.has(namespaced)) continue;
345
370
  seen.add(namespaced);
371
+ const effective = row.exportMetadata;
372
+ const maxTokens = effective?.maxTokens ?? row.maxOutputTokens;
373
+ const maxInputTokens = effective?.maxInputTokens ?? row.maxInputTokens;
374
+ const inputModalities = effective?.inputModalities ?? row.inputModalities;
375
+ const reasoningEfforts = effective?.reasoningEfforts ?? row.reasoningEfforts;
376
+ // The projection is the authority on the default: when it exists, an absent default means
377
+ // NO declared default, and falling back to the row would reintroduce the picker's
378
+ // synthetic medium/low value. Rows without one (older hubs, hand-built rows) keep the
379
+ // row's own value.
380
+ const defaultReasoningEffort = effective !== undefined
381
+ ? effective.defaultReasoningEffort
382
+ : row.defaultReasoningEffort;
383
+ const supportsTools = effective?.supportsTools ?? row.supportsTools;
384
+ const supportsReasoning = effective?.supportsReasoning ?? row.supportsReasoning;
385
+ const supportsReasoningSummaries = effective?.supportsReasoningSummaries ?? row.supportsReasoningSummaries;
346
386
  catalog.push({
347
387
  namespaced,
348
388
  native: row.native === true,
349
389
  provider: row.provider,
350
390
  id: row.id,
351
- contextWindow: row.contextWindow,
352
- ...(typeof row.maxOutputTokens === "number" ? { maxTokens: row.maxOutputTokens } : {}),
391
+ contextWindow: effective?.contextWindow ?? row.contextWindow,
392
+ ...(typeof maxTokens === "number" ? { maxTokens } : {}),
393
+ ...(typeof maxInputTokens === "number" ? { maxInputTokens } : {}),
353
394
  displayName: row.displayNameSource === "fallback" ? undefined : row.displayName,
354
- ...(Array.isArray(row.inputModalities) && row.inputModalities.length > 0
355
- ? { inputModalities: [...row.inputModalities] }
395
+ ...(Array.isArray(inputModalities) && inputModalities.length > 0
396
+ ? { inputModalities: [...inputModalities] }
356
397
  : {}),
357
398
  ...(typeof row.fastRowAvailable === "boolean" ? { fastRowAvailable: row.fastRowAvailable } : {}),
358
- ...(Array.isArray(row.reasoningEfforts) && row.reasoningEfforts.length > 0
359
- ? { reasoningEfforts: [...row.reasoningEfforts] }
360
- : {}),
361
- ...(typeof row.defaultReasoningEffort === "string" && row.defaultReasoningEffort.length > 0
362
- ? { defaultReasoningEffort: row.defaultReasoningEffort }
399
+ // `[]` is a declaration, not an absence: an explicit no-rungs ladder stays empty here.
400
+ ...(Array.isArray(reasoningEfforts) ? { reasoningEfforts: [...reasoningEfforts] } : {}),
401
+ ...(typeof defaultReasoningEffort === "string" && defaultReasoningEffort.length > 0
402
+ ? { defaultReasoningEffort }
363
403
  : {}),
404
+ // An explicit false survives the hop; only derivation is true-or-nothing.
405
+ ...(typeof supportsTools === "boolean" ? { supportsTools } : {}),
406
+ ...(typeof supportsReasoning === "boolean" ? { supportsReasoning } : {}),
407
+ ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
364
408
  });
365
409
  }
366
410
  return catalog;
@@ -377,7 +421,7 @@ export function isOpencodeRuntimeConfigError(
377
421
 
378
422
  /**
379
423
  * Merge inherited `OPENCODE_CONFIG_CONTENT` and override only our own blocks:
380
- * `provider.opencodex` (V1) and `providers.opencodex` (V2, the one carrying variants).
424
+ * `provider.opencodex` (V1 maps) and `providers.opencodex` (native V2 arrays).
381
425
  * When no inline layer is present, emit the minimal runtime object for this launcher.
382
426
  */
383
427
  export function mergeOpencodeRuntimeConfig(
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig } from "../config";
12
12
  import { apiKeyTransportConfigError, modelCapabilitiesConfigError, mergeModelCapabilities } from "../config/provider-validation";
13
- import { hasHelpFlag } from "./help";
13
+ import { hasHelpFlag, printSubcommandUsage } from "./help";
14
14
  import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
15
15
  import { providerConfigSeed } from "../providers/derive";
16
16
  import { dropProviderCustomModels } from "../providers/provider-id-rewrite";
@@ -472,38 +472,12 @@ function handleSetDefault(args: string[]): void {
472
472
  // Router (F2 fix: handle help flags internally, like service/codex-shim)
473
473
  // ---------------------------------------------------------------------------
474
474
 
475
- const PROVIDER_USAGE = `Usage: ocx provider <subcommand>
476
-
477
- Subcommands:
478
- list List configured and available providers
479
- add <name> Add a provider (registry or custom)
480
- edit <name> Edit live provider fields
481
- test <name> Test the provider's upstream model endpoint
482
- remove <name> Remove a configured provider
483
- show <name> Show provider config details
484
- set-default <name> Change the default provider
485
- selected <name> Show or set the provider model allowlist
486
- quota Show provider quota reports
487
- resets Show recently detected quota resets
488
- presets List GUI provider presets
489
- account-mode <mode> Set OpenAI Codex pool/direct mode
490
-
491
- Examples:
492
- ocx provider list
493
- ocx provider list --jsonl
494
- ocx provider add anthropic --api-key sk-ant-...
495
- ocx provider add my-ollama --adapter openai-chat --base-url http://localhost:11434/v1
496
- ocx provider show anthropic --json
497
- ocx provider set-default anthropic
498
- ocx provider edit xai --xai-chat on # opt Grok 4.5/4.6 into Chat Completions
499
- ocx provider edit xai --xai-chat off # use Responses again
500
- ocx provider remove my-ollama`;
501
475
 
502
476
  export async function handleProviderCommand(args: string[]): Promise<void> {
503
477
  const sub = args[0];
504
478
 
505
479
  if (!sub || sub === "help" || hasHelpFlag(args)) {
506
- console.log(PROVIDER_USAGE);
480
+ printSubcommandUsage("provider");
507
481
  process.exit(0);
508
482
  }
509
483
 
@@ -533,7 +507,7 @@ export async function handleProviderCommand(args: string[]): Promise<void> {
533
507
  break;
534
508
  }
535
509
  console.error(`Unknown provider subcommand: ${sub}`);
536
- console.error(PROVIDER_USAGE);
510
+ printSubcommandUsage("provider", undefined, { write: console.error });
537
511
  process.exit(1);
538
512
  }
539
513
  }
@@ -275,12 +275,36 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
275
275
  },
276
276
  {
277
277
  name: "provider",
278
- usage: "ocx provider <list|add|edit|test|remove|show|set-default|selected|quota|presets|account-mode>",
278
+ usage: "ocx provider <subcommand>",
279
279
  summary: "Non-interactive provider management.",
280
280
  details: [
281
- "Subcommands: list, add/edit/test/remove/show, set-default, selected, quota, presets, account-mode",
281
+ "Subcommands:",
282
+ " list List configured and available providers",
283
+ " add <name> Add a provider (registry or custom)",
284
+ " edit <name> Edit live provider fields",
285
+ " test <name> Test the provider's upstream model endpoint",
286
+ " remove <name> Remove a configured provider",
287
+ " show <name> Show provider config details",
288
+ " set-default <name> Change the default provider",
289
+ " selected <name> Show or set the provider model allowlist",
290
+ " quota Show provider quota reports",
291
+ " resets Show recently detected quota resets",
292
+ " presets List GUI provider presets",
293
+ " account-mode <mode> Set OpenAI Codex pool/direct mode",
294
+ "",
295
+ "Examples:",
296
+ " ocx provider list",
297
+ " ocx provider list --jsonl",
298
+ " ocx provider presets --json",
299
+ " ocx provider add anthropic --api-key <key>",
300
+ " ocx provider add my-ollama --adapter openai-chat --base-url http://localhost:11434/v1",
301
+ " ocx provider show anthropic --json",
302
+ " ocx provider set-default anthropic",
303
+ " ocx provider edit xai --xai-chat on # opt Grok 4.5/4.6 into Chat Completions",
304
+ " ocx provider edit xai --xai-chat off # use Responses again",
305
+ " ocx provider remove my-ollama",
306
+ "",
282
307
  "Registry providers are auto-configured by name. Custom providers need --adapter and --base-url.",
283
- "Run `ocx provider --help` for full usage and examples.",
284
308
  ],
285
309
  },
286
310
  {
@@ -383,11 +407,11 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
383
407
  "`inspect star` reads the repository star status only. Starring uses your GitHub identity and is available from the dashboard alone.",
384
408
  ],
385
409
  },
386
- { name: "logs", usage: "ocx logs [filters] [--follow] [--json|--jsonl]", summary: "Alias of ocx observe logs." },
410
+ { name: "logs", usage: "ocx logs [filters] [--follow] [--json|--jsonl]", summary: "Read or follow request logs (alias of ocx observe logs)." },
387
411
  {
388
412
  name: "usage",
389
413
  usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--json]",
390
- summary: "Alias of ocx observe usage.",
414
+ summary: "Report token usage and estimated cost (alias of ocx observe usage).",
391
415
  },
392
416
  {
393
417
  name: "storage",