@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.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 (166) hide show
  1. package/gui/dist/assets/index-CWXut3rG.js +115 -0
  2. package/gui/dist/assets/index-EdoPnm9_.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/gui/dist/provider-icons/devin.svg +49 -0
  5. package/gui/dist/provider-icons/omo.svg +42 -0
  6. package/package.json +3 -1
  7. package/src/AGENTS.md +1 -1
  8. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
  9. package/src/adapters/command-code.ts +0 -1
  10. package/src/adapters/cursor/checkpoint-store.ts +37 -0
  11. package/src/adapters/cursor/live-transport.ts +74 -2
  12. package/src/adapters/cursor.ts +6 -0
  13. package/src/adapters/devin/cloud-direct/auth.ts +264 -0
  14. package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
  15. package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
  16. package/src/adapters/devin/cloud-direct/index.ts +65 -0
  17. package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
  18. package/src/adapters/devin/cloud-direct/wire.ts +206 -0
  19. package/src/adapters/devin/live-models.ts +133 -0
  20. package/src/adapters/devin-cli/acp.ts +204 -0
  21. package/src/adapters/devin-cli/adapter.ts +345 -0
  22. package/src/adapters/devin-cli/binary.ts +69 -0
  23. package/src/adapters/devin-cli/models.ts +57 -0
  24. package/src/adapters/devin.ts +326 -0
  25. package/src/adapters/google.ts +1 -1
  26. package/src/adapters/openai-chat.ts +31 -10
  27. package/src/adapters/openai-responses.ts +16 -1
  28. package/src/adapters/registry.ts +25 -1
  29. package/src/bridge.ts +8 -2
  30. package/src/claude/inbound-cache-stabilize.ts +130 -0
  31. package/src/claude/inbound.ts +45 -5
  32. package/src/cli/account-auth.ts +60 -1
  33. package/src/cli/account-extended.ts +23 -30
  34. package/src/cli/account.ts +2 -1
  35. package/src/cli/capabilities.ts +50 -8
  36. package/src/cli/config-command.ts +2 -2
  37. package/src/cli/dispatch.ts +14 -2
  38. package/src/cli/export-command.ts +11 -25
  39. package/src/cli/help.ts +2 -2
  40. package/src/cli/opencode.ts +5 -0
  41. package/src/cli/registry.ts +15 -4
  42. package/src/clients/config-export/cline.ts +71 -0
  43. package/src/clients/config-export/contracts.ts +10 -1
  44. package/src/clients/config-export/model-metadata.ts +33 -0
  45. package/src/clients/config-export/zcode.ts +23 -12
  46. package/src/clients/config-export.ts +156 -4
  47. package/src/codex/account-store.ts +7 -2
  48. package/src/codex/auth-context.ts +1 -1
  49. package/src/codex/catalog/effort.ts +4 -3
  50. package/src/codex/catalog/metadata.ts +8 -4
  51. package/src/codex/catalog/native-models.ts +24 -4
  52. package/src/codex/catalog/parsing.ts +19 -1
  53. package/src/codex/catalog/provider-fetch.ts +57 -0
  54. package/src/codex/catalog/sync.ts +1 -1
  55. package/src/codex/context-compat.ts +97 -0
  56. package/src/codex/context-owner.ts +201 -0
  57. package/src/codex/history-provider.ts +161 -14
  58. package/src/codex/inject-coordination.ts +3 -2
  59. package/src/codex/inject.ts +128 -9
  60. package/src/codex/pool-rotation.ts +8 -292
  61. package/src/codex/quota.ts +41 -12
  62. package/src/codex/retired-model-migration.ts +41 -0
  63. package/src/codex/routing.ts +327 -22
  64. package/src/codex/warmup.ts +2 -2
  65. package/src/combos/failover.ts +5 -0
  66. package/src/combos/resolve.ts +11 -15
  67. package/src/config.ts +46 -14
  68. package/src/generated/compatibility-version.json +293 -113
  69. package/src/grok/grpc-web.ts +120 -0
  70. package/src/grok/reset-coupon-ledger.ts +139 -0
  71. package/src/grok/reset-coupons.ts +278 -0
  72. package/src/integrations/catalog-refresh.ts +1 -1
  73. package/src/integrations/cline-document.ts +73 -0
  74. package/src/integrations/cline-io.ts +149 -0
  75. package/src/integrations/cline-transaction.ts +42 -0
  76. package/src/integrations/config-io.ts +21 -2
  77. package/src/integrations/journal.ts +43 -1
  78. package/src/integrations/omp-yaml-source.ts +123 -4
  79. package/src/integrations/ownership-policy.ts +7 -0
  80. package/src/integrations/ownership.ts +36 -0
  81. package/src/integrations/registry.ts +27 -0
  82. package/src/integrations/state.ts +5 -2
  83. package/src/integrations/store.ts +6 -0
  84. package/src/integrations/writer.ts +53 -9
  85. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  86. package/src/lib/abort.ts +36 -0
  87. package/src/lib/local-destinations.ts +1 -1
  88. package/src/lib/upstream-retry.ts +36 -1
  89. package/src/oauth/account-quota-rank.ts +11 -0
  90. package/src/oauth/callback-server.ts +10 -5
  91. package/src/oauth/devin/api-base.ts +63 -0
  92. package/src/oauth/devin/login.ts +1 -0
  93. package/src/oauth/devin/register-user.ts +186 -0
  94. package/src/oauth/devin/types.ts +71 -0
  95. package/src/oauth/devin-cli.ts +149 -0
  96. package/src/oauth/devin.ts +170 -0
  97. package/src/oauth/generic-account-failover.ts +169 -1
  98. package/src/oauth/index.ts +20 -1
  99. package/src/oauth/login-cli.ts +19 -5
  100. package/src/oauth/pool-kernel.ts +321 -0
  101. package/src/oauth/pool-settings-capability.ts +127 -9
  102. package/src/oauth/store.ts +7 -3
  103. package/src/oauth/token-guardian.ts +1 -1
  104. package/src/providers/codebuddy-models.ts +0 -3
  105. package/src/providers/command-code-efforts.ts +23 -4
  106. package/src/providers/default-aliases.ts +4 -0
  107. package/src/providers/derive.ts +8 -0
  108. package/src/providers/devin-cli-authmode-migration.ts +57 -0
  109. package/src/providers/key-failover.ts +175 -0
  110. package/src/providers/model-rename-startup.ts +21 -1
  111. package/src/providers/qoder-models.ts +0 -1
  112. package/src/providers/quota-key-accounts.ts +50 -0
  113. package/src/providers/quota-routing-cache.ts +65 -6
  114. package/src/providers/quota-types.ts +7 -0
  115. package/src/providers/quota.ts +113 -30
  116. package/src/providers/registry.ts +237 -77
  117. package/src/providers/stale-context-window-migration.ts +92 -0
  118. package/src/providers/zai-responses-migration.ts +45 -0
  119. package/src/quota/reset-observer.ts +2 -1
  120. package/src/quota/reset-seen-store.ts +9 -1
  121. package/src/remote-control/crypto.ts +442 -0
  122. package/src/remote-control/host.ts +175 -0
  123. package/src/remote-control/index.ts +100 -0
  124. package/src/remote-control/protocol.ts +200 -0
  125. package/src/remote-control/relay.ts +162 -0
  126. package/src/remote-control/workspace-agent-protocol.ts +246 -0
  127. package/src/remote-control/workspace-rpc-framing.ts +128 -0
  128. package/src/remote-control/workspace-tools.ts +237 -0
  129. package/src/remote-control/workspace-utf8.ts +24 -0
  130. package/src/responses/custom-tool-compat.ts +23 -0
  131. package/src/router.ts +6 -1
  132. package/src/routing/compatibility/behavior.ts +3 -0
  133. package/src/server/adapter-resolve.ts +6 -2
  134. package/src/server/auth-cors.ts +71 -6
  135. package/src/server/chat-completions.ts +5 -3
  136. package/src/server/chat-native.ts +9 -0
  137. package/src/server/claude-messages.ts +25 -30
  138. package/src/server/context-history.ts +207 -0
  139. package/src/server/images.ts +28 -1
  140. package/src/server/index.ts +42 -22
  141. package/src/server/live.ts +2 -1
  142. package/src/server/management/agent-settings-routes.ts +7 -1
  143. package/src/server/management/config-routes.ts +3 -3
  144. package/src/server/management/grok-coupon-routes.ts +287 -0
  145. package/src/server/management/integration-routes.ts +6 -1
  146. package/src/server/management/oauth-account-routes.ts +124 -7
  147. package/src/server/management/provider-routes.ts +77 -2
  148. package/src/server/management/route-registry.ts +6 -1
  149. package/src/server/management-api.ts +7 -0
  150. package/src/server/request-log.ts +4 -0
  151. package/src/server/responses/codex-ws-exchange.ts +90 -14
  152. package/src/server/responses/codex-ws-wire.ts +51 -1
  153. package/src/server/responses/compact.ts +8 -1
  154. package/src/server/responses/core.ts +188 -22
  155. package/src/server/responses/ws-upstream.ts +1 -1
  156. package/src/server/responses-undeclared-tool-guard.ts +261 -12
  157. package/src/server/zai-responses-startup.ts +21 -0
  158. package/src/types/config.ts +33 -3
  159. package/src/types/provider.ts +54 -4
  160. package/src/types/request.ts +1 -1
  161. package/src/types/tools.ts +36 -6
  162. package/src/update/job.ts +33 -11
  163. package/src/vision/plan.ts +40 -37
  164. package/src/web-search/index.ts +1 -0
  165. package/gui/dist/assets/index-BoBRSehJ.css +0 -1
  166. package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
