@bitkyc08/opencodex 2.64.0-preview.20260923 → 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-BEq4OCOz.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-D5eN1TID.js +0 -50
  206. package/gui/dist/assets/App-I5AnaSLh.css +0 -1
  207. package/gui/dist/assets/Tray-Bh_ErQDh.js +0 -1
  208. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +0 -1
@@ -1,25 +1,26 @@
1
1
  /**
2
2
  * Gateway model-discovery aliases (devlog/260711_claude_inbound/020, 003 G1-G6).
3
3
  *
4
- * Claude Code's /model picker only lists discovery entries whose id literally
5
- * begins with `claude` or `anthropic`, so routed models are exposed as
6
- * `claude-ocx-<provider>--<model>` with an honest display_name. Aliases must be
7
- * deterministic, reversible, and STABLE across releases (picker selections
8
- * persist to Claude Code's settings.json `model` field).
4
+ * Claude Code's /model picker accepts discovery ids containing `claude` or
5
+ * `anthropic`. New routed models are exposed as `ocx-claude-<provider>--<model>`
6
+ * with an honest display_name. The id must contain `claude` so the picker keeps
7
+ * it, but must not START with `claude-`: Claude Code 2.1.278 treats an
8
+ * unrecognized `claude-` id as its own model and ignores
9
+ * CLAUDE_CODE_MAX_CONTEXT_TOKENS unless DISABLE_COMPACT=1. Starting with
10
+ * `ocx-claude-` keeps the real window and leaves compact enabled.
9
11
  *
10
- * Versioned prefixes:
11
- * - `claude-ocx-` (v1) — legacy / plain model ids with no `/` or `~`. Decode
12
- * is literal (no escape expansion), so a persisted model id that literally
13
- * contained the two-char sequences `~s` / `~t` keeps resolving.
14
- * - `claude-ocx2-` (v2) — used whenever the model id needs escape encoding
15
- * (`/` → `~s`, `~` → `~t`). Decode expands those escapes. New slash/tilde
16
- * models always mint v2 so they cannot collide with v1 literals.
12
+ * Persisted ids stay decodable:
13
+ * - `ocx-claude-` — current plain ids. Decode is literal.
14
+ * - `claude-ocx-` (v1) — legacy plain ids. Decode stays literal, so a persisted
15
+ * model id that contained the two-char sequences `~s` / `~t` keeps resolving.
16
+ * - `claude-ocx2-` (v2) — escape encoding (`/` → `~s`, `~` → `~t`). Decode
17
+ * expands those escapes. New slash/tilde models still mint v2.
17
18
  *
18
19
  * Reversibility rules:
19
20
  * - providers containing `--` or `/` are not aliased (split boundary safety);
20
21
  * - model ids MAY contain `/` or `~` — minted under the v2 prefix with escapes
21
22
  * (e.g. openrouter `anthropic/claude-opus-4-8` →
22
- * `claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`);
23
+ * `ocx-claude2-openrouter--anthropic~sclaude-opus-4-8`);
23
24
  * - model ids MAY contain `--` (resolve splits on the FIRST `--` only);
24
25
  * - native OpenAI slugs use the pseudo-provider `native` and resolve back to
25
26
  * the bare slug; a real provider named "native" is therefore never aliased.
@@ -27,15 +28,19 @@
27
28
 
28
29
  import { desktop3pAlias } from "./desktop-3p";
29
30
 
30
- /** Legacy / plain readable prefix (literal model portion on decode). */
31
+ /** Current plain prefix. Contains "claude" but does not start with "claude-". */
32
+ export const CLAUDE_ALIAS_PREFIX_CURRENT = "ocx-claude-";
33
+ /** Current escape-encoded prefix (`~s`/`~t` expanded on decode). */
34
+ export const CLAUDE_ALIAS_PREFIX_CURRENT_V2 = "ocx-claude2-";
35
+ /** Legacy plain prefix. Still decoded; no longer minted. */
31
36
  export const CLAUDE_ALIAS_PREFIX_V1 = "claude-ocx-";
