@yansigit/opencodex 2.31.3 → 2.33.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 (176) hide show
  1. package/README.md +2 -2
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  4. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/adapters/anthropic.ts +25 -13
  8. package/src/adapters/azure.ts +20 -4
  9. package/src/adapters/base.ts +5 -1
  10. package/src/adapters/command-code.ts +42 -10
  11. package/src/adapters/cursor/live-models.ts +8 -0
  12. package/src/adapters/cursor/live-transport.ts +1 -1
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/protobuf-events.ts +158 -7
  15. package/src/adapters/cursor/protobuf-request.ts +33 -15
  16. package/src/adapters/cursor/request-builder.ts +4 -3
  17. package/src/adapters/cursor/tool-definitions.ts +27 -1
  18. package/src/adapters/cursor/types.ts +4 -3
  19. package/src/adapters/cursor.ts +9 -0
  20. package/src/adapters/google-antigravity-replay.ts +2 -2
  21. package/src/adapters/google-antigravity-wire.ts +7 -0
  22. package/src/adapters/google-errors.ts +6 -2
  23. package/src/adapters/google-http.ts +30 -7
  24. package/src/adapters/google-truncation.ts +5 -0
  25. package/src/adapters/google-wire-compiler.ts +38 -6
  26. package/src/adapters/google.ts +154 -33
  27. package/src/adapters/kiro-tools.ts +20 -9
  28. package/src/adapters/kiro.ts +0 -3
  29. package/src/adapters/openai-chat.ts +9 -0
  30. package/src/adapters/openai-responses.ts +4 -1
  31. package/src/adapters/tool-catalog-nudge.ts +1 -1
  32. package/src/adapters/xai-web-search.ts +7 -2
  33. package/src/bridge.ts +133 -24
  34. package/src/claude/context-windows.ts +16 -9
  35. package/src/cli/dispatch.ts +50 -2
  36. package/src/cli/doctor.ts +26 -13
  37. package/src/cli/help.ts +4 -3
  38. package/src/cli/index.ts +20 -6
  39. package/src/cli/models.ts +13 -3
  40. package/src/cli/observe.ts +20 -5
  41. package/src/cli/provider.ts +8 -1
  42. package/src/cli/registry.ts +7 -5
  43. package/src/cli/status.ts +25 -1
  44. package/src/cli/system-restart-client.ts +1 -1
  45. package/src/cli/usage-report.ts +134 -0
  46. package/src/codex/app-server-processes.ts +3 -1
  47. package/src/codex/auth-api.ts +4 -2
  48. package/src/codex/autostart-health.ts +16 -0
  49. package/src/codex/catalog/aggregation.ts +12 -0
  50. package/src/codex/catalog/effort.ts +42 -12
  51. package/src/codex/catalog/metadata.ts +27 -1
  52. package/src/codex/catalog/model-metadata.ts +593 -0
  53. package/src/codex/catalog/parsing.ts +71 -27
  54. package/src/codex/catalog/provider-fetch.ts +189 -33
  55. package/src/codex/catalog/sync.ts +6 -5
  56. package/src/codex/convergence.ts +5 -0
  57. package/src/codex/desktop-app-restart.ts +342 -0
  58. package/src/codex/history-job.ts +32 -3
  59. package/src/codex/history-manifest.ts +112 -0
  60. package/src/codex/history-migration-guardian.ts +5 -5
  61. package/src/codex/history-provider.ts +825 -247
  62. package/src/codex/history-worker.ts +8 -5
  63. package/src/codex/inject.ts +49 -21
  64. package/src/codex/injected-marker.ts +1 -1
  65. package/src/codex/internal/history-writer.ts +4 -3
  66. package/src/codex/native-profile-startup.ts +157 -27
  67. package/src/codex/native-residue.ts +26 -33
  68. package/src/codex/shim.ts +56 -3
  69. package/src/combos/failover.ts +27 -0
  70. package/src/compatibility/index.ts +26 -0
  71. package/src/compatibility/manifest.ts +253 -0
  72. package/src/compatibility/openai-responses.ts +81 -0
  73. package/src/config/atomic-write.ts +219 -0
  74. package/src/config/paths.ts +40 -0
  75. package/src/config/process-state.ts +308 -0
  76. package/src/config/provider-validation.ts +214 -0
  77. package/src/config.ts +153 -814
  78. package/src/generated/compatibility-version.json +232 -148
  79. package/src/images/loop.ts +37 -6
  80. package/src/images/plan.ts +5 -4
  81. package/src/integrations/ownership-policy.ts +141 -0
  82. package/src/integrations/ownership.ts +10 -0
  83. package/src/integrations/state.ts +44 -5
  84. package/src/integrations/writer.ts +6 -0
  85. package/src/lib/azure-identity.ts +154 -0
  86. package/src/lib/bounded-body.ts +14 -2
  87. package/src/lib/debug.ts +42 -0
  88. package/src/lib/errors.ts +14 -0
  89. package/src/lib/process-control.ts +2 -1
  90. package/src/lib/provider-outbound.ts +45 -33
  91. package/src/lib/provider-tls-profile.ts +309 -0
  92. package/src/lib/proxy-env.ts +49 -0
  93. package/src/lib/redact.ts +10 -1
  94. package/src/lib/state-store-registrations.ts +2 -0
  95. package/src/lib/tool-argument-integers.ts +56 -5
  96. package/src/oauth/antigravity-routing.ts +282 -236
  97. package/src/oauth/callback-server.ts +22 -2
  98. package/src/oauth/command-code.ts +5 -16
  99. package/src/oauth/google-antigravity.ts +42 -5
  100. package/src/oauth/health.ts +1 -1
  101. package/src/oauth/index.ts +15 -3
  102. package/src/oauth/kimi.ts +9 -1
  103. package/src/oauth/open-browser-choice.ts +26 -0
  104. package/src/oauth/store.ts +6 -0
  105. package/src/providers/antigravity-quota.ts +3 -1
  106. package/src/providers/api-keys.ts +2 -1
  107. package/src/providers/auto-compact-budget.ts +65 -0
  108. package/src/providers/derive.ts +4 -0
  109. package/src/providers/key-failover.ts +5 -1
  110. package/src/providers/openai-tiers.ts +5 -0
  111. package/src/providers/provider-id-rewrite.ts +1 -0
  112. package/src/providers/quota.ts +59 -13
  113. package/src/providers/registry.ts +4 -2
  114. package/src/providers/request-pacing.ts +33 -6
  115. package/src/providers/xai-transport.ts +21 -0
  116. package/src/reasoning-effort.ts +19 -2
  117. package/src/responses/apply-patch-envelope.ts +63 -0
  118. package/src/responses/custom-tool-compat.ts +132 -38
  119. package/src/responses/google-provider-options.ts +36 -0
  120. package/src/responses/namespace-tool-compat.ts +84 -4
  121. package/src/responses/parser.ts +14 -2
  122. package/src/responses/provider-opaque-metadata.ts +3 -3
  123. package/src/responses/reasoning-replay-cache.ts +81 -3
  124. package/src/responses/schema.ts +37 -0
  125. package/src/responses/state.ts +94 -4
  126. package/src/router.ts +8 -2
  127. package/src/server/auth-cors.ts +37 -7
  128. package/src/server/images.ts +19 -35
  129. package/src/server/index.ts +102 -21
  130. package/src/server/local-management-read-client.ts +1 -1
  131. package/src/server/local-provider-reload-client.ts +1 -1
  132. package/src/server/management/agent-settings-routes.ts +206 -16
  133. package/src/server/management/combo-routes.ts +6 -0
  134. package/src/server/management/config-routes.ts +35 -6
  135. package/src/server/management/context.ts +1 -1
  136. package/src/server/management/logs-usage-routes.ts +27 -6
  137. package/src/server/management/model-routes.ts +8 -4
  138. package/src/server/management/model-rows.ts +4 -0
  139. package/src/server/management/native-integration-routes.ts +2 -1
  140. package/src/server/management/oauth-account-routes.ts +25 -4
  141. package/src/server/management/provider-capability-config.ts +1 -1
  142. package/src/server/management/provider-routes.ts +113 -15
  143. package/src/server/management/routing-profile-routes.ts +3 -0
  144. package/src/server/management/system-restart.ts +1 -1
  145. package/src/server/port-reclaim.ts +1 -1
  146. package/src/server/proxy-liveness.ts +2 -1
  147. package/src/server/request-log-conversation.ts +30 -0
  148. package/src/server/request-log.ts +21 -0
  149. package/src/server/responses/agent-task-recovery.ts +1 -1
  150. package/src/server/responses/codex-auth-error.ts +55 -0
  151. package/src/server/responses/combo-stream-preflight.ts +171 -0
  152. package/src/server/responses/compact.ts +36 -22
  153. package/src/server/responses/core.ts +584 -247
  154. package/src/server/responses/empty-completion-guard.ts +35 -6
  155. package/src/server/responses/fetch-helpers.ts +20 -102
  156. package/src/server/responses/v2-native-parent-override.ts +59 -0
  157. package/src/server/responses/ws-upstream.ts +75 -2
  158. package/src/server/responses-custom-tool-repair.ts +41 -5
  159. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  160. package/src/service.ts +9 -5
  161. package/src/types/config.ts +16 -1
  162. package/src/types/provider.ts +16 -0
  163. package/src/types/request.ts +34 -1
  164. package/src/types/tools.ts +114 -11
  165. package/src/types.ts +7 -1
  166. package/src/update/index.ts +5 -4
  167. package/src/update/job.ts +3 -1
  168. package/src/update/transactional-install.mjs +8 -1
  169. package/src/usage/log.ts +16 -8
  170. package/src/usage/summary.ts +201 -8
  171. package/src/vision/describe.ts +18 -13
  172. package/src/web-search/executor.ts +10 -3
  173. package/src/web-search/gemini-executor.ts +6 -4
  174. package/src/web-search/loop.ts +42 -6
  175. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  176. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -1,12 +1,63 @@
