@bitkyc08/opencodex 2.64.0 → 2.65.0-preview.20260925

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 (208) hide show
  1. package/README.md +28 -25
  2. package/bin/ocx.mjs +16 -2
  3. package/gui/dist/assets/App-8NMiZxT0.css +1 -0
  4. package/gui/dist/assets/App-CsYvvpr3.js +50 -0
  5. package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
  6. package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
  7. package/gui/dist/assets/{index-SggB6t3z.js → index-BB0iHG8-.js} +12 -12
  8. package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -4
  11. package/src/adapters/anthropic.ts +30 -1
  12. package/src/adapters/claude-cli/adapter.ts +218 -0
  13. package/src/adapters/claude-cli/profiles.ts +38 -0
  14. package/src/adapters/codebuddy/profiles.ts +2 -0
  15. package/src/adapters/coding-agent/profile.ts +12 -4
  16. package/src/adapters/coding-agent/turn.ts +16 -6
  17. package/src/adapters/command-code-tool-text.ts +133 -23
  18. package/src/adapters/cursor/catalog.ts +4 -8
  19. package/src/adapters/cursor/discovery.ts +10 -5
  20. package/src/adapters/cursor/effort-map.ts +2 -6
  21. package/src/adapters/cursor/envelope-echo.ts +10 -5
  22. package/src/adapters/cursor/request-builder.ts +9 -4
  23. package/src/adapters/cursor/thread-continuity.ts +53 -0
  24. package/src/adapters/cursor.ts +29 -13
  25. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  26. package/src/adapters/google-tool-schema.ts +26 -3
  27. package/src/adapters/identity.ts +214 -2
  28. package/src/adapters/openai-chat/passthrough.ts +1 -0
  29. package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
  30. package/src/adapters/openai-chat.ts +43 -15
  31. package/src/adapters/openai-responses/canonical-forward.ts +17 -10
  32. package/src/adapters/openai-responses/passthrough.ts +19 -4
  33. package/src/adapters/openai-responses/request-strips.ts +25 -0
  34. package/src/adapters/openai-responses/web-search.ts +22 -4
  35. package/src/adapters/qoder/profiles.ts +2 -0
  36. package/src/adapters/registry.ts +8 -0
  37. package/src/adapters/run-turn-queue.ts +7 -0
  38. package/src/bridge/response-json.ts +6 -4
  39. package/src/bridge/sse.ts +7 -5
  40. package/src/claude/alias.ts +73 -26
  41. package/src/claude/context-windows.ts +15 -6
  42. package/src/claude/desktop-3p-library.ts +3 -2
  43. package/src/claude/desktop-3p.ts +1 -1
  44. package/src/claude/desktop-first-party.ts +63 -22
  45. package/src/claude/desktop-picker-profile.ts +302 -0
  46. package/src/claude/desktop-picker.ts +365 -0
  47. package/src/claude/desktop-risk.ts +20 -0
  48. package/src/claude/gateway-cache.ts +13 -4
  49. package/src/claude/inbound.ts +5 -2
  50. package/src/claude/intercept/connect-proxy.ts +106 -29
  51. package/src/claude/intercept/local-ca.ts +76 -18
  52. package/src/claude/intercept/model-bindings.ts +145 -0
  53. package/src/claude/intercept/picker-bootstrap.ts +141 -0
  54. package/src/claude/intercept/picker-ca.ts +126 -0
  55. package/src/claude/intercept/picker-listener.ts +213 -0
  56. package/src/claude/intercept/picker-models.ts +101 -0
  57. package/src/claude/intercept/picker-runtime.ts +330 -0
  58. package/src/claude/intercept/picker-trust.ts +125 -0
  59. package/src/claude/intercept/runtime.ts +149 -2
  60. package/src/claude/model-info.ts +18 -4
  61. package/src/cli/agent.ts +16 -2
  62. package/src/cli/capabilities.ts +71 -2
  63. package/src/cli/claude-desktop.ts +319 -33
  64. package/src/cli/claude.ts +15 -7
  65. package/src/cli/codex-shim-autorestore.ts +1 -0
  66. package/src/cli/dispatch.ts +29 -23
  67. package/src/cli/effort.ts +16 -6
  68. package/src/cli/ensure-desired-integrations.ts +29 -4
  69. package/src/cli/ready.ts +1 -1
  70. package/src/cli/registry.ts +8 -0
  71. package/src/cli/restart-scope.ts +9 -1
  72. package/src/cli/runtime-api.ts +17 -0
  73. package/src/cli/system-command.ts +11 -12
  74. package/src/client/connect.ts +12 -2
  75. package/src/client/state.ts +39 -1
  76. package/src/clients/config-export.ts +57 -9
  77. package/src/codex/auth-context.ts +51 -11
  78. package/src/codex/catalog/derive-entry.ts +5 -5
  79. package/src/codex/catalog/gather-capture.ts +25 -6
  80. package/src/codex/catalog/metadata.ts +3 -3
  81. package/src/codex/catalog/parsing.ts +39 -1
  82. package/src/codex/catalog/provider-models.ts +8 -6
  83. package/src/codex/catalog/retained-sync.ts +17 -7
  84. package/src/codex/catalog/sync.ts +2 -2
  85. package/src/codex/desktop-app-restart.ts +8 -0
  86. package/src/codex/desktop-switches.ts +9 -1
  87. package/src/codex/home.ts +43 -4
  88. package/src/codex/inject/config-toml.ts +137 -0
  89. package/src/codex/inject/plan.ts +23 -0
  90. package/src/codex/inject/remove.ts +21 -1
  91. package/src/codex/inject.ts +63 -8
  92. package/src/codex/journal.ts +36 -0
  93. package/src/codex/main-account-hard-lock.ts +14 -1
  94. package/src/codex/main-account-policy-wait.ts +109 -0
  95. package/src/codex/native-profile-startup.ts +24 -6
  96. package/src/codex/native-residue.ts +28 -10
  97. package/src/codex/quota-types.ts +8 -1
  98. package/src/codex/quota.ts +49 -7
  99. package/src/codex/runtime.ts +31 -10
  100. package/src/combos/failover.ts +7 -6
  101. package/src/combos/resolve.ts +91 -14
  102. package/src/combos/types.ts +18 -1
  103. package/src/config/load-degrade.ts +2 -0
  104. package/src/config/schema/config-schema.ts +21 -2
  105. package/src/config/schema/leaf-validators.ts +6 -3
  106. package/src/config/subagent-models.ts +3 -1
  107. package/src/generated/compatibility-version.json +275 -163
  108. package/src/images/artifacts.ts +14 -0
  109. package/src/integrations/owned-refresh.ts +1 -1
  110. package/src/integrations/ownership-policy.ts +32 -1
  111. package/src/integrations/state.ts +3 -1
  112. package/src/integrations/writer.ts +9 -0
  113. package/src/lib/config-ownership.ts +3 -0
  114. package/src/lib/errors.ts +87 -0
  115. package/src/lib/package-tree-integrity.ts +1 -1
  116. package/src/lib/request-execution-budget.ts +42 -14
  117. package/src/lib/request-resend-gate.ts +4 -3
  118. package/src/lib/retry-delay.ts +7 -1
  119. package/src/lib/tool-envelope-echo-filter.ts +236 -0
  120. package/src/lib/upstream-retry.ts +20 -12
  121. package/src/oauth/index.ts +1 -0
  122. package/src/oauth/kiro-credentials.ts +14 -1
  123. package/src/oauth/login-cli.ts +1 -0
  124. package/src/providers/derive.ts +4 -0
  125. package/src/providers/fast-opt-in.ts +31 -0
  126. package/src/providers/model-rename-fields.ts +2 -0
  127. package/src/providers/provider-id-rewrite.ts +2 -0
  128. package/src/providers/quota/vendor-probes-key.ts +8 -2
  129. package/src/providers/registry/entries-core.ts +45 -2
  130. package/src/providers/registry/entries-extended.ts +99 -12
  131. package/src/providers/registry/model-ids.ts +2 -0
  132. package/src/providers/registry/types.ts +7 -1
  133. package/src/providers/resolved-model-policy.ts +11 -4
  134. package/src/providers/service-tier.ts +10 -3
  135. package/src/responses/code-mode-helper-compat.ts +3 -0
  136. package/src/responses/parser.ts +45 -3
  137. package/src/router.ts +6 -0
  138. package/src/server/auth-cors.ts +8 -0
  139. package/src/server/chat-completions.ts +3 -1
  140. package/src/server/claude-messages.ts +69 -14
  141. package/src/server/grok-upstream-envelope-echo.ts +120 -0
  142. package/src/server/index/claude-intercept-lifecycle.ts +24 -0
  143. package/src/server/index/serve-options.ts +2 -2
  144. package/src/server/index.ts +7 -0
  145. package/src/server/management/agent-settings-routes.ts +204 -112
  146. package/src/server/management/claude-desktop-picker-routes.ts +128 -0
  147. package/src/server/management/combo-routes.ts +59 -2
  148. package/src/server/management/config-routes.ts +81 -39
  149. package/src/server/management/context.ts +3 -0
  150. package/src/server/management/native-integration-routes.ts +169 -93
  151. package/src/server/management/provider-routes.ts +46 -4
  152. package/src/server/management/route-registry.ts +10 -0
  153. package/src/server/management/routing-profile-routes.ts +9 -0
  154. package/src/server/management/sidebar-routes.ts +52 -0
  155. package/src/server/management-api.ts +2 -0
  156. package/src/server/proxy-liveness.ts +38 -3
  157. package/src/server/request-log-account-rotation.ts +53 -0
  158. package/src/server/request-log.ts +3 -16
  159. package/src/server/responses/adapter-dispatch.ts +17 -1
  160. package/src/server/responses/agent-task-recovery.ts +49 -26
  161. package/src/server/responses/codex-ws-exchange.ts +18 -7
  162. package/src/server/responses/codex-ws-wire.ts +27 -4
  163. package/src/server/responses/core-combo.ts +21 -3
  164. package/src/server/responses/core-normalize.ts +7 -0
  165. package/src/server/responses/core-opaque-recovery.ts +3 -2
  166. package/src/server/responses/encrypted-payload.ts +15 -14
  167. package/src/server/responses/fetch-helpers.ts +1 -1
  168. package/src/server/responses/input-admission.ts +10 -6
  169. package/src/server/responses/native-response-control.ts +2 -2
  170. package/src/server/responses/passthrough-delivery.ts +182 -12
  171. package/src/server/responses/passthrough-dispatch.ts +102 -44
  172. package/src/server/responses/policy-fallback.ts +3 -0
  173. package/src/server/responses/policy-refusal.ts +67 -0
  174. package/src/server/responses/request-send-budget.ts +4 -0
  175. package/src/server/responses/run-turn-execution.ts +42 -11
  176. package/src/server/responses/ws-upstream.ts +2 -1
  177. package/src/server/system-env-shell.ts +0 -1
  178. package/src/server/system-env.ts +25 -4
  179. package/src/service/cli.ts +34 -3
  180. package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
  181. package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
  182. package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
  183. package/src/tray/windows-tray.ps1 +188 -7
  184. package/src/tray/windows.ts +23 -2
  185. package/src/types/config.ts +52 -12
  186. package/src/types/provider.ts +22 -5
  187. package/src/types/tools.ts +11 -5
  188. package/src/types.ts +1 -0
  189. package/src/update/async-check.ts +109 -0
  190. package/src/update/badge.ts +11 -17
  191. package/src/update/check-types.ts +9 -0
  192. package/src/update/desktop-badge.ts +102 -0
  193. package/src/update/index.ts +48 -8
  194. package/src/update/install-detection.d.mts +29 -2
  195. package/src/update/install-detection.mjs +213 -7
  196. package/src/update/job.ts +17 -12
  197. package/src/update/notify.ts +16 -11
  198. package/src/update/pnpm-owner-worker.ts +12 -0
  199. package/src/update/refresh-scheduler.ts +141 -0
  200. package/src/usage/cost.ts +31 -4
  201. package/src/usage/expected-prices.ts +57 -0
  202. package/assets/download-linux.svg +0 -10
  203. package/assets/download-macos.svg +0 -10
  204. package/assets/download-windows.svg +0 -10
  205. package/gui/dist/assets/App-CpuDF3ci.js +0 -50
  206. package/gui/dist/assets/App-I5AnaSLh.css +0 -1
  207. package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
  208. package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