32
- /** Escape-encoded readable prefix (`~s`/`~t` expanded on decode). */
37
+ /** Legacy escape-encoded prefix. Still decoded; no longer minted. */
33
38
  export const CLAUDE_ALIAS_PREFIX_V2 = "claude-ocx2-";
34
39
  /**
35
40
  * Current write prefix for plain (unescaped) model ids.
36
- * Escape-needing models mint {@link CLAUDE_ALIAS_PREFIX_V2} instead.
41
+ * Escape-needing models mint {@link CLAUDE_ALIAS_PREFIX_CURRENT_V2} instead.
37
42
  */
38
- export const CLAUDE_ALIAS_PREFIX = CLAUDE_ALIAS_PREFIX_V1;
43
+ export const CLAUDE_ALIAS_PREFIX = CLAUDE_ALIAS_PREFIX_CURRENT;
39
44
 
40
45
  /** Encoded `/` inside the model portion of a v2 Claude Code alias. */
41
46
  const CLAUDE_ALIAS_SLASH_ENC = "~s";
@@ -90,9 +95,42 @@ export function aliasForRoute(provider: string, modelId: string): string | null
90
95
  if (!provider || provider.includes("--") || provider.includes("/") || provider === NATIVE_PSEUDO_PROVIDER) return null;
91
96
  if (!modelId) return null;
92
97
  if (modelNeedsEscapeEncoding(modelId)) {
93
- return `${CLAUDE_ALIAS_PREFIX_V2}${provider}--${encodeModelId(modelId)}`;
98
+ return `${CLAUDE_ALIAS_PREFIX_CURRENT_V2}${provider}--${encodeModelId(modelId)}`;
94
99
  }
