@yansigit/opencodex 2.31.3 → 2.32.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 (107) hide show
  1. package/bin/ocx.mjs +99 -70
  2. package/gui/dist/assets/index-BG43zwVe.js +102 -0
  3. package/gui/dist/assets/index-CiSI-jrP.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +6 -8
  7. package/src/adapters/base.ts +2 -0
  8. package/src/adapters/command-code.ts +2 -3
  9. package/src/adapters/cursor/live-models.ts +8 -0
  10. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  11. package/src/adapters/cursor/tool-definitions.ts +1 -1
  12. package/src/adapters/google.ts +6 -7
  13. package/src/adapters/kiro.ts +0 -3
  14. package/src/adapters/openai-responses.ts +3 -0
  15. package/src/adapters/tool-catalog-nudge.ts +1 -1
  16. package/src/adapters/xai-web-search.ts +7 -2
  17. package/src/bridge.ts +21 -15
  18. package/src/cli/dispatch.ts +50 -2
  19. package/src/cli/doctor.ts +24 -11
  20. package/src/cli/help.ts +4 -3
  21. package/src/cli/index.ts +11 -4
  22. package/src/cli/models.ts +13 -3
  23. package/src/cli/observe.ts +20 -5
  24. package/src/cli/provider.ts +2 -1
  25. package/src/cli/registry.ts +7 -5
  26. package/src/cli/status.ts +2 -1
  27. package/src/cli/system-restart-client.ts +1 -1
  28. package/src/cli/usage-report.ts +134 -0
  29. package/src/codex/app-server-processes.ts +3 -1
  30. package/src/codex/catalog/aggregation.ts +13 -1
  31. package/src/codex/catalog/effort.ts +24 -9
  32. package/src/codex/catalog/model-metadata.ts +566 -0
  33. package/src/codex/catalog/parsing.ts +33 -0
  34. package/src/codex/catalog/provider-fetch.ts +123 -32
  35. package/src/codex/catalog/sync.ts +5 -4
  36. package/src/codex/desktop-app-restart.ts +342 -0
  37. package/src/codex/history-job.ts +32 -3
  38. package/src/codex/history-manifest.ts +112 -0
  39. package/src/codex/history-migration-guardian.ts +5 -5
  40. package/src/codex/history-provider.ts +825 -247
  41. package/src/codex/history-worker.ts +8 -5
  42. package/src/codex/inject.ts +49 -21
  43. package/src/codex/injected-marker.ts +1 -1
  44. package/src/codex/internal/history-writer.ts +4 -3
  45. package/src/codex/native-profile-startup.ts +157 -27
  46. package/src/codex/native-residue.ts +26 -33
  47. package/src/combos/failover.ts +27 -0
  48. package/src/compatibility/index.ts +26 -0
  49. package/src/compatibility/manifest.ts +253 -0
  50. package/src/compatibility/openai-responses.ts +81 -0
  51. package/src/config/atomic-write.ts +219 -0
  52. package/src/config/paths.ts +40 -0
  53. package/src/config/process-state.ts +308 -0
  54. package/src/config/provider-validation.ts +177 -0
  55. package/src/config.ts +75 -812
  56. package/src/generated/compatibility-version.json +145 -85
  57. package/src/images/plan.ts +5 -4
  58. package/src/integrations/ownership-policy.ts +141 -0
  59. package/src/integrations/ownership.ts +10 -0
  60. package/src/integrations/state.ts +44 -5
  61. package/src/integrations/writer.ts +6 -0
  62. package/src/lib/bounded-body.ts +14 -2
  63. package/src/lib/process-control.ts +2 -1
  64. package/src/lib/state-store-registrations.ts +2 -0
  65. package/src/lib/tool-argument-integers.ts +56 -5
  66. package/src/oauth/health.ts +1 -1
  67. package/src/providers/registry.ts +1 -1
  68. package/src/reasoning-effort.ts +19 -2
  69. package/src/responses/apply-patch-envelope.ts +63 -0
  70. package/src/responses/custom-tool-compat.ts +132 -38
  71. package/src/responses/parser.ts +3 -2
  72. package/src/responses/reasoning-replay-cache.ts +81 -3
  73. package/src/server/auth-cors.ts +9 -7
  74. package/src/server/index.ts +102 -21
  75. package/src/server/local-management-read-client.ts +1 -1
  76. package/src/server/local-provider-reload-client.ts +1 -1
  77. package/src/server/management/agent-settings-routes.ts +1 -1
  78. package/src/server/management/config-routes.ts +4 -1
  79. package/src/server/management/context.ts +1 -1
  80. package/src/server/management/logs-usage-routes.ts +27 -6
  81. package/src/server/management/model-routes.ts +8 -4
  82. package/src/server/management/native-integration-routes.ts +2 -1
  83. package/src/server/management/provider-capability-config.ts +1 -1
  84. package/src/server/management/system-restart.ts +1 -1
  85. package/src/server/port-reclaim.ts +1 -1
  86. package/src/server/proxy-liveness.ts +2 -1
  87. package/src/server/request-log-conversation.ts +30 -0
  88. package/src/server/responses/codex-auth-error.ts +55 -0
  89. package/src/server/responses/combo-stream-preflight.ts +171 -0
  90. package/src/server/responses/compact.ts +6 -21
  91. package/src/server/responses/core.ts +225 -94
  92. package/src/server/responses/fetch-helpers.ts +2 -97
  93. package/src/server/responses-custom-tool-repair.ts +41 -5
  94. package/src/server/responses-undeclared-tool-guard.ts +156 -15
  95. package/src/service.ts +8 -4
  96. package/src/types/request.ts +6 -1
  97. package/src/types/tools.ts +87 -11
  98. package/src/types.ts +1 -1
  99. package/src/update/index.ts +5 -4
  100. package/src/update/job.ts +3 -1
  101. package/src/update/transactional-install.mjs +8 -1
  102. package/src/usage/log.ts +16 -8
  103. package/src/usage/summary.ts +201 -8
  104. package/src/vision/describe.ts +18 -13
  105. package/src/web-search/executor.ts +10 -3
  106. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  107. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Human rendering for `ocx usage`.