1
- import { namespacedToolName } from "../types";
1
+ import { namespacedToolName, normalizeDeclaredToolName } from "../types";
2
2
  import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
3
3
 
4
+ /** Item types the client executes through a request-declared wire name. */
5
+ const CLIENT_EXECUTED_CALL_TYPES = new Set(["function_call", "custom_tool_call"]);
6
+
4
7
  /**
5
- * Item types the CLIENT executes by name. Hosted calls (`web_search_call`,
6
- * `image_generation_call`, `local_shell_call`, `tool_search_call`, …) are run upstream or
7
- * carry no tool name, so they are never matched against the request catalog.
8
+ * Hosted declarations whose response items the PROVIDER executes, keyed by the request
9
+ * declaration type. These need no client answer, so their names are deliberately absent from
10
+ * the request catalog and must not be read as an undeclared client tool.
11
+ *
12
+ * xAI surfaces hosted `x_search` as `custom_tool_call`. Probed 2026-08-23 against the OAuth CLI
13
+ * destination: its hosted calls use an `xs_call-` call-id prefix. Observed call names were
14
+ * `x_keyword_search`, `x_semantic_search`, and `x_user_search` — three literals for one tool,
15
+ * which is why authorization keys on the declaration, item type, and call-id prefix, never on
16
+ * the name.
8
17
  */