95
- return `${CLAUDE_ALIAS_PREFIX_V1}${provider}--${modelId}`;
100
+ return `${CLAUDE_ALIAS_PREFIX_CURRENT}${provider}--${modelId}`;
101
+ }
102
+
103
+ /**
104
+ * The spelling a release before `ocx-claude-` minted for the same route. Nothing
105
+ * is minted with it any more; it exists so a selector already saved in Claude
106
+ * Code's settings.json keeps its context-window lookup (context-windows.ts).
107
+ */
108
+ function toLegacyAlias(alias: string | null): string | null {
109
+ if (!alias) return null;
110
+ if (alias.startsWith(CLAUDE_ALIAS_PREFIX_CURRENT_V2)) return CLAUDE_ALIAS_PREFIX_V2 + alias.slice(CLAUDE_ALIAS_PREFIX_CURRENT_V2.length);
111
+ if (alias.startsWith(CLAUDE_ALIAS_PREFIX_CURRENT)) return CLAUDE_ALIAS_PREFIX_V1 + alias.slice(CLAUDE_ALIAS_PREFIX_CURRENT.length);
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * The current spelling of a selector that may still use a legacy prefix. Legacy and
117
+ * current prefixes decode to the same route (plain stays literal, v2 expands escapes), so
118
+ * swapping the prefix is safe. Only the prefix changes: a trailing `[1m]` marker survives.
119
+ */
120
+ export function currentClaudeAliasSpelling(selector: string): string {
121
+ if (selector.startsWith(CLAUDE_ALIAS_PREFIX_V2)) return CLAUDE_ALIAS_PREFIX_CURRENT_V2 + selector.slice(CLAUDE_ALIAS_PREFIX_V2.length);
122
+ if (selector.startsWith(CLAUDE_ALIAS_PREFIX_V1)) return CLAUDE_ALIAS_PREFIX_CURRENT + selector.slice(CLAUDE_ALIAS_PREFIX_V1.length);
123
+ return selector;
124
+ }
125
+
126
+ /** Legacy `claude-ocx-`/`claude-ocx2-` spelling of {@link aliasForRoute}. */
127
+ export function legacyAliasForRoute(provider: string, modelId: string): string | null {
128
+ return toLegacyAlias(aliasForRoute(provider, modelId));
129
+ }
130
+
131
+ /** Legacy `claude-ocx-`/`claude-ocx2-` spelling of {@link aliasForNative}. */
132
+ export function legacyAliasForNative(slug: string): string | null {
133
+ return toLegacyAlias(aliasForNative(slug));
96
134
  }
97
135
 
98
136
  /** Alias for a native OpenAI slug (bare model id, no provider namespace). */
@@ -100,9 +138,9 @@ export function aliasForNative(slug: string): string | null {
100
138
  // Reject "/" — native ids are bare slugs. Literal `~` is fine via v2 + ~t.
101
139
  if (!slug || slug.includes("/") || slug.includes("--")) return null;
102
140
  if (modelNeedsEscapeEncoding(slug)) {
103
- return `${CLAUDE_ALIAS_PREFIX_V2}${NATIVE_PSEUDO_PROVIDER}--${encodeModelId(slug)}`;
141
+ return `${CLAUDE_ALIAS_PREFIX_CURRENT_V2}${NATIVE_PSEUDO_PROVIDER}--${encodeModelId(slug)}`;
104
142
  }
105
- return `${CLAUDE_ALIAS_PREFIX_V1}${NATIVE_PSEUDO_PROVIDER}--${slug}`;
143
+ return `${CLAUDE_ALIAS_PREFIX_CURRENT}${NATIVE_PSEUDO_PROVIDER}--${slug}`;
106
144
  }
107
145
 
108
146
  /**
@@ -110,7 +148,15 @@ export function aliasForNative(slug: string): string | null {
110
148
  * routed -> "<provider>/<model>", native -> bare slug. Null when not an alias.
111
149
  */
112
150
  export function resolveAlias(id: string): string | null {
113
- // Check v2 before v1 for clarity (prefixes are disjoint: ocx2 vs ocx-).
151
+ // Current v2 before legacy v2, then plain prefixes. `ocx-claude2-` and
152
+ // `claude-ocx2-` are disjoint from their plain siblings.
153
+ if (id.startsWith(CLAUDE_ALIAS_PREFIX_CURRENT_V2)) {
154
+ const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_CURRENT_V2);
155
+ if (!parts) return null;
156
+ const model = decodeEscapedModelId(parts.model);
157
+ if (!model) return null;
158
+ return parts.provider === NATIVE_PSEUDO_PROVIDER ? model : `${parts.provider}/${model}`;
159
+ }
114
160
  if (id.startsWith(CLAUDE_ALIAS_PREFIX_V2)) {
115
161
  const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_V2);
116
162
  if (!parts) return null;
@@ -118,8 +164,9 @@ export function resolveAlias(id: string): string | null {
118
164
  if (!model) return null;
119
165
  return parts.provider === NATIVE_PSEUDO_PROVIDER ? model : `${parts.provider}/${model}`;
120
166
  }
121
- if (id.startsWith(CLAUDE_ALIAS_PREFIX_V1)) {
122
- const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_V1);
167
+ for (const prefix of [CLAUDE_ALIAS_PREFIX_CURRENT, CLAUDE_ALIAS_PREFIX_V1]) {
168
+ if (!id.startsWith(prefix)) continue;
169
+ const parts = splitAlias(id, prefix);
123
170
  if (!parts) return null;
124
171
  // Literal decode — preserves pre-escape aliases whose model id contained
125
172
  // the two-char sequences ~s / ~t.
@@ -131,11 +178,11 @@ export function resolveAlias(id: string): string | null {
131
178
  /**
132
179
  * Claude Code (CLI) surface alias — devlog 050 + audit 051 #2.
133
180
  *
134
- * The readable `claude-ocx*` form when representable; otherwise the desktop-3p
181
+ * The readable `ocx-claude*` form when representable; otherwise the desktop-3p
135
182
  * hash so the model still appears in discovery (collisions follow the same
136
183
  * first-wins policy as the desktop registry — audit 051 #1). Real Anthropic
137
184
  * models pass through unchanged (they must keep hitting the sk-ant passthrough).
138
- * Both families keep decoding forever in resolveInboundModel, so ids persisted
185
+ * Old `claude-ocx*` ids keep decoding forever in resolveInboundModel, so ids persisted
139
186
  * in Claude Code's settings.json never break when the surface style changes.
140
187
  */
141
188
  export function claudeCodeAlias(provider: string, modelId: string): string {
@@ -3,12 +3,13 @@
3
3
  * (devlog/260712_cli_context_cache/010 B2, audit R2#1/R3#1/R3#4/R4#3).
4
4
  *
5
5
  * The map registers EVERY selector form a Claude Code model slot might store —
6
- * bare native slug, provider/id, desktop3p alias, legacy claude-ocx-* alias —
6
+ * bare native slug, provider/id, desktop3p alias, current ocx-claude-* alias and
7
+ * the legacy claude-ocx-* spelling a saved selector may still carry —
7
8
  * with first-wins dedupe (mirrors the desktop3p registry collision policy).
8
9
  * Values are authoritative context windows only (native override table /
9
10
  * adapter-reported CatalogModel.contextWindow); nothing is guessed.
10
11
  */
11
- import { aliasForNative, aliasForRoute } from "./alias";
12
+ import { aliasForNative, aliasForRoute, currentClaudeAliasSpelling, legacyAliasForNative, legacyAliasForRoute } from "./alias";
12
13
  import { desktop3pAlias } from "./desktop-3p";
13
14
  import { nativeOpenAiContextWindow, type CatalogModel, type NativeContextLimitsInput } from "../codex/catalog";
14
15
 
@@ -62,9 +63,12 @@ function inAutoCompactRange(value: number): boolean {
62
63
 
63
64
  /**
64
65
  * Resolve the auto-context mode from claudeCode config. Disabled when the user
65
- * turned it off OR when the legacy maxContextTokens override is set — that pair
66
- * (MAX_CONTEXT_TOKENS + DISABLE_COMPACT) takes rule-1 precedence inside the CLI,
67
- * making both AUTO_COMPACT_WINDOW and [1m] accounting inert.
66
+ * turned it off OR when maxContextTokens is set. That override injects only
67
+ * CLAUDE_CODE_MAX_CONTEXT_TOKENS: compact stays enabled (no DISABLE_COMPACT) and
68
+ * Claude Code compacts against that window for ocx-claude-* ids, so no
69
+ * CLAUDE_CODE_AUTO_COMPACT_WINDOW is injected beside it and [1m] auto-marking stays
70
+ * off. maxContextTokens accepts values outside the compact variable's 100k–1M
71
+ * range, so deriving that variable from it could only produce ignored values.
68
72
  *
69
73
  * `envOverride` is the raw CLAUDE_CODE_AUTO_COMPACT_WINDOW the USER already
70
74
  * exported (user-wins injection keeps it): a valid value drives the marking
@@ -139,6 +143,7 @@ export function buildClaudeContextWindows(
139
143
  put(slug, window);
140
144
  put(desktop3pAlias("native", slug), window);
141
145
  put(aliasForNative(slug), window);
146
+ put(legacyAliasForNative(slug), window);
142
147
  }
143
148
  // Anthropic passthrough guard (audit 021 #3): canonical claude ids ride the
144
149
  // subscription passthrough — marking a sub-1M one would strap [1m]/1M-beta onto
@@ -161,6 +166,7 @@ export function buildClaudeContextWindows(
161
166
  put(`${m.provider}/${m.id}`, window);
162
167
  put(desktop3pAlias(m.provider, m.id), window);
163
168
  put(aliasForRoute(m.provider, m.id), window);
169
+ put(legacyAliasForRoute(m.provider, m.id), window);
164
170
  if (bareCounts.get(m.id) === 1) put(m.id, window);
165
171
  }
166
172
  return out;
@@ -204,8 +210,11 @@ export function effectiveModelEnv(
204
210
  ): Record<string, string> {
205
211
  const out: Record<string, string> = {};
206
212
  const auto = autoOverride ?? resolveAutoContext(claudeCode);
213
+ // A slot still configured with a legacy claude-ocx selector is emitted in its current
214
+ // ocx-claude spelling. The route is identical, but Claude Code applies the context window
215
+ // (and keeps compact) only for ids that do not start with "claude-".
207
216
  const set = (name: string, value: string | undefined) => {
208
- const marked = withOneMillionMarker(value, windows, auto);
217
+ const marked = withOneMillionMarker(value === undefined ? undefined : currentClaudeAliasSpelling(value), windows, auto);
209
218
  if (marked) out[name] = marked;
210
219
  };
211
220
  set("ANTHROPIC_MODEL", claudeCode?.model);
@@ -42,6 +42,8 @@ export interface Desktop3pMetadata {
42
42
  [key: string]: unknown;
43
43
  }
44
44
 
45
+ export const DESKTOP_PICKER_ENTRY_NAME = "opencodex-picker";
46
+
45
47
  export function parseMetadata(path: string): Desktop3pMetadata {
46
48
  if (!existsSync(path)) return { entries: [] };
47
49
  const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<Desktop3pMetadata>;
@@ -56,7 +58,7 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
56
58
  }
57
59
 
58
60
  export function isOwnedDesktopEntry(entry: Desktop3pMetadataEntry | undefined): boolean {
59
- return entry?.name === "opencodex" || entry?.name === "opencodex-standard";
61
+ return entry?.name === "opencodex" || entry?.name === "opencodex-standard" || entry?.name === DESKTOP_PICKER_ENTRY_NAME;
60
62
  }
61
63
 
62
64
  /** A gateway row is removable; the selected standard row must always remain. */
@@ -86,4 +88,3 @@ export function readDesktopProfileForeignKeys(path: string): Record<string, unkn
86
88
  Object.entries(parsed).filter(([key]) => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(key)),
87
89
  );
88
90
  }
89
-
@@ -146,7 +146,7 @@ export function legacyDesktop3pAlias(provider: string, modelId: string): string
146
146
  return `claude-opus-4-${deriveDesktop3pCode(`${provider}/${modelId}`)}`;
147
147
  }
148
148
 
149
- function displayModelId(modelId: string): string {
149
+ export function displayModelId(modelId: string): string {
150
150
  return modelId
151
151
  // Capability markers like [1m] are not name text: strip the brackets so the label
152
152
  // reads "K3 1M", never "K3[1m]".
@@ -3,21 +3,25 @@
3
3
  *
4
4
  * Desktop has two ways to reach opencodex:
5
5
  *
6
- * - `first-party` (default): the app keeps its ordinary claude.ai login, Chat tab, connectors
7
- * and remote control. Only the Claude Code process it spawns for the Code tab (and that
8
- * process's subagents) is redirected, through the `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` env
9
- * in `~/.claude/settings.json` (src/claude/intercept/settings.ts) and the server's intercept
10
- * pair (src/claude/intercept/runtime.ts). Nothing is written under Desktop's config library.
11
- * - `gateway`: the historical third-party deployment profile (src/claude/desktop-3p.ts). The
6
+ * - `gateway` (default): the third-party deployment profile (src/claude/desktop-3p.ts). The
12
7
  * whole app is switched to a gateway build; picker entries are opencodex aliases.
8
+ * - `first-party`: the app keeps its ordinary claude.ai login, Chat tab, connectors and remote
9
+ * control. Only the Claude Code process it spawns for the Code tab (and that process's
10
+ * subagents) is redirected, through the `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` env in
11
+ * `~/.claude/settings.json` (src/claude/intercept/settings.ts) and the server's intercept pair
12
+ * (src/claude/intercept/runtime.ts). It sends Claude subscription traffic through a local
13
+ * interception proxy, so every first-party surface carries the account-risk notice in
14
+ * src/claude/desktop-risk.ts.
13
15
  *
14
16
  * The two are mutually exclusive on disk: applying one removes the other. The mode is persisted
15
- * in `claudeCode.desktopMode`; installs that predate the field but already carry an applied
16
- * gateway profile keep `gateway` until they explicitly re-apply, so an update never flips a
17
- * working Desktop under the operator.
17
+ * in `claudeCode.desktopMode`. Installs that predate the field keep what they run: a selected
18
+ * gateway row or a gateway apply marker keeps `gateway`, and first-party env that opencodex wrote
19
+ * into Claude Code's settings keeps `first-party` (observeClaudeDesktopMode), so moving the default
20
+ * to gateway never flips a working Desktop under the operator.
18
21
  */
19
22
  import { getConfigDir } from "../config/paths";
20
23
  import type { OcxConfig } from "../types";
24
+ import { inspectDesktop3pConfigLibrary } from "./desktop-3p";
21
25
  import { claudeInterceptCaCertPath, ensureLocalInterceptCa } from "./intercept/local-ca";
22
26
  import { claudeInterceptEnabled, claudeInterceptProxyPort } from "./intercept/runtime";
23
27
  import {
@@ -33,7 +37,7 @@ import {
33
37
 
34
38
  export const CLAUDE_DESKTOP_MODES = ["first-party", "gateway"] as const;
35
39
  export type ClaudeDesktopMode = typeof CLAUDE_DESKTOP_MODES[number];
36
- export const DEFAULT_CLAUDE_DESKTOP_MODE: ClaudeDesktopMode = "first-party";
40
+ export const DEFAULT_CLAUDE_DESKTOP_MODE: ClaudeDesktopMode = "gateway";
37
41
 
38
42
  export function isClaudeDesktopMode(value: unknown): value is ClaudeDesktopMode {
39
43
  return typeof value === "string" && (CLAUDE_DESKTOP_MODES as readonly string[]).includes(value);
@@ -41,15 +45,30 @@ export function isClaudeDesktopMode(value: unknown): value is ClaudeDesktopMode
41
45
 
42
46
  type DesktopModeConfig = Pick<OcxConfig, "claudeCode">;
43
47
 
48
+ /** What the resolver may learn from disk. Only rows and settings opencodex owns count. */
49
+ export interface ClaudeDesktopModeObservation {
50
+ /** Desktop's selected config-library row is our gateway (current or drifted). */
51
+ ownedGatewaySelected?: boolean;
52
+ /** Claude Code's settings carry first-party env that opencodex wrote (applied or stale). */
53
+ ownedFirstPartySettings?: boolean;
54
+ }
55
+
44
56
  /**
45
- * Effective Desktop mode. An explicit `claudeCode.desktopMode` wins; otherwise a persisted
46
- * gateway apply marker (`desktopProfile.appliedFingerprint`) means a pre-existing gateway
47
- * install and keeps `gateway`; everything else is the first-party default.
57
+ * Effective Desktop mode. An explicit `claudeCode.desktopMode` wins. Without one, what is on disk
58
+ * decides for installs that predate the field: a selected owned gateway row or a persisted gateway
59
+ * apply marker keeps `gateway`, owned first-party settings keep `first-party`. Everything else is
60
+ * the gateway default. Pure: callers that decide an apply or a write pass
61
+ * `observeClaudeDesktopMode(config)`.
48
62
  */
49
- export function resolveClaudeDesktopMode(config: DesktopModeConfig): ClaudeDesktopMode {
63
+ export function resolveClaudeDesktopMode(
64
+ config: DesktopModeConfig,
65
+ observed: ClaudeDesktopModeObservation = {},
66
+ ): ClaudeDesktopMode {
50
67
  const explicit = config.claudeCode?.desktopMode;
51
68
  if (isClaudeDesktopMode(explicit)) return explicit;
69
+ if (observed.ownedGatewaySelected) return "gateway";
52
70
  if (config.claudeCode?.desktopProfile?.appliedFingerprint) return "gateway";
71
+ if (observed.ownedFirstPartySettings) return "first-party";
53
72
  return DEFAULT_CLAUDE_DESKTOP_MODE;
54
73
  }
55
74
 
@@ -78,18 +97,16 @@ export function recordClaudeDesktopMode(
78
97
  }
79
98
 
80
99
  /**
81
- * Mode an *apply* without an explicit choice should use. The first-party default only holds
82
- * where the intercept proxy actually runs; with it disabled (or on a client role) an implied
83
- * first-party apply would point Claude Code at a proxy that never starts, so fall back to the
84
- * gateway profile. An explicit `desktopMode: "first-party"` is still honoured (and refused
85
- * later with `intercept_disabled`, which names the fix).
100
+ * Mode an *apply* without an explicit choice should use. With gateway as the default, first-party
101
+ * only comes from an explicit choice or an observed first-party install; both are honoured even
102
+ * when the intercept is disabled, and the apply is then refused with `intercept_disabled`, which
103
+ * names the fix, instead of silently replacing the operator's mode.
86
104
  */
87
105
  export function resolveClaudeDesktopApplyMode(
88
106
  config: Pick<OcxConfig, "claudeCode" | "runtimeRole">,
107
+ observed: ClaudeDesktopModeObservation = {},
89
108
  ): ClaudeDesktopMode {
90
- const resolved = resolveClaudeDesktopMode(config);
91
- if (resolved === "gateway" || isClaudeDesktopMode(config.claudeCode?.desktopMode)) return resolved;
92
- return claudeInterceptEnabled(config) ? "first-party" : "gateway";
109
+ return resolveClaudeDesktopMode(config, observed);
93
110
  }
94
111
 
95
112
  export interface DesktopFirstPartyTarget {
@@ -151,6 +168,30 @@ export function inspectDesktopFirstParty(
151
168
  };
152
169
  }
153
170
 
171
+ /**
172
+ * Observe what Desktop runs today for the resolver. Never throws: an unreadable library or
173
+ * settings file is no evidence, and a foreign proxy env is never mistaken for ours.
174
+ */
175
+ export function observeClaudeDesktopMode(
176
+ config: Pick<OcxConfig, "claudeCode" | "port" | "runtimeRole">,
177
+ options: DesktopFirstPartyOptions = {},
178
+ ): ClaudeDesktopModeObservation {
179
+ const observed: ClaudeDesktopModeObservation = {};
180
+ try {
181
+ const library = inspectDesktop3pConfigLibrary({ appliedFingerprint: config.claudeCode?.desktopProfile?.appliedFingerprint ?? null });
182
+ observed.ownedGatewaySelected = library.kind === "gateway_ours" || library.kind === "gateway_drifted";
183
+ } catch { // no-excuse-ok: catch -- an unreadable library is no gateway evidence.
184
+ observed.ownedGatewaySelected = false;
185
+ }
186
+ try {
187
+ const kind = inspectDesktopFirstParty(config, options).settings.kind;
188
+ observed.ownedFirstPartySettings = kind === "applied" || kind === "stale";
189
+ } catch { // no-excuse-ok: catch -- unreadable settings are no first-party evidence.
190
+ observed.ownedFirstPartySettings = false;
191
+ }
192
+ return observed;
193
+ }
194
+
154
195
  export type DesktopFirstPartyApplyResult =
155
196
  | { ok: true; changed: boolean; path: string; env: ClaudeInterceptEnv; proxyPort: number }
156
197
  | { ok: false; reason: "intercept_disabled" | "ca_unavailable" | "unreadable" | "foreign_env"; path: string };