3
+ *
4
+ * Kept out of `observe.ts` and away from the shared `summaryLines()` helper on
5
+ * purpose. `summaryLines()` is a generic depth-1 flattener shared with
6
+ * storage/memory/debug/claude-inbound/injection; it renders any array as
7
+ * "N item(s)", which is why every per-model and per-provider cost the server
8
+ * computes used to vanish before reaching the terminal. Deepening it would
9
+ * change five unrelated commands.
10
+ *
11
+ * Formatting follows the existing CLI house style: dynamic `padEnd` columns
12
+ * (as in `formatAccountTable`), plain text, no ANSI colour.
13
+ */
14
+
15
+ interface CostRow {
16
+ provider: string;
17
+ model?: string;
18
+ requests: number;
19
+ totalTokens: number;
20
+ estimatedCostUsd?: number;
21
+ }
22
+
23
+ interface UsageReportInput {
24
+ range?: string;
25
+ surface?: string;
26
+ since?: number | null;
27
+ summary?: {
28
+ requests?: number;
29
+ totalTokens?: number;
30
+ inputTokens?: number;
31
+ outputTokens?: number;
32
+ cachedInputTokens?: number;
33
+ estimatedCostUsd?: number;
34
+ unpricedRequests?: number;
35
+ unmeteredRequests?: number;
36
+ };
37
+ models?: CostRow[];
38
+ providers?: CostRow[];
39
+ days?: { date: string; requests: number; totalTokens: number; estimatedCostUsd?: number }[];
40
+ filter?: { provider: string | null; model: string | null; matched: boolean; comboOverlap: boolean };
41
+ }
42
+
43
+ const MAX_MODEL_ROWS = 10;
44
+
45
+ function count(value: number | undefined): string {
46
+ return (value ?? 0).toLocaleString("en-US");
47
+ }
48
+
49
+ /**
50
+ * Matches the dashboard's `~$` with four fraction digits. Estimates below a
51
+ * hundredth of a cent still read as a number rather than collapsing to $0.00,
52
+ * which matters when a single request is being inspected.
53
+ */
54
+ function usd(value: number | undefined): string {
55
+ if (typeof value !== "number" || !Number.isFinite(value)) return "—";
56
+ return `~$${value.toFixed(4)}`;
57
+ }
58
+
59
+ function table(header: string[], rows: string[][]): string[] {
60
+ if (rows.length === 0) return [];
61
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? "").length)));
62
+ const line = (cols: string[]): string => cols.map((c, i) => (c ?? "").padEnd(widths[i]!)).join(" ").trimEnd();
63
+ return [line(header), ...rows.map(line)];
64
+ }
65
+
66
+ function describeScope(data: UsageReportInput): string {
67
+ const parts = [`Usage — ${data.range ?? "?"}`];
68
+ if (data.surface && data.surface !== "all") parts.push(`surface=${data.surface}`);
69
+ if (data.filter?.provider) parts.push(`provider=${data.filter.provider}`);
70
+ if (data.filter?.model) parts.push(`model=${data.filter.model}`);
71
+ return parts.join(", ");
72
+ }
73
+
74
+ export function formatUsageReport(data: UsageReportInput): string[] {
75
+ const summary = data.summary ?? {};
76
+ const lines: string[] = [describeScope(data), ""];
77
+
78
+ if (data.filter && !data.filter.matched) {
79
+ const what = [data.filter.provider && `provider "${data.filter.provider}"`, data.filter.model && `model "${data.filter.model}"`]
80
+ .filter(Boolean).join(" and ");
81
+ lines.push(`No usage recorded for ${what} in this range.`);
82
+ lines.push("Check the spelling against `ocx usage --json`, or widen --range.");
83
+ return lines;
84
+ }
85
+
86
+ const tokenSplit = [
87
+ summary.inputTokens !== undefined ? `in ${count(summary.inputTokens)}` : null,
88
+ summary.outputTokens !== undefined ? `out ${count(summary.outputTokens)}` : null,
89
+ summary.cachedInputTokens ? `cached ${count(summary.cachedInputTokens)}` : null,
90
+ ].filter(Boolean).join(" / ");
91
+
92
+ lines.push(`Requests ${count(summary.requests)}`);
93
+ lines.push(`Tokens ${count(summary.totalTokens)}${tokenSplit ? ` (${tokenSplit})` : ""}`);
94
+ lines.push(`Est. cost ${usd(summary.estimatedCostUsd)} API list-price equivalent (this range)`);
95
+
96
+ const unpriced = summary.unpricedRequests ?? 0;
97
+ const unmetered = summary.unmeteredRequests ?? 0;
98
+ if (unpriced > 0 || unmetered > 0) {
99
+ // Spelled out because a $0 total is ambiguous otherwise: it can mean "no
100
+ // spend" or "no price row matched", and those are very different answers.
101
+ lines.push(` ${count(unpriced)} unpriced, ${count(unmetered)} unmetered excluded from ~$`);
102
+ }
103
+
104
+ const providers = (data.providers ?? []).filter(row => row.requests > 0);
105
+ if (providers.length > 0) {
106
+ lines.push("");
107
+ lines.push(...table(
108
+ ["PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
109
+ providers.map(row => [row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
110
+ ));
111
+ }
112
+
113
+ const models = (data.models ?? []).filter(row => row.requests > 0);
114
+ if (models.length > 0) {
115
+ lines.push("");
116
+ const shown = models.slice(0, MAX_MODEL_ROWS);
117
+ lines.push(...table(
118
+ ["MODEL", "PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
119
+ shown.map(row => [row.model ?? "-", row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
120
+ ));
121
+ if (models.length > shown.length) {
122
+ lines.push(`... ${models.length - shown.length} more (use --json)`);
123
+ }
124
+ }
125
+
126
+ if (data.filter?.comboOverlap) {
127
+ lines.push("");
128
+ lines.push("Some requests ran as combos, so per-model request counts can overlap. Cost does not.");
129
+ }
130
+
131
+ lines.push("");
132
+ lines.push("Not a billing receipt. Subscription usage or provider credits may apply instead.");
133
+ return lines;
134
+ }
@@ -17,7 +17,8 @@ import {
17
17
  import { readCodexCatalogPath } from "./catalog/parsing";
18
18
 
19
19
  export const STALE_CODEX_APP_SERVER_HINT =
20
- "If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex).";
20
+ "If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex). "
21
+ + "On Windows the desktop app itself may also need a full restart (ocx sync --restart-desktop-app).";
21
22
 
22
23
  /** Attach the shared dashboard hint only after a catalog or models_cache write. */
23
24
  export function attachStaleAppServerHint<T extends {
@@ -505,6 +506,7 @@ export function formatStaleCodexAppServerWarning(
505
506
  `WARNING: ${processes.length} Codex app-server process(es) still running (PID${processes.length === 1 ? "" : "s"}: ${pids}). `
506
507
  + "Disk catalog/cache were updated, but Codex may keep showing the old model list until those processes restart. "
507
508
  + "Re-run with `ocx sync --restart-codex` (or `ocx sync-cache --restart-codex`) to send SIGTERM only to matching app-server processes. "
509
+ + "On Windows the desktop app itself may also need a full restart (`ocx sync --restart-desktop-app`). "
508
510
  + "Active turns may be interrupted."
509
511
  );
510
512
  }
@@ -152,10 +152,20 @@ export function deriveComboCatalogModel(
152
152
  // A combo is cap-limited only when every member defining its effective minimum was
153
153
  // itself reduced by a provider cap. An uncapped member at the same minimum means the
154
154
  // combo would have the same window even without the cap.
155
- const contextCapped = limitingMembers.every(member => member.contextCapped === true);
155
+ const contextCapped = limitingMembers.every(member => (
156
+ member.contextCapped === true
157
+ || (
158
+ typeof member.detectedContextWindow === "number"
159
+ && typeof member.contextWindow === "number"
160
+ && member.detectedContextWindow > member.contextWindow
161
+ )
162
+ ));
156
163
  const maxInputTokens = Math.min(
157
164
  ...members.map(member => member.maxInputTokens ?? member.contextWindow!),
158
165
  );
166
+ const detectedContextWindow = Math.min(
167
+ ...limitingMembers.map(member => member.detectedContextWindow ?? member.contextWindow!),
168
+ );
159
169
  const defaultReasoningEffort = effectiveComboDefault(
160
170
  combo.defaultEffort,
161
171
  reasoningEfforts,
@@ -167,6 +177,8 @@ export function deriveComboCatalogModel(
167
177
  owned_by: COMBO_NAMESPACE,
168
178
  contextWindow,
169
179
  maxInputTokens,
180
+ metadataSource: "derived",
181
+ detectedContextWindow,
170
182
  ...(hasLimitingContextCapMetadata ? { contextCapped } : {}),
171
183
  inputModalities,
172
184
  reasoningEfforts,
@@ -33,6 +33,7 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
33
33
 
34
34
  import { generatedModelMetadata, readCatalog, readCodexCatalogPath } from "./parsing";
35
35
  import type { CatalogModel, RawEntry } from "./parsing";
36
+ import { shouldStampContextProvenance } from "./model-metadata";
36
37
  import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
37
38
  import { nativeOpenAiCapabilitySourceSlug } from "./native-models";
38
39
  import { loadBundledCodexCatalog } from "./bundled";
@@ -171,33 +172,47 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
171
172
  * live in that table.
172
173
  */
173
174
  function stampCapabilityProvenance(entry: RawEntry, model: CatalogModel): void {
174
- // Virtual combo rows are synthesized from last-resort defaults (a generic 128k
175
- // context and a `["text"]` modality), so their values are placeholders rather
176
- // than assertions. Stamping them would reintroduce the exact false-evidence
177
- // defect this block exists to prevent.
178
- if (model.provider === COMBO_NAMESPACE) return;
175
+ // Virtual combo rows without derived member evidence are placeholders
176
+ // (generic 128k / `["text"]`). Stamping those would reintroduce the
177
+ // false-evidence defect this block exists to prevent. Combos that actually
178
+ // min() real member windows carry `metadataSource: "derived"`.
179
+ if (model.provider === COMBO_NAMESPACE && model.metadataSource !== "derived") return;
179
180
 
180
181
  const meta = generatedModelMetadata(model.provider, model.id);
181
182
  const metaContext = typeof meta?.contextWindow === "number" && meta.contextWindow > 0
182
- // The generated context is capped before it reaches the entry, so provenance
183
- // must apply the same cap or routing would advertise a window the cap refused.
184
183
  ? applyProviderContextCap(meta.contextWindow, model.contextCap) ?? meta.contextWindow
185
184
  : undefined;
186
- const contextWindow = typeof model.contextWindow === "number" && model.contextWindow > 0
185
+ const stampModelContext = shouldStampContextProvenance(model);
186
+ const contextWindow = stampModelContext
187
187
  ? model.contextWindow
188
- : metaContext;
188
+ : (model.metadataSource === undefined ? metaContext : undefined);
189
189
  const inputModalities = Array.isArray(model.inputModalities) && model.inputModalities.length > 0
190
190
  ? model.inputModalities
191
191
  : (Array.isArray(meta?.input) && meta.input.length > 0 ? meta.input : undefined);
192
+ const maxInputTokens = typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
193
+ ? model.maxInputTokens
194
+ : undefined;
195
+ const source = model.metadataSource;
196
+ const stampSource = source === "live" || source === "registry" || source === "snapshot" || source === "derived";
197
+
198
+ if (contextWindow === undefined && inputModalities === undefined
199
+ && !(Array.isArray(model.capabilities) && model.capabilities.length > 0)) {
200
+ return;
201
+ }
192
202
 
193
203
  entry.opencodex_capability_provenance = {
194
204
  provider: model.provider,
195
205
  model_id: model.id,
196
206
  ...(contextWindow !== undefined ? { context_window: contextWindow } : {}),
207
+ ...(maxInputTokens !== undefined && stampModelContext ? { max_input_tokens: maxInputTokens } : {}),
197
208
  ...(inputModalities !== undefined ? { input_modalities: [...inputModalities] } : {}),
198
209
  ...(Array.isArray(model.capabilities) && model.capabilities.length > 0
199
210
  ? { capabilities: [...model.capabilities] }
200
211
  : {}),
212
+ ...(stampSource ? { source } : {}),
213
+ ...(typeof model.metadataObservedAt === "string" && stampSource
214
+ ? { observed_at: model.metadataObservedAt }
215
+ : {}),
201
216
  };
202
217
  }
203
218