9
- const CLIENT_EXECUTED_CALL_TYPES = new Set(["function_call", "custom_tool_call"]);
18
+ export type ProviderExecutedCallType = Readonly<{
19
+ itemType: string;
20
+ callIdPrefix: string;
21
+ }>;
22
+
23
+ type ProviderExecutedCallTypes = ReadonlySet<ProviderExecutedCallType>;
24
+
25
+ export const PROVIDER_EXECUTED_DECLARATION_CALL_TYPES = new Map<string, ProviderExecutedCallType>([
26
+ ["x_search", { itemType: "custom_tool_call", callIdPrefix: "xs_call-" }],
27
+ ]);
28
+
29
+ /** Nameless declaration kinds whose response items still require client execution. */
30
+ const NAMELESS_CLIENT_DECLARATION_CALL_TYPES = new Map([
31
+ ["local_shell", "local_shell_call"],
32
+ ["tool_search", "tool_search_call"],
33
+ ["computer_use_preview", "computer_call"],
34
+ ["computer_use", "computer_call"],
35
+ ]);
36
+
37
+ const NAMELESS_CLIENT_CALL_DISPLAY_NAMES = new Map([
38
+ ["local_shell_call", "local_shell"],
39
+ ["tool_search_call", "tool_search"],
40
+ ["computer_call", "computer_use"],
41
+ ]);
42
+
43
+ const EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES: ReadonlySet<string> = new Set();
44
+ const EMPTY_PROVIDER_EXECUTED_CALL_TYPES: ReadonlySet<ProviderExecutedCallType> = new Set();
45
+
46
+ /** Supported hosted/private declarations that carry no client-executable wire name. */
47
+ const NAMELESS_TOOL_SPEC_TYPES = new Set([
48
+ "web_search",
49
+ "web_search_preview",
50
+ "file_search",
51
+ "computer_use_preview",
52
+ "computer_use",
53
+ "code_interpreter",
54
+ "image_generation",
55
+ "image_gen",
56
+ "mcp",
57
+ "tool_search",
58
+ "local_shell",
59
+ "x_search",
60
+ ]);
10
61
 
11
62
  /** An upstream-supplied name reaches the error message; keep it bounded. */
12
63
  const MAX_REPORTED_NAME_CHARS = 100;
@@ -18,12 +69,40 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
18
69
  }
19
70
 
20
71
  function addWireToolName(names: Set<string>, tool: unknown, namespace?: string): void {
21
- if (!isPlainObject(tool) || typeof tool.name !== "string" || tool.name.length === 0) return;
22
- names.add(tool.name);
72
+ if (!isPlainObject(tool)) return;
73
+ const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
74
+ ? tool.function
75
+ : undefined;
76
+ const name = typeof tool.name === "string" && tool.name.length > 0
77
+ ? tool.name
78
+ : typeof nestedFunction?.name === "string" && nestedFunction.name.length > 0
79
+ ? nestedFunction.name
80
+ : undefined;
81
+ if (!name) return;
82
+ names.add(name);
23
83
  // Codex routes MCP calls by an explicit `namespace` field, so the same tool is reachable
24
84
  // as a bare inner name or as the flattened form; accept both rather than guess which
25
85
  // coordinate system this provider echoes back.
26
- if (namespace) names.add(namespacedToolName(namespace, tool.name));
86
+ if (namespace) names.add(namespacedToolName(namespace, name));
87
+ }
88
+
89
+ /**
90
+ * Catalog view owned by the current Responses turn.
91
+ *
92
+ * `previous_response_id` expansion prepends stored input items, including historical
93
+ * `additional_tools` declarations. Those items remain conversation history but cannot grant
94
+ * execution authority to this turn. Top-level `tools` always belongs to the current request;
95
+ * only input catalogs at or after the replay boundary are current.
96
+ */
97
+ export function currentTurnWireToolCatalogBody(
98
+ body: unknown,
99
+ replayPrefixLength: number | undefined,
100
+ ): unknown {
101
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
102
+ if (typeof replayPrefixLength !== "number" || !Number.isFinite(replayPrefixLength)) return body;
103
+ const start = Math.min(body.input.length, Math.max(0, Math.trunc(replayPrefixLength)));
104
+ if (start === 0) return body;
105
+ return { ...body, input: body.input.slice(start) };
27
106
  }
28
107
 