@@ -17,6 +17,7 @@ export { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, e
17
17
  import { AnthropicRequestError, isRec, type Rec } from "./inbound-records";
18
18
  import { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, formatFromOutputConfig } from "./inbound-model-options";
19
19
  import { systemToInstructions, toolsToResponses, toolChoiceToResponses } from "./inbound-content-options";
20
+ import { stabilizeClaudeInstructionsForPromptCache } from "./inbound-cache-stabilize";
20
21
  import { decodeReasoningEnvelope, encodeReasoningEnvelope, OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
21
22
  import { createTranslatorBudget, type TranslatorBudget } from "../lib/translator-budget";
22
23
 
@@ -298,7 +299,10 @@ export interface ClaudeInboundTranslation {
298
299
  * Translate an Anthropic Messages request body into a /v1/responses request body.
299
300
  * Throws AnthropicRequestError (-> 400 invalid_request_error) on malformed input.
300
301
  */
301
- export function anthropicToResponsesBody(raw: unknown, cc?: OcxClaudeCodeConfig): Rec {
302
+ export function anthropicToResponsesBody(
303
+ raw: unknown,
304
+ cc?: OcxClaudeCodeConfig,
305
+ ): Rec {
302
306
  return anthropicToResponsesTranslation(raw, cc).body;
303
307
  }
304
308
 
@@ -307,7 +311,11 @@ export function anthropicToResponsesBody(raw: unknown, cc?: OcxClaudeCodeConfig)
307
311
  * OUT-OF-BODY tuple (audit 133 R3#1 — an in-body marker would leak upstream through
308
312
  * the native Responses forward and 400).
309
313
  */
310
- export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCodeConfig, budget?: TranslatorBudget): ClaudeInboundTranslation {
314
+ export function anthropicToResponsesTranslation(
315
+ raw: unknown,
316
+ cc?: OcxClaudeCodeConfig,
317
+ budget?: TranslatorBudget,
318
+ ): ClaudeInboundTranslation {
311
319
  const activeBudget = budget ?? createTranslatorBudget();
312
320
  try {
313
321
  return translateAnthropicRequest(raw, cc, activeBudget);
@@ -316,7 +324,11 @@ export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCode
316
324
  }
317
325
  }
318
326
 
319
- function translateAnthropicRequest(raw: unknown, cc: OcxClaudeCodeConfig | undefined, budget: TranslatorBudget): ClaudeInboundTranslation {
327
+ function translateAnthropicRequest(
328
+ raw: unknown,
329
+ cc: OcxClaudeCodeConfig | undefined,
330
+ budget: TranslatorBudget,
331
+ ): ClaudeInboundTranslation {
320
332
  if (!isRec(raw)) throw new AnthropicRequestError("request body must be a JSON object");
321
333
  if (typeof raw.model !== "string" || raw.model.length === 0) {
322
334
  throw new AnthropicRequestError("model is required");
@@ -357,7 +369,32 @@ function translateAnthropicRequest(raw: unknown, cc: OcxClaudeCodeConfig | undef
357
369
  stream: raw.stream === true,
358
370
  };
359
371
 
360
- if (systemParts.length > 0) body.instructions = systemParts.join("\n\n");
372
+ const joinedSystem = systemParts.length > 0 ? systemParts.join("\n\n") : "";
373
+ const stabilizePromptCache = cc?.stabilizePromptCache === true;
374
+ // Desktop fallback hashes raw systemParts unless the caller opted into
375
+ // harness cleanup. Opt-in then hashes the same string as body.instructions.
376
+ let cacheSystem: string | string[] = systemParts;
377
+ if (joinedSystem) {
378
+ if (stabilizePromptCache) {
379
+ // Claude Code appends growing <total_tokens>N tokens left</total_tokens>
380
+ // footers (and occasional TaskCreate nudges) into system text. That churn
381
+ // breaks Muse/Go prefix cache on the Responses instructions prefix even
382
+ // when tools stay stable. Relocation is caller-opted, not inferred from
383
+ // a matching suffix or metadata.user_id.
384
+ const stabilized = stabilizeClaudeInstructionsForPromptCache(joinedSystem);
385
+ if (stabilized.instructions) body.instructions = stabilized.instructions;
386
+ if (stabilized.dynamicNotice) {
387
+ input.push({
388
+ type: "message",
389
+ role: "user",
390
+ content: [{ type: "input_text", text: stabilized.dynamicNotice }],
391
+ });
392
+ }
393
+ cacheSystem = stabilized.instructions;
394
+ } else {
395
+ body.instructions = joinedSystem;
396
+ }
397
+ }
361
398
 
362
399
  const tools = toolsToResponses(raw.tools);
363
400
  if (tools) body.tools = tools;
@@ -395,11 +432,14 @@ function translateAnthropicRequest(raw: unknown, cc: OcxClaudeCodeConfig | undef
395
432
  // Exact-prefix matching still isolates content; the key only steers routing
396
433
  // affinity. Callers must NOT synthesize a session_id header from this fallback
397
434
  // (audit 133 R2#3).
435
+ // Outside opt-in, hash the raw systemParts array (pre-stabilize Desktop
436
+ // key). Opt-in hashes the same string used for body.instructions so the
437
+ // key tracks the cacheable prefix after peel.
398
438
  body.prompt_cache_key = createHash("sha256")
399
439
  .update(canonicalJson({
400
440
  version: 2,
401
441
  model: body.model,
402
- system: systemParts,
442
+ system: cacheSystem,
403
443
  tools: Array.isArray(body.tools) ? body.tools : [],
404
444
  }))
405
445
  .digest("hex").slice(0, 32);
@@ -37,6 +37,7 @@ const USAGE = `Usage:
37
37
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
38
38
  ocx account cancel <provider> [--flow <flow-id>] [--json]
39
39
  ocx account reset-credits <account-id|main> [--consume --yes [--operation-id <uuid>]] [--json]
40
+ ocx account grok-reset-coupons [<account-id>] [--consume --yes [--token-id <token-id>] [--operation-id <uuid>]] [--json]
40
41
 
41
42
  --device runs the OpenAI device-code login instead of the browser callback: use
42
43
  it when the proxy has no browser or nothing can reach localhost:1455, such as a
@@ -49,8 +50,20 @@ visible to anyone who can run ps:
49
50
  pbpaste | ocx account code <provider> --flow <flow-id>
50
51
  ocx account login <provider> --code - (same, for the login flow)`;
51
52
 
53
+ /**
54
+ * The Codex account pool answers to three spellings, and a user reaches for whichever
55
+ * one they already have a word for. `ocx login codex` routes here as well (dispatch.ts):
56
+ * the pool is deliberately not an `ocx login` provider -- it keeps its own account
57
+ * ledger and runs its browser flow inside the proxy -- but that is an implementation
58
+ * boundary, not something a user should have to know before they can log in.
59
+ */
52
60
  const CODEX_NAMES = new Set(["openai", "codex", "chatgpt"]);
53
61
 
62
+ /** True for every spelling that means "the Codex account pool" rather than an OAuth provider. */
63
+ export function isCodexAccountLoginName(name: string): boolean {
64
+ return CODEX_NAMES.has(name.trim().toLowerCase());
65
+ }
66
+
54
67
  interface LoginStart {
55
68
  url?: string;
56
69
  flowId?: string;
@@ -100,7 +113,12 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
100
113
  const id = takeOption(args, "--id");
101
114
  const suppliedCode = takeOptionWithSyntax(args, "--code");
102
115
  if (!provider) throw new CliUsageError("provider is required", USAGE);
103
- rejectArgs(args, USAGE);
116
+ // A bare leftover here is plausibly the authorization code itself: this flow takes one
117
+ // through --code, and a user who pastes it as a positional would otherwise see it echoed
118
+ // back in the usage error. `ocx login codex` reaches this parser too, so the paste lands
119
+ // one word away from a command people run constantly. Flag-shaped leftovers stay visible,
120
+ // because a mistyped flag is exactly what the message has to name.
121
+ rejectArgs(args, USAGE, { redactValues: true });
104
122
  // kimi, nous, and github-copilot are already device flows, so --device is a
105
123
  // true statement about them and is accepted as a no-op rather than an error.
106
124
  // Anything else has no device grant at all and must fail loudly.
@@ -284,12 +302,53 @@ async function resetCredits(argv: string[], deps: RuntimeApiDeps): Promise<void>
284
302
  printData(result, wantsJson);
285
303
  }
286
304
 
305
+ async function grokResetCoupons(argv: string[], deps: RuntimeApiDeps): Promise<void> {
306
+ const args = [...argv];
307
+ // The account id is optional here (the server falls back to the selected xAI
308
+ // account), so a flag-shaped first token must not be swallowed as the id:
309
+ // `grok-reset-coupons --consume` has to reach the --yes gate, not become a
310
+ // read of account "--consume".
311
+ const rawId = args[0]?.startsWith("--") ? undefined : args.shift()?.trim();
312
+ const wantsJson = takeFlag(args, "--json");
313
+ const consume = takeFlag(args, "--consume");
314
+ const yes = takeFlag(args, "--yes");
315
+ // Before rejectArgs: takeOption splices its two tokens out of `args`.
316
+ const tokenId = takeOption(args, "--token-id");
317
+ const operationId = takeOption(args, "--operation-id");
318
+ if (consume && !yes) throw new CliUsageError("consuming a Grok reset coupon requires --yes", USAGE);
319
+ if (operationId !== undefined && !consume) {
320
+ throw new CliUsageError("--operation-id requires --consume", USAGE);
321
+ }
322
+ if (tokenId !== undefined && !consume) {
323
+ throw new CliUsageError("--token-id requires --consume", USAGE);
324
+ }
325
+ if (operationId !== undefined && !isCodexResetCreditOperationId(operationId)) {
326
+ throw new CliUsageError("--operation-id must be a UUIDv4", USAGE);
327
+ }
328
+ rejectArgs(args, USAGE);
329
+ const accountId = rawId ? (rawId === "main" ? "__main__" : rawId) : undefined;
330
+ const result = consume
331
+ ? await runtimeRequest("/api/grok/reset-coupons/consume", {
332
+ method: "POST",
333
+ // Spread, not `operationId: undefined`: the server distinguishes an absent
334
+ // key from a caller who asked for a stable idempotency identity.
335
+ body: JSON.stringify({ accountId, tokenId, ...(operationId === undefined ? {} : { operationId }) }),
336
+ }, deps)
337
+ : await runtimeRequest(
338
+ `/api/grok/reset-coupons${accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""}`,
339
+ {},
340
+ deps,
341
+ );
342
+ printData(result, wantsJson);
343
+ }
344
+
287
345
  export async function handleAccountAuthCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
288
346
  let action: (() => Promise<void>) | undefined;
289
347
  if (sub === "login" || sub === "reauth") action = () => login(sub === "reauth" ? [...argv, "--reauth"] : argv, deps);
290
348
  else if (sub === "code") action = () => code(argv, deps);
291
349
  else if (sub === "cancel") action = () => cancel(argv, deps);
292
350
  else if (sub === "reset-credits") action = () => resetCredits(argv, deps);
351
+ else if (sub === "grok-reset-coupons") action = () => grokResetCoupons(argv, deps);
293
352
  if (!action) return null;
294
353
  return runCliAction(action);
295
354
  }
@@ -399,14 +399,22 @@ export async function cmdAutoSwitch(args: string[], deps: AccountDeps): Promise<
399
399
  const storedThreshold = typeof stored === "number" && Number.isInteger(stored) && stored >= 0 && stored <= 100
400
400
  ? stored : null;
401
401
  const poolEnabled = typeof settings.enabled === "boolean" ? settings.enabled : null;
402
- const inert = settings.inert === true ? true : null;
403
- // This CLI understands only the current inert generic threshold contract.
404
- const enabled = false;
402
+ // Three states, not two. `true` is stored-but-not-applied, `false` is applied by the
403
+ // shared kernel, and absent is a server that does not speak this field at all. Collapsing
404
+ // false into absent would render the live feature as an unknown capability.
405
+ const inert = typeof settings.inert === "boolean" ? settings.inert : null;
406
+ // A stored threshold only steers selection once the pool consumes it, which is exactly
407
+ // what `inert: false` reports.
408
+ const enabled = inert === false && storedThreshold !== null;
405
409
  if (wantsJson) {
406
410
  console.log(JSON.stringify({ provider: name, autoSwitchThreshold: storedThreshold, enabled, poolEnabled, inert }, null, 2));
407
411
  } else {
408
412
  const value = storedThreshold === null ? "unset" : `${storedThreshold}%`;
409
- console.log(`auto-switch: ${inert === true ? "inactive" : "unavailable"} (stored threshold ${value}; ${inert === true ? "not applied by this pool" : "threshold support is unknown"})`);
413
+ const state = inert === false ? (enabled ? "on" : "off") : inert === true ? "inactive" : "unavailable";
414
+ const why = inert === false
415
+ ? (enabled ? "applied by this pool" : "no threshold stored")
416
+ : inert === true ? "not applied by this pool" : "threshold support is unknown";
417
+ console.log(`auto-switch: ${state} (stored threshold ${value}; ${why})`);
410
418
  }
411
419
  return 0;
412
420
  }
@@ -839,21 +847,15 @@ export async function cmdPauseExhausted(args: string[], deps: AccountDeps): Prom
839
847
  }
840
848
 
841
849
  /**
842
- * Two pools expose strategy and sticky, and they are NOT reached the same way:
850
+ * One transport, because there is now one contract.
843
851
  *
844
- * | | Codex pool | Anthropic pool |
845
- * |---|---|---|
846
- * | read | `GET /api/codex-auth/active` | `GET /api/oauth/accounts/pool?provider=` |
847
- * | write | `PUT /api/codex-auth/pool-strategy` | `PUT /api/oauth/accounts/pool` |
848
- * | keys | `accountPoolStrategy`/`accountPoolStickyLimit` | `strategy`/`stickyLimit` |
849
- * | body | bare field | field **plus** a mandatory `provider` |
852
+ * This used to be a table of the differences between the Codex and Anthropic pools -- different
853
+ * read path, different write path, different response keys, and a `provider` field mandatory on
854
+ * one body and forbidden on the other. That table existed only because the two contracts
855
+ * disagreed; `/api/pool/settings` answers with the same keys for every kind, so the table
856
+ * collapses to a single shape and the asymmetry it encoded is gone rather than relocated.
850
857
  *
851
- * Omitting `provider` from the Anthropic write body earns a 400
852
- * (`oauth-account-routes.ts:344`), so the asymmetry has to be encoded somewhere. Encoding it
853
- * here keeps ONE verb pair working on both pools. The alternative the plan left open -- a second
854
- * `provider-strategy`/`provider-sticky` pair -- would double the surface an operator must learn
855
- * to express one idea, and a CLI that can steer one pool and not the other is exactly the trap
856
- * this unit exists to remove.
858
+ * The legacy paths still work and still have their own goldens. Nothing here reads them.
857
859
  */
858
860
  interface PoolTransport {
859
861
  readPath: string;
@@ -865,18 +867,10 @@ interface PoolTransport {
865
867
  writeBody: (field: "strategy" | "stickyLimit", value: unknown) => Record<string, unknown>;
866
868
  }
867
869
 
868
- const CODEX_POOL_TRANSPORT: PoolTransport = {
869
- readPath: "/api/codex-auth/active",
870
- writePath: "/api/codex-auth/pool-strategy",
871
- strategyKey: "accountPoolStrategy",
872
- stickyKey: "accountPoolStickyLimit",
873
- writeBody: (field, value) => ({ [field]: value }),
874
- };
875
-
876
- function anthropicPoolTransport(provider: string): PoolTransport {
870
+ function unifiedPoolTransport(provider: string): PoolTransport {
877
871
  return {
878
- readPath: `/api/oauth/accounts/pool?provider=${encodeURIComponent(provider)}`,
879
- writePath: "/api/oauth/accounts/pool",
872
+ readPath: `/api/pool/settings?provider=${encodeURIComponent(provider)}`,
873
+ writePath: "/api/pool/settings",
880
874
  strategyKey: "strategy",
881
875
  stickyKey: "stickyLimit",
882
876
  writeBody: (field, value) => ({ provider, [field]: value }),
@@ -892,8 +886,7 @@ function poolTransportFor(
892
886
  classified: { type: "codex" | "oauth" | "api-key" },
893
887
  name: string,
894
888
  ): PoolTransport | string {
895
- if (classified.type === "codex") return CODEX_POOL_TRANSPORT;
896
- if (classified.type === "oauth") return anthropicPoolTransport(name);
889
+ if (classified.type === "codex" || classified.type === "oauth") return unifiedPoolTransport(name);
897
890
  return `pool settings apply to OAuth account pools, not the API-key provider "${name}"`;
898
891
  }
899
892
 
@@ -60,6 +60,7 @@ const ACCOUNT_USAGE = `Usage:
60
60
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
61
61
  ocx account cancel <provider> [--flow <flow-id>] [--json]
62
62
  ocx account reset-credits <account-id|main> [--consume --yes] [--json]
63
+ ocx account grok-reset-coupons [<account-id>] [--consume --yes] [--token-id <token-id>] [--json]
63
64
  ocx account main <doctor|list|register|add|switch|recover> ...
64
65
 
65
66
  List and switch provider accounts and API-key pools (masked output only).
@@ -355,7 +356,7 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
355
356
  const { cmdNativeMainAccount } = await import("./account-main");
356
357
  return await cmdNativeMainAccount(rest, deps);
357
358
  }
358
- if (["login", "reauth", "code", "cancel", "reset-credits"].includes(sub ?? "")) {
359
+ if (["login", "reauth", "code", "cancel", "reset-credits", "grok-reset-coupons"].includes(sub ?? "")) {
359
360
  const { handleAccountAuthCommand } = await import("./account-auth");
360
361
  return await handleAccountAuthCommand(sub!, rest, deps) ?? 1;
361
362
  }
@@ -259,6 +259,27 @@ export const CAPABILITIES: readonly Capability[] = [
259
259
  json: "payload",
260
260
  details: ["CLI/admin-token refreshes only observe usage. After quota recovery, a human must click Refresh quotas in the dashboard to authorize model validation. Do not mint a GUI session to work around this consent boundary."],
261
261
  },
262
+ {
263
+ command: ["account", "grok-reset-coupons"],
264
+ summary: "Inspect or redeem Grok billing reset coupons; redemption is journaled and idempotent.",
265
+ routes: [
266
+ { method: "GET", path: "/api/grok/reset-coupons" },
267
+ { method: "POST", path: "/api/grok/reset-coupons/consume" },
268
+ ],
269
+ flags: [
270
+ { name: "--consume", value: "boolean", summary: "Redeem one reset coupon; requires --yes." },
271
+ { name: "--yes", value: "boolean", summary: "Explicit confirmation required by --consume." },
272
+ { name: "--token-id", value: "string", summary: "Redeem a specific reset token instead of the default selection." },
273
+ { name: "--operation-id", value: "string", summary: "UUIDv4 making a redemption idempotent: retries replay the journaled outcome." },
274
+ { name: "--json", value: "boolean", summary: "Emit the coupon list or redemption result as JSON." },
275
+ ],
276
+ mutates: true,
277
+ json: "payload",
278
+ details: [
279
+ "Without --consume this is a read: remaining coupons and their validity windows.",
280
+ "The operation is journaled before the upstream call, so retrying the same --operation-id replays the recorded outcome instead of spending a second coupon.",
281
+ ],
282
+ },
262
283
  {
263
284
  command: ["usage"],
264
285
  summary: "Token and estimated-cost report over a time range.",
@@ -316,10 +337,9 @@ export const CAPABILITIES: readonly Capability[] = [
316
337
  // Both pools, because both have the setting. The Codex pool reads its applied values
317
338
  // from the active payload; the Anthropic pool has its own GET.
318
339
  routes: [
319
- { method: "GET", path: "/api/codex-auth/active" },
320
- { method: "PUT", path: "/api/codex-auth/pool-strategy" },
321
- { method: "GET", path: "/api/oauth/accounts/pool" },
322
- { method: "PUT", path: "/api/oauth/accounts/pool" },
340
+ { method: "GET", path: "/api/pool/settings" },
341
+ { method: "PUT", path: "/api/pool/settings" },
342
+ { method: "PATCH", path: "/api/pool/settings" },
323
343
  ],
324
344
  flags: [{ name: "--json", value: "boolean", summary: "Emit the applied strategy and sticky limit as JSON." }],
325
345
  mutates: true,
@@ -328,23 +348,45 @@ export const CAPABILITIES: readonly Capability[] = [
328
348
  "A bare invocation reads and never writes.",
329
349
  "The APPLIED value is echoed, not the requested one, so a server-side normalization stays visible.",
330
350
  "Values are not re-validated in the CLI: the server owns the strategy names and the 1-100 sticky bound.",
331
- "`anthropic` owns the full pool contract. Other OAuth providers reach the same endpoint with a generic subset (enabled/strategy/autoSwitchThreshold) whose settings persist but do not yet steer selection; `sticky` and `quotaWindow` are refused for them.",
351
+ "One route answers for every pool kind and declares which fields that kind honours in `supported`, so an unsupported field is a stated null rather than an absence. `anthropic` alone carries `quotaWindow`. Generic-provider settings steer selection only while `pool.kernel` is on. The legacy per-pool paths still work and are unchanged.",
332
352
  ],
333
353
  },
334
354
  {
335
355
  command: ["account", "sticky"],
336
356
  summary: "Show or set how many consecutive requests stay on one account.",
357
+ routes: [
358
+ { method: "GET", path: "/api/pool/settings" },
359
+ { method: "PUT", path: "/api/pool/settings" },
360
+ { method: "PATCH", path: "/api/pool/settings" },
361
+ ],
362
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the applied strategy and sticky limit as JSON." }],
363
+ mutates: true,
364
+ json: "envelope",
365
+ details: ["Only meaningful under the sticky-capable strategies; the pool strategy is the other half of this setting."],
366
+ },
367
+ {
368
+ command: ["account", "auto-switch"],
369
+ summary: "Show or set the usage percentage at which a pool moves to another account.",
370
+ // Declared here rather than riding on `account strategy`, which is what it did before the
371
+ // unified route existed. `auto-switch` genuinely drives these three: the Codex pool reads
372
+ // its applied threshold from the active payload and writes through its own route, and a
373
+ // generic OAuth pool reads and writes the per-provider pool settings.
337
374
  routes: [
338
375
  { method: "GET", path: "/api/codex-auth/active" },
339
- { method: "PUT", path: "/api/codex-auth/pool-strategy" },
376
+ { method: "PUT", path: "/api/codex-auth/auto-switch" },
340
377
  { method: "GET", path: "/api/oauth/accounts/pool" },
341
378
  { method: "PUT", path: "/api/oauth/accounts/pool" },
342
379
  ],
343
- flags: [{ name: "--json", value: "boolean", summary: "Emit the applied strategy and sticky limit as JSON." }],
380
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the stored threshold and whether it is applied." }],
344
381
  mutates: true,
345
382
  json: "envelope",
346
- details: ["Only meaningful under the sticky-capable strategies; the pool strategy is the other half of this setting."],
383
+ details: [
384
+ "A bare invocation reads and never writes.",
385
+ "`on` stores 80%, `off` stores 0%, and `threshold <n>` accepts 0-100.",
386
+ "For a generic OAuth pool, `inert: true` means the threshold is stored but not applied, `inert: false` means the pool is applying it, and an absent `inert` is an unknown capability.",
387
+ ],
347
388
  },
389
+
348
390
  {
349
391
  command: ["logs"],
350
392
  summary: "Recent request log rows, filterable by provider, model, conversation, account, and status.",
@@ -144,8 +144,8 @@ function normalizeVisionConfig(config: OcxConfig): OcxConfig {
144
144
  const vision = config.visionSidecar;
145
145
  if (!vision || vision.reasoning === undefined) return config;
146
146
  // Keep CLI import/set semantics aligned with the execution path: an omitted or blank model means
147
- // the bounded OpenAI vision default, gpt-5.4-mini, not the Dashboard's web-search default.
148
- const model = vision.model || "gpt-5.4-mini";
147
+ // the bounded OpenAI vision default, gpt-5.6-luna, not the Dashboard's web-search default.
148
+ const model = vision.model || "gpt-5.6-luna";
149
149
  const normalized = normalizeVisionReasoningForModel(model, vision.reasoning);
150
150
  if (normalized === undefined) delete vision.reasoning;
151
151
  else vision.reasoning = normalized;
@@ -302,8 +302,20 @@ const commandRunners: Record<string, CommandRunner> = {
302
302
  return Number(process.exitCode ?? 0);
303
303
  },
304
304
  login: async deps => {
305
+ const loginArgs = deps.args.slice(1);
306
+ // 'ocx login codex' is the command people type first, and until now it answered with
307
+ // the full provider wall because the Codex pool lives behind 'ocx account login'.
308
+ // Route the three Codex spellings to that flow instead of making the user discover
309
+ // a second noun. Everything else stays on the local OAuth/API-key path.
310
+ const { isCodexAccountLoginName, handleAccountAuthCommand } = await import("./account-auth");
311
+ if (isCodexAccountLoginName(loginArgs[0] ?? "")) {
312
+ // null means "unknown subcommand", which "login" never is; the coalesce exists because
313
+ // the shared signature serves callers that do pass an unknown one.
314
+ const code = await handleAccountAuthCommand("login", loginArgs, { findLiveProxy: deps.findLiveProxy });
315
+ return code ?? 1;
316
+ }
305
317
  const { handleLogin } = await import("../oauth/login-cli");
306
- await handleLogin(deps.args[1]);
318
+ await handleLogin(loginArgs[0]);
307
319
  return 0;
308
320
  },
309
321
  logout: async deps => {
@@ -440,7 +452,7 @@ const commandRunners: Record<string, CommandRunner> = {
440
452
  },
441
453
  config,
442
454
  port: live.port,
443
- }, ["mcode", "pi", "raycast"]));
455
+ }, ["mcode", "pi", "raycast", "omo", "cline"]));
444
456
  } catch (error) {
445
457
  console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`);
446
458
  }
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * `ocx export --client <id>` — print a client config for the live proxy.
3
3
  *
4
- * Eight clients, four formats: OpenCode and Pi are JSON; OMP, Hermes, Gajae and
5
- * MiniMax Code are YAML; OpenClaw is JSON5; Kimi is TOML.
4
+ * Fourteen clients, five formats. The accepted list is `EXPORT_CLIENT_IDS`, not
5
+ * this comment: OpenCode, Pi, Prime, Aside, ZCode and omo are JSON; OMP,
6
+ * Hermes, gjc, DSH, MiniMax Code and Raycast are YAML; OpenClaw is JSON5; Kimi
7
+ * is TOML.
6
8
  *
7
9
  * Two consumers, one payload (devlog 260731_client_config_export/020):
8
10
  *
@@ -58,16 +60,6 @@ export interface ExportCommandDeps extends RuntimeApiDeps {
58
60
  configImpl?: () => OcxConfig;
59
61
  }
60
62
 
61
- /**
62
- * `/api/models` row plus the modality list Pi consumes. The launcher's row type predates
63
- * the Pi exporter and stops at the fields OpenCode needs.
64
- */
65
- type ExportProxyModelRow = OpencodeProxyModelRow & {
66
- inputModalities?: string[];
67
- reasoningEfforts?: string[];
68
- defaultReasoningEffort?: string;
69
- };
70
-
71
63
  /**
72
64
  * Export rows from proxy `/api/models` rows.
73
65
  *
@@ -78,20 +70,13 @@ type ExportProxyModelRow = OpencodeProxyModelRow & {
78
70
  * row as the model itself: a second lookup over the raw rows would let a hidden or disabled
79
71
  * duplicate donate its ladder to the visible entry.
80
72
  *
81
- * Only modalities are re-joined by `namespaced`, because the catalog type does not carry them.
73
+ * Modalities need no such lookup: `opencodeCatalogFromProxyRows` carries them on the catalog
74
+ * entry, so the clients that filter them are handed the same filtered, deduped row.
82
75
  */
83
76
  export function exportModelsFromProxyRows(
84
- rows: readonly ExportProxyModelRow[],
77
+ rows: readonly OpencodeProxyModelRow[],
85
78
  config: OcxConfig,
86
79
  ): ExportModel[] {
87
- const modalities = new Map<string, string[]>();
88
- for (const row of rows) {
89
- const namespaced = row.namespaced?.trim();
90
- if (!namespaced || modalities.has(namespaced)) continue;
91
- if (Array.isArray(row.inputModalities) && row.inputModalities.length > 0) {
92
- modalities.set(namespaced, [...row.inputModalities]);
93
- }
94
- }
95
80
  return opencodeCatalogFromProxyRows(rows, config).map(entry => {
96
81
  const model: ExportModel = {
97
82
  namespaced: entry.namespaced,
@@ -106,8 +91,9 @@ export function exportModelsFromProxyRows(
106
91
  model.reasoningEfforts = [...entry.reasoningEfforts];
107
92
  }
108
93
  if (entry.defaultReasoningEffort) model.defaultReasoningEffort = entry.defaultReasoningEffort;
109
- const input = modalities.get(entry.namespaced);
110
- if (input) model.inputModalities = [...input];
94
+ if (entry.inputModalities && entry.inputModalities.length > 0) {
95
+ model.inputModalities = [...entry.inputModalities];
96
+ }
111
97
  return model;
112
98
  });
113
99
  }
@@ -186,7 +172,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
186
172
  }
187
173
  built = { document: exported.config, text: exported.text };
188
174
  } else {
189
- const rows = await runtimeRequest<ExportProxyModelRow[]>("/api/models", {}, { ...deps, baseUrl: root });
175
+ const rows = await runtimeRequest<OpencodeProxyModelRow[]>("/api/models", {}, { ...deps, baseUrl: root });
190
176
  if (!Array.isArray(rows)) {
191
177
  throw new RuntimeApiError("Management API returned an unexpected /api/models payload.", 502, rows);
192
178
  }
package/src/cli/help.ts CHANGED
@@ -52,7 +52,7 @@ Usage:
52
52
  ocx doctor --recover-zero-byte-coordinator --yes
53
53
  Back up a proven zero-byte Codex coordinator after stopping the proxy
54
54
  ocx debug <scope> provider/usage/injection/claude on|off|status|reset
55
- ocx login <provider> OAuth or API-key provider login
55
+ ocx login <provider> OAuth or API-key provider login (ocx login codex for Codex/ChatGPT)
56
56
  ocx logout <provider> Remove a stored OAuth login
57
57
  ocx gui [pair --origin <browser-origin> [--json]]
58
58
  Open the dashboard or create a single-use remote pairing grant
@@ -81,7 +81,7 @@ Usage:
81
81
  ocx memory [--json] Alias of ocx observe memory
82
82
  ocx api-key <sub> Alias of ocx access key
83
83
  ocx access <sub> External API keys and endpoint information
84
- ocx export --client <id> Print a client config wired to the running proxy (13 clients)
84
+ ocx export --client <id> Print a client config wired to the running proxy (15 clients)
85
85
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
86
86
  ocx grok <sub> Grok Build model selection and apply
87
87
  ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
@@ -98,6 +98,8 @@ export interface OpencodeProxyModelRow {
98
98
  displayName?: string;
99
99
  displayNameSource?: "operator" | "provider" | "fallback";
100
100
  contextWindow?: number;
101
+ /** Declared input modalities from `/api/models`; carried into opencode model capabilities. */
102
+ inputModalities?: string[];
101
103
  /** Declared effort ladder from `/api/models`; carried into opencode model variants. */
102
104
  reasoningEfforts?: string[];
103
105
  /** Declared default effort from `/api/models`. */
@@ -396,6 +398,9 @@ export function opencodeCatalogFromProxyRows(
396
398
  id: row.id,
397
399
  contextWindow: row.contextWindow,
398
400
  displayName: row.displayNameSource === "fallback" ? undefined : row.displayName,
401
+ ...(Array.isArray(row.inputModalities) && row.inputModalities.length > 0
402
+ ? { inputModalities: [...row.inputModalities] }
403
+ : {}),
399
404
  ...(typeof row.fastRowAvailable === "boolean" ? { fastRowAvailable: row.fastRowAvailable } : {}),
400
405
  ...(Array.isArray(row.reasoningEfforts) && row.reasoningEfforts.length > 0
401
406
  ? { reasoningEfforts: [...row.reasoningEfforts] }
@@ -152,7 +152,16 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
152
152
  "Env default: OCX_DEBUG=1 (legacy OCX_DEBUG_FRAMES still works)",
153
153
  ],
154
154
  },
155
- { name: "login", usage: "ocx login <provider>", summary: "OAuth or API-key login for a provider." },
155
+ {
156
+ name: "login",
157
+ usage: "ocx login <provider>",
158
+ summary: "OAuth or API-key login for a provider.",
159
+ details: [
160
+ "Codex/ChatGPT: ocx login codex runs the Codex account-pool login (same flow as ocx account login codex).",
161
+ "That one needs a running proxy; the OAuth and API-key providers log in locally.",
162
+ "'chatgpt' and 'openai' are the same route; an OpenAI platform API key is 'ocx login openai-apikey'.",
163
+ ],
164
+ },
156
165
  { name: "logout", usage: "ocx logout <provider>", summary: "Remove a stored provider login." },
157
166
  {
158
167
  name: "gui",
@@ -212,7 +221,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
212
221
  },
213
222
  {
214
223
  name: "account",
215
- usage: "ocx account <list|current|use|refresh|auto-switch|priority|login|reauth|code|cancel|remove|add-key|reset-credits|main> ...",
224
+ usage: "ocx account <list|current|use|refresh|auto-switch|priority|login|reauth|code|cancel|remove|add-key|reset-credits|grok-reset-coupons|main> ...",
216
225
  summary: "List and switch provider accounts and API-key pools (GUI parity).",
217
226
  details: [
218
227
  "list [provider] Codex account pool, OAuth accounts and API keys (identifiers shown masked as the API returns them).",
@@ -225,6 +234,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
225
234
  "add-key <provider> [--label <label>] Add a key read only from piped stdin.",
226
235
  "login/reauth/code/cancel Run browser or manual-code auth from a headless shell.",
227
236
  "reset-credits <id|main> [--consume --yes] Inspect or consume Codex reset credits.",
237
+ "grok-reset-coupons [<id>] [--consume --yes] Inspect or redeem Grok reset coupons.",
228
238
  "main <subcommand> Manage the physical native Codex login separately from Pool routing.",
229
239
  "Switching the active account takes effect immediately; running threads move on their next request, and in-flight requests keep the account they captured.",
230
240
  "A selection-order change applies from the next unbound request and never moves a bound thread.",
@@ -327,11 +337,12 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
327
337
  { name: "api-key", usage: "ocx api-key <list|create|rotate|remove> ...", summary: "Alias of ocx access key." },
328
338
  {
329
339
  name: "export",
330
- usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|prime|aside|raycast> [--json] [--out <path>] [--force]",
331
- summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast) wired to the running proxy.",
340
+ usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|prime|aside|raycast|omo|cline> [--json] [--out <path>] [--force]",
341
+ summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline) wired to the running proxy.",
332
342
  details: [
333
343
  "--json prints the generated document as JSON on stdout; use --out for the client's native format.",
334
344
  "--out <path> writes the native config there and refuses to replace an existing file without --force.",
345
+ "Cline exports a two-document bundle: settings for providers.json and catalog for sibling models.json. Use integration client enable --client cline for a journaled write.",
335
346
  "The config never contains a real key; it carries a documented env reference or a non-secret loopback placeholder.",
336
347
  "The destination path is printed for merging by hand — ocx never writes your real client config.",
337
348
  ],