@yansigit/opencodex 2.33.0 → 2.35.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 (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -10,6 +10,21 @@ export class ChatCompletionsRequestError extends Error {}
10
10
  type Rec = Record<string, unknown>;
11
11
  type ChatCompletionsRoutingBody = Rec & { model: string; messages: unknown[] };
12
12
 
13
+ /** Session/thread headers the Chat -> Responses bridge must preserve for provider affinity. */
14
+ export const CHAT_RESPONSES_SESSION_HEADERS = [
15
+ "session_id",
16
+ "session-id",
17
+ "x-session-id",
18
+ "thread-id",
19
+ ] as const;
20
+
21
+ export function copyChatResponsesSessionHeaders(source: Headers, target: Headers): void {
22
+ for (const name of CHAT_RESPONSES_SESSION_HEADERS) {
23
+ const value = source.get(name);
24
+ if (value) target.set(name, value);
25
+ }
26
+ }
27
+
13
28
  function isRec(v: unknown): v is Rec {
14
29
  return !!v && typeof v === "object" && !Array.isArray(v);
15
30
  }
@@ -53,6 +68,14 @@ function imageUrlFromPart(part: Rec): string | null {
53
68
  return null;
54
69
  }
55
70
 
71
+ function videoUrlFromPart(part: Rec): string | null {
72
+ if (part.type !== "video_url") return null;
73
+ const videoUrl = part.video_url;
74
+ if (typeof videoUrl === "string" && videoUrl.length > 0) return videoUrl;
75
+ if (isRec(videoUrl) && typeof videoUrl.url === "string" && videoUrl.url.length > 0) return videoUrl.url;
76
+ return null;
77
+ }
78
+
56
79
  function userContentToBlocks(content: unknown): Rec[] {
57
80
  if (typeof content === "string") {
58
81
  return content.length > 0 ? [{ type: "input_text", text: content }] : [];
@@ -70,7 +93,12 @@ function userContentToBlocks(content: unknown): Rec[] {
70
93
  continue;
71
94
  }
72
95
  const imageUrl = imageUrlFromPart(raw);
73
- if (imageUrl) blocks.push({ type: "input_image", image_url: imageUrl });
96
+ if (imageUrl) {
97
+ blocks.push({ type: "input_image", image_url: imageUrl });
98
+ continue;
99
+ }
100
+ const videoUrl = videoUrlFromPart(raw);
101
+ if (videoUrl) blocks.push({ type: "input_video", video_url: videoUrl });
74
102
  }
75
103
  return blocks;
76
104
  }
@@ -9,7 +9,14 @@ type Rec = Record<string, unknown>;
9
9
 
10
10
  import { decodeServerSentEvents, sseFieldValue } from "../lib/sse-decoder";
11
11
  import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
12
- import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, isCyberPolicyMessage } from "../lib/errors";
12
+ import {
13
+ classifyError,
14
+ cyberPolicyErrorType,
15
+ CYBER_POLICY_ERROR_CODE,
16
+ isCyberPolicyCode,
17
+ isCyberPolicyMessage,
18
+ } from "../lib/errors";
19
+ import { redactSecretString } from "../lib/redact";
13
20
 