29
108
  function addWireToolSpecs(names: Set<string>, specs: unknown): void {
@@ -53,21 +132,159 @@ export function collectDeclaredWireToolNames(body: unknown): Set<string> {
53
132
  addWireToolSpecs(names, body.tools);
54
133
  if (Array.isArray(body.input)) {
55
134
  for (const item of body.input) {
56
- if (isPlainObject(item) && item.type === "additional_tools") addWireToolSpecs(names, item.tools);
135
+ if (
136
+ isPlainObject(item)
137
+ && (item.type === "additional_tools" || item.type === "tool_search_output")
138
+ ) addWireToolSpecs(names, item.tools);
57
139
  }
58
140
  }
59
141
  return names;
60
142
  }
61
143
 
62
- function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): string | undefined {
144
+ function addNamelessClientCallTypes(callTypes: Set<string>, specs: unknown): void {
145
+ if (!Array.isArray(specs)) return;
146
+ for (const spec of specs) {
147
+ if (!isPlainObject(spec) || typeof spec.type !== "string") continue;
148
+ const callType = NAMELESS_CLIENT_DECLARATION_CALL_TYPES.get(spec.type);
149
+ if (callType) callTypes.add(callType);
150
+ }
151
+ }
152
+
153
+ function addProviderExecutedCallTypes(
154
+ callTypes: Set<ProviderExecutedCallType>,
155
+ specs: unknown,
156
+ ): void {
157
+ if (!Array.isArray(specs)) return;
158
+ for (const spec of specs) {
159
+ if (!isPlainObject(spec) || typeof spec.type !== "string") continue;
160
+ const callType = PROVIDER_EXECUTED_DECLARATION_CALL_TYPES.get(spec.type);
161
+ if (callType) callTypes.add(callType);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Item types this turn's hosted declarations authorize the PROVIDER to emit unnamed.
167
+ *
168
+ * Caller must gate this on the destination actually being that provider; a declaration alone
169
+ * is not authority, or any upstream could claim a hosted shape it never serves.
170
+ */
171
+ export function collectProviderExecutedCallTypes(body: unknown): Set<ProviderExecutedCallType> {
172
+ const callTypes = new Set<ProviderExecutedCallType>();
173
+ if (!isPlainObject(body)) return callTypes;
174
+ addProviderExecutedCallTypes(callTypes, body.tools);
175
+ if (Array.isArray(body.input)) {
176
+ for (const item of body.input) {
177
+ if (
178
+ isPlainObject(item)
179
+ && (item.type === "additional_tools" || item.type === "tool_search_output")
180
+ ) addProviderExecutedCallTypes(callTypes, item.tools);
181
+ }
182
+ }
183
+ return callTypes;
184
+ }
185
+
186
+ function isAuthorizedProviderExecutedCall(
187
+ item: Record<string, unknown>,
188
+ callTypes: ProviderExecutedCallTypes,
189
+ ): boolean {
190
+ if (typeof item.call_id !== "string") return false;
191
+ for (const callType of callTypes) {
192
+ if (
193
+ item.type === callType.itemType
194
+ && item.call_id.startsWith(callType.callIdPrefix)
195
+ ) return true;
196
+ }
197
+ return false;
198
+ }
199
+
200
+ /** Nameless client-call item types authorized by supported request tool declarations. */
201
+ export function collectDeclaredNamelessClientCallTypes(body: unknown): Set<string> {
202
+ const callTypes = new Set<string>();
203
+ if (!isPlainObject(body)) return callTypes;
204
+ addNamelessClientCallTypes(callTypes, body.tools);
205
+ if (Array.isArray(body.input)) {
206
+ for (const item of body.input) {
207
+ if (
208
+ isPlainObject(item)
209
+ && (item.type === "additional_tools" || item.type === "tool_search_output")
210
+ ) {
211
+ addNamelessClientCallTypes(callTypes, item.tools);
212
+ }
213
+ }
214
+ }
215
+ return callTypes;
216
+ }
217
+
218
+ function isReadableWireToolSpec(spec: unknown): boolean {
219
+ if (!isPlainObject(spec) || typeof spec.type !== "string" || spec.type.length === 0) return false;
220
+ if (spec.type === "function") {
221
+ return (typeof spec.name === "string" && spec.name.length > 0)
222
+ || (isPlainObject(spec.function)
223
+ && typeof spec.function.name === "string"
224
+ && spec.function.name.length > 0);
225
+ }
226
+ if (spec.type === "custom") return typeof spec.name === "string" && spec.name.length > 0;
227
+ if (spec.type === "namespace") {
228
+ return typeof spec.name === "string"
229
+ && spec.name.length > 0
230
+ && Array.isArray(spec.tools)
231
+ && (spec.tools.length === 0 || spec.tools.some(inner =>
232
+ isPlainObject(inner)
233
+ && (inner.type === "function" || inner.type === "custom")
234
+ && typeof inner.name === "string"
235
+ && inner.name.length > 0
236
+ ));
237
+ }
238
+ if (NAMELESS_TOOL_SPEC_TYPES.has(spec.type)) return true;
239
+ return typeof spec.name === "string" && spec.name.length > 0;
240
+ }
241
+
242
+ function isReadableWireToolCatalog(value: unknown): boolean {
243
+ return Array.isArray(value)
244
+ && (value.length === 0 || value.some(isReadableWireToolSpec));
245
+ }
246
+
247
+ /** Whether a request contains a supported catalog, including an explicit empty deny-all array. */
248
+ export function hasExplicitWireToolCatalog(body: unknown): boolean {
249
+ if (!isPlainObject(body)) return false;
250
+ if (isReadableWireToolCatalog(body.tools)) return true;
251
+ if (!Array.isArray(body.input)) return false;
252
+ return body.input.some(item =>
253
+ isPlainObject(item)
254
+ && item.type === "additional_tools"
255
+ && isReadableWireToolCatalog(item.tools)
256
+ );
257
+ }
258
+
259
+ function undeclaredNameInItem(
260
+ item: unknown,
261
+ declared: ReadonlySet<string>,
262
+ declaredNamelessClientCallTypes: ReadonlySet<string>,
263
+ providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
264
+ ): string | undefined {
63
265
  if (!isPlainObject(item)) return undefined;
64
- if (typeof item.type !== "string" || !CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
266
+ if (typeof item.type !== "string") return undefined;
267
+ // The provider executes this exact measured shape itself, so there is no client name to
268
+ // authorize. The caller supplies these signatures only for the matching destination and
269
+ // declarations; the item must additionally carry the hosted call-id prefix.
270
+ if (isAuthorizedProviderExecutedCall(item, providerExecutedCallTypes)) return undefined;
271
+ const namelessDisplayName = NAMELESS_CLIENT_CALL_DISPLAY_NAMES.get(item.type);
272
+ if (namelessDisplayName !== undefined) {
273
+ // Only Codex's explicit `execution: "client"` form delegates tool search to the client.
274
+ if (item.type === "tool_search_call" && item.execution !== "client") return undefined;
275
+ return declaredNamelessClientCallTypes.has(item.type) ? undefined : namelessDisplayName;
276
+ }
277
+ if (!CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
65
278
  const name = item.name;
66
279
  if (typeof name !== "string" || name.length === 0) return undefined;
67
- if (declared.has(name)) return undefined;
68
- if (typeof item.namespace === "string" && declared.has(namespacedToolName(item.namespace, name))) {
69
- return undefined;
280
+ if (typeof item.namespace === "string") {
281
+ // Namespaced calls are matched by their full wire name only — never legacy-normalize
282
+ // them, or an undeclared namespaced `exec_command` could slip through as bare `exec`.
283
+ if (declared.has(namespacedToolName(item.namespace, name))) return undefined;
284
+ return name;
70
285
  }
286
+ const effectiveName = normalizeDeclaredToolName(name, declared);
287
+ if (declared.has(effectiveName)) return undefined;
71
288
  return name;
72
289
  }
73
290
 
@@ -75,14 +292,16 @@ function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): str
75
292
  export function undeclaredToolCallName(
76
293
  payload: unknown,
77
294
  declared: ReadonlySet<string>,
295
+ declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
296
+ providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
78
297
  ): string | undefined {
79
298
  if (!isPlainObject(payload)) return undefined;
80
299
  if (payload.type === "response.output_item.added" || payload.type === "response.output_item.done") {
81
- return undeclaredNameInItem(payload.item, declared);
300
+ return undeclaredNameInItem(payload.item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
82
301
  }
83
302
  // Sparse gateways skip incremental items and only ever ship the terminal snapshot.
84
303
  if (payload.type === "response.completed" || payload.type === "response.incomplete") {
85
- return undeclaredToolCallNameInResponse(payload.response, declared);
304
+ return undeclaredToolCallNameInResponse(payload.response, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
86
305
  }
87
306
  return undefined;
88
307
  }
@@ -91,10 +310,12 @@ export function undeclaredToolCallName(
91
310
  export function undeclaredToolCallNameInResponse(
92
311
  response: unknown,
93
312
  declared: ReadonlySet<string>,
313
+ declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
314
+ providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
94
315
  ): string | undefined {
95
316
  if (!isPlainObject(response) || !Array.isArray(response.output)) return undefined;
96
317
  for (const item of response.output) {
97
- const name = undeclaredNameInItem(item, declared);
318
+ const name = undeclaredNameInItem(item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
98
319
  if (name !== undefined) return name;
99
320
  }
100
321
  return undefined;
@@ -133,6 +354,8 @@ function failedBlocks(name: string, newline: string): readonly string[] {
133
354
  */
134
355
  export function createUndeclaredToolCallGuardBlockRewrite(
135
356
  declared: ReadonlySet<string>,
357
+ declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
358
+ providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
136
359
  ): SseBlockRewrite {
137
360
  let tripped = false;
138
361
  return (block: string) => {
@@ -145,7 +368,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
145
368
  } catch {
146
369
  return [block];
147
370
  }
148
- const name = undeclaredToolCallName(parsed, declared);
371
+ const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
149
372
  if (name === undefined) return [block];
150
373
  tripped = true;
151
374
  return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
package/src/service.ts CHANGED
@@ -10,8 +10,8 @@ import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/
10
10
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
11
11
  import { homedir, tmpdir } from "node:os";
12
12
  import { dirname, join, posix, resolve, win32 } from "node:path";
13
- import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
14
- import { loadConfig } from "./config";
13
+ import { expandUserPath, getConfigDir, loadConfig } from "./config";
14
+ import { readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config/process-state";
15
15
  import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
16
16
  import { stripGrokConfig } from "./grok/inject";
17
17
  import { isWslRuntime, resolveCodexHomeDir, type CodexHomeDeps } from "./codex/home";
@@ -97,13 +97,17 @@ function defaultOpenCodexHome(): string {
97
97
  return resolve(join(homedir(), ".opencodex"));
98
98
  }
99
99
 
100
- function serviceStatePaths(): string[] {
101
- const paths = [serviceStatePath()];
100
+ export function serviceStatePathsForOpenCodexHome(opencodexHome: string): string[] {
101
+ const paths = [join(opencodexHome, "service-state.json")];
102
102
  const defaultPath = join(defaultOpenCodexHome(), "service-state.json");
103
103
  if (normalizePathForCompare(defaultPath) !== normalizePathForCompare(paths[0])) paths.push(defaultPath);
104
104
  return paths;
105
105
  }
106
106
 
107
+ function serviceStatePaths(): string[] {
108
+ return serviceStatePathsForOpenCodexHome(currentOpenCodexHome());
109
+ }
110
+
107
111
  function currentCodexHome(deps: CodexHomeDeps = {}): string {
108
112
  // Service ownership must identify the same home as the runtime. In WSL an
109
113
  // unset CODEX_HOME can resolve to the single Windows Desktop home rather than
@@ -551,7 +555,7 @@ export function installedServiceListenPort(): number {
551
555
  ?? resolveServiceListenPort();
552
556
  }
553
557
 
554
- export const SERVICE_INSTALL_HEALTH_MS = 20_000;
558
+ export const SERVICE_INSTALL_HEALTH_MS = Number(process.env.OCX_SERVICE_HEALTH_TIMEOUT_MS) || 30_000;
555
559
 
556
560
  /**
557
561
  * Whether a proxy actually answers on the port this install/start just produced.
@@ -259,6 +259,19 @@ export interface OcxConfig {
259
259
  port: number;
260
260
  /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
261
261
  emptyCompletionRetry?: boolean;
262
+ /**
263
+ * Whether a login may open a browser on the machine running the proxy.
264
+ *
265
+ * Absent and `true` both mean "open", which is what every existing install
266
+ * already does. Only an explicit `false` declines — for an operator who wants
267
+ * to paste the authorization URL into a different browser profile, or who is
268
+ * driving the dashboard from a different machine than the proxy.
269
+ *
270
+ * Deliberately a boolean and not an "auto" mode: inferring headlessness from
271
+ * SSH_CONNECTION or a missing DISPLAY breaks a working login silently when
272
+ * the guess is wrong.
273
+ */
274
+ oauthOpenBrowser?: boolean;
262
275
  /** Maximum usage-log bytes read for one management snapshot. */
263
276
  managementUsageMaxReadBytes?: number;
264
277
  providers: Record<string, OcxProviderConfig>;
@@ -455,10 +468,12 @@ export interface OcxConfig {
455
468
  * Routed parents get v2 tools; Sol/Terra can still spawn Grok/Claude (issue #92).
456
469
  */
457
470
  keepNativeChatGptOnV1?: boolean;
471
+ /** Experimental routed target for eligible native V2 root parents. */
472
+ v2NativeParentOverride?: { enabled?: boolean; model?: string };
458
473
  /** Experimental, default-off ChatGPT recovery for encrypted V2 routed tasks. */
459
474
  agentTaskRecovery?: {
460
475
  enabled?: boolean;
461
- /** ChatGPT model used by the recovery request. Default: gpt-5.6-sol. */
476
+ /** ChatGPT model used by the recovery request. Default: gpt-5.6-luna. */
462
477
  model?: string;
463
478
  /** Recovery request timeout in milliseconds. Default: 45000. */
464
479
  timeoutMs?: number;
@@ -8,6 +8,8 @@ import type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode }
8
8
  */
9
9
  export type RefreshPolicy = "proactive" | "lazy-only" | "disabled";
10
10
 
11
+ export type ProviderTlsProfile = "antigravity-browser";
12
+
11
13
  export interface OpenRouterProviderRouting {
12
14
  /** OpenRouter provider slugs to try first, in priority order. */
13
15
  order?: string[];
@@ -68,6 +70,8 @@ export interface RequestPacingRule {
68
70
  requestsPerMinute?: number;
69
71
  /** Minimum delay between request starts. The slower configured value wins. */
70
72
  minIntervalMs?: number;
73
+ /** Positive-only random delay added to each request-start slot. */
74
+ jitterMs?: number;
71
75
  }
72
76
 
73
77
  export interface ProviderRequestPacingConfig extends RequestPacingRule {
@@ -136,6 +140,8 @@ export interface OcxProviderConfig {
136
140
  codexToolMode?: "code_mode_only" | "shell";
137
141
  /** Optional outbound request-start pacing shared by this provider and its model overrides. */
138
142
  requestPacing?: ProviderRequestPacingConfig;
143
+ /** Explicitly acknowledged experimental browser-compatible transport profile. */
144
+ tlsProfile?: ProviderTlsProfile;
139
145
  /** Cursor MCP compatibility bounds; positive integers when configured. */
140
146
  mcpMaxTools?: number;
141
147
  mcpMaxSchemaBytes?: number;
@@ -245,6 +251,11 @@ export interface OcxProviderConfig {
245
251
  */
246
252
  codexAccountMode?: CodexAccountMode;
247
253
  apiKey?: string;
254
+ /** Azure OpenAI identity authentication; mutually exclusive with API-key fields. */
255
+ azureCredential?: {
256
+ type: "default-azure-credential";
257
+ managedIdentityClientId?: string;
258
+ };
248
259
  /**
249
260
  * Key-auth header style for Anthropic-compatible providers.
250
261
  * Defaults to the native Anthropic `x-api-key`; gateways may require
@@ -281,6 +292,11 @@ export interface OcxProviderConfig {
281
292
  modelInputModalities?: Record<string, string[]>;
282
293
  /** Model-specific max input token limits. Values cap auto_compact_token_limit. */
283
294
  modelMaxInputTokens?: Record<string, number>;
295
+ /**
296
+ * Per-model soft compaction budgets. Values may only lower the effective
297
+ * context/max-input envelope; they never raise hard admission limits.
298
+ */
299
+ modelAutoCompactTokenLimits?: Record<string, number>;
284
300
  /**
285
301
  * Provider-wide fallback for chat-completions `max_tokens` when the caller omits
286
302
  * Responses `max_output_tokens`. Adapters still let an explicit request win.
@@ -30,6 +30,11 @@ export interface OcxReasoningReplayIdentity {
30
30
  * the holder, so late tool-call cache writes see the active physical identity.
31
31
  */
32
32
  export interface OcxReasoningReplayScopeRef {
33
+ /**
34
+ * Conversation namespace for replay state. Historically this was always the Codex parent-thread
35
+ * id; headerless Responses callers use a raw sanitized thread/Cursor/session fallback, never the
36
+ * hashed request-log conversation id.
37
+ */
33
38
  readonly clientThreadId: string;
34
39
  current?: Readonly<OcxReasoningReplayIdentity>;
35
40
  }
@@ -63,7 +68,9 @@ export interface OcxParsedRequest {
63
68
  _cursorConversationId?: string;
64
69
  /** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
65
70
  _clientThreadId?: string;
66
- /** Provider/account/model-bound namespace for process-local raw-reasoning replay. */
71
+ /** True when promptCacheKey is a shared cache cohort rather than a conversation identity. */
72
+ _promptCacheKeyIsSharedCohort?: boolean;
73
+ /** Conversation/provider/account/model-bound namespace for reasoning replay state. */
67
74
  _reasoningReplayScope?: OcxReasoningReplayScopeRef;
68
75
  /**
69
76
  * Set by bindRouteReasoningReplayScope after a proven serving-identity change, or by
@@ -266,6 +273,32 @@ export interface OcxRequestOptions {
266
273
  schema?: Record<string, unknown>;
267
274
  strict?: boolean;
268
275
  };
276
+ providerOptions?: { google?: GoogleProviderOptions };
277
+ }
278
+
279
+ export type GoogleSafetyCategory =
280
+ | "HARM_CATEGORY_HATE_SPEECH"
281
+ | "HARM_CATEGORY_SEXUALLY_EXPLICIT"
282
+ | "HARM_CATEGORY_DANGEROUS_CONTENT"
283
+ | "HARM_CATEGORY_HARASSMENT"
284
+ | "HARM_CATEGORY_CIVIC_INTEGRITY"
285
+ | "HARM_CATEGORY_JAILBREAK";
286
+ export type GoogleSafetyThreshold =
287
+ | "HARM_BLOCK_THRESHOLD_UNSPECIFIED"
288
+ | "BLOCK_LOW_AND_ABOVE"
289
+ | "BLOCK_MEDIUM_AND_ABOVE"
290
+ | "BLOCK_ONLY_HIGH"
291
+ | "BLOCK_NONE"
292
+ | "OFF";
293
+ export interface GoogleSafetySetting {
294
+ category: GoogleSafetyCategory;
295
+ threshold: GoogleSafetyThreshold;
296
+ }
297
+ export interface GoogleProviderOptions {
298
+ thinkingBudget?: number;
299
+ includeThoughts?: boolean;
300
+ safetySettings?: GoogleSafetySetting[];
301
+ cachedContent?: string;
269
302
  }
270
303
 
271
304
  export type OcxMessagePhase = "commentary" | "final_answer";
@@ -31,6 +31,33 @@ export function namespacedToolName(namespace: string | undefined, name: string):
31
31
  return namespace ? `${namespace}__${name}` : name;
32
32
  }
33
33
 
34
+ /**
35
+ * Codex 0.149 unified-exec name normalization.
36
+ *
37
+ * Codex's code-mode shell tool is declared as `exec` (a freeform custom tool whose own
38
+ * description mentions the nested `await tools.exec_command(...)` helper). Routed models —
39
+ * DeepSeek in particular — sometimes echo that helper name as the tool-call name, emitting
40
+ * `exec_command` instead of the declared `exec`. Accept the legacy shell bridge names only
41
+ * when the request catalog actually declares `exec` and does not itself declare the legacy
42
+ * name (an MCP server may legitimately advertise `exec_command` under its own namespace).
43
+ */
44
+ const LEGACY_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
45
+
46
+ export function normalizeDeclaredToolName(
47
+ name: string,
48
+ declared: ReadonlySet<string> | undefined,
49
+ ): string {
50
+ if (!declared || !declared.has("exec")) return name;
51
+ if (declared.has(name)) return name;
52
+ // When the catalog explicitly declares any legacy shell bridge name, the environment
53
+ // genuinely exposes that tool — turn normalization off so a call is never mis-routed
54
+ // to `exec`.
55
+ if ((LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).some(legacy => declared.has(legacy))) {
56
+ return name;
57
+ }
58
+ return (LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(name) ? "exec" : name;
59
+ }
60
+
34
61
  export function toolChoiceAliases(tool: Pick<OcxTool, "namespace" | "name">): string[] {
35
62
  const wireName = namespacedToolName(tool.namespace, tool.name);
36
63
  return tool.namespace ? [wireName, `${tool.namespace}.${tool.name}`] : [wireName];
@@ -43,6 +70,74 @@ function sameToolIdentity(
43
70
  return left.namespace === right.namespace && left.name === right.name;
44
71
  }
45
72
 
73
+ type ToolIdentity = Readonly<Pick<OcxTool, "namespace" | "name">>;
74
+
75
+ function snapshotToolIdentity(tool: Pick<OcxTool, "namespace" | "name">): ToolIdentity {
76
+ return Object.freeze({
77
+ name: tool.name,
78
+ ...(tool.namespace === undefined ? {} : { namespace: tool.namespace }),
79
+ });
80
+ }
81
+
82
+ function buildToolChoiceCatalog(
83
+ tools: readonly ToolIdentity[],
84
+ ): {
85
+ candidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>;
86
+ sourceCandidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>;
87
+ identitiesByTool: WeakMap<object, ToolIdentity>;
88
+ } {
89
+ const index = new Map<string, ToolIdentity[]>();
90
+ const sourceIndex = new Map<string, ToolIdentity[]>();
91
+ const identities = new Map<string, Set<string>>();
92
+ const identitiesByTool = new WeakMap<object, ToolIdentity>();
93
+ for (const tool of tools) {
94
+ const snapshot = snapshotToolIdentity(tool);
95
+ identitiesByTool.set(tool, snapshot);
96
+ const identity = JSON.stringify([snapshot.namespace ?? null, snapshot.name]);
97
+ for (const selector of [...toolChoiceAliases(snapshot), snapshot.name]) {
98
+ const candidates = index.get(selector);
99
+ if (!candidates) {
100
+ index.set(selector, [snapshot]);
101
+ sourceIndex.set(selector, [tool]);
102
+ identities.set(selector, new Set([identity]));
103
+ } else if (!identities.get(selector)!.has(identity)) {
104
+ candidates.push(snapshot);
105
+ sourceIndex.get(selector)!.push(tool);
106
+ identities.get(selector)!.add(identity);
107
+ }
108
+ }
109
+ }
110
+ return { candidatesByName: index, sourceCandidatesByName: sourceIndex, identitiesByTool };
111
+ }
112
+
113
+ /** Compile one immutable view of a request's tool catalog for repeated policy checks. */
114
+ export function createToolChoiceResolver(tools: readonly ToolIdentity[] | undefined) {
115
+ const compiled = tools ? buildToolChoiceCatalog(tools) : undefined;
116
+ const candidatesByName = compiled?.candidatesByName;
117
+ const snapshotFor = (tool: ToolIdentity): ToolIdentity | undefined => {
118
+ const snapshot = compiled?.identitiesByTool.get(tool);
119
+ return snapshot && sameToolIdentity(snapshot, tool) ? snapshot : undefined;
120
+ };
121
+ return {
122
+ candidates(name: string): ToolIdentity[] {
123
+ return (candidatesByName?.get(name) ?? []).map(candidate => ({ ...candidate }));
124
+ },
125
+ candidateCount(name: string): number {
126
+ return candidatesByName?.get(name)?.length ?? 0;
127
+ },
128
+ allows(tool: ToolIdentity, allowedTools: ReadonlySet<string>): boolean {
129
+ if (!candidatesByName) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
130
+ const snapshot = snapshotFor(tool);
131
+ return snapshot ? toolAllowedByChoiceFromIndex(snapshot, allowedTools, candidatesByName) : false;
132
+ },
133
+ selects(tool: ToolIdentity, name: string): boolean {
134
+ const snapshot = snapshotFor(tool);
135
+ const candidates = candidatesByName?.get(name);
136
+ return !!snapshot && candidates?.length === 1 && sameToolIdentity(candidates[0], snapshot);
137
+ },
138
+ };
139
+ }
140
+
46
141
  /**
47
142
  * All tools that could be selected by one client-facing name. Bare logical names are included
48
143
  * here because they are a compatibility selector for namespaced tools, while wire and dotted
@@ -53,12 +148,7 @@ export function toolChoiceCandidates(
53
148
  name: string,
54
149
  ): Pick<OcxTool, "namespace" | "name">[] {
55
150
  if (!tools) return [];
56
- const candidates: Pick<OcxTool, "namespace" | "name">[] = [];
57
- for (const tool of tools) {
58
- if (tool.name !== name && !toolChoiceAliases(tool).includes(name)) continue;
59
- if (!candidates.some(candidate => sameToolIdentity(candidate, tool))) candidates.push(tool);
60
- }
61
- return candidates;
151
+ return [...(buildToolChoiceCatalog(tools).sourceCandidatesByName.get(name) ?? [])];
62
152
  }
63
153
 
64
154
  /**
@@ -72,10 +162,22 @@ export function toolAllowedByChoice(
72
162
  tools?: readonly Pick<OcxTool, "namespace" | "name">[],
73
163
  ): boolean {
74
164
  if (!tools) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
165
+ return toolAllowedByChoiceFromIndex(
166
+ snapshotToolIdentity(tool),
167
+ allowedTools,
168
+ buildToolChoiceCatalog(tools).candidatesByName,
169
+ );
170
+ }
171
+
172
+ function toolAllowedByChoiceFromIndex(
173
+ tool: ToolIdentity,
174
+ allowedTools: ReadonlySet<string>,
175
+ candidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>,
176
+ ): boolean {
75
177
  for (const name of [...toolChoiceAliases(tool), tool.name]) {
76
178
  if (!allowedTools.has(name)) continue;
77
- const candidates = toolChoiceCandidates(tools, name);
78
- if (candidates.length === 1 && sameToolIdentity(candidates[0], tool)) return true;
179
+ const candidates = candidatesByName.get(name);
180
+ if (candidates?.length === 1 && sameToolIdentity(candidates[0], tool)) return true;
79
181
  }
80
182
  return false;
81
183
  }
@@ -123,9 +225,10 @@ export function toolChoiceToolPredicate(
123
225
  if (choice === "none") return () => false;
124
226
  if (isAllowedToolChoice(choice)) {
125
227
  const allowed = new Set(choice.allowedTools);
126
- return tool => toolAllowedByChoice(tool, allowed, tools);
228
+ const resolver = createToolChoiceResolver(tools);
229
+ return tool => resolver.allows(tool, allowed);
127
230
  }
128
231
  if (!tools) return tool => toolChoiceAliases(tool).includes(choice.name);
129
- const candidates = toolChoiceCandidates(tools, choice.name);
130
- return tool => candidates.length === 1 && sameToolIdentity(candidates[0], tool);
232
+ const resolver = createToolChoiceResolver(tools);
233
+ return tool => resolver.selects(tool, choice.name);
131
234
  }