@@ -555,24 +555,24 @@ const commandRunners: Record<string, CommandRunner> = {
555
555
  const cacheArgs = deps.args.slice(1);
556
556
  const restartScope = readRestartScope(cacheArgs, console);
557
557
  const { withCatalogWriteSerialization } = await import("../codex/catalog-write-serialization");
558
- const { invalidateCodexModelsCacheWithPermit } = await import("../codex/catalog/sync");
558
+ const { invalidateCodexModelsCacheWithPermitOutcome } = await import("../codex/catalog/sync");
559
559
  const { getCodexHome } = await import("../codex/paths");
560
- const { readCodexCatalogPathForHome } = await import("../codex/catalog/parsing");
561
- const { existsSync } = await import("node:fs");
562
560
  const owningCodexHome = getCodexHome();
563
561
  const cacheGateSnapshot = deps.loadConfig();
564
562
  const desiredDisabled = !shouldSyncCodexOnStart(cacheGateSnapshot);
565
563
  const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
566
- invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, { allowWhenDesiredDisabled: true }));
564
+ invalidateCodexModelsCacheWithPermitOutcome(permit, owningCodexHome, { allowWhenDesiredDisabled: true }));
567
565
  const cacheJson = cacheArgs.includes("--json");
568
566
  const jsonSafeLog = cacheJson