14
21
  function isRec(v: unknown): v is Rec {
15
22
  return !!v && typeof v === "object" && !Array.isArray(v);
@@ -48,14 +55,14 @@ export function chatCompletionsUsage(usage: unknown): Rec {
48
55
  export function chatCompletionsErrorBody(
49
56
  status: number,
50
57
  message: string,
51
- type = "invalid_request_error",
58
+ type?: string,
52
59
  code?: string | null,
53
60
  ): Rec {
54
61
  if (isCyberPolicyCode(code) || isCyberPolicyMessage(message)) {
55
62
  return {
56
63
  error: {
57
64
  message,
58
- type: "invalid_request_error",
65
+ type: cyberPolicyErrorType(type),
59
66
  param: null,
60
67
  code: CYBER_POLICY_ERROR_CODE,
61
68
  },
@@ -64,7 +71,7 @@ export function chatCompletionsErrorBody(
64
71
  return {
65
72
  error: {
66
73
  message,
67
- type,
74
+ type: type ?? "invalid_request_error",
68
75
  param: null,
69
76
  code: code !== undefined
70
77
  ? code
@@ -311,8 +318,9 @@ export function responsesSseToChatCompletionsSse(
311
318
  // Deliver the error frame then close the stream abnormally (no [DONE]).
312
319
  // Do not controller.error() — that can drop already-enqueued bytes from consumers
313
320
  // like response.text().
314
- const statusHint = details?.status ?? streamErrorStatus(message);
315
- const classified = classifyError(statusHint, details?.type ?? "upstream_error", message);
321
+ const safeMessage = redactSecretString(message);
322
+ const statusHint = details?.status ?? streamErrorStatus(safeMessage);
323
+ const classified = classifyError(statusHint, details?.type ?? "upstream_error", safeMessage);
316
324
  const translatorOverflow = details?.code === "translation_buffer_limit";
317
325
  if (translatorOverflow) {
318
326
  upstreamAbort.abort(new Error("upstream translation buffer exceeded the safe limit"));
@@ -324,7 +332,7 @@ export function responsesSseToChatCompletionsSse(
324
332
  classified.type = "upstream_error";
325
333
  } else if (isCyberPolicyCode(details?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
326
334
  classified.code = CYBER_POLICY_ERROR_CODE;
327
- classified.type = "invalid_request_error";
335
+ classified.type = cyberPolicyErrorType(details?.type);
328
336
  } else if (details?.code !== undefined && details.code !== null && !classified.code) {
329
337
  classified.code = details.code;
330
338
  }
@@ -218,7 +218,14 @@ function isOwnedFile(path: string): boolean {
218
218
  export function syncClaudeAgentDefs(defs: readonly ClaudeAgentDef[], configDir = claudeConfigDir()): string[] | null {
219
219
  try {
220
220
  const dir = join(configDir, "agents");
221
- mkdirSync(dir, { recursive: true });
221
+ if (defs.length === 0) {
222
+ try { lstatSync(dir); } catch (error) {
223
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
224
+ throw error;
225
+ }
226
+ } else {
227
+ mkdirSync(dir, { recursive: true });
228
+ }
222
229
  const keep = new Set(defs.map(d => d.file));
223
230
  for (const existing of readdirSync(dir)) {
224
231
  if (!existing.startsWith(OWNED_PREFIX) || !existing.endsWith(".md")) continue;
@@ -2,8 +2,9 @@
2
2
  * Claude Code outbound: internal /v1/responses output -> Anthropic Messages API shapes.
3
3
  *
4
4
  * Wire contract pinned in devlog/260711_claude_inbound/003_evidence.md (all Tier 2):
5
- * - SSE order: message_start -> (content_block_start -> deltas -> content_block_stop)*
6
- * -> message_delta -> message_stop; any number of `ping`.
5
+ * - Transport-only `ping` events may appear at any point, including before
6
+ * message_start. Semantic framing stays message_start ->
7
+ * (content_block_start -> deltas -> content_block_stop)* -> message_delta -> message_stop.
7
8
  * - thinking blocks get thinking_delta(s) then ONE synthetic signature_delta just
8
9
  * before content_block_stop (CCR precedent: Claude Code does not verify signatures).
9
10
  * - message_delta.usage is cumulative; message_start embeds a full message snapshot.
@@ -267,12 +268,12 @@ export function responsesSseToAnthropicSse(
267
268
  emit("message_start", { type: "message_start", message: messageSnapshot(model) });
268
269
  emit("ping", { type: "ping" });
269
270
  };
270
- // Once a semantic Anthropic message has started, keepalive pings protect remote
271
- // deployments behind LB/NAT idle timeouts. Transport-only Responses prelude frames
272
- // must not manufacture a message before a possible initial error.
271
+ // Keepalive pings protect remote deployments behind LB/NAT idle timeouts even
272
+ // before semantic output. They are transport-only and must not manufacture a
273
+ // message before a possible initial error.
273
274
  if (pingIntervalMs > 0) {
274
275
  pingTimer = setInterval(() => {
275
- if (terminated || !started) return;
276
+ if (terminated || (controller.desiredSize ?? 0) <= 0) return;
276
277
  try {
277
278
  emit("ping", { type: "ping" });
278
279
  } catch { /* controller torn down; the read loop is ending anyway */ }
@@ -352,7 +353,8 @@ export function responsesSseToAnthropicSse(
352
353
  const type = upstreamDerived && isTransientUpstreamStatus(status) ? "overloaded_error" : undefined;
353
354
  if (!started) {
354
355
  // An initial upstream failure is an Anthropic error stream, not a partial message.
355
- // Do not manufacture message_start/ping before the terminal error.
356
+ // Do not manufacture message_start before the terminal error. Earlier transport-only
357
+ // pings remain valid and do not turn the failure into a partial message.
356
358
  emit("error", anthropicErrorBody(status, message, type, code));
357
359
  return;
358
360
  }
@@ -366,7 +368,7 @@ export function responsesSseToAnthropicSse(
366
368
  // Transport prelude only. Start Anthropic framing on semantic output or completion.
367
369
  break;
368
370
  case "response.heartbeat":
369
- if (started) emit("ping", { type: "ping" });
371
+ if ((controller.desiredSize ?? 0) > 0) emit("ping", { type: "ping" });
370
372
  break;
371
373
  case "response.output_text.delta": {
372
374
  if (typeof data.delta !== "string" || data.delta.length === 0) break;
@@ -141,6 +141,8 @@ export interface FamilyRows {
141
141
  }
142
142
 
143
143
  export interface CodexQuotaDto {
144
+ fiveHourPercent?: number;
145
+ fiveHourResetAt?: number;
144
146
  weeklyPercent?: number;
145
147
  monthlyPercent?: number;
146
148
  weeklyResetAt?: number;
@@ -158,8 +160,6 @@ export interface ProviderQuotaWindowDto {
158
160
  }
159
161
 
160
162
  export interface ProviderQuotaDto extends CodexQuotaDto {
161
- fiveHourPercent?: number;
162
- fiveHourResetAt?: number;
163
163
  customWindows?: ProviderQuotaWindowDto[];
164
164
  updatedAt?: number;
165
165
  }
@@ -187,7 +187,7 @@ interface CodexAccountDto {
187
187
  function projectQuota(quota: CodexQuotaDto | null | undefined): CodexQuotaDto | null {
188
188
  if (!quota) return null;
189
189
  const projected: CodexQuotaDto = {};
190
- for (const key of ["weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt", "shortPercent", "shortResetAt", "shortWindowSeconds"] as const) {
190
+ for (const key of ["fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt", "shortPercent", "shortResetAt", "shortWindowSeconds"] as const) {
191
191
  if (typeof quota[key] === "number" && Number.isFinite(quota[key])) projected[key] = quota[key];
192
192
  }
193
193
  return projected;
@@ -240,10 +240,22 @@ interface OAuthAccountDto {
240
240
  email?: string;
241
241
  active?: boolean;
242
242
  needsReauth?: boolean;
243
+ quota?: CodexQuotaDto | null;
244
+ quotaUnavailable?: boolean;
243
245
  }
244
246
 
245
- async function fetchOAuthRows(deps: AccountDeps, baseUrl: string, name: string): Promise<FamilyRows> {
246
- const res = await apiJson(deps, baseUrl, "GET", `/api/oauth/accounts?provider=${encodeURIComponent(name)}`);
247
+ async function fetchOAuthRows(
248
+ deps: AccountDeps,
249
+ baseUrl: string,
250
+ name: string,
251
+ quota?: { refresh?: boolean },
252
+ ): Promise<FamilyRows> {
253
+ // Quota is opt-in: the server probes the upstream once per stored credential when `quota=1`
254
+ // is present, so the default listing must stay a cheap local read (#2566).
255
+ const query = quota
256
+ ? `?provider=${encodeURIComponent(name)}&quota=1${quota.refresh ? "&refresh=1" : ""}`
257
+ : `?provider=${encodeURIComponent(name)}`;
258
+ const res = await apiJson(deps, baseUrl, "GET", `/api/oauth/accounts${query}`);
247
259
  if (res.status === 0) return { rows: [], activeId: null, status: 0, networkDown: true };
248
260
  if (res.status !== 200) return { rows: [], activeId: null, status: res.status, errorJson: res.json };
249
261
  const activeId = typeof res.json.activeAccountId === "string" ? res.json.activeAccountId : null;
@@ -256,6 +268,8 @@ async function fetchOAuthRows(deps: AccountDeps, baseUrl: string, name: string):
256
268
  email: a.email,
257
269
  active: a.active ?? a.id === activeId,
258
270
  needsReauth: a.needsReauth,
271
+ ...(a.quota !== undefined ? { quota: a.quota } : {}),
272
+ ...(a.quotaUnavailable !== undefined ? { quotaUnavailable: a.quotaUnavailable } : {}),
259
273
  }));
260
274
  return { rows, activeId, status: 200 };
261
275
  }
@@ -284,9 +298,15 @@ async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string): P
284
298
  return { rows, activeId, status: 200 };
285
299
  }
286
300
 
287
- export function fetchRows(deps: AccountDeps, baseUrl: string, name: string, type: AccountType): Promise<FamilyRows> {
301
+ export function fetchRows(
302
+ deps: AccountDeps,
303
+ baseUrl: string,
304
+ name: string,
305
+ type: AccountType,
306
+ quota?: { refresh?: boolean },
307
+ ): Promise<FamilyRows> {
288
308
  if (type === "codex") return fetchCodexRows(deps, baseUrl);
289
- if (type === "oauth") return fetchOAuthRows(deps, baseUrl, name);
309
+ if (type === "oauth") return fetchOAuthRows(deps, baseUrl, name, quota);
290
310
  return fetchKeyRows(deps, baseUrl, name);
291
311
  }
292
312
 
@@ -250,9 +250,16 @@ function resetIso(value: number | undefined): string | null {
250
250
  function refreshLine(row: FamilyRows["rows"][number]): string {
251
251
  const parts = [row.id === MAIN_ID ? "main" : row.id, row.email, row.plan];
252
252
  const quota = row.quota;
253
- if (!quota || (quota.weeklyPercent === undefined && quota.monthlyPercent === undefined)) {
253
+ const fiveHourPercent = quota?.fiveHourPercent ?? quota?.shortPercent;
254
+ const fiveHourResetAt = quota?.fiveHourResetAt ?? quota?.shortResetAt;
255
+ if (!quota || (quota.weeklyPercent === undefined && quota.monthlyPercent === undefined && fiveHourPercent === undefined)) {
254
256
  parts.push("quota: unknown");
255
257
  } else {
258
+ if (fiveHourPercent !== undefined) {
259
+ parts.push(`5h ${fiveHourPercent}%`);
260
+ const fiveHourReset = resetIso(fiveHourResetAt);
261
+ if (fiveHourReset) parts.push(`resets ${fiveHourReset}`);
262
+ }
256
263
  if (quota.weeklyPercent !== undefined) parts.push(`weekly ${quota.weeklyPercent}%`);
257
264
  const weeklyReset = resetIso(quota.weeklyResetAt);
258
265
  if (weeklyReset) parts.push(`resets ${weeklyReset}`);
@@ -272,14 +279,14 @@ function quotaParts(quota: ProviderQuotaDto): string[] {
272
279
  const reset = resetIso(resetAt);
273
280
  if (reset) parts.push(`resets ${reset}`);
274
281
  };
275
- add("5h", quota.fiveHourPercent, quota.fiveHourResetAt);
282
+ add("5h", quota.fiveHourPercent ?? quota.shortPercent, quota.fiveHourResetAt ?? quota.shortResetAt);
276
283
  add("weekly", quota.weeklyPercent, quota.weeklyResetAt);
277
284
  add("monthly", quota.monthlyPercent, quota.monthlyResetAt);
278
285
  for (const window of quota.customWindows ?? []) add(window.label, window.percent, window.resetAt);
279
286
  return parts;
280
287
  }
281
288
 
282
- function providerQuotaLine(name: string, report: ProviderQuotaReportDto): string {
289
+ export function providerQuotaLine(name: string, report: ProviderQuotaReportDto): string {
283
290
  return [name, ...quotaParts(report.quota)].join(" ");
284
291
  }
285
292
 
@@ -16,7 +16,7 @@ const MAIN_CODEX_ID = "__main__";
16
16
  const REPLACEMENT_STYLE_OAUTH = new Set(["kiro"]);
17
17
 
18
18
  const ACCOUNT_USAGE = `Usage:
19
- ocx account list [provider] [--json] [--all]
19
+ ocx account list [provider] [--json] [--all] [--quota [--refresh]]
20
20
  ocx account current <provider> [--json]
21
21
  ocx account use <provider> <account-or-key-id|main> [--json]
22
22
  ocx account refresh <provider> [--json]
@@ -75,11 +75,29 @@ function priorityText(row: AccountRow): string {
75
75
  return row.priority > 0 ? `+${row.priority}` : String(row.priority);
76
76
  }
77
77
 
78
- export function formatAccountTable(rows: AccountRow[]): string {
78
+ /**
79
+ * Compact per-account quota for the opt-in QUOTA column: the two windows an operator actually
80
+ * decides on before a long session. The full breakdown stays in `--json`.
81
+ */
82
+ function quotaText(row: AccountRow): string {
83
+ if ((row as { quotaUnavailable?: boolean }).quotaUnavailable) return "unavailable";
84
+ const quota = row.quota;
85
+ if (!quota) return "-";
86
+ const parts: string[] = [];
87
+ // Two spellings reach this DTO: the per-account provider probe reports `fiveHourPercent`,
88
+ // while the Codex pool reports the same idea as `shortPercent`.
89
+ const short = quota.fiveHourPercent ?? quota.shortPercent;
90
+ if (typeof short === "number") parts.push(`5h ${short}%`);
91
+ if (typeof quota.weeklyPercent === "number") parts.push(`wk ${quota.weeklyPercent}%`);
92
+ return parts.length > 0 ? parts.join(" ") : "-";
93
+ }
94
+
95
+ export function formatAccountTable(rows: AccountRow[], withQuota = false): string {
79
96
  const header = ["PROVIDER", "TYPE", "ID", "PLAN/LABEL", "PRIORITY", "STATUS"];
97
+ if (withQuota) header.push("QUOTA");
80
98
  const data = rows.map(r => {
81
99
  const keyLabel = r.masked && r.label !== r.masked ? `${r.masked} (${r.label})` : r.masked;
82
- return [
100
+ const cols = [
83
101
  r.provider,
84
102
  r.type,
85
103
  displayId(r.id),
@@ -87,6 +105,8 @@ export function formatAccountTable(rows: AccountRow[]): string {
87
105
  priorityText(r),
88
106
  statusText(r),
89
107
  ];
108
+ if (withQuota) cols.push(quotaText(r));
109
+ return cols;
90
110
  });
91
111
  const widths = header.map((h, i) => Math.max(h.length, ...data.map(d => d[i]!.length)));
92
112
  const line = (cols: string[]) => cols.map((c, i) => c.padEnd(widths[i]!)).join(" ").trimEnd();
@@ -96,6 +116,10 @@ export function formatAccountTable(rows: AccountRow[]): string {
96
116
  async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
97
117
  const wantsJson = consumeFlag(rest, "--json");
98
118
  const showAll = consumeFlag(rest, "--all");
119
+ // Opt-in: the server probes the upstream once per stored credential, so the default listing
120
+ // stays a cheap local read (#2566). --refresh bypasses the server-side TTL.
121
+ const wantsQuota = consumeFlag(rest, "--quota");
122
+ const refreshQuota = consumeFlag(rest, "--refresh");
99
123
  const name = rest.shift();
100
124
  const leftover = leftoverArgsError(rest);
101
125
  if (leftover) {
@@ -139,7 +163,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
139
163
  const rows: AccountRow[] = [];
140
164
  const notes: string[] = [];
141
165
  for (const t of targets) {
142
- const r = await fetchRows(deps, baseUrl, t.name, t.type);
166
+ const r = await fetchRows(deps, baseUrl, t.name, t.type, wantsQuota ? { refresh: refreshQuota } : undefined);
143
167
  if (r.networkDown) return proxyUnreachable();
144
168
  if (r.errorJson) {
145
169
  if (name) return apiError(r.errorJson, `failed to list ${t.name}`);
@@ -174,7 +198,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
174
198
  console.log(JSON.stringify({ accounts: rows, notes }, null, 2));
175
199
  return 0;
176
200
  }
177
- if (rows.length > 0) console.log(formatAccountTable(rows));
201
+ if (rows.length > 0) console.log(formatAccountTable(rows, wantsQuota));
178
202
  for (const n of notes) console.log(n);
179
203
  if (rows.length === 0 && notes.length === 0) console.log("No stored accounts or keys.");
180
204
  return 0;
@@ -0,0 +1,66 @@
1
+ import { CliUsageError, printData, rejectArgs, runtimeRequest, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
2
+
3
+ const USAGE = `Usage:
4
+ ocx alias list [--json]
5
+ ocx alias set <provider> <alias>
6
+ ocx alias set <provider>/<native-model-id> <alias>
7
+ ocx alias rm <provider>[/<native-model-id>]
8
+ ocx alias defaults <on|off> [--provider <name>]`;
9
+
10
+ function selector(value: string): { provider: string; model?: string } {
11
+ const slash = value.indexOf("/");
12
+ return slash < 0 ? { provider: value } : { provider: value.slice(0, slash), model: value.slice(slash + 1) };
13
+ }
14
+
15
+ export async function handleAliasCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
16
+ const args = [...argv];
17
+ const action = (args.shift() ?? "list").toLowerCase();
18
+ const wantsJson = takeFlag(args, "--json");
19
+ if (action === "list") {
20
+ rejectArgs(args, USAGE);
21
+ const result = await runtimeRequest<Record<string, unknown>>("/api/aliases", {}, deps);
22
+ const lines: string[] = [];
23
+ for (const [target, alias] of Object.entries((result.providers ?? {}) as Record<string, string>)) lines.push(`provider ${target} ${alias} user`);
24
+ for (const [provider, rows] of Object.entries((result.models ?? {}) as Record<string, Record<string, { alias: string; source: string }>>)) {
25
+ for (const [model, value] of Object.entries(rows)) lines.push(`model ${provider}/${model} ${value.alias} ${value.source}`);
26
+ }
27
+ printData(result, wantsJson, lines.length ? lines : ["No aliases configured."]);
28
+ return 0;
29
+ }
30
+ if (action === "defaults") {
31
+ const state = args.shift()?.toLowerCase();
32
+ const provider = takeOption(args, "--provider");
33
+ rejectArgs(args, USAGE);
34
+ if (state !== "on" && state !== "off") throw new CliUsageError("defaults requires on or off", USAGE);
35
+ const result = await runtimeRequest("/api/default-aliases", { method: "PUT", body: JSON.stringify({ enabled: state === "on", ...(provider ? { provider } : {}) }) }, deps);
36
+ printData(result, wantsJson, [`Default aliases ${state}${provider ? ` for ${provider}` : " globally"}.`]);
37
+ return 0;
38
+ }
39
+ const target = args.shift()?.trim();
40
+ if (!target) throw new CliUsageError("alias target is required", USAGE);
41
+ const parsed = selector(target);
42
+ if (!parsed.provider || parsed.model === "") throw new CliUsageError("target must be provider or provider/native-model-id", USAGE);
43
+ if (action === "set") {
44
+ const alias = args.shift()?.trim();
45
+ rejectArgs(args, USAGE);
46
+ if (!alias) throw new CliUsageError("alias value is required", USAGE);
47
+ const path = parsed.model === undefined
48
+ ? `/api/providers/${encodeURIComponent(parsed.provider)}/alias`
49
+ : `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`;
50
+ const body = parsed.model === undefined ? { alias } : { set: { [parsed.model]: alias } };
51
+ const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps);
52
+ printData(result, wantsJson, [`${target} → ${alias}`]);
53
+ return 0;
54
+ }
55
+ if (action === "rm") {
56
+ rejectArgs(args, USAGE);
57
+ const path = parsed.model === undefined
58
+ ? `/api/providers/${encodeURIComponent(parsed.provider)}/alias`
59
+ : `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`;
60
+ const body = parsed.model === undefined ? { alias: null } : { remove: [parsed.model] };
61
+ const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps);
62
+ printData(result, wantsJson, [`Removed alias for ${target}.`]);
63
+ return 0;
64
+ }
65
+ throw new CliUsageError(`unknown alias action '${action}'`, USAGE);
66
+ }
package/src/cli/claude.ts CHANGED
@@ -34,6 +34,8 @@ export type ClaudeEnvDeps = {
34
34
  authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
35
35
  /** Test seam; production uses the authenticated Node-launcher context. */
36
36
  preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
37
+ /** Explicit unsafe opt-in from a root `--dangerously-skip-permissions` launch. */
38
+ allowRootSkipPermissions?: boolean;
37
39
  };
38
40
 
39
41
  function isClaudeLoopbackHostname(hostname: string): boolean {
@@ -115,6 +117,9 @@ export function buildClaudeEnv(
115
117
  if (env[name] !== undefined && env[name] !== "") return; // user wins
116
118
  env[name] = value;
117
119
  };
120
+ if (deps.allowRootSkipPermissions === true) {
121
+ setDefault("IS_SANDBOX", "1");
122
+ }
118
123
  setDefault("ANTHROPIC_BASE_URL", `http://127.0.0.1:${port}`);
119
124
  const existingBaseUrl = env.ANTHROPIC_BASE_URL;
120
125
  if (existingBaseUrl) {
@@ -301,6 +306,22 @@ export function claudeNotFoundHint(
301
306
  return platform === "win32" && code === 9009 && !signal ? CLAUDE_INSTALL_HINT : null;
302
307
  }
303
308
 
309
+ export function shouldAllowRootSkipPermissions(
310
+ args: readonly string[],
311
+ getuid: (() => number) | null | undefined = process.getuid,
312
+ ): boolean {
313
+ return args.includes("--dangerously-skip-permissions")
314
+ && typeof getuid === "function"
315
+ && getuid() === 0;
316
+ }
317
+
318
+ export function rootSkipPermissionsNotice(env: ClaudeLaunchEnv): string {
319
+ if (env.IS_SANDBOX === "1") {
320
+ return "⚠ Root --dangerously-skip-permissions requested: OpenCodex set IS_SANDBOX=1 to bypass Claude Code's root guard. OpenCodex did not create an OS sandbox; prefer running as a non-root user.";
321
+ }
322
+ return `⚠ Root --dangerously-skip-permissions requested: preserving user IS_SANDBOX=${env.IS_SANDBOX}; Claude Code's root guard remains in control.`;
323
+ }
324
+
304
325
  export async function cmdClaude(args: string[]): Promise<number> {
305
326
  const config = loadConfig();
306
327
  if (config.claudeCode?.enabled === false) {
@@ -313,7 +334,11 @@ export async function cmdClaude(args: string[]): Promise<number> {
313
334
  return 1;
314
335
  }
315
336
  const contextWindows = await fetchClaudeContextWindows(config, port);
316
- const env = buildClaudeEnv(config, port, process.env, contextWindows);
337
+ const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
338
+ const env = buildClaudeEnv(config, port, process.env, contextWindows, { allowRootSkipPermissions });
339
+ if (allowRootSkipPermissions) {
340
+ console.error(rootSkipPermissionsNotice(env));
341
+ }
317
342
  // Pre-write the CLI's gateway-model cache (devlog 030): without a token the CLI
318
343
  // never refreshes it, so the picker would keep showing yesterday's aliases.
319
344
  try {
@@ -430,6 +430,10 @@ const commandRunners: Record<string, CommandRunner> = {
430
430
  await handleModels(deps.args.slice(1));
431
431
  return 0;
432
432
  },
433
+ alias: async deps => {
434
+ const { handleAliasCommand } = await import("./alias");
435
+ return await handleAliasCommand(deps.args.slice(1));
436
+ },
433
437
  combo: async deps => {
434
438
  const { handleComboCommand } = await import("./combo");
435
439
  return await handleComboCommand(deps.args.slice(1));
@@ -615,6 +619,15 @@ async function handleDesktopAppRestart(log: Pick<Console, "log" | "error">): Pro
615
619
  + "Run 'ocx sync --restart-desktop-app' from an external terminal instead.",
616
620
  );
617
621
  return;
622
+ case "process_probe_failed":
623
+ // Distinct from `no_targets`: we could not look, which is not the same as looking and
624
+ // finding nothing. Saying "not running" here sent users away believing there was nothing
625
+ // to restart (#2557).
626
+ log.error(
627
+ "Could not enumerate Codex desktop processes, so the app was not restarted. "
628
+ + "Quit and relaunch the desktop app manually to refresh the model picker.",
629
+ );
630
+ return;
618
631
  case "no_targets":
619
632
  log.log("Codex desktop app is not running; nothing to restart.");
620
633
  return;
@@ -630,4 +643,3 @@ async function handleDesktopAppRestart(log: Pick<Console, "log" | "error">): Pro
630
643
  }
631
644
  }
632
645
  }
633
-
package/src/cli/help.ts CHANGED
@@ -52,6 +52,7 @@ Usage:
52
52
  ocx provider <sub> Providers, connectivity, quota, and selected models
53
53
  ocx account <sub> Accounts, login/reauth, key pools, and quota controls
54
54
  ocx models <sub> Live/custom models, visibility, context, and shadow calls
55
+ ocx alias <sub> Short names for providers and models (list, set, rm, defaults)
55
56
  ocx combo <sub> Combo failover/round-robin routing
56
57
  ocx agent <sub> Subagents, roles, injection, effort caps, and sidecars
57
58
  ocx observe <sub> Logs, usage, storage, memory, and debug data
package/src/cli/index.ts CHANGED
@@ -101,6 +101,11 @@ const head = await runCli(process.argv.slice(2));
101
101
  const args = head.args;
102
102
  const command = head.command;
103
103
 
104
+ if (command === "telemetry") {
105
+ const { runTelemetryCommand } = await import("./telemetry-commands");
106
+ process.exit(runTelemetryCommand(args.slice(1), loadConfig()));
107
+ }
108
+
104
109
  function parsePortOption(): number | undefined {
105
110
  if (args.length === 1) return undefined;
106
111
  if (args.length !== 3 || args[1] !== "--port") {
@@ -164,7 +169,7 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
164
169
  // Ghost LISTEN rows with a dead PID can outlive the process for a while.
165
170
  // SetTcpEntry(DELETE_TCB) needs elevation (often returns 317), so the only
166
171
  // reliable non-admin recovery is to wait for the OS to release the TCB.
167
- timeoutMs: 60_000,
172
+ timeoutMs: process.platform === "win32" ? 60_000 : 10_000,
168
173
  intervalMs: 100,
169
174
  scanIntervalMs: 500,
170
175
  killOcxHolders: false,
package/src/cli/init.ts CHANGED
@@ -165,6 +165,7 @@ export async function runInit(): Promise<void> {
165
165
  port,
166
166
  providers: { [providerName]: providerConfig },
167
167
  defaultProvider: providerName,
168
+ modelDiscovery: { newModelPolicy: "off" },
168
169
  };
169
170
 
170
171
  saveConfig(config);