569
567
  ? { log: (...values: unknown[]) => console.error(...values), error: (...values: unknown[]) => console.error(...values) }
570
568
  : console;
571
569
  // Only warn/restart when models_cache was actually rewritten from a readable catalog.
572
- if (invalidated.kind === "completed" && invalidated.value) {
570
+ if (invalidated.kind === "completed" && invalidated.value === "written") {
573
571
  await handleRestartScopeAfterWrite(restartScope, jsonSafeLog);
574
- } else if (desiredDisabled && !cacheJson) {
575
- // Worth saying in the human path, because it explains why nothing was written.
572
+ } else if (!cacheJson && invalidated.kind === "completed" && invalidated.value === "desired_disabled") {
573
+ // Only when the OFF gate itself stopped the write does OFF explain the outcome. An
574
+ // explicit sync-cache refreshes regardless of the toggle, so an unchanged cache, a
575
+ // missing catalog, or a contended writer is reported below on its own terms.
576
576
  // Under --json this belongs on the envelope, not as a second stdout line.
577
577
  console.log(localClientSkipMessage(
578
578
  cacheGateSnapshot,
@@ -580,8 +580,8 @@ const commandRunners: Record<string, CommandRunner> = {
580
580
  "No catalog or cache write resulted.",
581
581
  ));
582
582
  }
583
- // `completed` with a falsy value means the cache was NOT rewritten. Previously every
584
- // outcome exited 0, so a script could not tell a refreshed cache from a skipped one.
583
+ // An identical cache is a successful no-op, not a failed refresh. Only a real write
584
+ // should restart Codex; a missing catalog or contended writer is also a benign skip.
585
585
  //
586
586
  // Losing the catalog write lock to another process is a skip, not a failure:
587
587
  // serialization working as designed is the expected outcome under concurrency, and a
@@ -596,30 +596,25 @@ const commandRunners: Record<string, CommandRunner> = {
596
596
  // means the user asked for it regardless of the toggle. Treating OFF as automatic success
597
597
  // would report exit 0 and `skipped: true` for a refresh that actually failed.
598
598
  //
599
- // But `invalidateCodexModelsCacheWithPermit` returns a bare boolean for four different
600
- // situations -- wrote it, no catalog file exists, the OFF gate fired, or it threw -- so
601
- // `false` alone cannot be read as failure either. `!existsSync(catalogPath)` is a
602
- // legitimate nothing-to-do: with no catalog there is no cache to derive, which is the
603
- // normal state of a fully native home and the case
604
- // `codex-composed-acceptance.test.ts` pins at exit 0. It is checked here rather than by
605
- // widening that function's return type, because its boolean is consumed by a dozen
606
- // management routes that have no use for the distinction.
607
- const wrote = invalidated.kind === "completed" && Boolean(invalidated.value);
599
+ // The detailed outcome distinguishes an unchanged cache from a failed rewrite while
600
+ // the boolean wrapper remains available to callers that only care whether bytes changed.
601
+ const wrote = invalidated.kind === "completed" && invalidated.value === "written";
602
+ const unchanged = invalidated.kind === "completed" && invalidated.value === "unchanged";
608
603
  const contended = invalidated.kind === "unavailable" && invalidated.reason === "busy";
609
- const noCatalog = !wrote && !existsSync(readCodexCatalogPathForHome(owningCodexHome));
610
- const ok = wrote || contended || noCatalog;
604
+ const noCatalog = invalidated.kind === "completed" && invalidated.value === "missing_catalog";
605
+ const ok = wrote || unchanged || contended || noCatalog;
611
606
  if (cacheJson) {
612
607
  console.log(JSON.stringify({
613
608
  schemaVersion: 1,
614
609
  ok,
615
610
  wrote,
616
- skipped: contended || noCatalog,
611
+ skipped: unchanged || contended || noCatalog,
617
612
  outcome: invalidated.kind,
618
613
  // `outcome` alone cannot separate a contended lock from a hard serialization
619
614
  // failure -- both are `unavailable`. Carry the reason so a caller can.
620
615
  reason: invalidated.kind === "unavailable" ? invalidated.reason : undefined,
621
- // Which of the two benign skips this was, so `skipped: true` is never opaque.
622
- skippedReason: contended ? "contended" : noCatalog ? "no_catalog" : undefined,
616
+ // Which of the three benign skips this was, so `skipped: true` is never opaque.
617
+ skippedReason: unchanged ? "unchanged" : contended ? "contended" : noCatalog ? "no_catalog" : undefined,
623
618
  desiredDisabled,
624
619
  codexHome: owningCodexHome,
625
620
  }, null, 2));
@@ -627,6 +622,8 @@ const commandRunners: Record<string, CommandRunner> = {
627
622
  console.log("Another process owns the catalog write; cache sync skipped.");
628
623
  } else if (noCatalog) {
629
624
  console.log("No Codex catalog to derive a cache from; nothing to sync.");
625
+ } else if (unchanged) {
626
+ console.log("Codex model cache is already current; nothing to sync.");
630
627
  } else if (!ok) {
631
628
  console.error(`Cache refresh did not complete (${invalidated.kind}). The Codex model cache was not rewritten.`);
632
629
  }
@@ -733,6 +730,15 @@ const commandRunners: Record<string, CommandRunner> = {
733
730
  await refreshVersionCache(channel);
734
731
  return 0;
735
732
  },
733
+ "__update-badge": async deps => {
734
+ if (deps.args.length !== 1) {
735
+ console.error("Usage: ocx __update-badge");
736
+ return 64;
737
+ }
738
+ const { readUpdateBadge } = await import("../update/badge");
739
+ console.log(JSON.stringify(readUpdateBadge()));
740
+ return 0;
741
+ },
736
742
  "__tray-start": async deps => {
737
743
  return (await deps.handleTrayProxyStart()) ? 0 : 1;
738
744
  },
package/src/cli/effort.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  mapReasoningEffort,
8
8
  reasoningEffortMapFor,
9
9
  } from "../reasoning-effort";
10
+ import { decodeRoutedModelId } from "../providers/slug-codec";
11
+ import { knownModelIdsForProvider } from "../router";
10
12
  import { findLiveProxy } from "../server/proxy-liveness";
11
13
  import { modelInList, type OcxConfig } from "../types";
12
14
  import {
@@ -290,19 +292,26 @@ function inspectModelEffort(modelTarget: string, wantsJson: boolean): void {
290
292
  );
291
293
  }
292
294
 
293
- const isReasoningDisabled = modelInList(provider.noReasoningModels, modelId);
294
- const efforts = configuredReasoningEfforts(provider, modelId);
295
- const wireMap = reasoningEffortMapFor(provider, modelId);
295
+ // A Codex-facing slug encodes the inner "/" of a namespaced native id
296
+ // (`command-code/deepseek-deepseek-v4.1-flash` for `deepseek/deepseek-v4.1-flash`), so the
297
+ // literal id only resolves against the ladder through the decode the router already uses.
298
+ const known = knownModelIdsForProvider(providerName, provider, config);
299
+ const resolvedModelId = known.includes(modelId) ? modelId : decodeRoutedModelId(modelId, known);
300
+
301
+ const isReasoningDisabled = modelInList(provider.noReasoningModels, resolvedModelId);
302
+ const efforts = configuredReasoningEfforts(provider, resolvedModelId);
303
+ const wireMap = reasoningEffortMapFor(provider, resolvedModelId);
296
304
 
297
305
  // Derive sample ladder directly from canonical CODEX_REASONING_LEVELS (#3528 review)
298
306
  const mappedExamples: Record<string, string | undefined> = {};
299
307
  for (const { effort } of CODEX_REASONING_LEVELS) {
300
- mappedExamples[effort] = mapReasoningEffort(provider, modelId, effort);
308
+ mappedExamples[effort] = mapReasoningEffort(provider, resolvedModelId, effort);
301
309
  }
302
310
 
303
311
  const result = {
304
312
  provider: providerName,
305
- model: modelId,
313
+ model: resolvedModelId,
314
+ ...(resolvedModelId !== modelId ? { requestedModel: modelId } : {}),
306
315
  reasoningDisabled: isReasoningDisabled,
307
316
  supportedEfforts: efforts ?? null,
308
317
  wireMap: wireMap ?? null,
@@ -310,7 +319,8 @@ function inspectModelEffort(modelTarget: string, wantsJson: boolean): void {
310
319
  };
311
320
 
312
321
  const lines = [
313
- `Reasoning effort configuration for ${providerName}/${modelId}:`,
322
+ `Reasoning effort configuration for ${providerName}/${resolvedModelId}:`,
323
+ ...(resolvedModelId !== modelId ? [` Resolved from: ${modelId}`] : []),
314
324
  ` Reasoning disabled: ${isReasoningDisabled ? "yes (noReasoningModels)" : "no"}`,
315
325
  ` Supported ladder: ${efforts ? efforts.join(", ") : "(default / unconstrained)"}`,
316
326
  ` Wire mapping overrides: ${wireMap ? JSON.stringify(wireMap) : "(standard provider mapping)"}`,
@@ -9,11 +9,15 @@
9
9
  * each external-file mutation, and use that current config for sync inputs.
10
10
  */
11
11
  import { loadConfig } from "../config";
12
+ import { removeDesktopPickerArtifacts } from "../claude/desktop-picker";
13
+ import { findLiveProxy } from "../server/proxy-liveness";
14
+ import { runtimeRequest } from "./runtime-api";
12
15
  import { stripGrokConfig, type GrokInjectResult } from "../grok/inject";
13
16
  import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot } from "../claude/desktop-3p";
14
17
  import {
15
18
  applyDesktopFirstParty,
16
19
  inspectDesktopFirstParty,
20
+ observeClaudeDesktopMode,
17
21
  removeDesktopFirstParty,
18
22
  resolveClaudeDesktopMode,
19
23
  } from "../claude/desktop-first-party";
@@ -44,7 +48,11 @@ export interface EnsureDesiredIntegrationsDeps {
44
48
  removeDesktopFirstParty?: typeof removeDesktopFirstParty;
45
49
  applyDesktopFirstParty?: typeof applyDesktopFirstParty;
46
50
  inspectDesktopFirstParty?: typeof inspectDesktopFirstParty;
51
+ observeClaudeDesktopMode?: typeof observeClaudeDesktopMode;
47
52
  inspectDesktop3pConfigLibrary?: typeof inspectDesktop3pConfigLibrary;
53
+ findLiveProxyImpl?: typeof findLiveProxy;
54
+ runtimeRequestImpl?: typeof runtimeRequest;
55
+ removeDesktopPickerArtifacts?: typeof removeDesktopPickerArtifacts;
48
56
  log?: (message: string) => void;
49
57
  error?: (message: string) => void;
50
58
  }
@@ -130,13 +138,13 @@ export async function ensureGrokFenceMatchesDesired(
130
138
  * When it is ON in first-party mode, refresh a stale env (the intercept port follows the
131
139
  * public port, so a port change would otherwise leave Claude Code pointed at a dead proxy).
132
140
  */
133
- export function ensureClaudeDesktopMatchesDesired(
141
+ export async function ensureClaudeDesktopMatchesDesired(
134
142
  deps: EnsureDesiredIntegrationsDeps = productionDeps,
135
- ): void {
143
+ ): Promise<void> {
136
144
  const config = deps.loadConfig();
137
145
  const { log, error } = io(deps);
138
146
  if (claudeDesktopIntegrationEnabled(config)) {
139
- if (resolveClaudeDesktopMode(config) !== "first-party") return;
147
+ if (resolveClaudeDesktopMode(config, (deps.observeClaudeDesktopMode ?? observeClaudeDesktopMode)(config)) !== "first-party") return;
140
148
  const library = (deps.inspectDesktop3pConfigLibrary ?? inspectDesktop3pConfigLibrary)({
141
149
  appliedFingerprint: config.claudeCode?.desktopProfile?.appliedFingerprint ?? null,
142
150
  });
@@ -153,6 +161,23 @@ export function ensureClaudeDesktopMatchesDesired(
153
161
  else if (!applied.ok) error(`⚠️ Claude Desktop first-party env refresh skipped: ${applied.reason}.`);
154
162
  return;
155
163
  }
164
+ try {
165
+ const live = await (deps.findLiveProxyImpl ?? findLiveProxy)();
166
+ if (live) {
167
+ const request = deps.runtimeRequestImpl ?? runtimeRequest;
168
+ await request(
169
+ "/api/claude-desktop/picker",
170
+ { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ enabled: false, persist: false }) },
171
+ deps.findLiveProxyImpl ? { findLiveProxy: deps.findLiveProxyImpl } : {},
172
+ );
173
+ } else {
174
+ const removed = await (deps.removeDesktopPickerArtifacts ?? removeDesktopPickerArtifacts)({});
175
+ if (!removed.ok) error(`⚠️ Claude Desktop picker cleanup skipped${removed.residual?.length ? `: ${removed.residual.join(", ")}` : ""}.`);
176
+ }
177
+ } catch (err) {
178
+ const detail = err instanceof Error ? err.message : String(err);
179
+ error(`⚠️ Claude Desktop picker cleanup skipped: ${detail}.`);
180
+ }
156
181
  try {
157
182
  const env = (deps.removeDesktopFirstParty ?? removeDesktopFirstParty)();
158
183
  if (env.ok && env.changed) log(" ↩️ Claude Desktop first-party env removed.");
@@ -196,5 +221,5 @@ export async function reconcileEnsureDesiredIntegrations(
196
221
  liveHost ? { hostname: liveHost } : {},
197
222
  deps,
198
223
  );
199
- ensureClaudeDesktopMatchesDesired(deps);
224
+ await ensureClaudeDesktopMatchesDesired(deps);
200
225
  }
package/src/cli/ready.ts CHANGED
@@ -199,7 +199,7 @@ export async function runReady(args: ReadyArgs, io: ReadyIo = {}): Promise<numbe
199
199
  if (!args.wait) {
200
200
  // Default: exactly one identity-checked readiness probe. No wait deadline —
201
201
  // bounded only by the probe's own default timeout (remainingMs=undefined so
202
- // the production defaults keep their built-in 750ms ceiling; no semantic
202
+ // the production defaults keep their built-in DEFAULT_PROBE_TIMEOUT_MS ceiling; no semantic
203
203
  // regression vs. the single-probe behavior).
204
204
  const live = await find(undefined);
205
205
  if (!live) {
@@ -451,6 +451,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
451
451
  " ocx claude desktop [apply] Save and apply the four-family profile",
452
452
  " ocx claude desktop show [--json] Show routes, families, and defaults",
453
453
  " ocx claude desktop status [--json] Show applied state, drift, and health",
454
+ " ocx claude desktop bind <picker-id> <route> First-party: serve a Code tab picker model with a route",
455
+ " ocx claude desktop unbind <picker-id> Remove a first-party binding",
454
456
  " ocx claude desktop move <route> <family> [--default]",
455
457
  " ocx claude desktop default <family> <route|none>",
456
458
  " ocx claude desktop export <path|-> Export versioned JSON (`-` = stdout)",
@@ -591,6 +593,12 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
591
593
  usage: "ocx __refresh-version [preview|latest]",
592
594
  summary: "Hidden detached helper: refresh the cached latest version.",
593
595
  },
596
+ {
597
+ name: "__update-badge",
598
+ hidden: true,
599
+ usage: "ocx __update-badge",
600
+ summary: "Hidden internal: print cached package update badge JSON.",
601
+ },
594
602
  {
595
603
  name: "__tray-start",
596
604
  hidden: true,
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes";
8
8
  import type { AfterCatalogWriteAppServerResult } from "../codex/app-server-processes";
9
- import type { DesktopAppRestartResult } from "../codex/desktop-app-restart";
9
+ import type { DesktopAppRestartIo, DesktopAppRestartResult } from "../codex/desktop-app-restart";
10
10
 
11
11
  /**
12
12
  * Which restart a command was asked for.
@@ -102,10 +102,12 @@ export async function handleRestartScopeAfterWrite(
102
102
  */
103
103
  export async function handleDesktopAppRestart(
104
104
  log: Pick<Console, "log" | "error">,
105
+ io: DesktopAppRestartIo = {},
105
106
  ): Promise<DesktopAppRestartResult> {
106
107
  const { restartCodexDesktopApp } = await import("../codex/desktop-app-restart");
107
108
  const { startDesktopRestartHandoff } = await import("../codex/desktop-app/handoff");
108
109
  const result = restartCodexDesktopApp({
110
+ ...io,
109
111
  // The CLI is the one caller whose exit is exactly the signal the helper waits for,
110
112
  // so it is the one caller allowed to hand off. The management service is not (it
111
113
  // runs in a proxy that never exits) and the helper itself is not (recursion).
@@ -123,6 +125,12 @@ export async function handleDesktopAppRestart(
123
125
  + "nothing was stopped.",
124
126
  );
125
127
  return result;
128
+ case "test_environment":
129
+ log.error(
130
+ "Skipped the Codex desktop app restart: this is an armed opencodex test process "
131
+ + "(OCX_TEST_HOME_GUARD=1), so the real app was not touched.",
132
+ );
133
+ return result;
126
134
  case "restart_in_flight":
127
135
  log.error(
128
136
  "Another Codex desktop-app restart is already running; this one did nothing. "
@@ -394,6 +394,23 @@ export function printData(value: unknown, wantsJson: boolean, lines?: string[]):
394
394
  else for (const line of lines) console.log(line);
395
395
  }
396
396
 
397
+ /**
398
+ * Operator text for the Codex-config apply report a management write returns.
399
+ *
400
+ * The report shape is shared by every route that re-runs the injection on the spot: the Desktop
401
+ * switches (`ocx system settings`) and the web-search sidecar's master switch. One vocabulary for
402
+ * both, so the same failure cannot read as two different things depending on which command the
403
+ * operator used -- and because the reason codes are internal, the human line never prints them.
404
+ */
405
+ export function desktopSwitchApplyReason(reason: unknown): string {
406
+ if (reason === "not_requested") return "no desktop switch rewrite was requested";
407
+ if (reason === "proxy_not_running") return "the proxy is not running";
408
+ if (reason === "integration_disabled") return "Codex integration is disabled";
409
+ if (reason === "write_lock_busy") return "the Codex config write lock is busy";
410
+ if (reason === "injection_refused") return "Codex config injection was refused";
411
+ return "the rewrite could not be completed";
412
+ }
413
+
397
414
  /**
398
415
  * Render untrusted diagnostic text without letting it control the operator's terminal. Catalog
399
416
  * values are hub-supplied and surface on more than one CLI path -- first-time `ocx connect` and the
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CliUsageError,
3
+ desktopSwitchApplyReason,
3
4
  printData,
4
5
  rejectArgs,
5
6
  runCliAction,
@@ -56,15 +57,6 @@ function desktopSwitchInertReason(reason: unknown): string {
56
57
  return "the stored setting is not effective in the current runtime configuration";
57
58
  }
58
59
 
59
- function desktopSwitchApplyReason(reason: unknown): string {
60
- if (reason === "not_requested") return "no desktop switch rewrite was requested";
61
- if (reason === "proxy_not_running") return "the proxy is not running";
62
- if (reason === "integration_disabled") return "Codex integration is disabled";
63
- if (reason === "write_lock_busy") return "the Codex config write lock is busy";
64
- if (reason === "injection_refused") return "Codex config injection was refused";
65
- return "the rewrite could not be completed";
66
- }
67
-
68
60
  function settingsUpdateLines(
69
61
  result: unknown,
70
62
  changed: { desktopAuthless: boolean; clientCompaction: boolean },
@@ -202,13 +194,20 @@ export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps =
202
194
  printData(await runtimeRequest("/api/system/codex-app-server", {}, deps), wantsJson);
203
195
  } else if (sub === "codex-restart") {
204
196
  // --yes required: this fully quits and relaunches the user's Codex desktop app as well as
205
- // restarting app-servers; an agent guessing a subcommand must not interrupt that session.
197
+ // restarting app-servers, which can discard unsaved drafts, selections, and approval prompts.
206
198
  const args = [...rest];
207
199
  const wantsJson = takeFlag(args, "--json");
208
200
  const yes = takeFlag(args, "--yes");
209
- if (!yes) throw new CliUsageError("system codex-restart requires --yes: this fully quits and relaunches the Codex desktop app and restarts its app-servers", USAGE);
201
+ if (!yes) throw new CliUsageError(
202
+ "system codex-restart requires --yes: this fully quits and relaunches the Codex desktop app, so unsaved composer drafts, model-picker selections, and pending approval prompts may be lost; it also restarts the app-servers",
203
+ USAGE,
204
+ );
210
205
  rejectArgs(args, USAGE);
211
- printData(await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps), wantsJson, ["Codex desktop app and app-server restart requested."]);
206
+ printData(
207
+ await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps),
208
+ wantsJson,
209
+ ["Codex desktop app and app-server restart requested. Unsaved composer drafts, model-picker selections, and pending approval prompts may be lost."],
210
+ );
212
211
  } else if (sub === "update") await update(rest, deps);
213
212
  else throw new CliUsageError(`unknown system command ${sub}`, USAGE);
214
213
  });
@@ -37,6 +37,7 @@ import {
37
37
  replaceServiceApiTokenFile,
38
38
  restoreTokenBackup,
39
39
  serviceApiTokenBackupPath,
40
+ serviceApiTokenFingerprint,
40
41
  writeTokenBackup,
41
42
  writeServiceApiTokenFile,
42
43
  } from "../lib/service-secrets";
@@ -65,6 +66,7 @@ import {
65
66
  clearClientConnection,
66
67
  commitClientConnection,
67
68
  readClientConnectionState,
69
+ markClientConnectPending, clearClientConnectPending, pendingClientConnectMayOwnToken,
68
70
  assertNoClientDisconnectPending, assertClientConnectionUnchanged, sameClientConnectionOwner,
69
71
  } from "./state";
70
72
  import { assertClientCatalogCompatible, type CatalogCompatibilityDeps } from "./catalog-compatibility";
@@ -492,6 +494,7 @@ function assertConnectingState(expectedTokenFingerprint?: string): void {
492
494
  }
493
495
  }
494
496
 
497
+ /** Enroll a client key, keeping its pending ownership visible until commit or rollback. */
495
498
  export async function connectClient(
496
499
  options: ConnectOptions,
497
500
  deps: ClientConnectDeps = {},
@@ -501,6 +504,7 @@ export async function connectClient(
501
504
  let issued: IssuedClientKey | null = null;
502
505
  let cleanupCredential: { kind: "admin"; value: Uint8Array } | { kind: "gui-session"; value: ConnectGuiSession } | null = null;
503
506
  let tokenFingerprint: string | null = null;
507
+ let pendingConnectFingerprint: string | null = null;
504
508
  let priorCatalog: CatalogSnapshot | null = null;
505
509
  let writtenCatalogFingerprint: string | null = null;
506
510
  let injectionCommitted = false;
@@ -536,6 +540,9 @@ export async function connectClient(
536
540
 
537
541
  const initialFiles = withClientLifecycleSync(() => withConfigMutationLockSync(() => {
538
542
  assertConnectingState();
543
+ const fingerprint = serviceApiTokenFingerprint(issued!.key);
544
+ markClientConnectPending(fingerprint);
545
+ pendingConnectFingerprint = fingerprint;
539
546
  return { prior: catalogSnapshot(), persisted: writeServiceApiTokenFile(issued!.key) };
540
547
  }), deps.lifecycleLockDeps);
541
548
  priorCatalog = initialFiles.prior;
@@ -602,6 +609,7 @@ export async function connectClient(
602
609
  };
603
610
  withClientLifecycleSync(() => withConfigMutationLockSync(() => {
604
611
  assertConnectingState(persisted.fingerprint);
612
+ clearClientConnectPending(persisted.fingerprint);
605
613
  commitClientConnection(connection);
606
614
  committed = true;
607
615
  }), deps.lifecycleLockDeps);
@@ -618,9 +626,11 @@ export async function connectClient(
618
626
  if (priorCatalog && writtenCatalogFingerprint && !restoreCatalogSnapshot(priorCatalog, writtenCatalogFingerprint)) {
619
627
  rollbackFailures.push("catalog rollback did not match the written artifact");
620
628
  }
621
- if (tokenFingerprint) {
622
- const removed = removeServiceApiTokenFileIfOwned(tokenFingerprint);
629
+ if (pendingConnectFingerprint) {
630
+ const removed = removeServiceApiTokenFileIfOwned(pendingConnectFingerprint);
623
631
  if (removed === "changed") rollbackFailures.push("service token changed during rollback");
632
+ // Final commit may fail after this attempt already cleared its marker under the same lock.
633
+ else if (pendingClientConnectMayOwnToken(pendingConnectFingerprint)) clearClientConnectPending(pendingConnectFingerprint);
624
634
  }
625
635
  }), deps.lifecycleLockDeps);
626
636
  } catch { rollbackFailures.push("client cleanup ownership unavailable"); }
@@ -1,5 +1,7 @@
1
- import { readFileSync } from "node:fs";
1
+ import { lstatSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { join } from "node:path";
2
3
  import {
4
+ getConfigDir,
3
5
  getConfigPath,
4
6
  deleteConfigTopLevelKey,
5
7
  getDefaultConfig,
@@ -8,6 +10,7 @@ import {
8
10
  saveConfig,
9
11
  withConfigMutationLockSync,
10
12
  } from "../config";
13
+ import { atomicWriteFileNoFollowUnclaimed } from "../config/atomic-write";
11
14
  import type { OcxClientConnectionConfig } from "../types";
12
15
  import { inspectRemoteDesktopStore, readDesktopDisconnectReceipt } from "../claude/desktop-remote-store";
13
16
  import { withClientLifecycleSync, type ClientLifecycleLockDeps } from "./lifecycle-lock";
@@ -23,6 +26,41 @@ export type ClientConnectionState =
23
26
  | { kind: "invalid"; reason: string }
24
27
  | { kind: "mismatched"; reason: string };
25
28
 
29
+ const pendingConnectPath = (): string => join(getConfigDir(), "client-connect-pending");
30
+
31
+ /** Validate pending ownership; an optional fingerprint restricts it to that exact key. */
32
+ export function pendingClientConnectMayOwnToken(fingerprint?: string): boolean {
33
+ const path = pendingConnectPath();
34
+ let stat;
35
+ try { stat = lstatSync(path); }
36
+ catch (error) {
37
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
38
+ throw error;
39
+ }
40
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size !== 65) {
41
+ throw new Error("pending client connection owner is unsafe");
42
+ }
43
+ const marker = readFileSync(path, "utf8");
44
+ if (!/^[a-f0-9]{64}\n$/.test(marker)) throw new Error("pending client connection owner is malformed");
45
+ return fingerprint === undefined || marker === `${fingerprint}\n`;
46
+ }
47
+
48
+ /** Publish only the token fingerprint, before the key file, under the client lifecycle lock. */
49
+ export function markClientConnectPending(fingerprint: string): void {
50
+ if (!/^[a-f0-9]{64}$/.test(fingerprint)) throw new Error("invalid pending client fingerprint");
51
+ atomicWriteFileNoFollowUnclaimed(pendingConnectPath(), `${fingerprint}\n`);
52
+ }
53
+
54
+ /** Clear only the marker for this connect attempt while the client lifecycle lock is held. */
55
+ export function clearClientConnectPending(fingerprint: string): void {
56
+ const path = pendingConnectPath();
57
+ const stat = lstatSync(path);
58
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size !== 65 || readFileSync(path, "utf8") !== `${fingerprint}\n`) {
59
+ throw new Error("pending client connection owner changed");
60
+ }
61
+ unlinkSync(path);
62
+ }
63
+
26
64
  export type ClientRotationRecoveryGate =
27
65
  | { kind: "clean" }
28
66
  | { kind: "orphan-cleaned" }
@@ -20,7 +20,7 @@
20
20
  * targeting it is the caller's explicit act.
21
21
  */
22
22
  import { homedir } from "node:os";
23
- import { existsSync, readFileSync } from "node:fs";
23
+ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
24
24
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
25
25
  import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
26
26
  import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
@@ -556,7 +556,17 @@ export function omoConfigPath(env: OpencodeLaunchEnv = process.env, home: string
556
556
  * client-owned override to mirror, and this registry does not invent one.
557
557
  */
558
558
  export function asideHomeDir(_env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
559
- return join(home, ".aside");
559
+ const root = join(home, ".aside");
560
+ // A user who relocated Aside (for example to an external volume) leaves ~/.aside as a
561
+ // symlink, and Aside itself follows it (issue 5648). Canonicalize only that top-level
562
+ // alias, once, and only onto a directory: every boundary below the root (u/, account
563
+ // directories, models.json) keeps refusing links against the canonical path.
564
+ try {
565
+ if (lstatSync(root).isSymbolicLink() && statSync(root).isDirectory()) return realpathSync.native(root);
566
+ } catch {
567
+ // Missing or unreadable: the literal path lets the profile reader report it.
568
+ }
569
+ return root;
560
570
  }
561
571
 
562
572
  /**
@@ -812,10 +822,34 @@ export interface PiProviderBlock {
812
822
  baseUrl: string;
813
823
  api: string;
814
824
  apiKey: string;
815
- compat?: { sendSessionAffinityHeaders: boolean };
825
+ compat?: PiProviderCompat;
816
826
  models: PiModelEntry[];
817
827
  }
818
828
 
829
+ /**
830
+ * The subset of Pi's per-provider `compat` block this export writes. Both keys are part of
831
+ * Pi's own model-config schema; an unknown key there would empty the whole config, so nothing
832
+ * outside this set is ever emitted.
833
+ */
834
+ export interface PiProviderCompat {
835
+ sendSessionAffinityHeaders?: boolean;
836
+ supportsDeveloperRole?: boolean;
837
+ }
838
+
839
+ interface PiExportOptions {
840
+ sendSessionAffinityHeaders?: boolean;
841
+ /**
842
+ * Tell Pi to send its system prompt as `system` rather than `developer` (#5664).
843
+ *
844
+ * Pi sends `developer` for reasoning models by default. On `/v1/chat/completions` the proxy
845
+ * forwards the caller's roles verbatim unless a destination has recorded
846
+ * `foldDeveloperRoleToSystem`, and many OpenAI-compatible upstreams reject `developer` with a
847
+ * 400. `system` is accepted by every destination behind this one provider block, so the export
848
+ * states it rather than leaving each user to hand-edit a block the next export rewrites.
849
+ */
850
+ foldDeveloperRole?: boolean;
851
+ }
852
+
819
853
  export interface PiGeneratedConfig {
820
854
  providers: Record<string, PiProviderBlock>;
821
855
  }
@@ -829,6 +863,8 @@ export interface HermesProviderBlock {
829
863
  api: string;
830
864
  api_key: string;
831
865
  api_mode: "chat_completions";
866
+ /** Header name only; Hermes supplies a dynamic per-conversation value. */
867
+ session_affinity_header: "session-id";
832
868
  /** We supply the list, so skip their live `/models` probe. */
833
869
  discover_models: false;
834
870
  models: Record<string, HermesModelEntry>;
@@ -935,7 +971,7 @@ export interface GajaeGeneratedConfig {
935
971
  * model. The rest of this contract (omitting `cost`) is still ours rather than
936
972
  * a claim about Pi's acceptance.
937
973
  */
938
- function buildPiClientConfig(ctx: ExportContext, sendSessionAffinityHeaders = false): PiGeneratedConfig {
974
+ function buildPiClientConfig(ctx: ExportContext, options: PiExportOptions = {}): PiGeneratedConfig {
939
975
  const models: PiModelEntry[] = [];
940
976
  for (const model of normalizeExportModels(ctx.models)) {
941
977
  // Text is the one modality every routed model supports; anything richer must come
@@ -972,19 +1008,30 @@ function buildPiClientConfig(ctx: ExportContext, sendSessionAffinityHeaders = fa
972
1008
  }
973
1009
  models.push(entry);
974
1010
  }
1011
+ const compat: PiProviderCompat = {
1012
+ ...(options.sendSessionAffinityHeaders ? { sendSessionAffinityHeaders: true } : {}),
1013
+ ...(options.foldDeveloperRole ? { supportsDeveloperRole: false } : {}),
1014
+ };
975
1015
  return {
976
1016
  providers: {
977
1017
  [OPENCODE_PROVIDER_ID]: {
978
1018
  baseUrl: ctx.baseUrl,
979
1019
  api: PI_API_DIALECT,
980
1020
  apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
981
- ...(sendSessionAffinityHeaders ? { compat: { sendSessionAffinityHeaders: true } } : {}),
1021
+ ...(Object.keys(compat).length > 0 ? { compat } : {}),
982
1022
  models,
983
1023
  },
984
1024
  },
985
1025
  };
986
1026
  }
987
1027
 
1028
+ /**
1029
+ * Pi's export options, shared by `ocx export --client pi` and the managed contribution so the two
1030
+ * never drift apart at the first refresh. omo uses the same options: senpi documents both keys in
1031
+ * its models.json `compat` block (docs/models.md, docs/custom-provider.md).
1032
+ */
1033
+ const PI_EXPORT_OPTIONS: PiExportOptions = { sendSessionAffinityHeaders: true, foldDeveloperRole: true };
1034
+
988
1035
  /** Do not let provider-controlled catalog text become an environment lookup. */
989
1036
  function containsEnvInterpolation(value: string): boolean {
990
1037
  return value.includes("${");
@@ -1006,6 +1053,7 @@ function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig {
1006
1053
  api: ctx.baseUrl,
1007
1054
  api_key: HERMES_API_KEY_ENV_REF,
1008
1055
  api_mode: "chat_completions",
1056
+ session_affinity_header: "session-id",
1009
1057
  discover_models: false,
1010
1058
  models,
1011
1059
  ...(headers ? { extra_headers: headers } : {}),
@@ -1162,7 +1210,7 @@ function buildOpencodeContribution(ctx: ExportContext): ManagedContribution {
1162
1210
  }
1163
1211
 
1164
1212
  function buildPiContribution(ctx: ExportContext): ManagedContribution {
1165
- const doc = buildPiClientConfig(ctx, true);
1213
+ const doc = buildPiClientConfig(ctx, PI_EXPORT_OPTIONS);
1166
1214
  return singleFragment("pi", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
1167
1215
  }
1168
1216
 
@@ -1254,7 +1302,7 @@ function buildAsideContribution(ctx: ExportContext): ManagedContribution {
1254
1302
  * the two would drift apart at the first refresh.
1255
1303
  */
1256
1304
  function buildOmoContribution(ctx: ExportContext): ManagedContribution {
1257
- const doc = buildPiClientConfig(ctx, true);
1305
+ const doc = buildPiClientConfig(ctx, PI_EXPORT_OPTIONS);
1258
1306
  return singleFragment("omo", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
1259
1307
  }
1260
1308
 
@@ -1296,7 +1344,7 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1296
1344
  destination: env => piConfigPath(env),
1297
1345
  apiKeyEnv: "",
1298
1346
  exportHint: "Pi reads a non-secret placeholder from models.json; loopback needs no key.",
1299
- build: ctx => buildPiClientConfig(ctx, true),
1347
+ build: ctx => buildPiClientConfig(ctx, PI_EXPORT_OPTIONS),
1300
1348
  format: "json",
1301
1349
  summarize: summarizePi,
1302
1350
  buildContribution: buildPiContribution,
@@ -1479,7 +1527,7 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1479
1527
  destination: env => omoConfigPath(env),
1480
1528
  apiKeyEnv: "",
1481
1529
  exportHint: "omo reads a non-secret placeholder from models.json; loopback needs no key.",
1482
- build: ctx => buildPiClientConfig(ctx, true),
1530
+ build: ctx => buildPiClientConfig(ctx, PI_EXPORT_OPTIONS),
1483
1531
  format: "json",
1484
1532
  summarize: summarizePi,
1485
1533
  buildContribution: buildOmoContribution,